fix: 修复管理端前端 lint 与构建问题
- 收敛 easyflow-ui-admin 的 lint、格式和类型问题 - 修正 demo 页面与管理端前端构建失败点 - 验证 pnpm lint 与 pnpm build 均已通过
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': ['off']
|
||||
}
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': ['off'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,157 +1,163 @@
|
||||
import type {useSvelteFlow} from '@xyflow/svelte';
|
||||
import {componentName} from './consts';
|
||||
import type {TinyflowData, TinyflowOptions, TinyflowTheme} from './types';
|
||||
import type { useSvelteFlow } from '@xyflow/svelte';
|
||||
import { componentName } from './consts';
|
||||
import type { TinyflowData, TinyflowOptions, TinyflowTheme } from './types';
|
||||
|
||||
type FlowInstance = ReturnType<typeof useSvelteFlow>;
|
||||
|
||||
export class Tinyflow {
|
||||
private options!: TinyflowOptions;
|
||||
private rootEl!: Element;
|
||||
private svelteFlowInstance!: FlowInstance;
|
||||
private tinyflowEl!: HTMLElement & {
|
||||
options: TinyflowOptions;
|
||||
onInit: (svelteFlowInstance: FlowInstance) => void;
|
||||
private options!: TinyflowOptions;
|
||||
private rootEl!: Element;
|
||||
private svelteFlowInstance!: FlowInstance;
|
||||
private tinyflowEl!: HTMLElement & {
|
||||
options: TinyflowOptions;
|
||||
onInit: (svelteFlowInstance: FlowInstance) => void;
|
||||
};
|
||||
|
||||
constructor(options: TinyflowOptions) {
|
||||
if (
|
||||
typeof options.element !== 'string' &&
|
||||
!(options.element instanceof Element)
|
||||
) {
|
||||
throw new Error('element must be a string or Element');
|
||||
}
|
||||
this._setOptions(options);
|
||||
this._init();
|
||||
}
|
||||
|
||||
private _init() {
|
||||
if (typeof this.options.element === 'string') {
|
||||
this.rootEl = document.querySelector(this.options.element)!;
|
||||
if (!this.rootEl) {
|
||||
throw new Error(
|
||||
`element not found by document.querySelector('${this.options.element}')`,
|
||||
);
|
||||
}
|
||||
} else if (this.options.element instanceof Element) {
|
||||
this.rootEl = this.options.element;
|
||||
} else {
|
||||
throw new Error('element must be a string or Element');
|
||||
}
|
||||
|
||||
this.tinyflowEl = this._createTinyflowElement();
|
||||
this.rootEl.appendChild(this.tinyflowEl);
|
||||
}
|
||||
|
||||
private _setOptions(options: TinyflowOptions) {
|
||||
this.options = {
|
||||
theme: options.theme || 'light',
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(options: TinyflowOptions) {
|
||||
if (typeof options.element !== 'string' && !(options.element instanceof Element)) {
|
||||
throw new Error('element must be a string or Element');
|
||||
}
|
||||
this._setOptions(options);
|
||||
this._init();
|
||||
private _getFlowInstance() {
|
||||
if (!this.svelteFlowInstance) {
|
||||
console.warn('Tinyflow instance is not initialized');
|
||||
return null;
|
||||
}
|
||||
return this.svelteFlowInstance;
|
||||
}
|
||||
|
||||
private _applyThemeClass(targetEl: Element, theme?: TinyflowTheme) {
|
||||
targetEl.classList.remove('tf-theme-light', 'tf-theme-dark');
|
||||
targetEl.classList.add(
|
||||
theme === 'dark' ? 'tf-theme-dark' : 'tf-theme-light',
|
||||
);
|
||||
}
|
||||
|
||||
private _createTinyflowElement() {
|
||||
const tinyflowEl = document.createElement(componentName) as HTMLElement & {
|
||||
options: TinyflowOptions;
|
||||
onInit: (svelteFlowInstance: FlowInstance) => void;
|
||||
};
|
||||
tinyflowEl.style.display = 'block';
|
||||
tinyflowEl.style.width = '100%';
|
||||
tinyflowEl.style.height = '100%';
|
||||
this._applyThemeClass(tinyflowEl, this.options.theme);
|
||||
|
||||
tinyflowEl.options = this.options;
|
||||
tinyflowEl.onInit = (svelteFlowInstance: FlowInstance) => {
|
||||
this.svelteFlowInstance = svelteFlowInstance;
|
||||
};
|
||||
return tinyflowEl;
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
getData() {
|
||||
const flow = this._getFlowInstance();
|
||||
if (!flow) {
|
||||
return null;
|
||||
}
|
||||
return flow.toObject();
|
||||
}
|
||||
|
||||
async focusNode(
|
||||
nodeId: string,
|
||||
options?: { duration?: number; zoom?: number },
|
||||
) {
|
||||
const flow = this._getFlowInstance();
|
||||
if (!flow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private _init() {
|
||||
if (typeof this.options.element === 'string') {
|
||||
this.rootEl = document.querySelector(this.options.element)!;
|
||||
if (!this.rootEl) {
|
||||
throw new Error(
|
||||
`element not found by document.querySelector('${this.options.element}')`
|
||||
);
|
||||
}
|
||||
} else if (this.options.element instanceof Element) {
|
||||
this.rootEl = this.options.element;
|
||||
} else {
|
||||
throw new Error('element must be a string or Element');
|
||||
}
|
||||
|
||||
this.tinyflowEl = this._createTinyflowElement();
|
||||
this.rootEl.appendChild(this.tinyflowEl);
|
||||
const targetNode = flow.getNode(nodeId);
|
||||
if (!targetNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private _setOptions(options: TinyflowOptions) {
|
||||
this.options = {
|
||||
theme: options.theme || 'light',
|
||||
...options
|
||||
};
|
||||
// Keep only the target node selected so the canvas has a clear visual focus.
|
||||
flow.getNodes().forEach((node) => {
|
||||
flow.updateNode(node.id, { selected: node.id === nodeId });
|
||||
});
|
||||
|
||||
const internalNode = flow.getInternalNode(nodeId) as any;
|
||||
const absolutePosition = internalNode?.internals?.positionAbsolute ||
|
||||
(targetNode as any)?.positionAbsolute ||
|
||||
targetNode.position || { x: 0, y: 0 };
|
||||
const width =
|
||||
internalNode?.measured?.width ||
|
||||
(targetNode as any)?.measured?.width ||
|
||||
(targetNode as any)?.width ||
|
||||
260;
|
||||
const height =
|
||||
internalNode?.measured?.height ||
|
||||
(targetNode as any)?.measured?.height ||
|
||||
(targetNode as any)?.height ||
|
||||
120;
|
||||
|
||||
const centerX = absolutePosition.x + width / 2;
|
||||
const centerY = absolutePosition.y + height / 2;
|
||||
const nextZoom = options?.zoom ?? Math.max(flow.getZoom(), 1);
|
||||
|
||||
await flow.setCenter(centerX, centerY, {
|
||||
zoom: nextZoom,
|
||||
duration: options?.duration ?? 280,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setTheme(theme: TinyflowTheme) {
|
||||
this.options.theme = theme;
|
||||
if (this.tinyflowEl) {
|
||||
this._applyThemeClass(this.tinyflowEl, theme);
|
||||
}
|
||||
}
|
||||
|
||||
private _getFlowInstance() {
|
||||
if (!this.svelteFlowInstance) {
|
||||
console.warn('Tinyflow instance is not initialized');
|
||||
return null;
|
||||
}
|
||||
return this.svelteFlowInstance;
|
||||
}
|
||||
|
||||
private _applyThemeClass(targetEl: Element, theme?: TinyflowTheme) {
|
||||
targetEl.classList.remove('tf-theme-light', 'tf-theme-dark');
|
||||
targetEl.classList.add(theme === 'dark' ? 'tf-theme-dark' : 'tf-theme-light');
|
||||
}
|
||||
|
||||
private _createTinyflowElement() {
|
||||
const tinyflowEl = document.createElement(componentName) as HTMLElement & {
|
||||
options: TinyflowOptions;
|
||||
onInit: (svelteFlowInstance: FlowInstance) => void;
|
||||
};
|
||||
tinyflowEl.style.display = 'block';
|
||||
tinyflowEl.style.width = '100%';
|
||||
tinyflowEl.style.height = '100%';
|
||||
this._applyThemeClass(tinyflowEl, this.options.theme);
|
||||
|
||||
tinyflowEl.options = this.options;
|
||||
tinyflowEl.onInit = (svelteFlowInstance: FlowInstance) => {
|
||||
this.svelteFlowInstance = svelteFlowInstance;
|
||||
};
|
||||
return tinyflowEl;
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
getData() {
|
||||
const flow = this._getFlowInstance();
|
||||
if (!flow) {
|
||||
return null;
|
||||
}
|
||||
return flow.toObject();
|
||||
}
|
||||
|
||||
async focusNode(nodeId: string, options?: { duration?: number; zoom?: number }) {
|
||||
const flow = this._getFlowInstance();
|
||||
if (!flow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetNode = flow.getNode(nodeId);
|
||||
if (!targetNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep only the target node selected so the canvas has a clear visual focus.
|
||||
flow.getNodes().forEach((node) => {
|
||||
flow.updateNode(node.id, { selected: node.id === nodeId });
|
||||
});
|
||||
|
||||
const internalNode = flow.getInternalNode(nodeId) as any;
|
||||
const absolutePosition =
|
||||
internalNode?.internals?.positionAbsolute ||
|
||||
(targetNode as any)?.positionAbsolute ||
|
||||
targetNode.position ||
|
||||
{ x: 0, y: 0 };
|
||||
const width =
|
||||
internalNode?.measured?.width ||
|
||||
(targetNode as any)?.measured?.width ||
|
||||
(targetNode as any)?.width ||
|
||||
260;
|
||||
const height =
|
||||
internalNode?.measured?.height ||
|
||||
(targetNode as any)?.measured?.height ||
|
||||
(targetNode as any)?.height ||
|
||||
120;
|
||||
|
||||
const centerX = absolutePosition.x + width / 2;
|
||||
const centerY = absolutePosition.y + height / 2;
|
||||
const nextZoom = options?.zoom ?? Math.max(flow.getZoom(), 1);
|
||||
|
||||
await flow.setCenter(centerX, centerY, {
|
||||
zoom: nextZoom,
|
||||
duration: options?.duration ?? 280
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setTheme(theme: TinyflowTheme) {
|
||||
this.options.theme = theme;
|
||||
if (this.tinyflowEl) {
|
||||
this._applyThemeClass(this.tinyflowEl, theme);
|
||||
}
|
||||
}
|
||||
|
||||
setData(data: TinyflowData) {
|
||||
this.options.data = data;
|
||||
|
||||
this.tinyflowEl = this._createTinyflowElement();
|
||||
|
||||
this.destroy();
|
||||
this.rootEl.appendChild(this.tinyflowEl);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
while (this.rootEl.firstChild) {
|
||||
this.rootEl.removeChild(this.rootEl.firstChild);
|
||||
}
|
||||
setData(data: TinyflowData) {
|
||||
this.options.data = data;
|
||||
|
||||
this.tinyflowEl = this._createTinyflowElement();
|
||||
|
||||
this.destroy();
|
||||
this.rootEl.appendChild(this.tinyflowEl);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
while (this.rootEl.firstChild) {
|
||||
this.rootEl.removeChild(this.rootEl.firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,17 @@ import MenuButton from './menu-button.svelte';
|
||||
import MixedInput from './mixed-input.svelte';
|
||||
|
||||
export {
|
||||
Button,
|
||||
Checkbox,
|
||||
Chosen,
|
||||
Input,
|
||||
Textarea,
|
||||
Tabs,
|
||||
Collapse,
|
||||
Render,
|
||||
Select,
|
||||
FloatingTrigger,
|
||||
Heading,
|
||||
MenuButton,
|
||||
MixedInput
|
||||
Button,
|
||||
Checkbox,
|
||||
Chosen,
|
||||
Input,
|
||||
Textarea,
|
||||
Tabs,
|
||||
Collapse,
|
||||
Render,
|
||||
Select,
|
||||
FloatingTrigger,
|
||||
Heading,
|
||||
MenuButton,
|
||||
MixedInput,
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
{#snippet handle()}
|
||||
<Handle type="source" position={Position.Bottom} id="loop_handle" style="bottom: -12px;width: 100px"
|
||||
class="loop_handle_wrapper" />
|
||||
class="loop-handle-wrapper" />
|
||||
{/snippet}
|
||||
|
||||
<div class="heading">
|
||||
@@ -75,7 +75,7 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global(.loop_handle_wrapper ) {
|
||||
:global(.loop-handle-wrapper) {
|
||||
&::after {
|
||||
//display: none;
|
||||
content: '循环体';
|
||||
@@ -92,4 +92,3 @@
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type {NodeTypes} from '@xyflow/svelte';
|
||||
import type { NodeTypes } from '@xyflow/svelte';
|
||||
import StartNode from './StartNode.svelte';
|
||||
import EndNode from './EndNode.svelte';
|
||||
import LLMNode from './LLMNode.svelte';
|
||||
@@ -15,15 +15,15 @@ import ConditionNode from './ConditionNode.svelte';
|
||||
* @description 节点类型(en: node type)
|
||||
*/
|
||||
export const nodeTypes = {
|
||||
startNode: StartNode,
|
||||
codeNode: CodeNode,
|
||||
confirmNode: ConfirmNode,
|
||||
llmNode: LLMNode,
|
||||
templateNode: TemplateNode,
|
||||
httpNode: HttpNode,
|
||||
knowledgeNode: KnowledgeNode,
|
||||
searchEngineNode: SearchEngineNode,
|
||||
loopNode: LoopNode,
|
||||
conditionNode: ConditionNode,
|
||||
endNode: EndNode
|
||||
startNode: StartNode,
|
||||
codeNode: CodeNode,
|
||||
confirmNode: ConfirmNode,
|
||||
llmNode: LLMNode,
|
||||
templateNode: TemplateNode,
|
||||
httpNode: HttpNode,
|
||||
knowledgeNode: KnowledgeNode,
|
||||
searchEngineNode: SearchEngineNode,
|
||||
loopNode: LoopNode,
|
||||
conditionNode: ConditionNode,
|
||||
endNode: EndNode,
|
||||
} as any as NodeTypes;
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
export const genShortId = (length = 16) => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, (byte) => chars[byte % chars.length]).join('');
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, (byte) => chars[byte % chars.length]).join('');
|
||||
};
|
||||
|
||||
export const genUuid = () => {
|
||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c: any) =>
|
||||
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
|
||||
);
|
||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c: any) =>
|
||||
(
|
||||
c ^
|
||||
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
|
||||
).toString(16),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import {getContext} from 'svelte';
|
||||
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
|
||||
import type {TinyflowOptions} from '#types';
|
||||
import { getContext } from 'svelte';
|
||||
import { useNodesData, useSvelteFlow } from '@xyflow/svelte';
|
||||
import type { TinyflowOptions } from '#types';
|
||||
|
||||
export const getCurrentNodeId = () => {
|
||||
return getContext<string>('svelteflow__node_id');
|
||||
return getContext<string>('svelteflow__node_id');
|
||||
};
|
||||
|
||||
export const getOptions = () => {
|
||||
return getContext<TinyflowOptions>('tinyflow_options');
|
||||
return getContext<TinyflowOptions>('tinyflow_options');
|
||||
};
|
||||
|
||||
export const useCurrentNodeData = () => {
|
||||
const currentNodeId = getCurrentNodeId();
|
||||
return useNodesData<any>(currentNodeId);
|
||||
const currentNodeId = getCurrentNodeId();
|
||||
return useNodesData<any>(currentNodeId);
|
||||
};
|
||||
|
||||
export const useUpdateNodeData = () => {
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
const currentNodeId = getCurrentNodeId();
|
||||
return {
|
||||
updateNodeData: (data: any) => {
|
||||
updateNodeData(currentNodeId, data);
|
||||
}
|
||||
};
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
const currentNodeId = getCurrentNodeId();
|
||||
return {
|
||||
updateNodeData: (data: any) => {
|
||||
updateNodeData(currentNodeId, data);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,83 +1,109 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
createBusinessCompletions,
|
||||
normalizeCodeEngine,
|
||||
resolveBusinessCompletionContext,
|
||||
shouldAutoAppendCallParens,
|
||||
shouldSkipBusinessCompletion
|
||||
createBusinessCompletions,
|
||||
normalizeCodeEngine,
|
||||
resolveBusinessCompletionContext,
|
||||
shouldAutoAppendCallParens,
|
||||
shouldSkipBusinessCompletion,
|
||||
} from './codeCompletion';
|
||||
|
||||
describe('codeCompletion utils', () => {
|
||||
it('should normalize engine aliases', () => {
|
||||
expect(normalizeCodeEngine('py')).toBe('python');
|
||||
expect(normalizeCodeEngine('python')).toBe('python');
|
||||
expect(normalizeCodeEngine('js')).toBe('javascript');
|
||||
expect(normalizeCodeEngine('javascript')).toBe('javascript');
|
||||
expect(normalizeCodeEngine('')).toBe('javascript');
|
||||
it('should normalize engine aliases', () => {
|
||||
expect(normalizeCodeEngine('py')).toBe('python');
|
||||
expect(normalizeCodeEngine('python')).toBe('python');
|
||||
expect(normalizeCodeEngine('js')).toBe('javascript');
|
||||
expect(normalizeCodeEngine('javascript')).toBe('javascript');
|
||||
expect(normalizeCodeEngine('')).toBe('javascript');
|
||||
});
|
||||
|
||||
it('should create business completion list with stable priority and dedupe', () => {
|
||||
const completions = createBusinessCompletions('python', [
|
||||
{ name: 'input.text', resolved: true },
|
||||
{ name: 'input.text', resolved: false },
|
||||
{ name: 'question', resolved: false },
|
||||
]);
|
||||
|
||||
expect(completions[0]).toMatchObject({
|
||||
label: '_result',
|
||||
type: 'variable',
|
||||
});
|
||||
|
||||
it('should create business completion list with stable priority and dedupe', () => {
|
||||
const completions = createBusinessCompletions('python', [
|
||||
{ name: 'input.text', resolved: true },
|
||||
{ name: 'input.text', resolved: false },
|
||||
{ name: 'question', resolved: false }
|
||||
]);
|
||||
|
||||
expect(completions[0]).toMatchObject({
|
||||
label: '_result',
|
||||
type: 'variable'
|
||||
});
|
||||
|
||||
expect(completions.filter((item) => item.label === 'input.text').length).toBe(1);
|
||||
expect(completions.find((item) => item.label === 'input.text')).toMatchObject({
|
||||
apply: '{{input.text}}',
|
||||
detail: '参数模板'
|
||||
});
|
||||
expect(completions.find((item) => item.label === 'question')).toMatchObject({
|
||||
apply: '{{question}}',
|
||||
detail: '参数模板(未映射)'
|
||||
});
|
||||
|
||||
expect(completions.some((item) => item.type === 'snippet' && item.label === 'if-else')).toBe(true);
|
||||
expect(
|
||||
completions.filter((item) => item.label === 'input.text').length,
|
||||
).toBe(1);
|
||||
expect(
|
||||
completions.find((item) => item.label === 'input.text'),
|
||||
).toMatchObject({
|
||||
apply: '{{input.text}}',
|
||||
detail: '参数模板',
|
||||
});
|
||||
expect(completions.find((item) => item.label === 'question')).toMatchObject(
|
||||
{
|
||||
apply: '{{question}}',
|
||||
detail: '参数模板(未映射)',
|
||||
},
|
||||
);
|
||||
|
||||
it('should detect blocked nodes for comment/string/property contexts', () => {
|
||||
expect(shouldSkipBusinessCompletion('Comment')).toBe(true);
|
||||
expect(shouldSkipBusinessCompletion('LineComment')).toBe(true);
|
||||
expect(shouldSkipBusinessCompletion('String')).toBe(true);
|
||||
expect(shouldSkipBusinessCompletion('TemplateString')).toBe(true);
|
||||
expect(shouldSkipBusinessCompletion('PropertyName')).toBe(true);
|
||||
expect(shouldSkipBusinessCompletion('VariableName')).toBe(false);
|
||||
});
|
||||
expect(
|
||||
completions.some(
|
||||
(item) => item.type === 'snippet' && item.label === 'if-else',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should resolve template context for parameter token completion', () => {
|
||||
const source = "value = {{input.use";
|
||||
const pos = source.length;
|
||||
const context = resolveBusinessCompletionContext(source, pos);
|
||||
expect(context).toBeTruthy();
|
||||
expect(context?.from).toBe(8);
|
||||
expect(context?.validFor).toBeInstanceOf(RegExp);
|
||||
expect(context?.validFor.source).toBe('^[\\w.]*$');
|
||||
});
|
||||
it('should detect blocked nodes for comment/string/property contexts', () => {
|
||||
expect(shouldSkipBusinessCompletion('Comment')).toBe(true);
|
||||
expect(shouldSkipBusinessCompletion('LineComment')).toBe(true);
|
||||
expect(shouldSkipBusinessCompletion('String')).toBe(true);
|
||||
expect(shouldSkipBusinessCompletion('TemplateString')).toBe(true);
|
||||
expect(shouldSkipBusinessCompletion('PropertyName')).toBe(true);
|
||||
expect(shouldSkipBusinessCompletion('VariableName')).toBe(false);
|
||||
});
|
||||
|
||||
it('should resolve word context and skip property access context', () => {
|
||||
const source = '_result.value';
|
||||
expect(resolveBusinessCompletionContext(source, source.length)).toBeNull();
|
||||
it('should resolve template context for parameter token completion', () => {
|
||||
const source = 'value = {{input.use';
|
||||
const pos = source.length;
|
||||
const context = resolveBusinessCompletionContext(source, pos);
|
||||
expect(context).toBeTruthy();
|
||||
expect(context?.from).toBe(8);
|
||||
expect(context?.validFor).toBeInstanceOf(RegExp);
|
||||
expect(context?.validFor.source).toBe('^[\\w.]*$');
|
||||
});
|
||||
|
||||
const wordSource = 'ret';
|
||||
const context = resolveBusinessCompletionContext(wordSource, wordSource.length);
|
||||
expect(context).toBeTruthy();
|
||||
expect(context?.from).toBe(0);
|
||||
expect(context?.validFor).toBeInstanceOf(RegExp);
|
||||
expect(context?.validFor.source).toBe('^[\\w$]*$');
|
||||
});
|
||||
it('should resolve word context and skip property access context', () => {
|
||||
const source = '_result.value';
|
||||
expect(resolveBusinessCompletionContext(source, source.length)).toBeNull();
|
||||
|
||||
it('should auto append call parens for function/method completions only', () => {
|
||||
expect(shouldAutoAppendCallParens({ label: 'print', type: 'function' }, '')).toBe(true);
|
||||
expect(shouldAutoAppendCallParens({ label: 'run', type: 'method' }, '')).toBe(true);
|
||||
expect(shouldAutoAppendCallParens({ label: 'count', type: 'variable' }, '')).toBe(false);
|
||||
expect(shouldAutoAppendCallParens({ label: 'print', type: 'function' }, '(')).toBe(false);
|
||||
expect(shouldAutoAppendCallParens({ label: 'call', type: 'function', apply: 'call()' }, '')).toBe(false);
|
||||
});
|
||||
const wordSource = 'ret';
|
||||
const context = resolveBusinessCompletionContext(
|
||||
wordSource,
|
||||
wordSource.length,
|
||||
);
|
||||
expect(context).toBeTruthy();
|
||||
expect(context?.from).toBe(0);
|
||||
expect(context?.validFor).toBeInstanceOf(RegExp);
|
||||
expect(context?.validFor.source).toBe('^[\\w$]*$');
|
||||
});
|
||||
|
||||
it('should auto append call parens for function/method completions only', () => {
|
||||
expect(
|
||||
shouldAutoAppendCallParens({ label: 'print', type: 'function' }, ''),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldAutoAppendCallParens({ label: 'run', type: 'method' }, ''),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldAutoAppendCallParens({ label: 'count', type: 'variable' }, ''),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldAutoAppendCallParens({ label: 'print', type: 'function' }, '('),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldAutoAppendCallParens(
|
||||
{ label: 'call', type: 'function', apply: 'call()' },
|
||||
'',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,217 +1,245 @@
|
||||
import type { Completion, CompletionResult, CompletionSource } from '@codemirror/autocomplete';
|
||||
import type {
|
||||
Completion,
|
||||
CompletionResult,
|
||||
CompletionSource,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import type { ParameterCandidate } from './paramToken';
|
||||
|
||||
export type CodeEngine = 'javascript' | 'python';
|
||||
|
||||
export interface BusinessCompletionConfig {
|
||||
engine: CodeEngine;
|
||||
paramCandidates: ParameterCandidate[];
|
||||
engine: CodeEngine;
|
||||
paramCandidates: ParameterCandidate[];
|
||||
}
|
||||
|
||||
interface CompletionContextHint {
|
||||
from: number;
|
||||
validFor: RegExp;
|
||||
from: number;
|
||||
validFor: RegExp;
|
||||
}
|
||||
|
||||
const BLOCKED_NODE_NAMES = new Set([
|
||||
'String',
|
||||
'TemplateString',
|
||||
'FormatString',
|
||||
'Comment',
|
||||
'LineComment',
|
||||
'BlockComment',
|
||||
'PropertyName',
|
||||
'PrivatePropertyName'
|
||||
'String',
|
||||
'TemplateString',
|
||||
'FormatString',
|
||||
'Comment',
|
||||
'LineComment',
|
||||
'BlockComment',
|
||||
'PropertyName',
|
||||
'PrivatePropertyName',
|
||||
]);
|
||||
|
||||
const JS_SNIPPETS: Array<{ label: string; insert: string; detail: string }> = [
|
||||
{
|
||||
label: 'if-else',
|
||||
detail: '条件分支',
|
||||
insert: "if (condition) {\n _result.value = value;\n} else {\n _result.value = null;\n}"
|
||||
},
|
||||
{
|
||||
label: 'for-of',
|
||||
detail: '遍历数组',
|
||||
insert: "for (const item of items) {\n // TODO\n}\n_result.done = true;"
|
||||
},
|
||||
{
|
||||
label: 'result-object',
|
||||
detail: '返回对象',
|
||||
insert: "_result.message = 'ok';\n_result.data = data;"
|
||||
}
|
||||
{
|
||||
label: 'if-else',
|
||||
detail: '条件分支',
|
||||
insert:
|
||||
'if (condition) {\n _result.value = value;\n} else {\n _result.value = null;\n}',
|
||||
},
|
||||
{
|
||||
label: 'for-of',
|
||||
detail: '遍历数组',
|
||||
insert: 'for (const item of items) {\n // TODO\n}\n_result.done = true;',
|
||||
},
|
||||
{
|
||||
label: 'result-object',
|
||||
detail: '返回对象',
|
||||
insert: "_result.message = 'ok';\n_result.data = data;",
|
||||
},
|
||||
];
|
||||
|
||||
const PYTHON_SNIPPETS: Array<{ label: string; insert: string; detail: string }> = [
|
||||
{
|
||||
label: 'if-else',
|
||||
detail: '条件分支',
|
||||
insert: "if condition:\n _result['value'] = value\nelse:\n _result['value'] = None"
|
||||
},
|
||||
{
|
||||
label: 'for-loop',
|
||||
detail: '遍历数组',
|
||||
insert: "for item in items:\n # TODO\n pass\n_result['done'] = True"
|
||||
},
|
||||
{
|
||||
label: 'result-object',
|
||||
detail: '返回对象',
|
||||
insert: "_result['message'] = 'ok'\n_result['data'] = data"
|
||||
}
|
||||
const PYTHON_SNIPPETS: Array<{
|
||||
label: string;
|
||||
insert: string;
|
||||
detail: string;
|
||||
}> = [
|
||||
{
|
||||
label: 'if-else',
|
||||
detail: '条件分支',
|
||||
insert:
|
||||
"if condition:\n _result['value'] = value\nelse:\n _result['value'] = None",
|
||||
},
|
||||
{
|
||||
label: 'for-loop',
|
||||
detail: '遍历数组',
|
||||
insert: "for item in items:\n # TODO\n pass\n_result['done'] = True",
|
||||
},
|
||||
{
|
||||
label: 'result-object',
|
||||
detail: '返回对象',
|
||||
insert: "_result['message'] = 'ok'\n_result['data'] = data",
|
||||
},
|
||||
];
|
||||
|
||||
export function normalizeCodeEngine(rawEngine?: string): CodeEngine {
|
||||
const normalized = (rawEngine || 'js').trim().toLowerCase();
|
||||
if (normalized === 'python' || normalized === 'py') {
|
||||
return 'python';
|
||||
}
|
||||
return 'javascript';
|
||||
const normalized = (rawEngine || 'js').trim().toLowerCase();
|
||||
if (normalized === 'python' || normalized === 'py') {
|
||||
return 'python';
|
||||
}
|
||||
return 'javascript';
|
||||
}
|
||||
|
||||
export function shouldSkipBusinessCompletion(nodeName: string): boolean {
|
||||
if (!nodeName) {
|
||||
return false;
|
||||
}
|
||||
if (BLOCKED_NODE_NAMES.has(nodeName)) {
|
||||
return true;
|
||||
}
|
||||
if (nodeName.endsWith('Comment')) {
|
||||
return true;
|
||||
}
|
||||
return nodeName.includes('String');
|
||||
if (!nodeName) {
|
||||
return false;
|
||||
}
|
||||
if (BLOCKED_NODE_NAMES.has(nodeName)) {
|
||||
return true;
|
||||
}
|
||||
if (nodeName.endsWith('Comment')) {
|
||||
return true;
|
||||
}
|
||||
return nodeName.includes('String');
|
||||
}
|
||||
|
||||
export function createBusinessCompletions(
|
||||
targetEngine: CodeEngine,
|
||||
candidates: ParameterCandidate[]
|
||||
targetEngine: CodeEngine,
|
||||
candidates: ParameterCandidate[],
|
||||
): Completion[] {
|
||||
const resultCompletion: Completion = {
|
||||
label: '_result',
|
||||
type: 'variable',
|
||||
detail: '代码节点输出对象',
|
||||
boost: 900
|
||||
};
|
||||
const resultCompletion: Completion = {
|
||||
label: '_result',
|
||||
type: 'variable',
|
||||
detail: '代码节点输出对象',
|
||||
boost: 900,
|
||||
};
|
||||
|
||||
const parameterCompletions: Completion[] = candidates.map((candidate) => ({
|
||||
label: candidate.name,
|
||||
type: 'variable',
|
||||
detail: candidate.resolved ? '参数模板' : '参数模板(未映射)',
|
||||
apply: `{{${candidate.name}}}`,
|
||||
boost: 700
|
||||
}));
|
||||
const parameterCompletions: Completion[] = candidates.map((candidate) => ({
|
||||
label: candidate.name,
|
||||
type: 'variable',
|
||||
detail: candidate.resolved ? '参数模板' : '参数模板(未映射)',
|
||||
apply: `{{${candidate.name}}}`,
|
||||
boost: 700,
|
||||
}));
|
||||
|
||||
const snippetCompletions: Completion[] = (targetEngine === 'python' ? PYTHON_SNIPPETS : JS_SNIPPETS).map(
|
||||
(snippet) => ({
|
||||
label: snippet.label,
|
||||
type: 'snippet',
|
||||
detail: snippet.detail,
|
||||
apply: snippet.insert,
|
||||
boost: 500
|
||||
})
|
||||
);
|
||||
const snippetCompletions: Completion[] = (
|
||||
targetEngine === 'python' ? PYTHON_SNIPPETS : JS_SNIPPETS
|
||||
).map((snippet) => ({
|
||||
label: snippet.label,
|
||||
type: 'snippet',
|
||||
detail: snippet.detail,
|
||||
apply: snippet.insert,
|
||||
boost: 500,
|
||||
}));
|
||||
|
||||
return dedupeCompletions([resultCompletion, ...parameterCompletions, ...snippetCompletions]);
|
||||
return dedupeCompletions([
|
||||
resultCompletion,
|
||||
...parameterCompletions,
|
||||
...snippetCompletions,
|
||||
]);
|
||||
}
|
||||
|
||||
function dedupeCompletions(items: Completion[]): Completion[] {
|
||||
const seen = new Set<string>();
|
||||
const deduped: Completion[] = [];
|
||||
for (const item of items) {
|
||||
const key = `${item.label}::${item.type || ''}`;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
deduped.push(item);
|
||||
const seen = new Set<string>();
|
||||
const deduped: Completion[] = [];
|
||||
for (const item of items) {
|
||||
const key = `${item.label}::${item.type || ''}`;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
return deduped;
|
||||
seen.add(key);
|
||||
deduped.push(item);
|
||||
}
|
||||
return deduped;
|
||||
}
|
||||
|
||||
function resolveTemplateContext(source: string, pos: number): CompletionContextHint | null {
|
||||
const start = Math.max(0, pos - 300);
|
||||
const prefix = source.slice(start, pos);
|
||||
const match = /\{\{\s*([\w.]*)$/.exec(prefix);
|
||||
if (!match) {
|
||||
return null;
|
||||
function resolveTemplateContext(
|
||||
source: string,
|
||||
pos: number,
|
||||
): CompletionContextHint | null {
|
||||
const start = Math.max(0, pos - 300);
|
||||
const prefix = source.slice(start, pos);
|
||||
const match = /\{\{\s*([\w.]*)$/.exec(prefix);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
from: pos - match[0].length,
|
||||
validFor: /^[\w.]*$/,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveWordContext(
|
||||
source: string,
|
||||
pos: number,
|
||||
): CompletionContextHint | null {
|
||||
let from = pos;
|
||||
while (from > 0 && /[\w$]/.test(source[from - 1])) {
|
||||
from -= 1;
|
||||
}
|
||||
if (from === pos) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (from > 0 && source[from - 1] === '.') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
from,
|
||||
validFor: /^[\w$]*$/,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveBusinessCompletionContext(
|
||||
source: string,
|
||||
pos: number,
|
||||
): CompletionContextHint | null {
|
||||
return resolveTemplateContext(source, pos) || resolveWordContext(source, pos);
|
||||
}
|
||||
|
||||
export function createBusinessCompletionSource(
|
||||
config: BusinessCompletionConfig,
|
||||
): CompletionSource {
|
||||
return (context) => {
|
||||
const node = syntaxTree(context.state).resolveInner(context.pos, -1);
|
||||
if (shouldSkipBusinessCompletion(node.name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
from: pos - match[0].length,
|
||||
validFor: /^[\w.]*$/
|
||||
const docText = context.state.doc.toString();
|
||||
const resolved = resolveBusinessCompletionContext(docText, context.pos);
|
||||
if (!resolved && !context.explicit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: CompletionResult = {
|
||||
from: resolved?.from ?? context.pos,
|
||||
options: createBusinessCompletions(config.engine, config.paramCandidates),
|
||||
};
|
||||
|
||||
if (resolved?.validFor) {
|
||||
result.validFor = resolved.validFor;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function resolveWordContext(source: string, pos: number): CompletionContextHint | null {
|
||||
let from = pos;
|
||||
while (from > 0 && /[\w$]/.test(source[from - 1])) {
|
||||
from -= 1;
|
||||
}
|
||||
if (from === pos) {
|
||||
return null;
|
||||
}
|
||||
export function shouldAutoAppendCallParens(
|
||||
completion: Completion | null | undefined,
|
||||
nextChar: string,
|
||||
): boolean {
|
||||
if (!completion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (from > 0 && source[from - 1] === '.') {
|
||||
return null;
|
||||
}
|
||||
const completionType = (completion.type || '').toLowerCase();
|
||||
if (completionType !== 'function' && completionType !== 'method') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
from,
|
||||
validFor: /^[\w$]*$/
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveBusinessCompletionContext(source: string, pos: number): CompletionContextHint | null {
|
||||
return resolveTemplateContext(source, pos) || resolveWordContext(source, pos);
|
||||
}
|
||||
|
||||
export function createBusinessCompletionSource(config: BusinessCompletionConfig): CompletionSource {
|
||||
return (context) => {
|
||||
const node = syntaxTree(context.state).resolveInner(context.pos, -1);
|
||||
if (shouldSkipBusinessCompletion(node.name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const docText = context.state.doc.toString();
|
||||
const resolved = resolveBusinessCompletionContext(docText, context.pos);
|
||||
if (!resolved && !context.explicit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: CompletionResult = {
|
||||
from: resolved?.from ?? context.pos,
|
||||
options: createBusinessCompletions(config.engine, config.paramCandidates)
|
||||
};
|
||||
|
||||
if (resolved?.validFor) {
|
||||
result.validFor = resolved.validFor;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldAutoAppendCallParens(completion: Completion | null | undefined, nextChar: string): boolean {
|
||||
if (!completion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const completionType = (completion.type || '').toLowerCase();
|
||||
if (completionType !== 'function' && completionType !== 'method') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nextChar === '(') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof completion.apply === 'string' && completion.apply.includes('(')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof completion.apply === 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
if (nextChar === '(') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof completion.apply === 'string' && completion.apply.includes('(')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof completion.apply === 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7,212 +7,226 @@ import {
|
||||
type Placement,
|
||||
shift,
|
||||
type ShiftOptions,
|
||||
size
|
||||
size,
|
||||
} from '@floating-ui/dom';
|
||||
|
||||
export type FloatingOptions = {
|
||||
trigger: string | HTMLElement;
|
||||
triggerEvent?: string[];
|
||||
floatContent: string | HTMLElement;
|
||||
placement?: Placement;
|
||||
offsetOptions?: OffsetOptions;
|
||||
flipOptions?: FlipOptions;
|
||||
shiftOptions?: ShiftOptions;
|
||||
interactive?: boolean;
|
||||
showArrow?: boolean;
|
||||
onShow?: () => void;
|
||||
onHide?: () => void;
|
||||
syncWidth?: boolean;
|
||||
syncWidthMode?: 'min' | 'equal';
|
||||
trigger: string | HTMLElement;
|
||||
triggerEvent?: string[];
|
||||
floatContent: string | HTMLElement;
|
||||
placement?: Placement;
|
||||
offsetOptions?: OffsetOptions;
|
||||
flipOptions?: FlipOptions;
|
||||
shiftOptions?: ShiftOptions;
|
||||
interactive?: boolean;
|
||||
showArrow?: boolean;
|
||||
onShow?: () => void;
|
||||
onHide?: () => void;
|
||||
syncWidth?: boolean;
|
||||
syncWidthMode?: 'min' | 'equal';
|
||||
};
|
||||
|
||||
export type FloatingInstance = {
|
||||
destroy: () => void;
|
||||
hide: () => void;
|
||||
isVisible: () => boolean;
|
||||
destroy: () => void;
|
||||
hide: () => void;
|
||||
isVisible: () => boolean;
|
||||
};
|
||||
|
||||
export const createFloating = ({
|
||||
trigger,
|
||||
triggerEvent,
|
||||
floatContent,
|
||||
placement = 'bottom',
|
||||
offsetOptions,
|
||||
flipOptions,
|
||||
shiftOptions,
|
||||
interactive,
|
||||
showArrow,
|
||||
onShow,
|
||||
onHide,
|
||||
syncWidth = false,
|
||||
syncWidthMode = 'min'
|
||||
trigger,
|
||||
triggerEvent,
|
||||
floatContent,
|
||||
placement = 'bottom',
|
||||
offsetOptions,
|
||||
flipOptions,
|
||||
shiftOptions,
|
||||
interactive,
|
||||
showArrow,
|
||||
onShow,
|
||||
onHide,
|
||||
syncWidth = false,
|
||||
syncWidthMode = 'min',
|
||||
}: FloatingOptions): FloatingInstance => {
|
||||
if (typeof trigger === 'string') {
|
||||
const triggerEl = document.querySelector(trigger);
|
||||
if (!triggerEl) {
|
||||
throw new Error("element not found by document.querySelector('" + trigger + "')");
|
||||
} else {
|
||||
trigger = triggerEl as HTMLElement;
|
||||
}
|
||||
}
|
||||
|
||||
let floating: HTMLElement;
|
||||
if (typeof floatContent === 'string') {
|
||||
const floatContentEl = document.querySelector(floatContent);
|
||||
if (!floatContentEl) {
|
||||
throw new Error("element not found by document.querySelector('" + floatContent + "')");
|
||||
} else {
|
||||
floating = floatContentEl as HTMLElement;
|
||||
}
|
||||
if (typeof trigger === 'string') {
|
||||
const triggerEl = document.querySelector(trigger);
|
||||
if (!triggerEl) {
|
||||
throw new Error(
|
||||
"element not found by document.querySelector('" + trigger + "')",
|
||||
);
|
||||
} else {
|
||||
floating = floatContent as HTMLElement;
|
||||
trigger = triggerEl as HTMLElement;
|
||||
}
|
||||
}
|
||||
|
||||
let arrowElement: HTMLElement;
|
||||
if (showArrow) {
|
||||
arrowElement = document.createElement('div');
|
||||
arrowElement.style.position = 'absolute';
|
||||
arrowElement.style.backgroundColor = '#222';
|
||||
arrowElement.style.width = '8px';
|
||||
arrowElement.style.height = '8px';
|
||||
arrowElement.style.transform = 'rotate(45deg)';
|
||||
arrowElement.style.display = 'none';
|
||||
|
||||
floating.firstElementChild!.before(arrowElement);
|
||||
let floating: HTMLElement;
|
||||
if (typeof floatContent === 'string') {
|
||||
const floatContentEl = document.querySelector(floatContent);
|
||||
if (!floatContentEl) {
|
||||
throw new Error(
|
||||
"element not found by document.querySelector('" + floatContent + "')",
|
||||
);
|
||||
} else {
|
||||
floating = floatContentEl as HTMLElement;
|
||||
}
|
||||
} else {
|
||||
floating = floatContent as HTMLElement;
|
||||
}
|
||||
|
||||
function updatePosition() {
|
||||
computePosition(trigger as Element, floating, {
|
||||
placement: placement,
|
||||
middleware: [
|
||||
offset(offsetOptions), // 手动偏移配置
|
||||
// flip(flipOptions), // 注释掉自动翻转,强制向下弹出,避免遮挡顶部工具栏
|
||||
shift(shiftOptions), //自动偏移(使得浮动元素能够进入视野)
|
||||
...(showArrow ? [arrow({ element: arrowElement })] : []),
|
||||
...(syncWidth ? [size({
|
||||
apply({ rects, elements }) {
|
||||
if (syncWidthMode === 'equal') {
|
||||
Object.assign(elements.floating.style, {
|
||||
width: `${rects.reference.width}px`,
|
||||
minWidth: `${rects.reference.width}px`
|
||||
});
|
||||
} else {
|
||||
Object.assign(elements.floating.style, {
|
||||
width: '',
|
||||
minWidth: `${rects.reference.width}px`
|
||||
});
|
||||
}
|
||||
}
|
||||
})] : [])
|
||||
let arrowElement: HTMLElement;
|
||||
if (showArrow) {
|
||||
arrowElement = document.createElement('div');
|
||||
arrowElement.style.position = 'absolute';
|
||||
arrowElement.style.backgroundColor = '#222';
|
||||
arrowElement.style.width = '8px';
|
||||
arrowElement.style.height = '8px';
|
||||
arrowElement.style.transform = 'rotate(45deg)';
|
||||
arrowElement.style.display = 'none';
|
||||
|
||||
floating.firstElementChild!.before(arrowElement);
|
||||
}
|
||||
|
||||
function updatePosition() {
|
||||
computePosition(trigger as Element, floating, {
|
||||
placement: placement,
|
||||
middleware: [
|
||||
offset(offsetOptions), // 手动偏移配置
|
||||
// flip(flipOptions), // 注释掉自动翻转,强制向下弹出,避免遮挡顶部工具栏
|
||||
shift(shiftOptions), //自动偏移(使得浮动元素能够进入视野)
|
||||
...(showArrow ? [arrow({ element: arrowElement })] : []),
|
||||
...(syncWidth
|
||||
? [
|
||||
size({
|
||||
apply({ rects, elements }) {
|
||||
if (syncWidthMode === 'equal') {
|
||||
Object.assign(elements.floating.style, {
|
||||
width: `${rects.reference.width}px`,
|
||||
minWidth: `${rects.reference.width}px`,
|
||||
});
|
||||
} else {
|
||||
Object.assign(elements.floating.style, {
|
||||
width: '',
|
||||
minWidth: `${rects.reference.width}px`,
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
]
|
||||
}).then(({ x, y, placement, middlewareData }) => {
|
||||
Object.assign(floating.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
position: 'absolute'
|
||||
});
|
||||
: []),
|
||||
],
|
||||
}).then(({ x, y, placement, middlewareData }) => {
|
||||
Object.assign(floating.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
position: 'absolute',
|
||||
});
|
||||
|
||||
if (showArrow) {
|
||||
const { x: arrowX, y: arrowY } = middlewareData.arrow as { x: number; y: number };
|
||||
const staticSide = {
|
||||
top: 'bottom',
|
||||
right: 'left',
|
||||
bottom: 'top',
|
||||
left: 'right'
|
||||
}[placement.split('-')[0]] as string;
|
||||
if (showArrow) {
|
||||
const { x: arrowX, y: arrowY } = middlewareData.arrow as {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
const staticSide = {
|
||||
top: 'bottom',
|
||||
right: 'left',
|
||||
bottom: 'top',
|
||||
left: 'right',
|
||||
}[placement.split('-')[0]] as string;
|
||||
|
||||
Object.assign(arrowElement.style, {
|
||||
zIndex: -1,
|
||||
left: arrowX != null ? `${arrowX}px` : '',
|
||||
top: arrowY != null ? `${arrowY}px` : '',
|
||||
right: '',
|
||||
bottom: '',
|
||||
[staticSide]: '2px'
|
||||
});
|
||||
}
|
||||
Object.assign(arrowElement.style, {
|
||||
zIndex: -1,
|
||||
left: arrowX != null ? `${arrowX}px` : '',
|
||||
top: arrowY != null ? `${arrowY}px` : '',
|
||||
right: '',
|
||||
bottom: '',
|
||||
[staticSide]: '2px',
|
||||
});
|
||||
}
|
||||
|
||||
let visible = false;
|
||||
|
||||
function showTooltip() {
|
||||
floating.style.display = 'block';
|
||||
floating.style.visibility = 'visible';
|
||||
floating.style.position = 'absolute';
|
||||
|
||||
if (showArrow) {
|
||||
arrowElement.style.display = 'block';
|
||||
}
|
||||
|
||||
visible = true;
|
||||
updatePosition();
|
||||
onShow?.();
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
floating.style.display = 'none';
|
||||
if (showArrow) {
|
||||
arrowElement.style.display = 'none';
|
||||
}
|
||||
visible = false;
|
||||
onHide?.();
|
||||
}
|
||||
|
||||
function onTrigger(event: any) {
|
||||
if (!visible) {
|
||||
showTooltip();
|
||||
} else {
|
||||
hideTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
function hideTooltipCompute(event: any) {
|
||||
if (floating.contains(event.target as Node) || (trigger as Node).contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
hideTooltip();
|
||||
}
|
||||
|
||||
if (!triggerEvent || triggerEvent.length == 0) {
|
||||
if (interactive) {
|
||||
triggerEvent = ['click'];
|
||||
} else {
|
||||
triggerEvent = ['mouseenter', 'focus'];
|
||||
}
|
||||
}
|
||||
|
||||
triggerEvent.forEach((event) => {
|
||||
(trigger as HTMLElement).addEventListener(event, onTrigger);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (interactive) {
|
||||
document.addEventListener('click', hideTooltipCompute);
|
||||
} else {
|
||||
['mouseleave', 'blur'].forEach((event) => {
|
||||
trigger.addEventListener(event, hideTooltip);
|
||||
});
|
||||
let visible = false;
|
||||
|
||||
function showTooltip() {
|
||||
floating.style.display = 'block';
|
||||
floating.style.visibility = 'visible';
|
||||
floating.style.position = 'absolute';
|
||||
|
||||
if (showArrow) {
|
||||
arrowElement.style.display = 'block';
|
||||
}
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
triggerEvent.forEach((event) => {
|
||||
(trigger as HTMLElement).removeEventListener(event, onTrigger);
|
||||
});
|
||||
visible = true;
|
||||
updatePosition();
|
||||
onShow?.();
|
||||
}
|
||||
|
||||
if (interactive) {
|
||||
document.removeEventListener('click', hideTooltipCompute);
|
||||
} else {
|
||||
['mouseleave', 'blur'].forEach((event) => {
|
||||
trigger.removeEventListener(event, hideTooltip);
|
||||
});
|
||||
}
|
||||
},
|
||||
hide() {
|
||||
hideTooltip();
|
||||
},
|
||||
function hideTooltip() {
|
||||
floating.style.display = 'none';
|
||||
if (showArrow) {
|
||||
arrowElement.style.display = 'none';
|
||||
}
|
||||
visible = false;
|
||||
onHide?.();
|
||||
}
|
||||
|
||||
isVisible() {
|
||||
return visible;
|
||||
}
|
||||
};
|
||||
function onTrigger(event: any) {
|
||||
if (!visible) {
|
||||
showTooltip();
|
||||
} else {
|
||||
hideTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
function hideTooltipCompute(event: any) {
|
||||
if (
|
||||
floating.contains(event.target as Node) ||
|
||||
(trigger as Node).contains(event.target as Node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
hideTooltip();
|
||||
}
|
||||
|
||||
if (!triggerEvent || triggerEvent.length == 0) {
|
||||
if (interactive) {
|
||||
triggerEvent = ['click'];
|
||||
} else {
|
||||
triggerEvent = ['mouseenter', 'focus'];
|
||||
}
|
||||
}
|
||||
|
||||
triggerEvent.forEach((event) => {
|
||||
(trigger as HTMLElement).addEventListener(event, onTrigger);
|
||||
});
|
||||
|
||||
if (interactive) {
|
||||
document.addEventListener('click', hideTooltipCompute);
|
||||
} else {
|
||||
['mouseleave', 'blur'].forEach((event) => {
|
||||
trigger.addEventListener(event, hideTooltip);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
triggerEvent.forEach((event) => {
|
||||
(trigger as HTMLElement).removeEventListener(event, onTrigger);
|
||||
});
|
||||
|
||||
if (interactive) {
|
||||
document.removeEventListener('click', hideTooltipCompute);
|
||||
} else {
|
||||
['mouseleave', 'blur'].forEach((event) => {
|
||||
trigger.removeEventListener(event, hideTooltip);
|
||||
});
|
||||
}
|
||||
},
|
||||
hide() {
|
||||
hideTooltip();
|
||||
},
|
||||
|
||||
isVisible() {
|
||||
return visible;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const deepClone = <T>(obj: T): T => {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
};
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
export const deepEqual = <T>(obj1: T, obj2: T) => {
|
||||
if (obj1 === obj2) return true;
|
||||
if (obj1 === obj2) return true;
|
||||
|
||||
// 处理 null 和 object 类型
|
||||
if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
|
||||
return false;
|
||||
// 处理 null 和 object 类型
|
||||
if (
|
||||
typeof obj1 !== 'object' ||
|
||||
obj1 === null ||
|
||||
typeof obj2 !== 'object' ||
|
||||
obj2 === null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 判断是否都是数组
|
||||
const isArray1 = Array.isArray(obj1);
|
||||
const isArray2 = Array.isArray(obj2);
|
||||
|
||||
if (isArray1 !== isArray2) return false; // 一个是数组另一个不是,不相等
|
||||
|
||||
// 数组的情况
|
||||
if (isArray1 && isArray2) {
|
||||
if (obj1.length !== obj2.length) return false;
|
||||
for (let i = 0; i < obj1.length; i++) {
|
||||
if (!deepEqual(obj1[i], obj2[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// 普通对象的情况
|
||||
else {
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
|
||||
for (const key of keys1) {
|
||||
if (!(key in obj2)) return false;
|
||||
if (!deepEqual((obj1 as any)[key], (obj2 as any)[key])) return false;
|
||||
}
|
||||
|
||||
// 判断是否都是数组
|
||||
const isArray1 = Array.isArray(obj1);
|
||||
const isArray2 = Array.isArray(obj2);
|
||||
|
||||
if (isArray1 !== isArray2) return false; // 一个是数组另一个不是,不相等
|
||||
|
||||
// 数组的情况
|
||||
if (isArray1 && isArray2) {
|
||||
if (obj1.length !== obj2.length) return false;
|
||||
for (let i = 0; i < obj1.length; i++) {
|
||||
if (!deepEqual(obj1[i], obj2[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// 普通对象的情况
|
||||
else {
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
|
||||
for (const key of keys1) {
|
||||
if (!(key in obj2)) return false;
|
||||
if (!deepEqual((obj1 as any)[key], (obj2 as any)[key])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
* 适用于快捷键、全局事件监听等需要避免干扰用户输入的场景。
|
||||
*/
|
||||
export const isInEditableElement = () => {
|
||||
const el = document.activeElement;
|
||||
if (!el || !(el instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
const el = document.activeElement;
|
||||
if (!el || !(el instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
el instanceof HTMLInputElement ||
|
||||
el instanceof HTMLTextAreaElement ||
|
||||
el.isContentEditable
|
||||
);
|
||||
return (
|
||||
el instanceof HTMLInputElement ||
|
||||
el instanceof HTMLTextAreaElement ||
|
||||
el.isContentEditable
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,136 +1,137 @@
|
||||
import type {Node} from '@xyflow/svelte';
|
||||
import type { Node } from '@xyflow/svelte';
|
||||
|
||||
import type {TinyflowOptions} from '#types';
|
||||
import type { TinyflowOptions } from '#types';
|
||||
|
||||
export type NodePaletteItem = {
|
||||
icon?: string;
|
||||
title: string;
|
||||
type: string;
|
||||
sortNo?: number;
|
||||
description?: string;
|
||||
category: string;
|
||||
extra?: Partial<Node['data']>;
|
||||
icon?: string;
|
||||
title: string;
|
||||
type: string;
|
||||
sortNo?: number;
|
||||
description?: string;
|
||||
category: string;
|
||||
extra?: Partial<Node['data']>;
|
||||
};
|
||||
|
||||
const BUILT_IN_NODES: NodePaletteItem[] = [
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z"></path></svg>',
|
||||
title: '开始节点',
|
||||
type: 'startNode',
|
||||
sortNo: 100,
|
||||
description: '开始定义输入参数',
|
||||
category: '输入输出'
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path></svg>',
|
||||
title: '循环',
|
||||
type: 'loopNode',
|
||||
sortNo: 200,
|
||||
description: '用于循环执行任务',
|
||||
category: '逻辑'
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4H10V10H4V4ZM14 4H20V10H14V4ZM4 14H10V20H4V14ZM14 14H20V20H14V14ZM10 7H14V9H10V7ZM7 10H9V14H7V10ZM15 10H17V14H15V10Z"></path></svg>',
|
||||
title: '条件判断',
|
||||
type: 'conditionNode',
|
||||
sortNo: 250,
|
||||
description: '根据参数值分流到不同分支',
|
||||
category: '逻辑'
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.7134 7.12811L20.4668 7.69379C20.2864 8.10792 19.7136 8.10792 19.5331 7.69379L19.2866 7.12811C18.8471 6.11947 18.0555 5.31641 17.0677 4.87708L16.308 4.53922C15.8973 4.35653 15.8973 3.75881 16.308 3.57612L17.0252 3.25714C18.0384 2.80651 18.8442 1.97373 19.2761 0.930828L19.5293 0.319534C19.7058 -0.106511 20.2942 -0.106511 20.4706 0.319534L20.7238 0.930828C21.1558 1.97373 21.9616 2.80651 22.9748 3.25714L23.6919 3.57612C24.1027 3.75881 24.1027 4.35653 23.6919 4.53922L22.9323 4.87708C21.9445 5.31641 21.1529 6.11947 20.7134 7.12811ZM9 2C13.0675 2 16.426 5.03562 16.9337 8.96494L19.1842 12.5037C19.3324 12.7367 19.3025 13.0847 18.9593 13.2317L17 14.071V17C17 18.1046 16.1046 19 15 19H13.001L13 22H4L4.00025 18.3061C4.00033 17.1252 3.56351 16.0087 2.7555 15.0011C1.65707 13.6313 1 11.8924 1 10C1 5.58172 4.58172 2 9 2ZM9 4C5.68629 4 3 6.68629 3 10C3 11.3849 3.46818 12.6929 4.31578 13.7499C5.40965 15.114 6.00036 16.6672 6.00025 18.3063L6.00013 20H11.0007L11.0017 17H15V12.7519L16.5497 12.0881L15.0072 9.66262L14.9501 9.22118C14.5665 6.25141 12.0243 4 9 4ZM19.4893 16.9929L21.1535 18.1024C22.32 16.3562 23 14.2576 23 12.0001C23 11.317 22.9378 10.6486 22.8186 10L20.8756 10.5C20.9574 10.9878 21 11.489 21 12.0001C21 13.8471 20.4436 15.5642 19.4893 16.9929Z"></path></svg>',
|
||||
title: '大模型',
|
||||
type: 'llmNode',
|
||||
sortNo: 300,
|
||||
description: '使用大模型处理问题',
|
||||
category: '模型与流程'
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 5C13.567 5 12 6.567 12 8.5C12 10.433 13.567 12 15.5 12C17.433 12 19 10.433 19 8.5C19 6.567 17.433 5 15.5 5ZM10 8.5C10 5.46243 12.4624 3 15.5 3C18.5376 3 21 5.46243 21 8.5C21 9.6575 20.6424 10.7315 20.0317 11.6175L22.7071 14.2929L21.2929 15.7071L18.6175 13.0317C17.7315 13.6424 16.6575 14 15.5 14C12.4624 14 10 11.5376 10 8.5ZM3 4H8V6H3V4ZM3 11H8V13H3V11ZM21 18V20H3V18H21Z"></path></svg>',
|
||||
title: '知识库',
|
||||
type: 'knowledgeNode',
|
||||
sortNo: 400,
|
||||
description: '通过知识库获取内容',
|
||||
category: '数据与集成'
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path></svg>',
|
||||
title: '搜索引擎',
|
||||
type: 'searchEngineNode',
|
||||
sortNo: 500,
|
||||
description: '通过搜索引擎搜索内容',
|
||||
category: '模型与流程'
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.23509 6.45329C4.85101 7.89148 4 9.84636 4 12C4 16.4183 7.58172 20 12 20C13.0808 20 14.1116 19.7857 15.0521 19.3972C15.1671 18.6467 14.9148 17.9266 14.8116 17.6746C14.582 17.115 13.8241 16.1582 12.5589 14.8308C12.2212 14.4758 12.2429 14.2035 12.3636 13.3943L12.3775 13.3029C12.4595 12.7486 12.5971 12.4209 14.4622 12.1248C15.4097 11.9746 15.6589 12.3533 16.0043 12.8777C16.0425 12.9358 16.0807 12.9928 16.1198 13.0499C16.4479 13.5297 16.691 13.6394 17.0582 13.8064C17.2227 13.881 17.428 13.9751 17.7031 14.1314C18.3551 14.504 18.3551 14.9247 18.3551 15.8472V15.9518C18.3551 16.3434 18.3168 16.6872 18.2566 16.9859C19.3478 15.6185 20 13.8854 20 12C20 8.70089 18.003 5.8682 15.1519 4.64482C14.5987 5.01813 13.8398 5.54726 13.575 5.91C13.4396 6.09538 13.2482 7.04166 12.6257 7.11976C12.4626 7.14023 12.2438 7.12589 12.012 7.11097C11.3905 7.07058 10.5402 7.01606 10.268 7.75495C10.0952 8.2232 10.0648 9.49445 10.6239 10.1543C10.7134 10.2597 10.7307 10.4547 10.6699 10.6735C10.59 10.9608 10.4286 11.1356 10.3783 11.1717C10.2819 11.1163 10.0896 10.8931 9.95938 10.7412C9.64554 10.3765 9.25405 9.92233 8.74797 9.78176C8.56395 9.73083 8.36166 9.68867 8.16548 9.64736C7.6164 9.53227 6.99443 9.40134 6.84992 9.09302C6.74442 8.8672 6.74488 8.55621 6.74529 8.22764C6.74529 7.8112 6.74529 7.34029 6.54129 6.88256C6.46246 6.70541 6.35689 6.56446 6.23509 6.45329ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22Z"></path></svg>',
|
||||
title: 'Http 请求',
|
||||
type: 'httpNode',
|
||||
sortNo: 600,
|
||||
description: '通过 HTTP 请求获取数据',
|
||||
category: '数据与集成'
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M23 12L15.9289 19.0711L14.5147 17.6569L20.1716 12L14.5147 6.34317L15.9289 4.92896L23 12ZM3.82843 12L9.48528 17.6569L8.07107 19.0711L1 12L8.07107 4.92896L9.48528 6.34317L3.82843 12Z"></path></svg>',
|
||||
title: '动态代码',
|
||||
type: 'codeNode',
|
||||
sortNo: 700,
|
||||
description: '动态执行代码',
|
||||
category: '逻辑'
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4ZM4 5V19H20V5H4ZM7 8H17V11H15V10H13V14H14.5V16H9.5V14H11V10H9V11H7V8Z"></path></svg>',
|
||||
title: '内容模板',
|
||||
type: 'templateNode',
|
||||
sortNo: 800,
|
||||
description: '通过模板引擎生成内容',
|
||||
category: '逻辑'
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.3873 13.4975L17.9403 20.5117L13.2418 22.2218L10.6889 15.2076L6.79004 17.6529L8.4086 1.63318L19.9457 12.8646L15.3873 13.4975ZM15.3768 19.3163L12.6618 11.8568L15.6212 11.4459L9.98201 5.9561L9.19088 13.7863L11.7221 12.1988L14.4371 19.6583L15.3768 19.3163Z"></path></svg>',
|
||||
title: '用户确认',
|
||||
type: 'confirmNode',
|
||||
sortNo: 900,
|
||||
description: '确认继续或选择内容',
|
||||
category: '输入输出'
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 5.1438V16.0002H18.3391L6 5.1438ZM4 2.932C4 2.07155 5.01456 1.61285 5.66056 2.18123L21.6501 16.2494C22.3423 16.8584 21.9116 18.0002 20.9896 18.0002H6V22H4V2.932Z"></path></svg>',
|
||||
title: '结束节点',
|
||||
type: 'endNode',
|
||||
sortNo: 1000,
|
||||
description: '结束定义输出参数',
|
||||
category: '输入输出'
|
||||
}
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z"></path></svg>',
|
||||
title: '开始节点',
|
||||
type: 'startNode',
|
||||
sortNo: 100,
|
||||
description: '开始定义输入参数',
|
||||
category: '输入输出',
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path></svg>',
|
||||
title: '循环',
|
||||
type: 'loopNode',
|
||||
sortNo: 200,
|
||||
description: '用于循环执行任务',
|
||||
category: '逻辑',
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4H10V10H4V4ZM14 4H20V10H14V4ZM4 14H10V20H4V14ZM14 14H20V20H14V14ZM10 7H14V9H10V7ZM7 10H9V14H7V10ZM15 10H17V14H15V10Z"></path></svg>',
|
||||
title: '条件判断',
|
||||
type: 'conditionNode',
|
||||
sortNo: 250,
|
||||
description: '根据参数值分流到不同分支',
|
||||
category: '逻辑',
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.7134 7.12811L20.4668 7.69379C20.2864 8.10792 19.7136 8.10792 19.5331 7.69379L19.2866 7.12811C18.8471 6.11947 18.0555 5.31641 17.0677 4.87708L16.308 4.53922C15.8973 4.35653 15.8973 3.75881 16.308 3.57612L17.0252 3.25714C18.0384 2.80651 18.8442 1.97373 19.2761 0.930828L19.5293 0.319534C19.7058 -0.106511 20.2942 -0.106511 20.4706 0.319534L20.7238 0.930828C21.1558 1.97373 21.9616 2.80651 22.9748 3.25714L23.6919 3.57612C24.1027 3.75881 24.1027 4.35653 23.6919 4.53922L22.9323 4.87708C21.9445 5.31641 21.1529 6.11947 20.7134 7.12811ZM9 2C13.0675 2 16.426 5.03562 16.9337 8.96494L19.1842 12.5037C19.3324 12.7367 19.3025 13.0847 18.9593 13.2317L17 14.071V17C17 18.1046 16.1046 19 15 19H13.001L13 22H4L4.00025 18.3061C4.00033 17.1252 3.56351 16.0087 2.7555 15.0011C1.65707 13.6313 1 11.8924 1 10C1 5.58172 4.58172 2 9 2ZM9 4C5.68629 4 3 6.68629 3 10C3 11.3849 3.46818 12.6929 4.31578 13.7499C5.40965 15.114 6.00036 16.6672 6.00025 18.3063L6.00013 20H11.0007L11.0017 17H15V12.7519L16.5497 12.0881L15.0072 9.66262L14.9501 9.22118C14.5665 6.25141 12.0243 4 9 4ZM19.4893 16.9929L21.1535 18.1024C22.32 16.3562 23 14.2576 23 12.0001C23 11.317 22.9378 10.6486 22.8186 10L20.8756 10.5C20.9574 10.9878 21 11.489 21 12.0001C21 13.8471 20.4436 15.5642 19.4893 16.9929Z"></path></svg>',
|
||||
title: '大模型',
|
||||
type: 'llmNode',
|
||||
sortNo: 300,
|
||||
description: '使用大模型处理问题',
|
||||
category: '模型与流程',
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 5C13.567 5 12 6.567 12 8.5C12 10.433 13.567 12 15.5 12C17.433 12 19 10.433 19 8.5C19 6.567 17.433 5 15.5 5ZM10 8.5C10 5.46243 12.4624 3 15.5 3C18.5376 3 21 5.46243 21 8.5C21 9.6575 20.6424 10.7315 20.0317 11.6175L22.7071 14.2929L21.2929 15.7071L18.6175 13.0317C17.7315 13.6424 16.6575 14 15.5 14C12.4624 14 10 11.5376 10 8.5ZM3 4H8V6H3V4ZM3 11H8V13H3V11ZM21 18V20H3V18H21Z"></path></svg>',
|
||||
title: '知识库',
|
||||
type: 'knowledgeNode',
|
||||
sortNo: 400,
|
||||
description: '通过知识库获取内容',
|
||||
category: '数据与集成',
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path></svg>',
|
||||
title: '搜索引擎',
|
||||
type: 'searchEngineNode',
|
||||
sortNo: 500,
|
||||
description: '通过搜索引擎搜索内容',
|
||||
category: '模型与流程',
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.23509 6.45329C4.85101 7.89148 4 9.84636 4 12C4 16.4183 7.58172 20 12 20C13.0808 20 14.1116 19.7857 15.0521 19.3972C15.1671 18.6467 14.9148 17.9266 14.8116 17.6746C14.582 17.115 13.8241 16.1582 12.5589 14.8308C12.2212 14.4758 12.2429 14.2035 12.3636 13.3943L12.3775 13.3029C12.4595 12.7486 12.5971 12.4209 14.4622 12.1248C15.4097 11.9746 15.6589 12.3533 16.0043 12.8777C16.0425 12.9358 16.0807 12.9928 16.1198 13.0499C16.4479 13.5297 16.691 13.6394 17.0582 13.8064C17.2227 13.881 17.428 13.9751 17.7031 14.1314C18.3551 14.504 18.3551 14.9247 18.3551 15.8472V15.9518C18.3551 16.3434 18.3168 16.6872 18.2566 16.9859C19.3478 15.6185 20 13.8854 20 12C20 8.70089 18.003 5.8682 15.1519 4.64482C14.5987 5.01813 13.8398 5.54726 13.575 5.91C13.4396 6.09538 13.2482 7.04166 12.6257 7.11976C12.4626 7.14023 12.2438 7.12589 12.012 7.11097C11.3905 7.07058 10.5402 7.01606 10.268 7.75495C10.0952 8.2232 10.0648 9.49445 10.6239 10.1543C10.7134 10.2597 10.7307 10.4547 10.6699 10.6735C10.59 10.9608 10.4286 11.1356 10.3783 11.1717C10.2819 11.1163 10.0896 10.8931 9.95938 10.7412C9.64554 10.3765 9.25405 9.92233 8.74797 9.78176C8.56395 9.73083 8.36166 9.68867 8.16548 9.64736C7.6164 9.53227 6.99443 9.40134 6.84992 9.09302C6.74442 8.8672 6.74488 8.55621 6.74529 8.22764C6.74529 7.8112 6.74529 7.34029 6.54129 6.88256C6.46246 6.70541 6.35689 6.56446 6.23509 6.45329ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22Z"></path></svg>',
|
||||
title: 'Http 请求',
|
||||
type: 'httpNode',
|
||||
sortNo: 600,
|
||||
description: '通过 HTTP 请求获取数据',
|
||||
category: '数据与集成',
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M23 12L15.9289 19.0711L14.5147 17.6569L20.1716 12L14.5147 6.34317L15.9289 4.92896L23 12ZM3.82843 12L9.48528 17.6569L8.07107 19.0711L1 12L8.07107 4.92896L9.48528 6.34317L3.82843 12Z"></path></svg>',
|
||||
title: '动态代码',
|
||||
type: 'codeNode',
|
||||
sortNo: 700,
|
||||
description: '动态执行代码',
|
||||
category: '逻辑',
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4ZM4 5V19H20V5H4ZM7 8H17V11H15V10H13V14H14.5V16H9.5V14H11V10H9V11H7V8Z"></path></svg>',
|
||||
title: '内容模板',
|
||||
type: 'templateNode',
|
||||
sortNo: 800,
|
||||
description: '通过模板引擎生成内容',
|
||||
category: '逻辑',
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.3873 13.4975L17.9403 20.5117L13.2418 22.2218L10.6889 15.2076L6.79004 17.6529L8.4086 1.63318L19.9457 12.8646L15.3873 13.4975ZM15.3768 19.3163L12.6618 11.8568L15.6212 11.4459L9.98201 5.9561L9.19088 13.7863L11.7221 12.1988L14.4371 19.6583L15.3768 19.3163Z"></path></svg>',
|
||||
title: '用户确认',
|
||||
type: 'confirmNode',
|
||||
sortNo: 900,
|
||||
description: '确认继续或选择内容',
|
||||
category: '输入输出',
|
||||
},
|
||||
{
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 5.1438V16.0002H18.3391L6 5.1438ZM4 2.932C4 2.07155 5.01456 1.61285 5.66056 2.18123L21.6501 16.2494C22.3423 16.8584 21.9116 18.0002 20.9896 18.0002H6V22H4V2.932Z"></path></svg>',
|
||||
title: '结束节点',
|
||||
type: 'endNode',
|
||||
sortNo: 1000,
|
||||
description: '结束定义输出参数',
|
||||
category: '输入输出',
|
||||
},
|
||||
];
|
||||
|
||||
export function getAvailableNodes(options?: TinyflowOptions) {
|
||||
const nodes: NodePaletteItem[] = [...BUILT_IN_NODES];
|
||||
const customNodes = options?.customNodes;
|
||||
const nodes: NodePaletteItem[] = [...BUILT_IN_NODES];
|
||||
const customNodes = options?.customNodes;
|
||||
|
||||
if (customNodes) {
|
||||
const keys = Object.keys(customNodes).sort((a, b) => {
|
||||
return (customNodes[a].sortNo || 0) - (customNodes[b].sortNo || 0);
|
||||
});
|
||||
if (customNodes) {
|
||||
const keys = Object.keys(customNodes).sort((a, b) => {
|
||||
return (customNodes[a].sortNo || 0) - (customNodes[b].sortNo || 0);
|
||||
});
|
||||
|
||||
for (let key of keys) {
|
||||
const item = customNodes[key];
|
||||
nodes.push({
|
||||
icon: item.icon,
|
||||
title: item.title,
|
||||
type: key,
|
||||
sortNo: item.sortNo,
|
||||
description: item.description,
|
||||
category: item.group === 'tools' ? '扩展工具' : '基础节点'
|
||||
});
|
||||
}
|
||||
for (let key of keys) {
|
||||
const item = customNodes[key];
|
||||
nodes.push({
|
||||
icon: item.icon,
|
||||
title: item.title,
|
||||
type: key,
|
||||
sortNo: item.sortNo,
|
||||
description: item.description,
|
||||
category: item.group === 'tools' ? '扩展工具' : '基础节点',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const hiddenNodes = typeof options?.hiddenNodes === 'function'
|
||||
? options?.hiddenNodes()
|
||||
: options?.hiddenNodes;
|
||||
const hiddenNodes =
|
||||
typeof options?.hiddenNodes === 'function'
|
||||
? options?.hiddenNodes()
|
||||
: options?.hiddenNodes;
|
||||
|
||||
const hiddenSet = new Set(Array.isArray(hiddenNodes) ? hiddenNodes : []);
|
||||
const filtered = nodes.filter((node) => !hiddenSet.has(node.type));
|
||||
filtered.sort((a, b) => (a.sortNo || 0) - (b.sortNo || 0));
|
||||
return filtered;
|
||||
const hiddenSet = new Set(Array.isArray(hiddenNodes) ? hiddenNodes : []);
|
||||
const filtered = nodes.filter((node) => !hiddenSet.has(node.type));
|
||||
filtered.sort((a, b) => (a.sortNo || 0) - (b.sortNo || 0));
|
||||
return filtered;
|
||||
}
|
||||
|
||||
@@ -1,175 +1,175 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
flattenParameterCandidates,
|
||||
findBackspaceTokenRange,
|
||||
findTokenRangeAtCursor,
|
||||
flattenParameterNames,
|
||||
insertTextAtCursor,
|
||||
parseTokenParts,
|
||||
splitTokenDisplay
|
||||
flattenParameterCandidates,
|
||||
findBackspaceTokenRange,
|
||||
findTokenRangeAtCursor,
|
||||
flattenParameterNames,
|
||||
insertTextAtCursor,
|
||||
parseTokenParts,
|
||||
splitTokenDisplay,
|
||||
} from './paramToken';
|
||||
|
||||
describe('paramToken utils', () => {
|
||||
it('should flatten parameter names with nested paths', () => {
|
||||
const result = flattenParameterNames([
|
||||
{
|
||||
name: 'input'
|
||||
},
|
||||
{
|
||||
name: 'documents',
|
||||
children: [
|
||||
{
|
||||
name: 'title'
|
||||
},
|
||||
{
|
||||
name: 'meta',
|
||||
children: [
|
||||
{
|
||||
name: 'author'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
it('should flatten parameter names with nested paths', () => {
|
||||
const result = flattenParameterNames([
|
||||
{
|
||||
name: 'input',
|
||||
},
|
||||
{
|
||||
name: 'documents',
|
||||
children: [
|
||||
{
|
||||
name: 'title',
|
||||
},
|
||||
{
|
||||
name: 'meta',
|
||||
children: [
|
||||
{
|
||||
name: 'author',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toEqual([
|
||||
'input',
|
||||
'documents',
|
||||
'documents.title',
|
||||
'documents.meta',
|
||||
'documents.meta.author'
|
||||
]);
|
||||
expect(result).toEqual([
|
||||
'input',
|
||||
'documents',
|
||||
'documents.title',
|
||||
'documents.meta',
|
||||
'documents.meta.author',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should keep unresolved candidates in parameter list', () => {
|
||||
const result = flattenParameterCandidates([
|
||||
{
|
||||
name: 'input',
|
||||
refType: 'ref',
|
||||
ref: '',
|
||||
},
|
||||
{
|
||||
name: 'docs',
|
||||
refType: 'ref',
|
||||
ref: 'documents',
|
||||
},
|
||||
{
|
||||
name: 'runtimeInput',
|
||||
refType: 'input',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'input',
|
||||
resolved: false,
|
||||
},
|
||||
{
|
||||
name: 'docs',
|
||||
resolved: true,
|
||||
},
|
||||
{
|
||||
name: 'runtimeInput',
|
||||
resolved: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should insert token text in the middle by cursor range', () => {
|
||||
const result = insertTextAtCursor('hello world', '{{input}}', 6, 11);
|
||||
expect(result).toEqual({
|
||||
value: 'hello {{input}}',
|
||||
cursor: 15,
|
||||
});
|
||||
});
|
||||
|
||||
it('should append token text when cursor info is missing', () => {
|
||||
const result = insertTextAtCursor('hello', '{{name}}');
|
||||
expect(result).toEqual({
|
||||
value: 'hello{{name}}',
|
||||
cursor: 13,
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse token parts and mark valid tokens', () => {
|
||||
const parts = parseTokenParts('你好 {{ user.name }} 与 {{unknown}}', [
|
||||
'user.name',
|
||||
'docs',
|
||||
]);
|
||||
|
||||
expect(parts).toEqual([
|
||||
{
|
||||
type: 'text',
|
||||
text: '你好 ',
|
||||
},
|
||||
{
|
||||
type: 'token',
|
||||
text: '{{ user.name }}',
|
||||
key: 'user.name',
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ' 与 ',
|
||||
},
|
||||
{
|
||||
type: 'token',
|
||||
text: '{{unknown}}',
|
||||
key: 'unknown',
|
||||
valid: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should keep plain text when token syntax is invalid', () => {
|
||||
const parts = parseTokenParts('abc {{}} def', ['a']);
|
||||
expect(parts).toEqual([
|
||||
{
|
||||
type: 'text',
|
||||
text: 'abc {{}} def',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should split token display and hide braces text', () => {
|
||||
const result = splitTokenDisplay('{{ user.name }}', 'user.name');
|
||||
expect(result).toEqual({
|
||||
hiddenPrefix: '{{ ',
|
||||
visibleText: 'user.name',
|
||||
hiddenSuffix: ' }}',
|
||||
});
|
||||
});
|
||||
|
||||
it('should find full token range for backspace delete', () => {
|
||||
const content = 'hello {{input}} world';
|
||||
const tokenEndCursor = 'hello {{input}}'.length;
|
||||
const range = findBackspaceTokenRange(content, tokenEndCursor);
|
||||
|
||||
expect(range).toEqual({
|
||||
start: 6,
|
||||
end: 15,
|
||||
text: '{{input}}',
|
||||
key: 'input',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support boundary match for arrow skip behavior', () => {
|
||||
const content = 'x{{docs}}y';
|
||||
const tokenStart = 1;
|
||||
const tokenEnd = 9;
|
||||
|
||||
const rightBoundary = findTokenRangeAtCursor(content, tokenStart, {
|
||||
includeStart: true,
|
||||
includeEnd: false,
|
||||
});
|
||||
const leftBoundary = findTokenRangeAtCursor(content, tokenEnd, {
|
||||
includeStart: false,
|
||||
includeEnd: true,
|
||||
});
|
||||
|
||||
it('should keep unresolved candidates in parameter list', () => {
|
||||
const result = flattenParameterCandidates([
|
||||
{
|
||||
name: 'input',
|
||||
refType: 'ref',
|
||||
ref: ''
|
||||
},
|
||||
{
|
||||
name: 'docs',
|
||||
refType: 'ref',
|
||||
ref: 'documents'
|
||||
},
|
||||
{
|
||||
name: 'runtimeInput',
|
||||
refType: 'input'
|
||||
}
|
||||
]);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'input',
|
||||
resolved: false
|
||||
},
|
||||
{
|
||||
name: 'docs',
|
||||
resolved: true
|
||||
},
|
||||
{
|
||||
name: 'runtimeInput',
|
||||
resolved: true
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should insert token text in the middle by cursor range', () => {
|
||||
const result = insertTextAtCursor('hello world', '{{input}}', 6, 11);
|
||||
expect(result).toEqual({
|
||||
value: 'hello {{input}}',
|
||||
cursor: 15
|
||||
});
|
||||
});
|
||||
|
||||
it('should append token text when cursor info is missing', () => {
|
||||
const result = insertTextAtCursor('hello', '{{name}}');
|
||||
expect(result).toEqual({
|
||||
value: 'hello{{name}}',
|
||||
cursor: 13
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse token parts and mark valid tokens', () => {
|
||||
const parts = parseTokenParts(
|
||||
'你好 {{ user.name }} 与 {{unknown}}',
|
||||
['user.name', 'docs']
|
||||
);
|
||||
|
||||
expect(parts).toEqual([
|
||||
{
|
||||
type: 'text',
|
||||
text: '你好 '
|
||||
},
|
||||
{
|
||||
type: 'token',
|
||||
text: '{{ user.name }}',
|
||||
key: 'user.name',
|
||||
valid: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ' 与 '
|
||||
},
|
||||
{
|
||||
type: 'token',
|
||||
text: '{{unknown}}',
|
||||
key: 'unknown',
|
||||
valid: false
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should keep plain text when token syntax is invalid', () => {
|
||||
const parts = parseTokenParts('abc {{}} def', ['a']);
|
||||
expect(parts).toEqual([
|
||||
{
|
||||
type: 'text',
|
||||
text: 'abc {{}} def'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should split token display and hide braces text', () => {
|
||||
const result = splitTokenDisplay('{{ user.name }}', 'user.name');
|
||||
expect(result).toEqual({
|
||||
hiddenPrefix: '{{ ',
|
||||
visibleText: 'user.name',
|
||||
hiddenSuffix: ' }}'
|
||||
});
|
||||
});
|
||||
|
||||
it('should find full token range for backspace delete', () => {
|
||||
const content = 'hello {{input}} world';
|
||||
const tokenEndCursor = 'hello {{input}}'.length;
|
||||
const range = findBackspaceTokenRange(content, tokenEndCursor);
|
||||
|
||||
expect(range).toEqual({
|
||||
start: 6,
|
||||
end: 15,
|
||||
text: '{{input}}',
|
||||
key: 'input'
|
||||
});
|
||||
});
|
||||
|
||||
it('should support boundary match for arrow skip behavior', () => {
|
||||
const content = 'x{{docs}}y';
|
||||
const tokenStart = 1;
|
||||
const tokenEnd = 9;
|
||||
|
||||
const rightBoundary = findTokenRangeAtCursor(content, tokenStart, {
|
||||
includeStart: true,
|
||||
includeEnd: false
|
||||
});
|
||||
const leftBoundary = findTokenRangeAtCursor(content, tokenEnd, {
|
||||
includeStart: false,
|
||||
includeEnd: true
|
||||
});
|
||||
|
||||
expect(rightBoundary?.key).toBe('docs');
|
||||
expect(leftBoundary?.key).toBe('docs');
|
||||
});
|
||||
expect(rightBoundary?.key).toBe('docs');
|
||||
expect(leftBoundary?.key).toBe('docs');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,272 +1,292 @@
|
||||
export interface ParameterLike {
|
||||
name?: string;
|
||||
ref?: string;
|
||||
refType?: string;
|
||||
children?: ParameterLike[];
|
||||
name?: string;
|
||||
ref?: string;
|
||||
refType?: string;
|
||||
children?: ParameterLike[];
|
||||
}
|
||||
|
||||
export interface TokenRange {
|
||||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
key: string;
|
||||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export type TokenPart =
|
||||
| {
|
||||
type: 'text';
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: 'token';
|
||||
text: string;
|
||||
key: string;
|
||||
valid: boolean;
|
||||
};
|
||||
| {
|
||||
type: 'text';
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: 'token';
|
||||
text: string;
|
||||
key: string;
|
||||
valid: boolean;
|
||||
};
|
||||
|
||||
export interface ParameterCandidate {
|
||||
name: string;
|
||||
resolved: boolean;
|
||||
name: string;
|
||||
resolved: boolean;
|
||||
}
|
||||
|
||||
const TOKEN_PATTERN = /\{\{\s*([^{}]+?)\s*}}/g;
|
||||
|
||||
export function normalizeTokenKey(tokenKey: string): string {
|
||||
return tokenKey.trim();
|
||||
return tokenKey.trim();
|
||||
}
|
||||
|
||||
export function flattenParameterNames(parameters?: ParameterLike[] | null): string[] {
|
||||
return flattenParameterCandidates(parameters).map((item) => item.name);
|
||||
export function flattenParameterNames(
|
||||
parameters?: ParameterLike[] | null,
|
||||
): string[] {
|
||||
return flattenParameterCandidates(parameters).map((item) => item.name);
|
||||
}
|
||||
|
||||
function isParameterResolved(parameter?: ParameterLike): boolean {
|
||||
if (!parameter) {
|
||||
return false;
|
||||
}
|
||||
if (!parameter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const refType = (parameter.refType || '').trim();
|
||||
if (refType === 'fixed' || refType === 'input') {
|
||||
return true;
|
||||
}
|
||||
const refType = (parameter.refType || '').trim();
|
||||
if (refType === 'fixed' || refType === 'input') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const ref = (parameter.ref || '').trim();
|
||||
return !!ref;
|
||||
const ref = (parameter.ref || '').trim();
|
||||
return !!ref;
|
||||
}
|
||||
|
||||
export function flattenParameterCandidates(parameters?: ParameterLike[] | null): ParameterCandidate[] {
|
||||
if (!parameters || parameters.length === 0) {
|
||||
return [];
|
||||
export function flattenParameterCandidates(
|
||||
parameters?: ParameterLike[] | null,
|
||||
): ParameterCandidate[] {
|
||||
if (!parameters || parameters.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const candidates: ParameterCandidate[] = [];
|
||||
const indexMap = new Map<string, number>();
|
||||
|
||||
const addCandidate = (name: string, resolved: boolean) => {
|
||||
const normalized = name.trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
const exists = indexMap.get(normalized);
|
||||
if (exists === undefined) {
|
||||
indexMap.set(normalized, candidates.length);
|
||||
candidates.push({
|
||||
name: normalized,
|
||||
resolved,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const candidates: ParameterCandidate[] = [];
|
||||
const indexMap = new Map<string, number>();
|
||||
// 同名参数只要有一个可解析,就视为可解析
|
||||
if (resolved) {
|
||||
candidates[exists].resolved = true;
|
||||
}
|
||||
};
|
||||
|
||||
const addCandidate = (name: string, resolved: boolean) => {
|
||||
const normalized = name.trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
const exists = indexMap.get(normalized);
|
||||
if (exists === undefined) {
|
||||
indexMap.set(normalized, candidates.length);
|
||||
candidates.push({
|
||||
name: normalized,
|
||||
resolved
|
||||
});
|
||||
return;
|
||||
}
|
||||
const walk = (
|
||||
items: ParameterLike[],
|
||||
parentPath = '',
|
||||
inheritedResolved = true,
|
||||
) => {
|
||||
for (const item of items) {
|
||||
const rawName = item?.name?.trim();
|
||||
if (!rawName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 同名参数只要有一个可解析,就视为可解析
|
||||
if (resolved) {
|
||||
candidates[exists].resolved = true;
|
||||
}
|
||||
};
|
||||
const currentPath = parentPath ? `${parentPath}.${rawName}` : rawName;
|
||||
const currentResolved = inheritedResolved && isParameterResolved(item);
|
||||
addCandidate(currentPath, currentResolved);
|
||||
|
||||
const walk = (items: ParameterLike[], parentPath = '', inheritedResolved = true) => {
|
||||
for (const item of items) {
|
||||
const rawName = item?.name?.trim();
|
||||
if (!rawName) {
|
||||
continue;
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
walk(item.children, currentPath, currentResolved);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const currentPath = parentPath ? `${parentPath}.${rawName}` : rawName;
|
||||
const currentResolved = inheritedResolved && isParameterResolved(item);
|
||||
addCandidate(currentPath, currentResolved);
|
||||
|
||||
if (item.children && item.children.length > 0) {
|
||||
walk(item.children, currentPath, currentResolved);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(parameters);
|
||||
return candidates;
|
||||
walk(parameters);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export function parseTokenParts(content: string, validParams: string[] = []): TokenPart[] {
|
||||
const source = content ?? '';
|
||||
const validSet = new Set(validParams.map(normalizeTokenKey));
|
||||
const parts: TokenPart[] = [];
|
||||
export function parseTokenParts(
|
||||
content: string,
|
||||
validParams: string[] = [],
|
||||
): TokenPart[] {
|
||||
const source = content ?? '';
|
||||
const validSet = new Set(validParams.map(normalizeTokenKey));
|
||||
const parts: TokenPart[] = [];
|
||||
|
||||
let lastIndex = 0;
|
||||
TOKEN_PATTERN.lastIndex = 0;
|
||||
let match: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
|
||||
let lastIndex = 0;
|
||||
TOKEN_PATTERN.lastIndex = 0;
|
||||
let match: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
|
||||
|
||||
while (match) {
|
||||
if (match.index > lastIndex) {
|
||||
parts.push({
|
||||
type: 'text',
|
||||
text: source.slice(lastIndex, match.index)
|
||||
});
|
||||
}
|
||||
|
||||
const rawToken = match[0];
|
||||
const tokenKey = normalizeTokenKey(match[1] || '');
|
||||
parts.push({
|
||||
type: 'token',
|
||||
text: rawToken,
|
||||
key: tokenKey,
|
||||
valid: validSet.has(tokenKey)
|
||||
});
|
||||
|
||||
lastIndex = match.index + rawToken.length;
|
||||
match = TOKEN_PATTERN.exec(source);
|
||||
while (match) {
|
||||
if (match.index > lastIndex) {
|
||||
parts.push({
|
||||
type: 'text',
|
||||
text: source.slice(lastIndex, match.index),
|
||||
});
|
||||
}
|
||||
|
||||
if (lastIndex < source.length) {
|
||||
parts.push({
|
||||
type: 'text',
|
||||
text: source.slice(lastIndex)
|
||||
});
|
||||
}
|
||||
const rawToken = match[0];
|
||||
const tokenKey = normalizeTokenKey(match[1] || '');
|
||||
parts.push({
|
||||
type: 'token',
|
||||
text: rawToken,
|
||||
key: tokenKey,
|
||||
valid: validSet.has(tokenKey),
|
||||
});
|
||||
|
||||
if (parts.length === 0) {
|
||||
parts.push({
|
||||
type: 'text',
|
||||
text: source
|
||||
});
|
||||
}
|
||||
lastIndex = match.index + rawToken.length;
|
||||
match = TOKEN_PATTERN.exec(source);
|
||||
}
|
||||
|
||||
return parts;
|
||||
if (lastIndex < source.length) {
|
||||
parts.push({
|
||||
type: 'text',
|
||||
text: source.slice(lastIndex),
|
||||
});
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
parts.push({
|
||||
type: 'text',
|
||||
text: source,
|
||||
});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
export function getTokenRanges(content: string): TokenRange[] {
|
||||
const source = content ?? '';
|
||||
const ranges: TokenRange[] = [];
|
||||
TOKEN_PATTERN.lastIndex = 0;
|
||||
let match: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
|
||||
const source = content ?? '';
|
||||
const ranges: TokenRange[] = [];
|
||||
TOKEN_PATTERN.lastIndex = 0;
|
||||
let match: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
|
||||
|
||||
while (match) {
|
||||
const rawToken = match[0];
|
||||
ranges.push({
|
||||
start: match.index,
|
||||
end: match.index + rawToken.length,
|
||||
text: rawToken,
|
||||
key: normalizeTokenKey(match[1] || '')
|
||||
});
|
||||
match = TOKEN_PATTERN.exec(source);
|
||||
}
|
||||
while (match) {
|
||||
const rawToken = match[0];
|
||||
ranges.push({
|
||||
start: match.index,
|
||||
end: match.index + rawToken.length,
|
||||
text: rawToken,
|
||||
key: normalizeTokenKey(match[1] || ''),
|
||||
});
|
||||
match = TOKEN_PATTERN.exec(source);
|
||||
}
|
||||
|
||||
return ranges;
|
||||
return ranges;
|
||||
}
|
||||
|
||||
export function findTokenRangeAtCursor(
|
||||
content: string,
|
||||
cursor: number,
|
||||
options?: {
|
||||
includeStart?: boolean;
|
||||
includeEnd?: boolean;
|
||||
}
|
||||
content: string,
|
||||
cursor: number,
|
||||
options?: {
|
||||
includeStart?: boolean;
|
||||
includeEnd?: boolean;
|
||||
},
|
||||
): TokenRange | null {
|
||||
if (!Number.isInteger(cursor) || cursor < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const includeStart = options?.includeStart ?? false;
|
||||
const includeEnd = options?.includeEnd ?? false;
|
||||
const ranges = getTokenRanges(content);
|
||||
for (const range of ranges) {
|
||||
const leftValid = includeStart ? cursor >= range.start : cursor > range.start;
|
||||
const rightValid = includeEnd ? cursor <= range.end : cursor < range.end;
|
||||
if (leftValid && rightValid) {
|
||||
return range;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isInteger(cursor) || cursor < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const includeStart = options?.includeStart ?? false;
|
||||
const includeEnd = options?.includeEnd ?? false;
|
||||
const ranges = getTokenRanges(content);
|
||||
for (const range of ranges) {
|
||||
const leftValid = includeStart
|
||||
? cursor >= range.start
|
||||
: cursor > range.start;
|
||||
const rightValid = includeEnd ? cursor <= range.end : cursor < range.end;
|
||||
if (leftValid && rightValid) {
|
||||
return range;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findBackspaceTokenRange(content: string, cursor: number): TokenRange | null {
|
||||
return findTokenRangeAtCursor(content, cursor, {
|
||||
includeStart: false,
|
||||
includeEnd: true
|
||||
});
|
||||
export function findBackspaceTokenRange(
|
||||
content: string,
|
||||
cursor: number,
|
||||
): TokenRange | null {
|
||||
return findTokenRangeAtCursor(content, cursor, {
|
||||
includeStart: false,
|
||||
includeEnd: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function splitTokenDisplay(rawToken: string, normalizedKey?: string): {
|
||||
hiddenPrefix: string;
|
||||
visibleText: string;
|
||||
hiddenSuffix: string;
|
||||
export function splitTokenDisplay(
|
||||
rawToken: string,
|
||||
normalizedKey?: string,
|
||||
): {
|
||||
hiddenPrefix: string;
|
||||
visibleText: string;
|
||||
hiddenSuffix: string;
|
||||
} {
|
||||
const source = rawToken ?? '';
|
||||
if (!source.startsWith('{{') || !source.endsWith('}}')) {
|
||||
return {
|
||||
hiddenPrefix: '',
|
||||
visibleText: normalizedKey || source,
|
||||
hiddenSuffix: ''
|
||||
};
|
||||
}
|
||||
|
||||
const inner = source.slice(2, -2);
|
||||
const visibleText = normalizeTokenKey(normalizedKey || inner);
|
||||
if (!visibleText) {
|
||||
return {
|
||||
hiddenPrefix: '',
|
||||
visibleText: source,
|
||||
hiddenSuffix: ''
|
||||
};
|
||||
}
|
||||
|
||||
const innerStart = inner.indexOf(visibleText);
|
||||
const leading = innerStart >= 0 ? inner.slice(0, innerStart) : '';
|
||||
const trailing = innerStart >= 0 ? inner.slice(innerStart + visibleText.length) : '';
|
||||
|
||||
const source = rawToken ?? '';
|
||||
if (!source.startsWith('{{') || !source.endsWith('}}')) {
|
||||
return {
|
||||
hiddenPrefix: `{{${leading}`,
|
||||
visibleText,
|
||||
hiddenSuffix: `${trailing}}}`
|
||||
hiddenPrefix: '',
|
||||
visibleText: normalizedKey || source,
|
||||
hiddenSuffix: '',
|
||||
};
|
||||
}
|
||||
|
||||
const inner = source.slice(2, -2);
|
||||
const visibleText = normalizeTokenKey(normalizedKey || inner);
|
||||
if (!visibleText) {
|
||||
return {
|
||||
hiddenPrefix: '',
|
||||
visibleText: source,
|
||||
hiddenSuffix: '',
|
||||
};
|
||||
}
|
||||
|
||||
const innerStart = inner.indexOf(visibleText);
|
||||
const leading = innerStart >= 0 ? inner.slice(0, innerStart) : '';
|
||||
const trailing =
|
||||
innerStart >= 0 ? inner.slice(innerStart + visibleText.length) : '';
|
||||
|
||||
return {
|
||||
hiddenPrefix: `{{${leading}`,
|
||||
visibleText,
|
||||
hiddenSuffix: `${trailing}}}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function insertTextAtCursor(
|
||||
content: string,
|
||||
insertedText: string,
|
||||
selectionStart?: number | null,
|
||||
selectionEnd?: number | null
|
||||
content: string,
|
||||
insertedText: string,
|
||||
selectionStart?: number | null,
|
||||
selectionEnd?: number | null,
|
||||
): {
|
||||
value: string;
|
||||
cursor: number;
|
||||
value: string;
|
||||
cursor: number;
|
||||
} {
|
||||
const source = content ?? '';
|
||||
const start = Number.isInteger(selectionStart)
|
||||
? Math.max(0, Math.min(selectionStart as number, source.length))
|
||||
: source.length;
|
||||
const end = Number.isInteger(selectionEnd)
|
||||
? Math.max(start, Math.min(selectionEnd as number, source.length))
|
||||
: start;
|
||||
const source = content ?? '';
|
||||
const start = Number.isInteger(selectionStart)
|
||||
? Math.max(0, Math.min(selectionStart as number, source.length))
|
||||
: source.length;
|
||||
const end = Number.isInteger(selectionEnd)
|
||||
? Math.max(start, Math.min(selectionEnd as number, source.length))
|
||||
: start;
|
||||
|
||||
const nextValue = source.slice(0, start) + insertedText + source.slice(end);
|
||||
return {
|
||||
value: nextValue,
|
||||
cursor: start + insertedText.length
|
||||
};
|
||||
const nextValue = source.slice(0, start) + insertedText + source.slice(end);
|
||||
return {
|
||||
value: nextValue,
|
||||
cursor: start + insertedText.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
return text
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
@@ -1,63 +1,63 @@
|
||||
import {genShortId} from './IdGen';
|
||||
import {useSvelteFlow} from '@xyflow/svelte';
|
||||
import type {Parameter} from '#types';
|
||||
import { genShortId } from './IdGen';
|
||||
import { useSvelteFlow } from '@xyflow/svelte';
|
||||
import type { Parameter } from '#types';
|
||||
|
||||
export const fillParameterId = (parameters?: Parameter[]) => {
|
||||
if (!parameters || parameters.length == 0) {
|
||||
return parameters;
|
||||
}
|
||||
parameters.forEach((parameter) => {
|
||||
if (!parameter.id) {
|
||||
parameter.id = genShortId();
|
||||
}
|
||||
fillParameterId(parameter.children);
|
||||
});
|
||||
|
||||
if (!parameters || parameters.length == 0) {
|
||||
return parameters;
|
||||
}
|
||||
parameters.forEach((parameter) => {
|
||||
if (!parameter.id) {
|
||||
parameter.id = genShortId();
|
||||
}
|
||||
fillParameterId(parameter.children);
|
||||
});
|
||||
|
||||
return parameters;
|
||||
};
|
||||
|
||||
export const useAddParameter = () => {
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
return {
|
||||
addParameter: (
|
||||
nodeId: string,
|
||||
dataKey: string = 'parameters',
|
||||
parameter?: Parameter | Parameter[]
|
||||
) => {
|
||||
if (Array.isArray(parameter)) {
|
||||
parameter.forEach((p) => fillParameterId(p?.children));
|
||||
} else {
|
||||
fillParameterId(parameter?.children);
|
||||
}
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
return {
|
||||
addParameter: (
|
||||
nodeId: string,
|
||||
dataKey: string = 'parameters',
|
||||
parameter?: Parameter | Parameter[],
|
||||
) => {
|
||||
if (Array.isArray(parameter)) {
|
||||
parameter.forEach((p) => fillParameterId(p?.children));
|
||||
} else {
|
||||
fillParameterId(parameter?.children);
|
||||
}
|
||||
|
||||
function createNewParameter(parameter: Parameter) {
|
||||
return {
|
||||
name: '',
|
||||
dataType: 'String',
|
||||
refType: 'ref',
|
||||
...parameter,
|
||||
id: genShortId()
|
||||
};
|
||||
}
|
||||
function createNewParameter(parameter: Parameter) {
|
||||
return {
|
||||
name: '',
|
||||
dataType: 'String',
|
||||
refType: 'ref',
|
||||
...parameter,
|
||||
id: genShortId(),
|
||||
};
|
||||
}
|
||||
|
||||
const newParameters: Parameter[] = [];
|
||||
if (Array.isArray(parameter)) {
|
||||
newParameters.push(...parameter.map(createNewParameter));
|
||||
} else {
|
||||
newParameters.push(createNewParameter(parameter as Parameter));
|
||||
}
|
||||
const newParameters: Parameter[] = [];
|
||||
if (Array.isArray(parameter)) {
|
||||
newParameters.push(...parameter.map(createNewParameter));
|
||||
} else {
|
||||
newParameters.push(createNewParameter(parameter as Parameter));
|
||||
}
|
||||
|
||||
updateNodeData(nodeId, (node) => {
|
||||
let parameters = node.data[dataKey] as Array<any>;
|
||||
if (parameters) {
|
||||
parameters.push(...newParameters);
|
||||
} else {
|
||||
parameters = [...newParameters];
|
||||
}
|
||||
return {
|
||||
[dataKey]: [...parameters]
|
||||
};
|
||||
});
|
||||
updateNodeData(nodeId, (node) => {
|
||||
let parameters = node.data[dataKey] as Array<any>;
|
||||
if (parameters) {
|
||||
parameters.push(...newParameters);
|
||||
} else {
|
||||
parameters = [...newParameters];
|
||||
}
|
||||
};
|
||||
return {
|
||||
[dataKey]: [...parameters],
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import {genShortId} from './IdGen';
|
||||
import {store} from '#store/stores.svelte';
|
||||
import { genShortId } from './IdGen';
|
||||
import { store } from '#store/stores.svelte';
|
||||
|
||||
export const useCopyNode = () => {
|
||||
const copyNode = (id: string) => {
|
||||
const node = store.getNode(id);
|
||||
if (node) {
|
||||
const newNodeId = genShortId();
|
||||
const newNode = {
|
||||
...node,
|
||||
id: newNodeId,
|
||||
position: {
|
||||
x: node.position.x + 50,
|
||||
y: node.position.y + 50
|
||||
}
|
||||
};
|
||||
const copyNode = (id: string) => {
|
||||
const node = store.getNode(id);
|
||||
if (node) {
|
||||
const newNodeId = genShortId();
|
||||
const newNode = {
|
||||
...node,
|
||||
id: newNodeId,
|
||||
position: {
|
||||
x: node.position.x + 50,
|
||||
y: node.position.y + 50,
|
||||
},
|
||||
};
|
||||
|
||||
store.updateNodes((nodes) => {
|
||||
const newNodes = nodes.map((n) => ({ ...n, selected: false }));
|
||||
return [...newNodes, newNode];
|
||||
});
|
||||
}
|
||||
};
|
||||
return {
|
||||
copyNode
|
||||
};
|
||||
store.updateNodes((nodes) => {
|
||||
const newNodes = nodes.map((n) => ({ ...n, selected: false }));
|
||||
return [...newNodes, newNode];
|
||||
});
|
||||
}
|
||||
};
|
||||
return {
|
||||
copyNode,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
import {genShortId} from '#components/utils/IdGen';
|
||||
import {type Edge, type Node, useSvelteFlow} from '@xyflow/svelte';
|
||||
import { store } from '#store/stores.svelte';
|
||||
import { genShortId } from '#components/utils/IdGen';
|
||||
import { type Edge, type Node, useSvelteFlow } from '@xyflow/svelte';
|
||||
|
||||
interface ClipboardData {
|
||||
tinyflowNodes: Node[];
|
||||
tinyflowEdges?: Edge[];
|
||||
version: string;
|
||||
tinyflowNodes: Node[];
|
||||
tinyflowEdges?: Edge[];
|
||||
version: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理节点中不可序列化的字段,确保可安全 JSON.stringify
|
||||
*/
|
||||
function sanitizeNode(node: Node): Node {
|
||||
const { id, type, position, data, parentId } = node;
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
position: { x: position.x, y: position.y },
|
||||
parentId: parentId ? parentId : undefined,
|
||||
data: data ? JSON.parse(JSON.stringify(data)) : {},
|
||||
...(parentId !== undefined && { parentId })
|
||||
};
|
||||
const { id, type, position, data, parentId } = node;
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
position: { x: position.x, y: position.y },
|
||||
parentId: parentId ? parentId : undefined,
|
||||
data: data ? JSON.parse(JSON.stringify(data)) : {},
|
||||
...(parentId !== undefined && { parentId }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,255 +28,254 @@ function sanitizeNode(node: Node): Node {
|
||||
* 使用 Kahn 算法(拓扑排序)处理任意嵌套层级。
|
||||
*/
|
||||
export function sortNodesForSvelteFlow(nodes: Node[]): Node[] {
|
||||
const nodeMap = new Map<string, Node>();
|
||||
const inDegree = new Map<string, number>(); // 每个节点的“依赖数”(即是否为子节点)
|
||||
const childrenMap = new Map<string, string[]>(); // parentId -> childIds
|
||||
const nodeMap = new Map<string, Node>();
|
||||
const inDegree = new Map<string, number>(); // 每个节点的“依赖数”(即是否为子节点)
|
||||
const childrenMap = new Map<string, string[]>(); // parentId -> childIds
|
||||
|
||||
// 初始化
|
||||
for (const node of nodes) {
|
||||
nodeMap.set(node.id, node);
|
||||
inDegree.set(node.id, 0); // 默认无依赖
|
||||
if (node.parentId) {
|
||||
// 子节点依赖父节点
|
||||
inDegree.set(node.id, 1);
|
||||
if (!childrenMap.has(node.parentId)) {
|
||||
childrenMap.set(node.parentId, []);
|
||||
}
|
||||
childrenMap.get(node.parentId)!.push(node.id);
|
||||
}
|
||||
// 初始化
|
||||
for (const node of nodes) {
|
||||
nodeMap.set(node.id, node);
|
||||
inDegree.set(node.id, 0); // 默认无依赖
|
||||
if (node.parentId) {
|
||||
// 子节点依赖父节点
|
||||
inDegree.set(node.id, 1);
|
||||
if (!childrenMap.has(node.parentId)) {
|
||||
childrenMap.set(node.parentId, []);
|
||||
}
|
||||
childrenMap.get(node.parentId)!.push(node.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 所有根节点(无 parentId 或父不存在)入队
|
||||
const queue: Node[] = [];
|
||||
for (const node of nodes) {
|
||||
if (!node.parentId || !nodeMap.has(node.parentId)) {
|
||||
queue.push(node);
|
||||
}
|
||||
// 所有根节点(无 parentId 或父不存在)入队
|
||||
const queue: Node[] = [];
|
||||
for (const node of nodes) {
|
||||
if (!node.parentId || !nodeMap.has(node.parentId)) {
|
||||
queue.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
const result: Node[] = [];
|
||||
const visited = new Set<string>();
|
||||
const result: Node[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
// BFS 拓扑排序
|
||||
while (queue.length > 0) {
|
||||
const node = queue.shift()!;
|
||||
if (visited.has(node.id)) continue;
|
||||
visited.add(node.id);
|
||||
result.push(node);
|
||||
// BFS 拓扑排序
|
||||
while (queue.length > 0) {
|
||||
const node = queue.shift()!;
|
||||
if (visited.has(node.id)) continue;
|
||||
visited.add(node.id);
|
||||
result.push(node);
|
||||
|
||||
// 将该节点的所有直接子节点入队(如果其父已处理)
|
||||
const children = childrenMap.get(node.id) || [];
|
||||
for (const childId of children) {
|
||||
if (!visited.has(childId)) {
|
||||
queue.push(nodeMap.get(childId)!);
|
||||
}
|
||||
}
|
||||
// 将该节点的所有直接子节点入队(如果其父已处理)
|
||||
const children = childrenMap.get(node.id) || [];
|
||||
for (const childId of children) {
|
||||
if (!visited.has(childId)) {
|
||||
queue.push(nodeMap.get(childId)!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 补充可能遗漏的节点(如循环引用或孤立子节点)
|
||||
for (const node of nodes) {
|
||||
if (!visited.has(node.id)) {
|
||||
result.push(node);
|
||||
}
|
||||
// 补充可能遗漏的节点(如循环引用或孤立子节点)
|
||||
for (const node of nodes) {
|
||||
if (!visited.has(node.id)) {
|
||||
result.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理边中不可序列化的字段
|
||||
*/
|
||||
function sanitizeEdge(edge: Edge): Edge {
|
||||
const { id, source, target, sourceHandle, targetHandle, type, data } = edge;
|
||||
return {
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
...(sourceHandle !== undefined && { sourceHandle }),
|
||||
...(targetHandle !== undefined && { targetHandle }),
|
||||
...(type !== undefined && { type }),
|
||||
data: data ? JSON.parse(JSON.stringify(data)) : {}
|
||||
};
|
||||
const { id, source, target, sourceHandle, targetHandle, type, data } = edge;
|
||||
return {
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
...(sourceHandle !== undefined && { sourceHandle }),
|
||||
...(targetHandle !== undefined && { targetHandle }),
|
||||
...(type !== undefined && { type }),
|
||||
data: data ? JSON.parse(JSON.stringify(data)) : {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历对象,仅当遇到 { refType: 'ref', ref: string } 时重写 ref 的 id
|
||||
*/
|
||||
function rewriteRefsInData(obj: any, idMap: Map<string, string>): any {
|
||||
if (obj === null || obj === undefined) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// 如果是数组,递归处理每个元素
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => rewriteRefsInData(item, idMap));
|
||||
}
|
||||
|
||||
// 如果是对象,检查是否为引用对象
|
||||
if (typeof obj === 'object') {
|
||||
// 检查是否是引用定义:refType === 'ref' 且有 ref 字段
|
||||
if (obj.refType === 'ref' && typeof obj.ref === 'string') {
|
||||
const match = obj.ref.match(/^([^.\s]+)\.(.+)$/);
|
||||
if (match) {
|
||||
const [_, oldNodeId, paramId] = match;
|
||||
const newNodeId = idMap.get(oldNodeId);
|
||||
if (newNodeId !== undefined) {
|
||||
// 返回新对象,避免修改原数据
|
||||
return {
|
||||
...obj,
|
||||
ref: `${newNodeId}.${paramId}`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理所有子属性
|
||||
const result: Record<string, any> = {};
|
||||
for (const key in obj) {
|
||||
if (Object.hasOwn(obj, key)) {
|
||||
result[key] = rewriteRefsInData(obj[key], idMap);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 原始类型(string/number/boolean)直接返回
|
||||
if (obj === null || obj === undefined) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// 如果是数组,递归处理每个元素
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => rewriteRefsInData(item, idMap));
|
||||
}
|
||||
|
||||
// 如果是对象,检查是否为引用对象
|
||||
if (typeof obj === 'object') {
|
||||
// 检查是否是引用定义:refType === 'ref' 且有 ref 字段
|
||||
if (obj.refType === 'ref' && typeof obj.ref === 'string') {
|
||||
const match = obj.ref.match(/^([^.\s]+)\.(.+)$/);
|
||||
if (match) {
|
||||
const [_, oldNodeId, paramId] = match;
|
||||
const newNodeId = idMap.get(oldNodeId);
|
||||
if (newNodeId !== undefined) {
|
||||
// 返回新对象,避免修改原数据
|
||||
return {
|
||||
...obj,
|
||||
ref: `${newNodeId}.${paramId}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理所有子属性
|
||||
const result: Record<string, any> = {};
|
||||
for (const key in obj) {
|
||||
if (Object.hasOwn(obj, key)) {
|
||||
result[key] = rewriteRefsInData(obj[key], idMap);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 原始类型(string/number/boolean)直接返回
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制粘贴处理器 Hook
|
||||
*/
|
||||
export const useCopyPasteHandler = () => {
|
||||
const svelteFlow = useSvelteFlow();
|
||||
const svelteFlow = useSvelteFlow();
|
||||
|
||||
const copyHandler = async (event: ClipboardEvent | KeyboardEvent) => {
|
||||
const selectedNodes = store.getNodes().filter((node) => node.selected);
|
||||
if (selectedNodes.length === 0) return;
|
||||
const copyHandler = async (event: ClipboardEvent | KeyboardEvent) => {
|
||||
const selectedNodes = store.getNodes().filter((node) => node.selected);
|
||||
if (selectedNodes.length === 0) return;
|
||||
|
||||
// 获取完全包含在选中节点之间的边(起点和终点都被选中)
|
||||
const allEdges = store.getEdges();
|
||||
const relatedEdges = allEdges.filter(
|
||||
(edge) =>
|
||||
selectedNodes.some((n) => n.id === edge.source) &&
|
||||
selectedNodes.some((n) => n.id === edge.target)
|
||||
);
|
||||
// 获取完全包含在选中节点之间的边(起点和终点都被选中)
|
||||
const allEdges = store.getEdges();
|
||||
const relatedEdges = allEdges.filter(
|
||||
(edge) =>
|
||||
selectedNodes.some((n) => n.id === edge.source) &&
|
||||
selectedNodes.some((n) => n.id === edge.target),
|
||||
);
|
||||
|
||||
const serializableNodes = selectedNodes.map(sanitizeNode);
|
||||
const serializableEdges = relatedEdges.map(sanitizeEdge);
|
||||
const serializableNodes = selectedNodes.map(sanitizeNode);
|
||||
const serializableEdges = relatedEdges.map(sanitizeEdge);
|
||||
|
||||
const clipboardData: ClipboardData = {
|
||||
tinyflowNodes: serializableNodes,
|
||||
tinyflowEdges: serializableEdges,
|
||||
version: '1.0'
|
||||
};
|
||||
|
||||
const jsonStr = JSON.stringify(clipboardData, null, 0);
|
||||
|
||||
try {
|
||||
// 优先使用 event.clipboardData(在 copy 事件中可用,无需权限)
|
||||
if ('clipboardData' in event && event.clipboardData) {
|
||||
event.clipboardData.setData('text/plain', jsonStr);
|
||||
if (event instanceof ClipboardEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
} else {
|
||||
// 降级到 navigator.clipboard(需用户手势)
|
||||
await navigator.clipboard.writeText(jsonStr);
|
||||
}
|
||||
console.log('Copied nodes and edges to clipboard');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
// 可选:同源降级存储
|
||||
try {
|
||||
sessionStorage.setItem('tinyflow_clipboard', jsonStr);
|
||||
} catch {}
|
||||
}
|
||||
const clipboardData: ClipboardData = {
|
||||
tinyflowNodes: serializableNodes,
|
||||
tinyflowEdges: serializableEdges,
|
||||
version: '1.0',
|
||||
};
|
||||
|
||||
const jsonStr = JSON.stringify(clipboardData, null, 0);
|
||||
|
||||
const pasteHandler = (event: ClipboardEvent) => {
|
||||
const text = event.clipboardData?.getData('text/plain');
|
||||
if (!text) return;
|
||||
|
||||
let parsed: ClipboardData | null = null;
|
||||
try {
|
||||
parsed = JSON.parse(text);
|
||||
} catch {
|
||||
return; // 忽略非 JSON 内容
|
||||
try {
|
||||
// 优先使用 event.clipboardData(在 copy 事件中可用,无需权限)
|
||||
if ('clipboardData' in event && event.clipboardData) {
|
||||
event.clipboardData.setData('text/plain', jsonStr);
|
||||
if (event instanceof ClipboardEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
} else {
|
||||
// 降级到 navigator.clipboard(需用户手势)
|
||||
await navigator.clipboard.writeText(jsonStr);
|
||||
}
|
||||
console.log('Copied nodes and edges to clipboard');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
// 可选:同源降级存储
|
||||
try {
|
||||
sessionStorage.setItem('tinyflow_clipboard', jsonStr);
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
|
||||
if (!parsed?.tinyflowNodes || !Array.isArray(parsed.tinyflowNodes)) {
|
||||
return;
|
||||
}
|
||||
const pasteHandler = (event: ClipboardEvent) => {
|
||||
const text = event.clipboardData?.getData('text/plain');
|
||||
if (!text) return;
|
||||
|
||||
event.preventDefault();
|
||||
let parsed: ClipboardData | null = null;
|
||||
try {
|
||||
parsed = JSON.parse(text);
|
||||
} catch {
|
||||
return; // 忽略非 JSON 内容
|
||||
}
|
||||
|
||||
const pastedNodes = sortNodesForSvelteFlow(parsed.tinyflowNodes);
|
||||
const pastedEdges = parsed.tinyflowEdges || [];
|
||||
if (!parsed?.tinyflowNodes || !Array.isArray(parsed.tinyflowNodes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建新节点(带新 ID 和偏移)
|
||||
const newNodeIdMap = new Map<string, string>();
|
||||
const newNodes: Node[] = [];
|
||||
event.preventDefault();
|
||||
|
||||
for (const node of pastedNodes) {
|
||||
const newId = `node_${genShortId()}`;
|
||||
newNodeIdMap.set(node.id, newId);
|
||||
}
|
||||
const pastedNodes = sortNodesForSvelteFlow(parsed.tinyflowNodes);
|
||||
const pastedEdges = parsed.tinyflowEdges || [];
|
||||
|
||||
// 构建新节点(含重写后的 data)
|
||||
for (const node of pastedNodes) {
|
||||
const newId = newNodeIdMap.get(node.id)!;
|
||||
// 创建新节点(带新 ID 和偏移)
|
||||
const newNodeIdMap = new Map<string, string>();
|
||||
const newNodes: Node[] = [];
|
||||
|
||||
const newParentId =
|
||||
node.parentId !== undefined
|
||||
? newNodeIdMap.get(node.parentId) // 安全:即使父不在粘贴范围内,也会是 undefined
|
||||
: undefined;
|
||||
for (const node of pastedNodes) {
|
||||
const newId = `node_${genShortId()}`;
|
||||
newNodeIdMap.set(node.id, newId);
|
||||
}
|
||||
|
||||
const newData = rewriteRefsInData(node.data, newNodeIdMap);
|
||||
// 构建新节点(含重写后的 data)
|
||||
for (const node of pastedNodes) {
|
||||
const newId = newNodeIdMap.get(node.id)!;
|
||||
|
||||
newNodes.push({
|
||||
...node,
|
||||
id: newId,
|
||||
parentId: newParentId,
|
||||
data: newData,
|
||||
position: {
|
||||
x: (node.position?.x ?? 0) + 50,
|
||||
y: (node.position?.y ?? 0) + 50
|
||||
},
|
||||
selected: true
|
||||
});
|
||||
}
|
||||
const newParentId =
|
||||
node.parentId !== undefined
|
||||
? newNodeIdMap.get(node.parentId) // 安全:即使父不在粘贴范围内,也会是 undefined
|
||||
: undefined;
|
||||
|
||||
// 创建新边(仅当两端都在粘贴范围内)
|
||||
const newEdges: Edge[] = [];
|
||||
for (const edge of pastedEdges) {
|
||||
const newSource = newNodeIdMap.get(edge.source);
|
||||
const newTarget = newNodeIdMap.get(edge.target);
|
||||
if (newSource && newTarget) {
|
||||
newEdges.push({
|
||||
...edge,
|
||||
id: `edge_${genShortId()}`,
|
||||
source: newSource,
|
||||
target: newTarget
|
||||
});
|
||||
}
|
||||
}
|
||||
const newData = rewriteRefsInData(node.data, newNodeIdMap);
|
||||
|
||||
// 更新 store:取消其他节点选中,添加新内容
|
||||
store.updateNodes((nodes) => {
|
||||
const unselected = nodes.map((n) => ({ ...n, selected: false }));
|
||||
return [...unselected, ...newNodes];
|
||||
newNodes.push({
|
||||
...node,
|
||||
id: newId,
|
||||
parentId: newParentId,
|
||||
data: newData,
|
||||
position: {
|
||||
x: (node.position?.x ?? 0) + 50,
|
||||
y: (node.position?.y ?? 0) + 50,
|
||||
},
|
||||
selected: true,
|
||||
});
|
||||
}
|
||||
|
||||
// 创建新边(仅当两端都在粘贴范围内)
|
||||
const newEdges: Edge[] = [];
|
||||
for (const edge of pastedEdges) {
|
||||
const newSource = newNodeIdMap.get(edge.source);
|
||||
const newTarget = newNodeIdMap.get(edge.target);
|
||||
if (newSource && newTarget) {
|
||||
newEdges.push({
|
||||
...edge,
|
||||
id: `edge_${genShortId()}`,
|
||||
source: newSource,
|
||||
target: newTarget,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
store.updateEdges((edges) => {
|
||||
const unselected = edges.map((n) => ({ ...n, selected: false }));
|
||||
return [...unselected, ...newEdges];
|
||||
});
|
||||
};
|
||||
// 更新 store:取消其他节点选中,添加新内容
|
||||
store.updateNodes((nodes) => {
|
||||
const unselected = nodes.map((n) => ({ ...n, selected: false }));
|
||||
return [...unselected, ...newNodes];
|
||||
});
|
||||
|
||||
return {
|
||||
copyHandler,
|
||||
pasteHandler
|
||||
};
|
||||
store.updateEdges((edges) => {
|
||||
const unselected = edges.map((n) => ({ ...n, selected: false }));
|
||||
return [...unselected, ...newEdges];
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
copyHandler,
|
||||
pasteHandler,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
import { store } from '#store/stores.svelte';
|
||||
|
||||
export const useDeleteEdge = () => {
|
||||
const deleteEdge = (id: string) => {
|
||||
store.removeEdge( id);
|
||||
};
|
||||
return {
|
||||
deleteEdge
|
||||
};
|
||||
const deleteEdge = (id: string) => {
|
||||
store.removeEdge(id);
|
||||
};
|
||||
return {
|
||||
deleteEdge,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
import { store } from '#store/stores.svelte';
|
||||
|
||||
export const useDeleteNode = () => {
|
||||
const deleteNode = (id: string) => {
|
||||
store.removeNode(id);
|
||||
store.updateEdges(edges => edges.filter(edge => edge.source !== id && edge.target !== id))
|
||||
};
|
||||
return {
|
||||
deleteNode
|
||||
};
|
||||
const deleteNode = (id: string) => {
|
||||
store.removeNode(id);
|
||||
store.updateEdges((edges) =>
|
||||
edges.filter((edge) => edge.source !== id && edge.target !== id),
|
||||
);
|
||||
};
|
||||
return {
|
||||
deleteNode,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,43 +1,49 @@
|
||||
import {store} from '../../store/stores.svelte';
|
||||
import { store } from '../../store/stores.svelte';
|
||||
|
||||
export const useEnsureParentInNodesBefore = () => {
|
||||
const ensureParentInNodesBefore = (parentNodeId: string, childNodeId: string) => {
|
||||
store.updateNodes((nodeArray) => {
|
||||
let parentIndex = -1;
|
||||
for (let i = 0; i < nodeArray.length; i++) {
|
||||
if (nodeArray[i].id === parentNodeId) {
|
||||
parentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const ensureParentInNodesBefore = (
|
||||
parentNodeId: string,
|
||||
childNodeId: string,
|
||||
) => {
|
||||
store.updateNodes((nodeArray) => {
|
||||
let parentIndex = -1;
|
||||
for (let i = 0; i < nodeArray.length; i++) {
|
||||
if (nodeArray[i].id === parentNodeId) {
|
||||
parentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (parentIndex <= 0) {
|
||||
return nodeArray;
|
||||
}
|
||||
if (parentIndex <= 0) {
|
||||
return nodeArray;
|
||||
}
|
||||
|
||||
let firstChildIndex = -1;
|
||||
for (let i = 0; i < parentIndex; i++) {
|
||||
if (nodeArray[i].parentId === parentNodeId || nodeArray[i].id === childNodeId) {
|
||||
firstChildIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let firstChildIndex = -1;
|
||||
for (let i = 0; i < parentIndex; i++) {
|
||||
if (
|
||||
nodeArray[i].parentId === parentNodeId ||
|
||||
nodeArray[i].id === childNodeId
|
||||
) {
|
||||
firstChildIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstChildIndex == -1) {
|
||||
return nodeArray;
|
||||
}
|
||||
if (firstChildIndex == -1) {
|
||||
return nodeArray;
|
||||
}
|
||||
|
||||
const parentNode = nodeArray[parentIndex];
|
||||
for (let i = parentIndex; i > firstChildIndex; i--) {
|
||||
nodeArray[i] = nodeArray[i - 1];
|
||||
}
|
||||
nodeArray[firstChildIndex] = parentNode;
|
||||
const parentNode = nodeArray[parentIndex];
|
||||
for (let i = parentIndex; i > firstChildIndex; i--) {
|
||||
nodeArray[i] = nodeArray[i - 1];
|
||||
}
|
||||
nodeArray[firstChildIndex] = parentNode;
|
||||
|
||||
return nodeArray;
|
||||
});
|
||||
};
|
||||
return nodeArray;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
ensureParentInNodesBefore
|
||||
};
|
||||
return {
|
||||
ensureParentInNodesBefore,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
import { store } from '#store/stores.svelte';
|
||||
|
||||
export const useGetEdgesBySource = () => {
|
||||
const getEdgesBySource = (target: string) => {
|
||||
const edges = store.getEdges();
|
||||
return edges.filter((edge) => edge.source === target);
|
||||
};
|
||||
return {
|
||||
getEdgesBySource
|
||||
};
|
||||
const getEdgesBySource = (target: string) => {
|
||||
const edges = store.getEdges();
|
||||
return edges.filter((edge) => edge.source === target);
|
||||
};
|
||||
return {
|
||||
getEdgesBySource,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
import { store } from '#store/stores.svelte';
|
||||
|
||||
export const useGetEdgesByTarget = () => {
|
||||
const getEdgesByTarget = (target: string) => {
|
||||
const edges = store.getEdges();
|
||||
return edges.filter((edge) => edge.target === target);
|
||||
};
|
||||
return {
|
||||
getEdgesByTarget
|
||||
};
|
||||
const getEdgesByTarget = (target: string) => {
|
||||
const edges = store.getEdges();
|
||||
return edges.filter((edge) => edge.target === target);
|
||||
};
|
||||
return {
|
||||
getEdgesByTarget,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
import { store } from '#store/stores.svelte';
|
||||
|
||||
export const useGetNode = () => {
|
||||
const getNode = (id: string) => {
|
||||
return store.getNode(id);
|
||||
};
|
||||
return {
|
||||
getNode
|
||||
};
|
||||
const getNode = (id: string) => {
|
||||
return store.getNode(id);
|
||||
};
|
||||
return {
|
||||
getNode,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
import { store } from '#store/stores.svelte';
|
||||
|
||||
export const useGetNodeRelativePosition = () => {
|
||||
const getNodeRelativePosition = (parentNodeId: string) => {
|
||||
let node = store.getNode(parentNodeId);
|
||||
const position = { x: 0, y: 0 };
|
||||
while (node) {
|
||||
position.x += node.position.x;
|
||||
position.y += node.position.y;
|
||||
const getNodeRelativePosition = (parentNodeId: string) => {
|
||||
let node = store.getNode(parentNodeId);
|
||||
const position = { x: 0, y: 0 };
|
||||
while (node) {
|
||||
position.x += node.position.x;
|
||||
position.y += node.position.y;
|
||||
|
||||
if (node.parentId) {
|
||||
node = store.getNode(node.parentId);
|
||||
} else {
|
||||
node = undefined;
|
||||
}
|
||||
}
|
||||
return position;
|
||||
};
|
||||
return {
|
||||
getNodeRelativePosition
|
||||
};
|
||||
if (node.parentId) {
|
||||
node = store.getNode(node.parentId);
|
||||
} else {
|
||||
node = undefined;
|
||||
}
|
||||
}
|
||||
return position;
|
||||
};
|
||||
return {
|
||||
getNodeRelativePosition,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
import type {Edge, Node} from '@xyflow/svelte';
|
||||
import { store } from '#store/stores.svelte';
|
||||
import type { Edge, Node } from '@xyflow/svelte';
|
||||
|
||||
export const useGetNodesFromSource = () => {
|
||||
const getEdgesBySource = (target: string, edges: Edge[]) => {
|
||||
return edges.filter(
|
||||
// 排除循环节点的子节点,否则在多层循环嵌套时不正确
|
||||
(edge) => edge.source === target && edge.sourceHandle !== 'loop_handle'
|
||||
);
|
||||
};
|
||||
const getEdgesBySource = (target: string, edges: Edge[]) => {
|
||||
return edges.filter(
|
||||
// 排除循环节点的子节点,否则在多层循环嵌套时不正确
|
||||
(edge) => edge.source === target && edge.sourceHandle !== 'loop_handle',
|
||||
);
|
||||
};
|
||||
|
||||
const getNodesFromSource = (sourceNodeId: string) => {
|
||||
const edges = store.getEdges();
|
||||
const result: Node[] = [];
|
||||
const getNodesFromSource = (sourceNodeId: string) => {
|
||||
const edges = store.getEdges();
|
||||
const result: Node[] = [];
|
||||
|
||||
let edgesFromSource = getEdgesBySource(sourceNodeId, edges);
|
||||
while (edgesFromSource.length > 0) {
|
||||
const newEdgesFromSource: Edge[] = [];
|
||||
edgesFromSource.forEach((edge) => {
|
||||
result.push(store.getNode(edge.target)!);
|
||||
newEdgesFromSource.push(...getEdgesBySource(edge.target, edges));
|
||||
});
|
||||
edgesFromSource = newEdgesFromSource;
|
||||
}
|
||||
let edgesFromSource = getEdgesBySource(sourceNodeId, edges);
|
||||
while (edgesFromSource.length > 0) {
|
||||
const newEdgesFromSource: Edge[] = [];
|
||||
edgesFromSource.forEach((edge) => {
|
||||
result.push(store.getNode(edge.target)!);
|
||||
newEdgesFromSource.push(...getEdgesBySource(edge.target, edges));
|
||||
});
|
||||
edgesFromSource = newEdgesFromSource;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
return {
|
||||
getNodesFromSource
|
||||
};
|
||||
return result;
|
||||
};
|
||||
return {
|
||||
getNodesFromSource,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,154 +1,172 @@
|
||||
import {type Edge, type Node, useNodesData, useStore} from '@xyflow/svelte';
|
||||
import type {Parameter} from '#types';
|
||||
import {getCurrentNodeId, getOptions} from '#components/utils/NodeUtils';
|
||||
import {nodeIcons} from '../../consts';
|
||||
import { type Edge, type Node, useNodesData, useStore } from '@xyflow/svelte';
|
||||
import type { Parameter } from '#types';
|
||||
import { getCurrentNodeId, getOptions } from '#components/utils/NodeUtils';
|
||||
import { nodeIcons } from '../../consts';
|
||||
|
||||
const fillRefNodeIds = (refNodeIds: string[], currentNodeId: string, edges: Edge[]) => {
|
||||
for (const edge of edges) {
|
||||
if (edge.target === currentNodeId && edge.source) {
|
||||
refNodeIds.push(edge.source);
|
||||
fillRefNodeIds(refNodeIds, edge.source, edges);
|
||||
}
|
||||
const fillRefNodeIds = (
|
||||
refNodeIds: string[],
|
||||
currentNodeId: string,
|
||||
edges: Edge[],
|
||||
) => {
|
||||
for (const edge of edges) {
|
||||
if (edge.target === currentNodeId && edge.source) {
|
||||
refNodeIds.push(edge.source);
|
||||
fillRefNodeIds(refNodeIds, edge.source, edges);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getChildren = (params: any, parentId: string, nodeIsChildren: boolean, nodeType: string) => {
|
||||
if (!params || params.length === 0) return [];
|
||||
return params.map((param: any) => {
|
||||
const getChildren = (
|
||||
params: any,
|
||||
parentId: string,
|
||||
nodeIsChildren: boolean,
|
||||
nodeType: string,
|
||||
) => {
|
||||
if (!params || params.length === 0) return [];
|
||||
return params.map((param: any) => {
|
||||
const dataType = nodeIsChildren
|
||||
? `Array<${param.dataType || 'String'}>`
|
||||
: param.dataType || 'String';
|
||||
return {
|
||||
label: param.name,
|
||||
dataType: dataType,
|
||||
value: parentId + '.' + param.name,
|
||||
selectable: true,
|
||||
nodeType: nodeType,
|
||||
children: getChildren(
|
||||
param.children,
|
||||
parentId + '.' + param.name,
|
||||
nodeIsChildren,
|
||||
nodeType,
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const nodeToOptions = (
|
||||
node: Node,
|
||||
nodeIsChildren: boolean,
|
||||
currentNode: Node,
|
||||
) => {
|
||||
const options = getOptions();
|
||||
const nodeType = node.type || '';
|
||||
|
||||
let icon: string | undefined = nodeIcons[nodeType];
|
||||
if (!icon && options?.customNodes && options.customNodes[nodeType]) {
|
||||
icon = options.customNodes[nodeType].icon;
|
||||
}
|
||||
|
||||
// 如果仍然获取不到,尝试使用 data.icon (作为回退)
|
||||
if (!icon && node.data && node.data.icon) {
|
||||
icon = node.data.icon as string;
|
||||
}
|
||||
|
||||
const title = node.data.title;
|
||||
|
||||
if (nodeType === 'startNode') {
|
||||
const parameters = node.data.parameters as Array<Parameter>;
|
||||
const children = [];
|
||||
if (parameters)
|
||||
for (const parameter of parameters) {
|
||||
const dataType = nodeIsChildren
|
||||
? `Array<${param.dataType || 'String'}>`
|
||||
: (param.dataType || 'String');
|
||||
return {
|
||||
label: param.name,
|
||||
dataType: dataType,
|
||||
value: parentId + '.' + param.name,
|
||||
selectable: true,
|
||||
nodeType: nodeType,
|
||||
children: getChildren(param.children, parentId + '.' + param.name, nodeIsChildren, nodeType)
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const nodeToOptions = (node: Node, nodeIsChildren: boolean, currentNode: Node) => {
|
||||
const options = getOptions();
|
||||
const nodeType = node.type || '';
|
||||
|
||||
let icon: string | undefined = nodeIcons[nodeType];
|
||||
if (!icon && options?.customNodes && options.customNodes[nodeType]) {
|
||||
icon = options.customNodes[nodeType].icon;
|
||||
}
|
||||
|
||||
// 如果仍然获取不到,尝试使用 data.icon (作为回退)
|
||||
if (!icon && node.data && node.data.icon) {
|
||||
icon = node.data.icon as string;
|
||||
}
|
||||
|
||||
const title = node.data.title;
|
||||
|
||||
if (nodeType === 'startNode') {
|
||||
const parameters = node.data.parameters as Array<Parameter>;
|
||||
const children = [];
|
||||
if (parameters)
|
||||
for (const parameter of parameters) {
|
||||
const dataType = nodeIsChildren
|
||||
? `Array<${parameter.dataType || 'String'}>`
|
||||
: (parameter.dataType || 'String');
|
||||
children.push({
|
||||
label: parameter.name,
|
||||
dataType: dataType,
|
||||
value: node.id + '.' + parameter.name,
|
||||
selectable: true,
|
||||
nodeType: nodeType
|
||||
});
|
||||
}
|
||||
return {
|
||||
label: title,
|
||||
icon: icon,
|
||||
value: node.id,
|
||||
selectable: false,
|
||||
nodeType: nodeType,
|
||||
children
|
||||
};
|
||||
} else if (nodeType === 'loopNode' && currentNode.parentId) {
|
||||
return {
|
||||
label: title,
|
||||
icon: icon,
|
||||
value: node.id,
|
||||
selectable: false,
|
||||
nodeType: nodeType,
|
||||
children: [
|
||||
{
|
||||
label: 'loopItem',
|
||||
dataType: 'Any',
|
||||
value: node.id + '.loopItem',
|
||||
selectable: true,
|
||||
nodeType: nodeType
|
||||
},
|
||||
{
|
||||
label: 'index',
|
||||
dataType: 'Number',
|
||||
value: node.id + '.index',
|
||||
selectable: true,
|
||||
nodeType: nodeType
|
||||
}
|
||||
]
|
||||
};
|
||||
} else {
|
||||
const outputDefs = node.data.outputDefs;
|
||||
if (outputDefs) {
|
||||
return {
|
||||
label: title,
|
||||
icon: icon,
|
||||
value: node.id,
|
||||
selectable: false,
|
||||
nodeType: nodeType,
|
||||
children: getChildren(outputDefs, node.id, nodeIsChildren, nodeType)
|
||||
};
|
||||
}
|
||||
? `Array<${parameter.dataType || 'String'}>`
|
||||
: parameter.dataType || 'String';
|
||||
children.push({
|
||||
label: parameter.name,
|
||||
dataType: dataType,
|
||||
value: node.id + '.' + parameter.name,
|
||||
selectable: true,
|
||||
nodeType: nodeType,
|
||||
});
|
||||
}
|
||||
return {
|
||||
label: title,
|
||||
icon: icon,
|
||||
value: node.id,
|
||||
selectable: false,
|
||||
nodeType: nodeType,
|
||||
children,
|
||||
};
|
||||
} else if (nodeType === 'loopNode' && currentNode.parentId) {
|
||||
return {
|
||||
label: title,
|
||||
icon: icon,
|
||||
value: node.id,
|
||||
selectable: false,
|
||||
nodeType: nodeType,
|
||||
children: [
|
||||
{
|
||||
label: 'loopItem',
|
||||
dataType: 'Any',
|
||||
value: node.id + '.loopItem',
|
||||
selectable: true,
|
||||
nodeType: nodeType,
|
||||
},
|
||||
{
|
||||
label: 'index',
|
||||
dataType: 'Number',
|
||||
value: node.id + '.index',
|
||||
selectable: true,
|
||||
nodeType: nodeType,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
const outputDefs = node.data.outputDefs;
|
||||
if (outputDefs) {
|
||||
return {
|
||||
label: title,
|
||||
icon: icon,
|
||||
value: node.id,
|
||||
selectable: false,
|
||||
nodeType: nodeType,
|
||||
children: getChildren(outputDefs, node.id, nodeIsChildren, nodeType),
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const useRefOptions: any = (useChildrenOnly: boolean = false) => {
|
||||
const currentNodeId = getCurrentNodeId();
|
||||
const currentNode = useNodesData(currentNodeId);
|
||||
const { nodes, edges, nodeLookup } = $derived(useStore());
|
||||
const currentNodeId = getCurrentNodeId();
|
||||
const currentNode = useNodesData(currentNodeId);
|
||||
const { nodes, edges, nodeLookup } = $derived(useStore());
|
||||
|
||||
let selectItems = $derived.by(() => {
|
||||
const resultOptions = [];
|
||||
if (!currentNode.current) {
|
||||
return [];
|
||||
let selectItems = $derived.by(() => {
|
||||
const resultOptions = [];
|
||||
if (!currentNode.current) {
|
||||
return [];
|
||||
}
|
||||
|
||||
//通过 nodeLookup.get 才会得到有 parentId 的 node
|
||||
const cNode = nodeLookup.get(currentNodeId)!;
|
||||
|
||||
if (useChildrenOnly) {
|
||||
for (const node of nodes) {
|
||||
const nodeIsChildren = node.parentId === currentNode.current.id;
|
||||
if (nodeIsChildren) {
|
||||
const nodeOptions = nodeToOptions(node, nodeIsChildren, cNode);
|
||||
nodeOptions && resultOptions.push(nodeOptions);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const refNodeIds: string[] = [];
|
||||
fillRefNodeIds(refNodeIds, currentNodeId, edges);
|
||||
|
||||
//通过 nodeLookup.get 才会得到有 parentId 的 node
|
||||
const cNode = nodeLookup.get(currentNodeId)!;
|
||||
|
||||
if (useChildrenOnly) {
|
||||
for (const node of nodes) {
|
||||
const nodeIsChildren = node.parentId === currentNode.current.id;
|
||||
if (nodeIsChildren) {
|
||||
const nodeOptions = nodeToOptions(node, nodeIsChildren, cNode);
|
||||
nodeOptions && resultOptions.push(nodeOptions);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const refNodeIds: string[] = [];
|
||||
fillRefNodeIds(refNodeIds, currentNodeId, edges);
|
||||
|
||||
for (const node of nodes) {
|
||||
if (refNodeIds.includes(node.id)) {
|
||||
const nodeIsChildren = node.parentId === currentNode.current.id;
|
||||
const nodeOptions = nodeToOptions(node, nodeIsChildren, cNode);
|
||||
nodeOptions && resultOptions.push(nodeOptions);
|
||||
}
|
||||
}
|
||||
for (const node of nodes) {
|
||||
if (refNodeIds.includes(node.id)) {
|
||||
const nodeIsChildren = node.parentId === currentNode.current.id;
|
||||
const nodeOptions = nodeToOptions(node, nodeIsChildren, cNode);
|
||||
nodeOptions && resultOptions.push(nodeOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultOptions;
|
||||
});
|
||||
return resultOptions;
|
||||
});
|
||||
|
||||
return {
|
||||
get current() {
|
||||
return selectItems;
|
||||
}
|
||||
};
|
||||
return {
|
||||
get current() {
|
||||
return selectItems;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import {store} from '#store/stores.svelte';
|
||||
import { store } from '#store/stores.svelte';
|
||||
|
||||
export const useUpdateEdgeData = () => {
|
||||
const updateEdgeData = (id: string, dataUpdate: any, options?: { replace: boolean }) => {
|
||||
const edge = store.getEdge(id);
|
||||
if (!edge) {
|
||||
return;
|
||||
const updateEdgeData = (
|
||||
id: string,
|
||||
dataUpdate: any,
|
||||
options?: { replace: boolean },
|
||||
) => {
|
||||
const edge = store.getEdge(id);
|
||||
if (!edge) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextData =
|
||||
typeof dataUpdate === 'function' ? dataUpdate(edge) : dataUpdate;
|
||||
edge.data = options?.replace ? nextData : { ...edge.data, ...nextData };
|
||||
|
||||
store.updateEdges((edges) => {
|
||||
return edges.map((e) => {
|
||||
if (e.id === id) {
|
||||
return edge;
|
||||
}
|
||||
|
||||
const nextData = typeof dataUpdate === 'function' ? dataUpdate(edge) : dataUpdate;
|
||||
edge.data = options?.replace ? nextData : { ...edge.data, ...nextData };
|
||||
|
||||
store.updateEdges((edges) => {
|
||||
return edges.map((e) => {
|
||||
if (e.id === id) {
|
||||
return edge;
|
||||
}
|
||||
return e;
|
||||
});
|
||||
});
|
||||
};
|
||||
return {
|
||||
updateEdgeData
|
||||
};
|
||||
return e;
|
||||
});
|
||||
});
|
||||
};
|
||||
return {
|
||||
updateEdgeData,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,79 +1,90 @@
|
||||
export const componentName = 'tinyflow-component';
|
||||
|
||||
export const nodeIcons: Record<string, string> = {
|
||||
startNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z"></path></svg>',
|
||||
loopNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path></svg>',
|
||||
conditionNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4H10V10H4V4ZM14 4H20V10H14V4ZM4 14H10V20H4V14ZM14 14H20V20H14V14ZM10 7H14V9H10V7ZM7 10H9V14H7V10ZM15 10H17V14H15V10Z"></path></svg>',
|
||||
llmNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.7134 7.12811L20.4668 7.69379C20.2864 8.10792 19.7136 8.10792 19.5331 7.69379L19.2866 7.12811C18.8471 6.11947 18.0555 5.31641 17.0677 4.87708L16.308 4.53922C15.8973 4.35653 15.8973 3.75881 16.308 3.57612L17.0252 3.25714C18.0384 2.80651 18.8442 1.97373 19.2761 0.930828L19.5293 0.319534C19.7058 -0.106511 20.2942 -0.106511 20.4706 0.319534L20.7238 0.930828C21.1558 1.97373 21.9616 2.80651 22.9748 3.25714L23.6919 3.57612C24.1027 3.75881 24.1027 4.35653 23.6919 4.53922L22.9323 4.87708C21.9445 5.31641 21.1529 6.11947 20.7134 7.12811ZM9 2C13.0675 2 16.426 5.03562 16.9337 8.96494L19.1842 12.5037C19.3324 12.7367 19.3025 13.0847 18.9593 13.2317L17 14.071V17C17 18.1046 16.1046 19 15 19H13.001L13 22H4L4.00025 18.3061C4.00033 17.1252 3.56351 16.0087 2.7555 15.0011C1.65707 13.6313 1 11.8924 1 10C1 5.58172 4.58172 2 9 2ZM9 4C5.68629 4 3 6.68629 3 10C3 11.3849 3.46818 12.6929 4.31578 13.7499C5.40965 15.114 6.00036 16.6672 6.00025 18.3063L6.00013 20H11.0007L11.0017 17H15V12.7519L16.5497 12.0881L15.0072 9.66262L14.9501 9.22118C14.5665 6.25141 12.0243 4 9 4ZM19.4893 16.9929L21.1535 18.1024C22.32 16.3562 23 14.2576 23 12.0001C23 11.317 22.9378 10.6486 22.8186 10L20.8756 10.5C20.9574 10.9878 21 11.489 21 12.0001C21 13.8471 20.4436 15.5642 19.4893 16.9929Z"></path></svg>',
|
||||
knowledgeNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 5C13.567 5 12 6.567 12 8.5C12 10.433 13.567 12 15.5 12C17.433 12 19 10.433 19 8.5C19 6.567 17.433 5 15.5 5ZM10 8.5C10 5.46243 12.4624 3 15.5 3C18.5376 3 21 5.46243 21 8.5C21 9.6575 20.6424 10.7315 20.0317 11.6175L22.7071 14.2929L21.2929 15.7071L18.6175 13.0317C17.7315 13.6424 16.6575 14 15.5 14C12.4624 14 10 11.5376 10 8.5ZM3 4H8V6H3V4ZM3 11H8V13H3V11ZM21 18V20H3V18H21Z"></path></svg>',
|
||||
searchEngineNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path></svg>',
|
||||
httpNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.23509 6.45329C4.85101 7.89148 4 9.84636 4 12C4 16.4183 7.58172 20 12 20C13.0808 20 14.1116 19.7857 15.0521 19.3972C15.1671 18.6467 14.9148 17.9266 14.8116 17.6746C14.582 17.115 13.8241 16.1582 12.5589 14.8308C12.2212 14.4758 12.2429 14.2035 12.3636 13.3943L12.3775 13.3029C12.4595 12.7486 12.5971 12.4209 14.4622 12.1248C15.4097 11.9746 15.6589 12.3533 16.0043 12.8777C16.0425 12.9358 16.0807 12.9928 16.1198 13.0499C16.4479 13.5297 16.691 13.6394 17.0582 13.8064C17.2227 13.881 17.428 13.9751 17.7031 14.1314C18.3551 14.504 18.3551 14.9247 18.3551 15.8472V15.9518C18.3551 16.3434 18.3168 16.6872 18.2566 16.9859C19.3478 15.6185 20 13.8854 20 12C20 8.70089 18.003 5.8682 15.1519 4.64482C14.5987 5.01813 13.8398 5.54726 13.575 5.91C13.4396 6.09538 13.2482 7.04166 12.6257 7.11976C12.4626 7.14023 12.2438 7.12589 12.012 7.11097C11.3905 7.07058 10.5402 7.01606 10.268 7.75495C10.0952 8.2232 10.0648 9.49445 10.6239 10.1543C10.7134 10.2597 10.7307 10.4547 10.6699 10.6735C10.59 10.9608 10.4286 11.1356 10.3783 11.1717C10.2819 11.1163 10.0896 10.8931 9.95938 10.7412C9.64554 10.3765 9.25405 9.92233 8.74797 9.78176C8.56395 9.73083 8.36166 9.68867 8.16548 9.64736C7.6164 9.53227 6.99443 9.40134 6.84992 9.09302C6.74442 8.8672 6.74488 8.55621 6.74529 8.22764C6.74529 7.8112 6.74529 7.34029 6.54129 6.88256C6.46246 6.70541 6.35689 6.56446 6.23509 6.45329ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22Z"></path></svg>',
|
||||
codeNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M23 12L15.9289 19.0711L14.5147 17.6569L20.1716 12L14.5147 6.34317L15.9289 4.92896L23 12ZM3.82843 12L9.48528 17.6569L8.07107 19.0711L1 12L8.07107 4.92896L9.48528 6.34317L3.82843 12Z"></path></svg>',
|
||||
templateNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4ZM4 5V19H20V5H4ZM7 8H17V11H15V10H13V14H14.5V16H9.5V14H11V10H9V11H7V8Z"></path></svg>',
|
||||
confirmNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.3873 13.4975L17.9403 20.5117L13.2418 22.2218L10.6889 15.2076L6.79004 17.6529L8.4086 1.63318L19.9457 12.8646L15.3873 13.4975ZM15.3768 19.3163L12.6618 11.8568L15.6212 11.4459L9.98201 5.9561L9.19088 13.7863L11.7221 12.1988L14.4371 19.6583L15.3768 19.3163Z"></path></svg>',
|
||||
endNode: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 5.1438V16.0002H18.3391L6 5.1438ZM4 2.932C4 2.07155 5.01456 1.61285 5.66056 2.18123L21.6501 16.2494C22.3423 16.8584 21.9116 18.0002 20.9896 18.0002H6V22H4V2.932Z"></path></svg>'
|
||||
startNode:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z"></path></svg>',
|
||||
loopNode:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path></svg>',
|
||||
conditionNode:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4H10V10H4V4ZM14 4H20V10H14V4ZM4 14H10V20H4V14ZM14 14H20V20H14V14ZM10 7H14V9H10V7ZM7 10H9V14H7V10ZM15 10H17V14H15V10Z"></path></svg>',
|
||||
llmNode:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.7134 7.12811L20.4668 7.69379C20.2864 8.10792 19.7136 8.10792 19.5331 7.69379L19.2866 7.12811C18.8471 6.11947 18.0555 5.31641 17.0677 4.87708L16.308 4.53922C15.8973 4.35653 15.8973 3.75881 16.308 3.57612L17.0252 3.25714C18.0384 2.80651 18.8442 1.97373 19.2761 0.930828L19.5293 0.319534C19.7058 -0.106511 20.2942 -0.106511 20.4706 0.319534L20.7238 0.930828C21.1558 1.97373 21.9616 2.80651 22.9748 3.25714L23.6919 3.57612C24.1027 3.75881 24.1027 4.35653 23.6919 4.53922L22.9323 4.87708C21.9445 5.31641 21.1529 6.11947 20.7134 7.12811ZM9 2C13.0675 2 16.426 5.03562 16.9337 8.96494L19.1842 12.5037C19.3324 12.7367 19.3025 13.0847 18.9593 13.2317L17 14.071V17C17 18.1046 16.1046 19 15 19H13.001L13 22H4L4.00025 18.3061C4.00033 17.1252 3.56351 16.0087 2.7555 15.0011C1.65707 13.6313 1 11.8924 1 10C1 5.58172 4.58172 2 9 2ZM9 4C5.68629 4 3 6.68629 3 10C3 11.3849 3.46818 12.6929 4.31578 13.7499C5.40965 15.114 6.00036 16.6672 6.00025 18.3063L6.00013 20H11.0007L11.0017 17H15V12.7519L16.5497 12.0881L15.0072 9.66262L14.9501 9.22118C14.5665 6.25141 12.0243 4 9 4ZM19.4893 16.9929L21.1535 18.1024C22.32 16.3562 23 14.2576 23 12.0001C23 11.317 22.9378 10.6486 22.8186 10L20.8756 10.5C20.9574 10.9878 21 11.489 21 12.0001C21 13.8471 20.4436 15.5642 19.4893 16.9929Z"></path></svg>',
|
||||
knowledgeNode:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 5C13.567 5 12 6.567 12 8.5C12 10.433 13.567 12 15.5 12C17.433 12 19 10.433 19 8.5C19 6.567 17.433 5 15.5 5ZM10 8.5C10 5.46243 12.4624 3 15.5 3C18.5376 3 21 5.46243 21 8.5C21 9.6575 20.6424 10.7315 20.0317 11.6175L22.7071 14.2929L21.2929 15.7071L18.6175 13.0317C17.7315 13.6424 16.6575 14 15.5 14C12.4624 14 10 11.5376 10 8.5ZM3 4H8V6H3V4ZM3 11H8V13H3V11ZM21 18V20H3V18H21Z"></path></svg>',
|
||||
searchEngineNode:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path></svg>',
|
||||
httpNode:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.23509 6.45329C4.85101 7.89148 4 9.84636 4 12C4 16.4183 7.58172 20 12 20C13.0808 20 14.1116 19.7857 15.0521 19.3972C15.1671 18.6467 14.9148 17.9266 14.8116 17.6746C14.582 17.115 13.8241 16.1582 12.5589 14.8308C12.2212 14.4758 12.2429 14.2035 12.3636 13.3943L12.3775 13.3029C12.4595 12.7486 12.5971 12.4209 14.4622 12.1248C15.4097 11.9746 15.6589 12.3533 16.0043 12.8777C16.0425 12.9358 16.0807 12.9928 16.1198 13.0499C16.4479 13.5297 16.691 13.6394 17.0582 13.8064C17.2227 13.881 17.428 13.9751 17.7031 14.1314C18.3551 14.504 18.3551 14.9247 18.3551 15.8472V15.9518C18.3551 16.3434 18.3168 16.6872 18.2566 16.9859C19.3478 15.6185 20 13.8854 20 12C20 8.70089 18.003 5.8682 15.1519 4.64482C14.5987 5.01813 13.8398 5.54726 13.575 5.91C13.4396 6.09538 13.2482 7.04166 12.6257 7.11976C12.4626 7.14023 12.2438 7.12589 12.012 7.11097C11.3905 7.07058 10.5402 7.01606 10.268 7.75495C10.0952 8.2232 10.0648 9.49445 10.6239 10.1543C10.7134 10.2597 10.7307 10.4547 10.6699 10.6735C10.59 10.9608 10.4286 11.1356 10.3783 11.1717C10.2819 11.1163 10.0896 10.8931 9.95938 10.7412C9.64554 10.3765 9.25405 9.92233 8.74797 9.78176C8.56395 9.73083 8.36166 9.68867 8.16548 9.64736C7.6164 9.53227 6.99443 9.40134 6.84992 9.09302C6.74442 8.8672 6.74488 8.55621 6.74529 8.22764C6.74529 7.8112 6.74529 7.34029 6.54129 6.88256C6.46246 6.70541 6.35689 6.56446 6.23509 6.45329ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22Z"></path></svg>',
|
||||
codeNode:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M23 12L15.9289 19.0711L14.5147 17.6569L20.1716 12L14.5147 6.34317L15.9289 4.92896L23 12ZM3.82843 12L9.48528 17.6569L8.07107 19.0711L1 12L8.07107 4.92896L9.48528 6.34317L3.82843 12Z"></path></svg>',
|
||||
templateNode:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4ZM4 5V19H20V5H4ZM7 8H17V11H15V10H13V14H14.5V16H9.5V14H11V10H9V11H7V8Z"></path></svg>',
|
||||
confirmNode:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.3873 13.4975L17.9403 20.5117L13.2418 22.2218L10.6889 15.2076L6.79004 17.6529L8.4086 1.63318L19.9457 12.8646L15.3873 13.4975ZM15.3768 19.3163L12.6618 11.8568L15.6212 11.4459L9.98201 5.9561L9.19088 13.7863L11.7221 12.1988L14.4371 19.6583L15.3768 19.3163Z"></path></svg>',
|
||||
endNode:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 5.1438V16.0002H18.3391L6 5.1438ZM4 2.932C4 2.07155 5.01456 1.61285 5.66056 2.18123L21.6501 16.2494C22.3423 16.8584 21.9116 18.0002 20.9896 18.0002H6V22H4V2.932Z"></path></svg>',
|
||||
};
|
||||
|
||||
export const parameterDataTypes = [
|
||||
{
|
||||
value: 'String',
|
||||
label: 'String'
|
||||
},
|
||||
{
|
||||
value: 'Number',
|
||||
label: 'Number'
|
||||
},
|
||||
{
|
||||
value: 'Boolean',
|
||||
label: 'Boolean'
|
||||
},
|
||||
{
|
||||
value: 'File',
|
||||
label: 'File'
|
||||
},
|
||||
{
|
||||
value: 'Object',
|
||||
label: 'Object'
|
||||
},
|
||||
{
|
||||
value: 'Array',
|
||||
label: 'Array'
|
||||
}
|
||||
{
|
||||
value: 'String',
|
||||
label: 'String',
|
||||
},
|
||||
{
|
||||
value: 'Number',
|
||||
label: 'Number',
|
||||
},
|
||||
{
|
||||
value: 'Boolean',
|
||||
label: 'Boolean',
|
||||
},
|
||||
{
|
||||
value: 'File',
|
||||
label: 'File',
|
||||
},
|
||||
{
|
||||
value: 'Object',
|
||||
label: 'Object',
|
||||
},
|
||||
{
|
||||
value: 'Array',
|
||||
label: 'Array',
|
||||
},
|
||||
];
|
||||
|
||||
export const parameterRefTypes = [
|
||||
{
|
||||
value: 'ref',
|
||||
label: '引用'
|
||||
},
|
||||
{
|
||||
value: 'fixed',
|
||||
label: '固定值'
|
||||
}
|
||||
// {
|
||||
// value: 'input',
|
||||
// label: '输入'
|
||||
// }
|
||||
{
|
||||
value: 'ref',
|
||||
label: '引用',
|
||||
},
|
||||
{
|
||||
value: 'fixed',
|
||||
label: '固定值',
|
||||
},
|
||||
// {
|
||||
// value: 'input',
|
||||
// label: '输入'
|
||||
// }
|
||||
];
|
||||
|
||||
export const contentTypes = [
|
||||
{ label: '文字', value: 'text' },
|
||||
{ label: '图片', value: 'image' },
|
||||
{ label: '视频', value: 'video' },
|
||||
{ label: '音频', value: 'audio' },
|
||||
{ label: '文件', value: 'file' },
|
||||
{ label: '其他', value: 'other' }
|
||||
{ label: '文字', value: 'text' },
|
||||
{ label: '图片', value: 'image' },
|
||||
{ label: '视频', value: 'video' },
|
||||
{ label: '音频', value: 'audio' },
|
||||
{ label: '文件', value: 'file' },
|
||||
{ label: '其他', value: 'other' },
|
||||
];
|
||||
|
||||
export const startFormTypes = [
|
||||
{ label: '单行输入框', value: 'input' },
|
||||
{ label: '多行输入框', value: 'textarea' },
|
||||
{ label: '下拉菜单', value: 'select' },
|
||||
{ label: '单选', value: 'radio' },
|
||||
{ label: '多选', value: 'checkbox' }
|
||||
{ label: '单行输入框', value: 'input' },
|
||||
{ label: '多行输入框', value: 'textarea' },
|
||||
{ label: '下拉菜单', value: 'select' },
|
||||
{ label: '单选', value: 'radio' },
|
||||
{ label: '多选', value: 'checkbox' },
|
||||
];
|
||||
|
||||
export const confirmFormTypes = [
|
||||
{ label: '单选', value: 'radio' },
|
||||
{ label: '多选', value: 'checkbox' }
|
||||
{ label: '单选', value: 'radio' },
|
||||
{ label: '多选', value: 'checkbox' },
|
||||
];
|
||||
|
||||
@@ -1,74 +1,78 @@
|
||||
import {type Edge, type Node, type Viewport} from '@xyflow/svelte';
|
||||
import { type Edge, type Node, type Viewport } from '@xyflow/svelte';
|
||||
|
||||
const createStore = () => {
|
||||
let nodesInternal = $state.raw([] as Node[]);
|
||||
let edgesInternal = $state.raw([] as Edge[]);
|
||||
let viewport = $state.raw({ x: 250, y: 100, zoom: 1 } as Viewport);
|
||||
let nodesInternal = $state.raw([] as Node[]);
|
||||
let edgesInternal = $state.raw([] as Edge[]);
|
||||
let viewport = $state.raw({ x: 250, y: 100, zoom: 1 } as Viewport);
|
||||
|
||||
return {
|
||||
// nodes: nodesInternal,
|
||||
// edges: edgesInternal,
|
||||
// viewport,
|
||||
init: (nodes: Node[], edges: Edge[]) => {
|
||||
nodesInternal = nodes;
|
||||
edgesInternal = edges;
|
||||
},
|
||||
return {
|
||||
// nodes: nodesInternal,
|
||||
// edges: edgesInternal,
|
||||
// viewport,
|
||||
init: (nodes: Node[], edges: Edge[]) => {
|
||||
nodesInternal = nodes;
|
||||
edgesInternal = edges;
|
||||
},
|
||||
|
||||
getNodes: () => nodesInternal,
|
||||
setNodes: (nodes: Node[]) => {
|
||||
nodesInternal = nodes;
|
||||
},
|
||||
getEdges: () => edgesInternal,
|
||||
setEdges: (edges: Edge[]) => {
|
||||
edgesInternal = edges;
|
||||
},
|
||||
getViewport: () => viewport,
|
||||
setViewport: (v: Viewport) => {
|
||||
viewport = v;
|
||||
},
|
||||
getNodes: () => nodesInternal,
|
||||
setNodes: (nodes: Node[]) => {
|
||||
nodesInternal = nodes;
|
||||
},
|
||||
getEdges: () => edgesInternal,
|
||||
setEdges: (edges: Edge[]) => {
|
||||
edgesInternal = edges;
|
||||
},
|
||||
getViewport: () => viewport,
|
||||
setViewport: (v: Viewport) => {
|
||||
viewport = v;
|
||||
},
|
||||
|
||||
getNode: (id: string) => nodesInternal.find((node) => node.id === id),
|
||||
addNode: (node: Node) => {
|
||||
nodesInternal = [...nodesInternal, node];
|
||||
},
|
||||
removeNode: (id: string) => {
|
||||
nodesInternal = nodesInternal.filter((node) => node.id !== id);
|
||||
},
|
||||
updateNode: (id: string, node: Node) => {
|
||||
nodesInternal = nodesInternal.map((n) => (n.id === id ? { ...n, ...node } : n));
|
||||
},
|
||||
updateNodes: (update: (nodes: Node[]) => Node[]) => {
|
||||
nodesInternal = update(nodesInternal);
|
||||
},
|
||||
updateNodeData: (id: string, data: Node['data']) => {
|
||||
nodesInternal = nodesInternal.map((n) =>
|
||||
n.id === id ? { ...n, data: { ...n.data, ...data } } : n
|
||||
);
|
||||
},
|
||||
selectNodeOnly: (id: string) => {
|
||||
nodesInternal = nodesInternal.map((n) =>
|
||||
n.id === id ? { ...n, selected: true } : { ...n, selected: false }
|
||||
);
|
||||
},
|
||||
getEdge: (id: string) => edgesInternal.find((edge) => edge.id === id),
|
||||
addEdge: (edge: Edge) => {
|
||||
edgesInternal = [...edgesInternal, edge];
|
||||
},
|
||||
removeEdge: (id: string) => {
|
||||
edgesInternal = edgesInternal.filter((edge) => edge.id !== id);
|
||||
},
|
||||
updateEdge: (id: string, edge: Edge) => {
|
||||
edgesInternal = edgesInternal.map((e) => (e.id === id ? { ...e, ...edge } : e));
|
||||
},
|
||||
updateEdges: (update: (edges: Edge[]) => Edge[]) => {
|
||||
edgesInternal = update(edgesInternal);
|
||||
},
|
||||
updateEdgeData: (id: string, data: Edge['data']) => {
|
||||
edgesInternal = edgesInternal.map((e) =>
|
||||
e.id === id ? { ...e, data: { ...e.data, ...data } } : e
|
||||
);
|
||||
}
|
||||
};
|
||||
getNode: (id: string) => nodesInternal.find((node) => node.id === id),
|
||||
addNode: (node: Node) => {
|
||||
nodesInternal = [...nodesInternal, node];
|
||||
},
|
||||
removeNode: (id: string) => {
|
||||
nodesInternal = nodesInternal.filter((node) => node.id !== id);
|
||||
},
|
||||
updateNode: (id: string, node: Node) => {
|
||||
nodesInternal = nodesInternal.map((n) =>
|
||||
n.id === id ? { ...n, ...node } : n,
|
||||
);
|
||||
},
|
||||
updateNodes: (update: (nodes: Node[]) => Node[]) => {
|
||||
nodesInternal = update(nodesInternal);
|
||||
},
|
||||
updateNodeData: (id: string, data: Node['data']) => {
|
||||
nodesInternal = nodesInternal.map((n) =>
|
||||
n.id === id ? { ...n, data: { ...n.data, ...data } } : n,
|
||||
);
|
||||
},
|
||||
selectNodeOnly: (id: string) => {
|
||||
nodesInternal = nodesInternal.map((n) =>
|
||||
n.id === id ? { ...n, selected: true } : { ...n, selected: false },
|
||||
);
|
||||
},
|
||||
getEdge: (id: string) => edgesInternal.find((edge) => edge.id === id),
|
||||
addEdge: (edge: Edge) => {
|
||||
edgesInternal = [...edgesInternal, edge];
|
||||
},
|
||||
removeEdge: (id: string) => {
|
||||
edgesInternal = edgesInternal.filter((edge) => edge.id !== id);
|
||||
},
|
||||
updateEdge: (id: string, edge: Edge) => {
|
||||
edgesInternal = edgesInternal.map((e) =>
|
||||
e.id === id ? { ...e, ...edge } : e,
|
||||
);
|
||||
},
|
||||
updateEdges: (update: (edges: Edge[]) => Edge[]) => {
|
||||
edgesInternal = update(edgesInternal);
|
||||
},
|
||||
updateEdgeData: (id: string, data: Edge['data']) => {
|
||||
edgesInternal = edgesInternal.map((e) =>
|
||||
e.id === id ? { ...e, data: { ...e.data, ...data } } : e,
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const store = createStore();
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
.tf-btn {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
background: var(--tf-bg-surface);
|
||||
border: 1px solid var(--tf-border-color-strong);
|
||||
color: var(--tf-text-primary);
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
height: fit-content;
|
||||
width: fit-content;
|
||||
color: var(--tf-text-primary);
|
||||
cursor: pointer;
|
||||
background: var(--tf-bg-surface);
|
||||
border: 1px solid var(--tf-border-color-strong);
|
||||
border-radius: 5px;
|
||||
|
||||
svg {
|
||||
fill: currentColor;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -25,49 +25,48 @@
|
||||
}
|
||||
|
||||
.tf-btn.tf-btn-primary {
|
||||
background: var(--tf-primary-color);
|
||||
color: var(--tf-loop-handle-text);
|
||||
background: var(--tf-primary-color);
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--tf-border-color-strong);
|
||||
background: var(--tf-primary-color-hover);
|
||||
border: 1px solid var(--tf-border-color-strong);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tf-input, .tf-textarea {
|
||||
display: flex;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--tf-border-color-strong);
|
||||
background: var(--tf-bg-input);
|
||||
color: var(--tf-text-primary);
|
||||
padding: 5px 8px;
|
||||
.tf-input,
|
||||
.tf-textarea {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
padding: 5px 8px;
|
||||
overflow-y: hidden;
|
||||
line-height: normal;
|
||||
color: var(--tf-text-primary);
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
line-height: normal;
|
||||
overflow-y: hidden;
|
||||
background: var(--tf-bg-input);
|
||||
border: 1px solid var(--tf-border-color-strong);
|
||||
border-radius: 5px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--tf-text-muted);
|
||||
font-size: 12px;
|
||||
color: var(--tf-text-muted);
|
||||
}
|
||||
|
||||
// 获得焦点时的样式
|
||||
/* 获得焦点时的样式 */
|
||||
&:focus {
|
||||
border-color: var(--tf-primary-color);
|
||||
box-shadow: var(--tf-focus-shadow);
|
||||
}
|
||||
|
||||
// 禁用时的样式
|
||||
/* 禁用时的样式 */
|
||||
&[disabled] {
|
||||
background-color: var(--tf-bg-input-disabled);
|
||||
cursor: not-allowed;
|
||||
color: var(--tf-text-disabled);
|
||||
cursor: not-allowed;
|
||||
background-color: var(--tf-bg-input-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tf-select {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
@@ -75,47 +74,48 @@
|
||||
|
||||
&-input {
|
||||
display: flex;
|
||||
border: 1px solid var(--tf-border-color);
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 4px 10px;
|
||||
font-size: 13px;
|
||||
color: var(--tf-text-primary);
|
||||
cursor: pointer;
|
||||
background: var(--tf-bg-input);
|
||||
height: 32px;
|
||||
border: 1px solid var(--tf-border-color);
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
width: 100%;
|
||||
color: var(--tf-text-primary);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--tf-border-color-strong);
|
||||
}
|
||||
|
||||
&:focus, &.active {
|
||||
&:focus,
|
||||
&.active {
|
||||
border-color: var(--tf-primary-color);
|
||||
box-shadow: var(--tf-focus-shadow);
|
||||
}
|
||||
|
||||
&-value {
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--tf-text-secondary);
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
color: var(--tf-text-secondary);
|
||||
}
|
||||
|
||||
&-placeholder {
|
||||
@@ -124,7 +124,7 @@
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: none; // 现在改用 .tf-select-wrapper
|
||||
display: none; /* 现在改用 .tf-select-wrapper */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,67 +135,66 @@
|
||||
|
||||
.tf-tabs {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
background: var(--tf-bg-muted);
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
|
||||
.tf-tabs-item {
|
||||
flex-grow: 1;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
color: var(--tf-text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.tf-tabs-item.active {
|
||||
background: var(--tf-bg-surface);
|
||||
color: var(--tf-text-primary);
|
||||
font-weight: 500;
|
||||
color: var(--tf-text-primary);
|
||||
background: var(--tf-bg-surface);
|
||||
box-shadow: var(--tf-shadow-soft);
|
||||
}
|
||||
}
|
||||
|
||||
h3.tf-heading {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 3px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--tf-text-primary);
|
||||
}
|
||||
|
||||
|
||||
.tf-collapse {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
//padding: 5px;
|
||||
|
||||
/* padding: 5px; */
|
||||
|
||||
&-item {
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
|
||||
&-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 3px;
|
||||
margin-right: 10px;
|
||||
color: var(--tf-icon-color);
|
||||
background: var(--tf-icon-bg);
|
||||
border-radius: 5px;
|
||||
padding: 3px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
|
||||
svg {
|
||||
width: 22px;
|
||||
@@ -212,10 +211,9 @@ h3.tf-heading {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&-description {
|
||||
font-size: 12px;
|
||||
margin: 10px 0;
|
||||
font-size: 12px;
|
||||
color: var(--tf-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
.svelte-flow__nodes {
|
||||
|
||||
.svelte-flow__node {
|
||||
box-sizing: border-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
|
||||
|
||||
.svelte-flow__handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: none;
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
background: var(--tf-primary-color);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
content: ' ';
|
||||
background: var(--tf-primary-color);
|
||||
border-radius: 100%;
|
||||
transition: width 0.1s, height 0.1s;
|
||||
transition:
|
||||
width 0.1s,
|
||||
height 0.1s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -32,17 +32,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
div.loop_handle_wrapper {
|
||||
div.loop-handle-wrapper {
|
||||
&::after {
|
||||
content: '循环体';
|
||||
background: var(--tf-primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 20px;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
color: var(--tf-loop-handle-text);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
content: '循环体';
|
||||
background: var(--tf-primary-color);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -53,15 +53,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: ' ';
|
||||
&::after {
|
||||
position: absolute;
|
||||
border-radius: 5px;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
border: 1px solid var(--tf-node-outline-color);
|
||||
height: calc(100% + 2px);
|
||||
width: calc(100% + 2px);
|
||||
height: calc(100% + 2px);
|
||||
content: ' ';
|
||||
border: 1px solid var(--tf-node-outline-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -73,11 +73,11 @@
|
||||
box-shadow: var(--xy-node-boxshadow-selected);
|
||||
}
|
||||
|
||||
&:hover:after {
|
||||
&:hover::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.selectable.selected:after {
|
||||
&.selectable.selected::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,10 @@
|
||||
|
||||
.tf-flow-line-path {
|
||||
stroke: var(--xy-edge-stroke);
|
||||
stroke-width: var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));
|
||||
stroke-width: var(
|
||||
--xy-edge-stroke-width,
|
||||
var(--xy-edge-stroke-width-default)
|
||||
);
|
||||
}
|
||||
|
||||
.tf-flow-line-path--animated {
|
||||
@@ -97,16 +100,16 @@
|
||||
from {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dashoffset: -14;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tf-node-wrapper {
|
||||
border-radius: 5px;
|
||||
min-width: 300px;
|
||||
background: var(--tf-bg-surface);
|
||||
border-radius: 5px;
|
||||
|
||||
&-body {
|
||||
padding: 10px;
|
||||
@@ -118,67 +121,69 @@
|
||||
}
|
||||
|
||||
.tf-bottom-dock {
|
||||
z-index: 200;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: var(--tf-toolbar-bottom, 20px);
|
||||
transform: translateX(-50%);
|
||||
left: 50%;
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
// 单行统一工具栏
|
||||
/* 单行统一工具栏 */
|
||||
.tf-unified-bar {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0 4px;
|
||||
background: var(--tf-bg-surface);
|
||||
border: 1px solid var(--tf-border-color);
|
||||
border-radius: 10px;
|
||||
background: var(--tf-bg-surface);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
padding: 0 4px;
|
||||
gap: 2px;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
||||
|
||||
> * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 分割线
|
||||
/* 分割线 */
|
||||
.tf-bar-divider {
|
||||
flex-shrink: 0;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: var(--tf-border-color);
|
||||
margin: 0 4px;
|
||||
flex-shrink: 0;
|
||||
background: var(--tf-border-color);
|
||||
}
|
||||
|
||||
// 图标按钮
|
||||
/* 图标按钮 */
|
||||
.tf-bar-btn {
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 7px;
|
||||
color: var(--tf-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
flex-shrink: 0;
|
||||
border-radius: 7px;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s;
|
||||
|
||||
svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
fill: currentColor;
|
||||
pointer-events: none;
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--tf-bg-hover);
|
||||
color: var(--tf-text-primary);
|
||||
background: var(--tf-bg-hover);
|
||||
}
|
||||
|
||||
&.tf-bar-btn-active {
|
||||
@@ -191,26 +196,26 @@
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
background: #13b33f;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
background: #13b33f;
|
||||
border-radius: 8px;
|
||||
transition: filter 0.15s;
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: currentColor;
|
||||
pointer-events: none;
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -222,36 +227,38 @@
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 8px;
|
||||
background: #e9edff;
|
||||
color: var(--tf-primary-color);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: var(--tf-primary-color);
|
||||
cursor: pointer;
|
||||
transition: filter 0.15s, background 0.15s;
|
||||
background: #e9edff;
|
||||
border-radius: 8px;
|
||||
transition:
|
||||
filter 0.15s,
|
||||
background 0.15s;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.98);
|
||||
background: #dde5ff;
|
||||
filter: brightness(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
// 百分比缩放选择器单元
|
||||
/* 百分比缩放选择器单元 */
|
||||
.tf-zoom-select-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 6px 0 8px;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
border-radius: 7px;
|
||||
padding: 0 6px 0 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 7px;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
@@ -259,83 +266,80 @@
|
||||
}
|
||||
|
||||
.tf-zoom-icon {
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: var(--tf-text-secondary);
|
||||
pointer-events: none;
|
||||
flex-shrink: 0;
|
||||
fill: var(--tf-text-secondary);
|
||||
}
|
||||
|
||||
.tf-zoom-select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
min-width: 48px;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: var(--tf-text-primary);
|
||||
text-align: center;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
min-width: 48px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.tf-zoom-chevron {
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: var(--tf-text-secondary);
|
||||
pointer-events: none;
|
||||
flex-shrink: 0;
|
||||
fill: var(--tf-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.tf-zoom-select-simple {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
height: 32px;
|
||||
min-width: 64px;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
border-radius: 7px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: var(--tf-text-primary);
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 7px;
|
||||
|
||||
&:hover {
|
||||
background: var(--tf-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tf-toolbar {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&-panel {
|
||||
width: min(520px, calc(100vw - 40px));
|
||||
max-height: min(390px, calc(100vh - 220px));
|
||||
border: 1px solid var(--tf-border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--tf-bg-surface);
|
||||
box-shadow: var(--tf-shadow-soft);
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
z-index: 260;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(100% + 8px);
|
||||
transform: translateX(-50%);
|
||||
z-index: 260;
|
||||
width: min(520px, calc(100vw - 40px));
|
||||
max-height: min(390px, calc(100vh - 220px));
|
||||
padding: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--tf-bg-surface);
|
||||
border: 1px solid var(--tf-border-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--tf-shadow-soft);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&-trigger {
|
||||
@@ -350,7 +354,7 @@
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
--tf-primary-color-hover: #3a6fe3;
|
||||
--tf-primary-soft-bg: #eff6ff;
|
||||
--tf-primary-soft-border: #bfdbfe;
|
||||
--tf-primary-ghost-bg: rgba(37, 99, 235, 0.08);
|
||||
--tf-primary-ghost-bg: rgb(37 99 235 / 8%);
|
||||
--tf-danger-color: #ef4444;
|
||||
--tf-danger-hover: #dc2626;
|
||||
--tf-danger-soft-bg: #fff1f1;
|
||||
@@ -15,41 +15,35 @@
|
||||
--tf-warning-soft-border: #f7d79e;
|
||||
--tf-warning-soft-text: #92400e;
|
||||
--tf-warning-icon-color: #d97706;
|
||||
|
||||
--tf-bg-canvas: #f8fafc;
|
||||
--tf-bg-surface: #ffffff;
|
||||
--tf-bg-surface: #fff;
|
||||
--tf-bg-surface-alt: #f9fafb;
|
||||
--tf-bg-muted: #f3f4f6;
|
||||
--tf-bg-hover: #f1f5f9;
|
||||
--tf-bg-active: #e2e8f0;
|
||||
--tf-bg-input: #ffffff;
|
||||
--tf-bg-input: #fff;
|
||||
--tf-bg-input-disabled: #f3f4f6;
|
||||
--tf-bg-tag: rgba(0, 0, 0, 0.06);
|
||||
|
||||
--tf-bg-tag: rgb(0 0 0 / 6%);
|
||||
--tf-border-color: #e5e7eb;
|
||||
--tf-border-color-strong: #d1d5db;
|
||||
--tf-border-color-soft: #f3f4f6;
|
||||
--tf-node-outline-color: #d1d5db;
|
||||
--tf-node-hover-border: rgba(186, 202, 239, 0.49);
|
||||
|
||||
--tf-node-hover-border: rgb(186 202 239 / 49%);
|
||||
--tf-text-primary: #111827;
|
||||
--tf-text-secondary: #6b7280;
|
||||
--tf-text-muted: #9ca3af;
|
||||
--tf-text-disabled: #9ca3af;
|
||||
|
||||
--tf-icon-bg: #cedafb;
|
||||
--tf-icon-color: #3474ff;
|
||||
--tf-loop-handle-text: #ffffff;
|
||||
|
||||
--tf-shadow-soft: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
--tf-shadow-medium: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
--tf-focus-shadow: 0 0 0 2px rgba(37, 99, 235, 0.12);
|
||||
--tf-loop-handle-text: #fff;
|
||||
--tf-shadow-soft: 0 2px 4px rgb(0 0 0 / 8%);
|
||||
--tf-shadow-medium: 0 10px 25px rgb(0 0 0 / 15%);
|
||||
--tf-focus-shadow: 0 0 0 2px rgb(37 99 235 / 12%);
|
||||
--tf-tip-bg: #0f172a;
|
||||
--tf-tip-text: #f8fafc;
|
||||
--tf-scrollbar-thumb: #d6dce8;
|
||||
--tf-slider-track-bg: #d1d5db;
|
||||
--tf-slider-thumb-bg: var(--tf-primary-color);
|
||||
|
||||
--xy-node-boxshadow-selected: 0 0 0 1px var(--tf-primary-color);
|
||||
--xy-handle-background-color: var(--tf-primary-color);
|
||||
--xy-handle-border-color: var(--tf-bg-surface);
|
||||
@@ -65,7 +59,7 @@
|
||||
--xy-controls-button-color-hover: var(--tf-text-primary);
|
||||
--xy-controls-button-border-color: var(--tf-border-color);
|
||||
--xy-minimap-background-color: var(--tf-bg-surface);
|
||||
--xy-minimap-mask-background-color: rgba(241, 245, 249, 0.68);
|
||||
--xy-minimap-mask-background-color: rgb(241 245 249 / 68%);
|
||||
--xy-minimap-node-background-color: #cbd5e1;
|
||||
--xy-edge-label-background-color: var(--tf-bg-surface);
|
||||
--xy-edge-label-color: var(--tf-text-primary);
|
||||
@@ -74,20 +68,19 @@
|
||||
:root .tf-theme-dark {
|
||||
--tf-primary-color: #60a5fa;
|
||||
--tf-primary-color-hover: #93c5fd;
|
||||
--tf-primary-soft-bg: rgba(96, 165, 250, 0.18);
|
||||
--tf-primary-soft-border: rgba(96, 165, 250, 0.35);
|
||||
--tf-primary-ghost-bg: rgba(96, 165, 250, 0.12);
|
||||
--tf-primary-soft-bg: rgb(96 165 250 / 18%);
|
||||
--tf-primary-soft-border: rgb(96 165 250 / 35%);
|
||||
--tf-primary-ghost-bg: rgb(96 165 250 / 12%);
|
||||
--tf-danger-color: #f87171;
|
||||
--tf-danger-hover: #ef4444;
|
||||
--tf-danger-soft-bg: rgba(248, 113, 113, 0.16);
|
||||
--tf-danger-soft-border: rgba(248, 113, 113, 0.28);
|
||||
--tf-danger-soft-bg: rgb(248 113 113 / 16%);
|
||||
--tf-danger-soft-border: rgb(248 113 113 / 28%);
|
||||
--tf-danger-soft-text: #fca5a5;
|
||||
--tf-danger-icon-color: #f87171;
|
||||
--tf-warning-soft-bg: rgba(217, 119, 6, 0.2);
|
||||
--tf-warning-soft-border: rgba(217, 119, 6, 0.35);
|
||||
--tf-warning-soft-bg: rgb(217 119 6 / 20%);
|
||||
--tf-warning-soft-border: rgb(217 119 6 / 35%);
|
||||
--tf-warning-soft-text: #fbbf24;
|
||||
--tf-warning-icon-color: #f59e0b;
|
||||
|
||||
--tf-bg-canvas: #0b1220;
|
||||
--tf-bg-surface: #111827;
|
||||
--tf-bg-surface-alt: #1f2937;
|
||||
@@ -96,32 +89,27 @@
|
||||
--tf-bg-active: #3b4b63;
|
||||
--tf-bg-input: #0f172a;
|
||||
--tf-bg-input-disabled: #0b1324;
|
||||
--tf-bg-tag: rgba(148, 163, 184, 0.2);
|
||||
|
||||
--tf-bg-tag: rgb(148 163 184 / 20%);
|
||||
--tf-border-color: #334155;
|
||||
--tf-border-color-strong: #475569;
|
||||
--tf-border-color-soft: #263449;
|
||||
--tf-node-outline-color: #475569;
|
||||
--tf-node-hover-border: rgba(96, 165, 250, 0.38);
|
||||
|
||||
--tf-node-hover-border: rgb(96 165 250 / 38%);
|
||||
--tf-text-primary: #e5e7eb;
|
||||
--tf-text-secondary: #cbd5e1;
|
||||
--tf-text-muted: #94a3b8;
|
||||
--tf-text-disabled: #64748b;
|
||||
|
||||
--tf-icon-bg: rgba(96, 165, 250, 0.2);
|
||||
--tf-icon-bg: rgb(96 165 250 / 20%);
|
||||
--tf-icon-color: #93c5fd;
|
||||
--tf-loop-handle-text: #f8fafc;
|
||||
|
||||
--tf-shadow-soft: 0 2px 8px rgba(2, 6, 23, 0.35);
|
||||
--tf-shadow-medium: 0 10px 25px rgba(2, 6, 23, 0.48);
|
||||
--tf-focus-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2);
|
||||
--tf-shadow-soft: 0 2px 8px rgb(2 6 23 / 35%);
|
||||
--tf-shadow-medium: 0 10px 25px rgb(2 6 23 / 48%);
|
||||
--tf-focus-shadow: 0 0 0 2px rgb(96 165 250 / 20%);
|
||||
--tf-tip-bg: #020617;
|
||||
--tf-tip-text: #e2e8f0;
|
||||
--tf-scrollbar-thumb: #475569;
|
||||
--tf-slider-track-bg: #475569;
|
||||
--tf-slider-thumb-bg: #93c5fd;
|
||||
|
||||
--xy-node-boxshadow-selected: 0 0 0 1px var(--tf-primary-color);
|
||||
--xy-handle-background-color: var(--tf-primary-color);
|
||||
--xy-handle-border-color: var(--tf-bg-canvas);
|
||||
@@ -137,7 +125,7 @@
|
||||
--xy-controls-button-color-hover: var(--tf-text-primary);
|
||||
--xy-controls-button-border-color: var(--tf-border-color);
|
||||
--xy-minimap-background-color: var(--tf-bg-surface);
|
||||
--xy-minimap-mask-background-color: rgba(15, 23, 42, 0.7);
|
||||
--xy-minimap-mask-background-color: rgb(15 23 42 / 70%);
|
||||
--xy-minimap-node-background-color: #475569;
|
||||
--xy-edge-label-background-color: var(--tf-bg-surface);
|
||||
--xy-edge-label-color: var(--tf-text-primary);
|
||||
|
||||
@@ -1,70 +1,70 @@
|
||||
import {beforeEach, describe, expect, it} from 'vitest';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import {componentName} from './consts';
|
||||
import {Tinyflow} from './Tinyflow';
|
||||
import { componentName } from './consts';
|
||||
import { Tinyflow } from './Tinyflow';
|
||||
|
||||
describe('tinyflow theme', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('should use light theme by default', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.append(container);
|
||||
|
||||
const tinyflow = new Tinyflow({
|
||||
element: container,
|
||||
});
|
||||
|
||||
it('should use light theme by default', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.append(container);
|
||||
const tinyflowEl = container.querySelector(componentName);
|
||||
expect(tinyflowEl).toBeTruthy();
|
||||
expect(tinyflowEl?.classList.contains('tf-theme-light')).toBe(true);
|
||||
expect(tinyflowEl?.classList.contains('tf-theme-dark')).toBe(false);
|
||||
|
||||
const tinyflow = new Tinyflow({
|
||||
element: container
|
||||
});
|
||||
tinyflow.destroy();
|
||||
});
|
||||
|
||||
const tinyflowEl = container.querySelector(componentName);
|
||||
expect(tinyflowEl).toBeTruthy();
|
||||
expect(tinyflowEl?.classList.contains('tf-theme-light')).toBe(true);
|
||||
expect(tinyflowEl?.classList.contains('tf-theme-dark')).toBe(false);
|
||||
it('should toggle theme class without remount', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.append(container);
|
||||
|
||||
tinyflow.destroy();
|
||||
const tinyflow = new Tinyflow({
|
||||
element: container,
|
||||
theme: 'dark',
|
||||
});
|
||||
|
||||
it('should toggle theme class without remount', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.append(container);
|
||||
const tinyflowEl = container.querySelector(componentName);
|
||||
expect(tinyflowEl?.classList.contains('tf-theme-dark')).toBe(true);
|
||||
|
||||
const tinyflow = new Tinyflow({
|
||||
element: container,
|
||||
theme: 'dark'
|
||||
});
|
||||
tinyflow.setTheme('light');
|
||||
|
||||
const tinyflowEl = container.querySelector(componentName);
|
||||
expect(tinyflowEl?.classList.contains('tf-theme-dark')).toBe(true);
|
||||
expect(tinyflowEl?.classList.contains('tf-theme-light')).toBe(true);
|
||||
expect(tinyflowEl?.classList.contains('tf-theme-dark')).toBe(false);
|
||||
|
||||
tinyflow.setTheme('light');
|
||||
tinyflow.destroy();
|
||||
});
|
||||
|
||||
expect(tinyflowEl?.classList.contains('tf-theme-light')).toBe(true);
|
||||
expect(tinyflowEl?.classList.contains('tf-theme-dark')).toBe(false);
|
||||
it('should keep theme when data is reset', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.append(container);
|
||||
|
||||
tinyflow.destroy();
|
||||
const tinyflow = new Tinyflow({
|
||||
element: container,
|
||||
theme: 'dark',
|
||||
});
|
||||
|
||||
it('should keep theme when data is reset', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.append(container);
|
||||
|
||||
const tinyflow = new Tinyflow({
|
||||
element: container,
|
||||
theme: 'dark'
|
||||
});
|
||||
|
||||
const firstRenderEl = container.querySelector(componentName);
|
||||
tinyflow.setData({
|
||||
nodes: [],
|
||||
edges: []
|
||||
});
|
||||
const secondRenderEl = container.querySelector(componentName);
|
||||
|
||||
expect(firstRenderEl).toBeTruthy();
|
||||
expect(secondRenderEl).toBeTruthy();
|
||||
expect(secondRenderEl).not.toBe(firstRenderEl);
|
||||
expect(secondRenderEl?.classList.contains('tf-theme-dark')).toBe(true);
|
||||
|
||||
tinyflow.destroy();
|
||||
const firstRenderEl = container.querySelector(componentName);
|
||||
tinyflow.setData({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
});
|
||||
const secondRenderEl = container.querySelector(componentName);
|
||||
|
||||
expect(firstRenderEl).toBeTruthy();
|
||||
expect(secondRenderEl).toBeTruthy();
|
||||
expect(secondRenderEl).not.toBe(firstRenderEl);
|
||||
expect(secondRenderEl?.classList.contains('tf-theme-dark')).toBe(true);
|
||||
|
||||
tinyflow.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,125 +1,127 @@
|
||||
import type {Snippet} from 'svelte';
|
||||
import type {Node, useSvelteFlow} from '@xyflow/svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { Node, useSvelteFlow } from '@xyflow/svelte';
|
||||
|
||||
export type TinyflowData = Partial<ReturnType<ReturnType<typeof useSvelteFlow>['toObject']>>;
|
||||
export type TinyflowData = Partial<
|
||||
ReturnType<ReturnType<typeof useSvelteFlow>['toObject']>
|
||||
>;
|
||||
export type TinyflowTheme = 'light' | 'dark';
|
||||
|
||||
export type SelectItem = {
|
||||
value: number | string;
|
||||
label: string | Snippet;
|
||||
description?: string;
|
||||
selectable?: boolean;
|
||||
icon?: string;
|
||||
nodeType?: string;
|
||||
dataType?: string;
|
||||
displayLabel?: string;
|
||||
tags?: string[];
|
||||
children?: SelectItem[];
|
||||
value: number | string;
|
||||
label: string | Snippet;
|
||||
description?: string;
|
||||
selectable?: boolean;
|
||||
icon?: string;
|
||||
nodeType?: string;
|
||||
dataType?: string;
|
||||
displayLabel?: string;
|
||||
tags?: string[];
|
||||
children?: SelectItem[];
|
||||
};
|
||||
|
||||
export type CustomNodeForm = {
|
||||
type:
|
||||
| 'input'
|
||||
| 'textarea'
|
||||
| 'select'
|
||||
| 'chosen'
|
||||
// | 'checkbox'
|
||||
// | 'radio'
|
||||
// | 'switch'
|
||||
| 'slider'
|
||||
// | 'number'
|
||||
// | 'date'
|
||||
// | 'time'
|
||||
// | 'datetime'
|
||||
// | 'color'
|
||||
// | 'file'
|
||||
// | 'range'
|
||||
// | 'image'
|
||||
// | 'password'
|
||||
| 'heading';
|
||||
label: string;
|
||||
description?: string;
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string | number | boolean;
|
||||
attrs?: Record<string, any>;
|
||||
options?: SelectItem[];
|
||||
templateSupport?: boolean;
|
||||
chosen?: {
|
||||
labelDataKey: string;
|
||||
valueDataKey: string;
|
||||
buttonText?: string;
|
||||
onChosen?: (
|
||||
updateNodeData: (data: Record<string, any>) => void,
|
||||
value?: string,
|
||||
label?: string,
|
||||
event?: Event
|
||||
) => void;
|
||||
};
|
||||
type:
|
||||
| 'input'
|
||||
| 'textarea'
|
||||
| 'select'
|
||||
| 'chosen'
|
||||
// | 'checkbox'
|
||||
// | 'radio'
|
||||
// | 'switch'
|
||||
| 'slider'
|
||||
// | 'number'
|
||||
// | 'date'
|
||||
// | 'time'
|
||||
// | 'datetime'
|
||||
// | 'color'
|
||||
// | 'file'
|
||||
// | 'range'
|
||||
// | 'image'
|
||||
// | 'password'
|
||||
| 'heading';
|
||||
label: string;
|
||||
description?: string;
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string | number | boolean;
|
||||
attrs?: Record<string, any>;
|
||||
options?: SelectItem[];
|
||||
templateSupport?: boolean;
|
||||
chosen?: {
|
||||
labelDataKey: string;
|
||||
valueDataKey: string;
|
||||
buttonText?: string;
|
||||
onChosen?: (
|
||||
updateNodeData: (data: Record<string, any>) => void,
|
||||
value?: string,
|
||||
label?: string,
|
||||
event?: Event,
|
||||
) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export type CustomNode = {
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
sortNo?: number;
|
||||
group?: 'base' | 'tools';
|
||||
rootClass?: string;
|
||||
rootStyle?: string;
|
||||
parameters?: Parameter[];
|
||||
parametersEnable?: boolean;
|
||||
parametersAddEnable?: boolean;
|
||||
outputDefs?: Parameter[];
|
||||
outputDefsEnable?: boolean;
|
||||
outputDefsAddEnable?: boolean;
|
||||
render?: (
|
||||
parent: HTMLElement,
|
||||
node: Node,
|
||||
flowInstance: ReturnType<typeof useSvelteFlow>
|
||||
) => void;
|
||||
onUpdate?: (parent: HTMLElement, node: Node) => void;
|
||||
forms?: CustomNodeForm[];
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
sortNo?: number;
|
||||
group?: 'base' | 'tools';
|
||||
rootClass?: string;
|
||||
rootStyle?: string;
|
||||
parameters?: Parameter[];
|
||||
parametersEnable?: boolean;
|
||||
parametersAddEnable?: boolean;
|
||||
outputDefs?: Parameter[];
|
||||
outputDefsEnable?: boolean;
|
||||
outputDefsAddEnable?: boolean;
|
||||
render?: (
|
||||
parent: HTMLElement,
|
||||
node: Node,
|
||||
flowInstance: ReturnType<typeof useSvelteFlow>,
|
||||
) => void;
|
||||
onUpdate?: (parent: HTMLElement, node: Node) => void;
|
||||
forms?: CustomNodeForm[];
|
||||
};
|
||||
|
||||
export type TinyflowOptions = {
|
||||
element: string | Element;
|
||||
theme?: TinyflowTheme;
|
||||
data?: TinyflowData | string;
|
||||
provider?: {
|
||||
llm?: () => SelectItem[] | Promise<SelectItem[]>;
|
||||
knowledge?: () => SelectItem[] | Promise<SelectItem[]>;
|
||||
searchEngine?: () => SelectItem[] | Promise<SelectItem[]>;
|
||||
codeEngine?: () => SelectItem[] | Promise<SelectItem[]>;
|
||||
} & Record<string, () => SelectItem[] | Promise<SelectItem[]>>;
|
||||
//type : node
|
||||
customNodes?: Record<string, CustomNode>;
|
||||
onNodeExecute?: (node: Node) => void;
|
||||
onRunTest?: () => void | Promise<void>;
|
||||
hiddenNodes?: string[] | (() => string[]);
|
||||
onDataChange?: (data: TinyflowData) => void;
|
||||
element: string | Element;
|
||||
theme?: TinyflowTheme;
|
||||
data?: TinyflowData | string;
|
||||
provider?: {
|
||||
llm?: () => SelectItem[] | Promise<SelectItem[]>;
|
||||
knowledge?: () => SelectItem[] | Promise<SelectItem[]>;
|
||||
searchEngine?: () => SelectItem[] | Promise<SelectItem[]>;
|
||||
codeEngine?: () => SelectItem[] | Promise<SelectItem[]>;
|
||||
} & Record<string, () => SelectItem[] | Promise<SelectItem[]>>;
|
||||
//type : node
|
||||
customNodes?: Record<string, CustomNode>;
|
||||
onNodeExecute?: (node: Node) => void;
|
||||
onRunTest?: () => void | Promise<void>;
|
||||
hiddenNodes?: string[] | (() => string[]);
|
||||
onDataChange?: (data: TinyflowData) => void;
|
||||
};
|
||||
|
||||
export type Parameter = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
nameDisabled?: boolean;
|
||||
dataType?: string;
|
||||
dataTypeItems?: SelectItem[];
|
||||
dataTypeDisabled?: boolean;
|
||||
contentType?: string; //数据内容类型
|
||||
ref?: string;
|
||||
refType?: string;
|
||||
value?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
defaultValue?: string;
|
||||
deleteDisabled?: boolean;
|
||||
addChildDisabled?: boolean;
|
||||
children?: Parameter[];
|
||||
enums?: string[];
|
||||
formType?: string;
|
||||
formLabel?: string;
|
||||
formDescription?: string;
|
||||
formPlaceholder?: string;
|
||||
formAttrs?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
nameDisabled?: boolean;
|
||||
dataType?: string;
|
||||
dataTypeItems?: SelectItem[];
|
||||
dataTypeDisabled?: boolean;
|
||||
contentType?: string; //数据内容类型
|
||||
ref?: string;
|
||||
refType?: string;
|
||||
value?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
defaultValue?: string;
|
||||
deleteDisabled?: boolean;
|
||||
addChildDisabled?: boolean;
|
||||
children?: Parameter[];
|
||||
enums?: string[];
|
||||
formType?: string;
|
||||
formLabel?: string;
|
||||
formDescription?: string;
|
||||
formPlaceholder?: string;
|
||||
formAttrs?: string;
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import {vitePreprocess} from '@sveltejs/vite-plugin-svelte';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: [
|
||||
vitePreprocess({
|
||||
script: true
|
||||
})
|
||||
],
|
||||
compilerOptions: {
|
||||
// accessors: true,
|
||||
customElement: true
|
||||
}
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: [
|
||||
vitePreprocess({
|
||||
script: true,
|
||||
}),
|
||||
],
|
||||
compilerOptions: {
|
||||
// accessors: true,
|
||||
customElement: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import {defineConfig} from 'vite';
|
||||
import {resolve} from 'path';
|
||||
import {svelte} from '@sveltejs/vite-plugin-svelte';
|
||||
import { defineConfig } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
cssFileName: 'index',
|
||||
fileName: (format) => {
|
||||
return format === 'umd' ? 'index.umd.js' : 'index.js';
|
||||
},
|
||||
formats: ['es', 'umd'],
|
||||
name: 'Tinyflow'
|
||||
},
|
||||
rollupOptions:{
|
||||
output: {
|
||||
// 确保所有依赖都被打包
|
||||
inlineDynamicImports: true,
|
||||
exports: 'named',
|
||||
},
|
||||
plugins: [
|
||||
replace({
|
||||
preventAssignment: true,
|
||||
'process.env.NODE_ENV': JSON.stringify('production') // 替换为生产环境
|
||||
})
|
||||
]
|
||||
}
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
cssFileName: 'index',
|
||||
fileName: (format) => {
|
||||
return format === 'umd' ? 'index.umd.js' : 'index.js';
|
||||
},
|
||||
formats: ['es', 'umd'],
|
||||
name: 'Tinyflow',
|
||||
},
|
||||
plugins: [
|
||||
svelte({ emitCss: false }),
|
||||
dts({
|
||||
rollupTypes: true,
|
||||
tsconfigPath: './tsconfig.app.json'
|
||||
})
|
||||
]
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 确保所有依赖都被打包
|
||||
inlineDynamicImports: true,
|
||||
exports: 'named',
|
||||
},
|
||||
plugins: [
|
||||
replace({
|
||||
preventAssignment: true,
|
||||
'process.env.NODE_ENV': JSON.stringify('production'), // 替换为生产环境
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
svelte({ emitCss: false }),
|
||||
dts({
|
||||
rollupTypes: true,
|
||||
tsconfigPath: './tsconfig.app.json',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user