workflow底层UI库整合至项目,优化构建逻辑

This commit is contained in:
2026-02-24 11:20:18 +08:00
parent 094b185c49
commit 12accb2575
91 changed files with 6820 additions and 115 deletions

View File

@@ -29,7 +29,7 @@
"@easyflow/types": "workspace:*",
"@easyflow/utils": "workspace:*",
"@element-plus/icons-vue": "^2.3.2",
"@tinyflow-ai/vue": "^1.2.2",
"@tinyflow-ai/vue": "workspace:*",
"@vueuse/core": "catalog:",
"dayjs": "catalog:",
"dompurify": "^3.3.1",

View File

@@ -6,6 +6,13 @@ export default defineConfig(async () => {
return {
application: {},
vite: {
resolve: {
alias: {
// @vueuse/motion expects tslib default export during dev pre-bundling
// and breaks when Vite resolves to ESM-only modules/index.js.
tslib: 'tslib/tslib.js',
},
},
plugins: [
ElementPlus({
format: 'esm',

View File

@@ -2,4 +2,8 @@
import {defineConfig} from '@easyflow/eslint-config';
export default defineConfig();
export default defineConfig([
{
ignores: ['packages/tinyflow-ui/**', 'packages/tinyflow-vue/**'],
},
]);

View File

@@ -1,4 +1,5 @@
import type {CSSOptions, UserConfig} from 'vite';
import {defineConfig, loadEnv, mergeConfig} from 'vite';
import type {DefineApplicationOptions} from '../typing';
@@ -7,7 +8,6 @@ import path, { relative } from 'node:path';
import {findMonorepoRoot} from '@easyflow/node-utils';
import {NodePackageImporter} from 'sass';
import { defineConfig, loadEnv, mergeConfig } from 'vite';
import {defaultImportmapOptions, getDefaultPwaOptions} from '../options';
import {loadApplicationPlugins} from '../plugins';
@@ -78,6 +78,11 @@ function defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) {
: [],
legalComments: 'none',
},
// Workspace packages expose `development` entry to source files.
// Keep this condition enabled in build mode to avoid resolving to stubbed dist artifacts.
resolve: {
conditions: ['development'],
},
plugins,
server: {
host: true,

View File

@@ -18,8 +18,11 @@
"check:type": "turbo run typecheck",
"clean": "node ./scripts/clean.mjs",
"commit": "czg",
"dev": "turbo-run dev",
"dev": "pnpm run dev:app",
"dev:app": "pnpm -F @easyflow/app run dev",
"dev:all": "turbo-run dev",
"dev:tinyflow": "pnpm --filter @tinyflow-ai/ui run build && pnpm --filter @tinyflow-ai/vue run build && pnpm --parallel --filter @tinyflow-ai/ui --filter @tinyflow-ai/vue run build:watch",
"dev:app:tinyflow": "pnpm run dev:tinyflow & pnpm run dev:app",
"format": "vsh lint --format",
"lint": "vsh lint",
"postinstall": "pnpm -r run stub --if-present",

View File

@@ -0,0 +1,5 @@
module.exports = {
rules: {
'@typescript-eslint/no-explicit-any': ['off']
}
};

View File

@@ -0,0 +1,76 @@
{
"name": "@tinyflow-ai/ui",
"version": "1.2.2",
"type": "module",
"keywords": [
"tinyflow",
"ai",
"ai flow",
"agent flow",
"agents flow"
],
"types": "./dist/index.d.ts",
"main": "./dist/index.js",
"module": "./dist/index.js",
"browser": "./dist/index.umd.js",
"license": "LGPL-3.0-or-later",
"files": [
"dist",
"LICENSE",
"README.md"
],
"scripts": {
"dev": "vite",
"build": "vite build",
"build:watch": "vite build --watch --emptyOutDir false",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
},
"devDependencies": {
"@rollup/plugin-replace": "^6.0.2",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tsconfig/svelte": "^5.0.5",
"@types/node": "^22.18.12",
"autoprefixer": "^10.4.21",
"less": "^4.2.2",
"svelte": "^5.45.2",
"svelte-check": "^4.3.4",
"typescript": "^5.6.2",
"vite": "^7.2.4",
"vite-plugin-dts": "^4.5.4"
},
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"@xyflow/svelte": "^1.4.2"
},
"imports": {
"#*": [
"./src/*.ts",
"./src/*.svelte"
]
},
"exports": {
".": {
"svelte": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.js"
},
"./dist/index.css": {
"import": "./dist/index.css",
"require": "./dist/index.css"
}
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "https://github.com/tinyflow-ai/tinyflow"
},
"bugs": {
"url": "https://github.com/tinyflow-ai/tinyflow/issues"
},
"homepage": "https://github.com/tinyflow-ai/tinyflow#readme"
}

View File

@@ -0,0 +1,91 @@
import {type useSvelteFlow} from '@xyflow/svelte';
import {componentName} from './consts';
import type {TinyflowData, TinyflowOptions} from './types';
type FlowInstance = ReturnType<typeof useSvelteFlow>;
export class Tinyflow {
private options!: TinyflowOptions;
private rootEl!: Element;
private svelteFlowInstance!: FlowInstance;
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');
}
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%';
tinyflowEl.classList.add('tf-theme-light');
tinyflowEl.options = this.options;
tinyflowEl.onInit = (svelteFlowInstance: FlowInstance) => {
this.svelteFlowInstance = svelteFlowInstance;
};
this.rootEl.appendChild(tinyflowEl);
}
private _setOptions(options: TinyflowOptions) {
this.options = {
...options
};
}
getOptions() {
return this.options;
}
getData() {
return this.svelteFlowInstance.toObject();
}
setData(data: TinyflowData) {
this.options.data = data;
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%';
tinyflowEl.classList.add('tf-theme-light');
tinyflowEl.options = this.options;
tinyflowEl.onInit = (svelteFlowInstance: FlowInstance) => {
this.svelteFlowInstance = svelteFlowInstance;
};
this.destroy();
this.rootEl.appendChild(tinyflowEl);
}
destroy() {
while (this.rootEl.firstChild) {
this.rootEl.removeChild(this.rootEl.firstChild);
}
}
}

View File

@@ -0,0 +1,38 @@
<svelte:options customElement={{
tag: "tinyflow-component",
shadow: "none",
// props: {
// options: { reflect: true, type: 'Object', attribute: 'options' },
// onInit: { reflect: true, type: 'Object', attribute: 'onInit' }
// },
}} />
<script lang="ts">
import {SvelteFlowProvider, type useSvelteFlow} from '@xyflow/svelte';
import TinyflowCore from './TinyflowCore.svelte';
import {store} from '#store/stores.svelte';
import type {TinyflowData, TinyflowOptions} from '#types';
import {setContext} from 'svelte';
const { options, onInit }: {
options: TinyflowOptions,
onInit: (svelteFlow: ReturnType<typeof useSvelteFlow>) => void,
} = $props();
let { data } = options;
if (typeof data === 'string') {
try {
data = JSON.parse(data.trim());
} catch (e) {
console.error('Invalid JSON data:', data);
}
}
store.init((data as TinyflowData)?.nodes || [], (data as TinyflowData)?.edges || []);
setContext('tinyflow_options', options);
</script>
<SvelteFlowProvider>
<TinyflowCore {onInit} />
</SvelteFlowProvider>

View File

@@ -0,0 +1,441 @@
<script lang="ts">
import {
Background,
Controls,
type Edge,
type Handle,
MarkerType,
MiniMap,
type Node,
type NodeTypes,
Panel,
SvelteFlow,
useSvelteFlow
} from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
import '../styles/index.ts';
import {store} from '#store/stores.svelte';
import {nodeTypes} from './nodes';
import Toolbar from './Toolbar.svelte';
import {genShortId} from './utils/IdGen';
import {useGetNode} from './utils/useGetNode.svelte';
import {useEnsureParentInNodesBefore} from './utils/useEnsureParentInNodesBefore.svelte';
import {Textarea} from './base';
import {useGetEdgesByTarget} from './utils/useGetEdgesByTarget.svelte';
import {getOptions} from './utils/NodeUtils';
import CustomNode from './nodes/CustomNode.svelte';
import {useUpdateEdgeData} from './utils/useUpdateEdgeData.svelte';
import {Button} from '#components/base/index';
import {useDeleteEdge} from '#components/utils/useDeleteEdge.svelte';
import {useGetNodesFromSource} from '#components/utils/useGetNodesFromSource.svelte';
import {useGetNodeRelativePosition} from '#components/utils/useGetNodeRelativePosition.svelte';
import {useCopyPasteHandler} from '#components/utils/useCopyPasteHandler.svelte';
import {onDestroy, onMount} from 'svelte';
import {isInEditableElement} from '#components/utils/isInEditableElement';
const { onInit, ...rest } = $props();
const svelteFlow = useSvelteFlow();
console.log('props', rest);
onInit(svelteFlow);
let showEdgePanel = $state(false);
let currentEdge = $state<Edge | null>(null);
const { updateEdgeData } = useUpdateEdgeData();
const onDragOver = (event: DragEvent) => {
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'move';
}
};
const onDrop = (event: DragEvent) => {
event.preventDefault();
const position = svelteFlow.screenToFlowPosition({
x: event.clientX - 250,
y: event.clientY - 100
});
const baseNodeJsonString = event.dataTransfer?.getData('application/tinyflow');
if (!baseNodeJsonString) {
return;
}
const baseNode = JSON.parse(baseNodeJsonString);
const newNode = {
id: `node_${genShortId()}`,
position,
data: {},
...baseNode
} satisfies Node;
store.addNode(newNode);
store.selectNodeOnly(newNode.id);
};
const { getNode } = useGetNode();
const isValidConnection = (conn: any) => {
const sourceNode = getNode(conn.source)!;
const targetNode = getNode(conn.target)!;
// 阻止循环节点连接到父级节点 或者 父级节点连接到子级节点
if (conn.sourceHandle === 'loop_handle' || sourceNode.parentId) {
const edges = svelteFlow.getEdges();
for (let edge of edges) {
if (edge.target === conn.target) {
const edgeSourceNode = getNode(edge.source) as Node;
if (conn.sourceHandle === 'loop_handle' && edgeSourceNode.parentId !== sourceNode.id) {
return false;
}
if (sourceNode.parentId && edgeSourceNode.parentId !== sourceNode.parentId) {
return false;
}
}
}
}
if (!sourceNode.parentId && targetNode.parentId && targetNode.parentId !== sourceNode.id) {
return false;
}
// 允许链接
return true;
};
const { getNodesFromSource } = useGetNodesFromSource();
const { getNodeRelativePosition } = useGetNodeRelativePosition();
const { ensureParentInNodesBefore } = useEnsureParentInNodesBefore();
const onconnectend = (_: any, state: any) => {
if (!state.isValid) {
return;
}
const toNode = state.toNode as Node;
if (toNode.parentId) {
return;
}
const fromNode = state.fromNode as Node;
const fromHande = state.fromHandle as Handle;
const newNode = {
position: { ...toNode.position }
} as Node;
if (fromHande.id === 'loop_handle') {
newNode.parentId = fromNode.id;
} else if (fromNode.parentId) {
newNode.parentId = fromNode.parentId;
}
if (newNode.parentId) {
const { x, y } = getNodeRelativePosition(newNode.parentId);
newNode.position = {
x: toNode.position.x - x,
y: toNode.position.y - y
};
svelteFlow.updateNode(toNode.id, newNode);
// 更新目标节点的所有后续的链接节点
const nodesFromToNode = getNodesFromSource(toNode.id);
nodesFromToNode.forEach((node) => {
svelteFlow.updateNode(node.id, {
parentId: newNode.parentId,
position: {
x: node.position.x - x,
y: node.position.y - y
}
});
});
ensureParentInNodesBefore(newNode.parentId, toNode.id);
}
// 显示边面板
setTimeout(() => {
store.getEdges().forEach((edge) => {
if (edge.target === toNode.id && edge.source == fromNode.id) {
showEdgePanel = true;
currentEdge = edge;
}
});
});
};
const { getEdgesByTarget } = useGetEdgesByTarget();
const onDelete = (params: any) => {
const deleteEdges = params.edges as Edge[];
deleteEdges.forEach((edge) => {
if (edge.id === currentEdge?.id) {
currentEdge = null;
showEdgePanel = false;
}
const targetNode = getNode(edge.target) as Node;
if (targetNode && targetNode.parentId) {
const nodeEdges = getEdgesByTarget(edge.target);
// const loopNode = getNode(targetNode.parentId) as Node;
const { x, y } = getNodeRelativePosition(targetNode.parentId);
if (nodeEdges.length === 0) {
svelteFlow.updateNode(targetNode.id, {
parentId: undefined,
position: {
x: targetNode.position.x + x,
y: targetNode.position.y + y
}
});
// 更新目标节点的所有后续的链接节点
const nodesFromSource = getNodesFromSource(targetNode.id);
nodesFromSource.forEach((node) => {
svelteFlow.updateNode(node.id, {
parentId: undefined,
position: {
x: node.position.x + x,
y: node.position.y + y
}
});
});
} else {
let hasSameParent = false;
for (let i = 0; i < nodeEdges.length; i++) {
const edge = nodeEdges[i];
const sourceNode = getNode(edge.source) as Node;
if (sourceNode.parentId || sourceNode.type === 'loopNode') {
hasSameParent = true;
break;
}
}
if (!hasSameParent) {
svelteFlow.updateNode(targetNode.id, {
parentId: undefined,
position: {
x: targetNode.position.x + x,
y: targetNode.position.y + y
}
});
// 更新目标节点的所有后续的链接节点
const nodesFromSource = getNodesFromSource(targetNode.id);
nodesFromSource.forEach((node) => {
svelteFlow.updateNode(node.id, {
parentId: undefined,
position: {
x: node.position.x + x,
y: node.position.y + y
}
});
});
}
}
}
});
};
const { deleteEdge } = useDeleteEdge();
const onconnectstart = (event: any, node: any) => {
// console.log('onconnectstart: ', event, node);
};
const onconnect = (event: any) => {
// console.log('onconnect: ', event);
};
const { copyHandler, pasteHandler } = useCopyPasteHandler();
const handleKeyDown = (e: KeyboardEvent) => {
if (isInEditableElement()) {
return;
}
if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
e.preventDefault();
copyHandler(e);
}
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
e.preventDefault();
// 全选所有节点
store.updateNodes((nodes) => {
return nodes.map(node => ({ ...node, selected: true }));
});
store.updateEdges((edges) => {
return edges.map(edge => ({ ...edge, selected: true }));
});
}
};
const handleGlobalPaste = async (event: ClipboardEvent) => {
// 只在“非输入态”下处理流程图粘贴
if (isInEditableElement()) {
return;
}
pasteHandler(event);
};
onMount(() => {
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('paste', handleGlobalPaste);
});
onDestroy(() => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('paste', handleGlobalPaste);
});
const customNodeTypes = {
// ...nodeTypes
} as NodeTypes;
const customNodes = getOptions().customNodes;
if (customNodes) {
for (let key of Object.keys(customNodes)) {
customNodeTypes[key] = CustomNode as any;
}
}
const onDataChange = getOptions().onDataChange;
$effect(() => {
onDataChange?.({
nodes: store.getNodes(),
edges: store.getEdges(),
viewport: store.getViewport()
});
});
</script>
<div style="position: relative; height: 100%; width: 100%;overflow: hidden">
<SvelteFlow nodeTypes={{ ...nodeTypes, ...customNodeTypes}}
bind:nodes={store.getNodes, store.setNodes}
bind:edges={store.getEdges, store.setEdges}
bind:viewport={store.getViewport, store.setViewport}
ondrop={onDrop}
ondragover={onDragOver}
isValidConnection={isValidConnection}
onconnectend={onconnectend}
onconnectstart={onconnectstart}
onconnect={onconnect}
connectionRadius={50}
onedgeclick={(e) => {
showEdgePanel = true;
currentEdge = e.edge;
}}
onbeforeconnect={(edge) => {
return {
...edge,
id:genShortId(),
}
}}
ondelete={onDelete}
onclick={(e) => {
const el = e.target as HTMLElement;
if (el.classList.contains("svelte-flow__edge-interaction")
|| el.classList.contains('panel-content')
|| el.closest('.panel-content')){
return
}
showEdgePanel = false;
currentEdge = null;
}}
defaultEdgeOptions={{
// animated: true,
// label: 'edge label',
markerEnd: {
type: MarkerType.ArrowClosed,
// color: 'red',
width: 20,
height: 20
}
}}
>
<Background />
<Controls />
<MiniMap />
{#if showEdgePanel}
<Panel>
<div class="panel-content">
<div>边属性设置</div>
<div class="setting-title">边条件设置</div>
<div class="setting-item">
<Textarea
rows={3}
placeholder="请输入边条件"
style="width: 100%"
value={currentEdge?.data?.condition}
onchange={(e)=>{
if (currentEdge){
updateEdgeData(currentEdge.id, {
condition: e.target?.value
})
}
}}
/>
</div>
<div class="setting-item" style="padding: 8px 0">
<Button
onclick={() => {
deleteEdge(currentEdge?.id)
showEdgePanel = false;
}}
>
删除
</Button>
<Button
primary={true}
onclick={() => {
showEdgePanel = false;
}}
>
保存
</Button>
</div>
</div>
</Panel>
{/if}
</SvelteFlow>
<Toolbar />
</div>
<style>
.panel-content {
padding: 10px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
width: 200px;
border: 1px solid #efefef;
}
.setting-title {
margin: 10px 0;
font-size: 12px;
color: #999;
}
.setting-item {
display: flex;
gap: 5px;
align-items: center;
justify-content: end;
}
</style>

View File

@@ -0,0 +1,176 @@
<script lang="ts">
import {Button, Tabs} from './base/index';
import DraggableButton from './core/DraggableButton.svelte';
import {getOptions} from './utils/NodeUtils';
let showType = $state('base');
let containerShowClass = $state('show');
const baseNodes = [
{
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: '开始定义输入参数'
},
{
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: '用于循环执行任务'
},
{
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: '使用大模型处理问题'
},
{
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: '通过知识库获取内容'
},
{
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: '通过搜索引擎搜索内容'
},
{
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 请求获取数据'
},
{
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: '动态执行代码'
},
{
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: '通过模板引擎生成内容'
},
{
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: '确认继续或选择内容'
},
{
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: '结束定义输出参数'
}
];
const tableItems = [
{
label: '基础节点',
value: 'base'
},
{
label: '业务工具',
value: 'tools'
}
];
const customNodes = [] as any[];
const options = getOptions();
const userCustomNodes = options.customNodes;
if (userCustomNodes) {
const keys = Object.keys(userCustomNodes).sort((a, b) => {
return (userCustomNodes[a].sortNo || 0) - (userCustomNodes[b].sortNo || 0);
});
for (let key of keys) {
if (userCustomNodes[key].group === 'base') {
baseNodes.push({
type: key,
...userCustomNodes[key]
} as any);
} else {
customNodes.push({
icon: userCustomNodes[key].icon,
title: userCustomNodes[key].title,
type: key
});
}
}
baseNodes.sort((a, b) => {
return (a.sortNo || 0) - (b.sortNo || 0);
});
}
if (options.hiddenNodes) {
const hiddenNodes = typeof options.hiddenNodes === 'function' ? options.hiddenNodes() : options.hiddenNodes;
if (Array.isArray(hiddenNodes)) {
for (let hiddenNode of hiddenNodes) {
for (let i = 0; i < baseNodes.length; i++) {
if (baseNodes[i].type === hiddenNode) {
baseNodes.splice(i, 1);
break;
}
}
}
}
}
</script>
<div class="tf-toolbar {containerShowClass}">
<div class="tf-toolbar-container ">
<div class="tf-toolbar-container-header">
<Tabs style="width: 100%" items={tableItems} onChange={(item)=>{
showType = item.value.toString();
}}
/>
</div>
<div class="tf-toolbar-container-body">
<div class="tf-toolbar-container-base" style="display: {showType === 'base' ? 'flex' : 'none'}">
{#each baseNodes as node}
<DraggableButton {...node} />
{/each}
</div>
<div class="tf-toolbar-container-tools" style="display: {showType !== 'base' ? 'flex' : 'none'}">
{#each customNodes as node}
<DraggableButton {...node} />
{/each}
</div>
</div>
</div>
<Button onclick={()=>{
containerShowClass = containerShowClass ? '' :'show';
}}>
{#if containerShowClass === 'show'}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M4.83582 12L11.0429 18.2071L12.4571 16.7929L7.66424 12L12.4571 7.20712L11.0429 5.79291L4.83582 12ZM10.4857 12L16.6928 18.2071L18.107 16.7929L13.3141 12L18.107 7.20712L16.6928 5.79291L10.4857 12Z"></path>
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M19.1642 12L12.9571 5.79291L11.5429 7.20712L16.3358 12L11.5429 16.7929L12.9571 18.2071L19.1642 12ZM13.5143 12L7.30722 5.79291L5.89301 7.20712L10.6859 12L5.89301 16.7929L7.30722 18.2071L13.5143 12Z"></path>
</svg>
{/if}
</Button>
</div>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import type {MyHTMLButtonAttributes} from './types';
import type {Snippet} from 'svelte';
const { children, primary, ...rest }: MyHTMLButtonAttributes & {
children?: Snippet;
primary?: boolean;
} = $props();
</script>
<button type="button" {...rest} class="tf-btn {primary?'tf-btn-primary':''} nopan nodrag {rest.class}">
{@render children?.()}
</button>

View File

@@ -0,0 +1,6 @@
<script lang="ts">
import type {MyHTMLInputAttributes} from './types';
const { ...rest }: MyHTMLInputAttributes = $props();
</script>
<input type="checkbox" {...rest} class="tf-checkbox nopan nodrag {rest.class}" />

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import {Button, Input} from './index';
import type {MyHTMLAttributes} from './types';
const { placeholder, label, value, buttonText = "选择...",onChosen, ...rest }: {
placeholder?: string;
label?: any;
value?: any;
buttonText?:string
onChosen?: (value?: any, label?: any, event?: Event) => void,
} & MyHTMLAttributes = $props();
</script>
<div {...rest} class="tf-chosen nopan nodrag {rest.class}">
<input type="hidden" value={value}>
<Input value={label} {placeholder} style="flex-grow: 1;" disabled/>
<Button onclick={(e)=>{
onChosen?.(value, label, e);
}} style="padding: 3px" >{buttonText}</Button>
</div>
<style>
.tf-chosen {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 5px;
}
</style>

View File

@@ -0,0 +1,78 @@
<script lang="ts">
import type {Snippet} from 'svelte';
import {Render} from './index';
interface Item {
key: string;
icon?: string | Snippet;
title: string | Snippet;
description?: string | Snippet;
content: string | Snippet;
}
let { items, onChange, activeKeys = $bindable([]), ...rest }: {
items: Item[],
onChange?: (item: Item, activeKeys: string[]) => void,
activeKeys?: string[],
[key: string]: any
} = $props();
function handlerOnChange(item: Item) {
if (activeKeys.includes(item.key)) {
activeKeys = activeKeys.filter(key => key !== item.key);
} else {
activeKeys.push(item.key);
activeKeys = activeKeys;
}
onChange?.(item, activeKeys);
}
</script>
<div style={rest.style} class="tf-collapse {rest.class}">
{#each items as item, index}
<div class="tf-collapse-item">
<div class="tf-collapse-item-title" role="button" tabindex={index}
onclick={() => handlerOnChange(item)}
onkeydown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handlerOnChange(item);
}
}}
>
{#if item.icon}
<span class="tf-collapse-item-title-icon">
<Render target={item.icon} />
</span>
{/if}
<Render target={item.title} />
<span class="tf-collapse-item-title-arrow {activeKeys.includes(item.key) ? 'rotate-90' : ''}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path
d="M13.1717 12.0007L8.22192 7.05093L9.63614 5.63672L16.0001 12.0007L9.63614 18.3646L8.22192 16.9504L13.1717 12.0007Z"></path></svg>
</span>
</div>
{#if item.description}
<div class="tf-collapse-item-description">
<Render target={item.description} />
</div>
{/if}
{#if activeKeys.includes(item.key)}
<div class="tf-collapse-item-content">
<Render target={item.content} />
</div>
{/if}
</div>
{/each}
</div>
<style>
/* 定义旋转的 CSS 类 */
.rotate-90 {
transform: rotate(90deg);
transition: transform 0.3s ease;
}
</style>

View File

@@ -0,0 +1,40 @@
<script lang="ts">
import {onMount, type Snippet} from 'svelte';
import {createFloating} from '../utils/createFloating';
import type {Placement} from '@floating-ui/dom';
const { children, floating, placement = 'bottom' }:
{
children: Snippet,
floating: Snippet,
placement?: Placement,
} = $props();
let triggerEl!: HTMLDivElement, contentEl!: HTMLDivElement;
let floatingInstance: ReturnType<typeof createFloating>;
onMount(() => {
floatingInstance = createFloating({
trigger: triggerEl,
floatContent: contentEl,
interactive: true,
placement
});
return () => {
floatingInstance.destroy();
};
});
export function hide() {
floatingInstance.hide();
}
</script>
<div style="position: relative">
<div bind:this={triggerEl}>
{@render children() }
</div>
<div style="display: none; width: 100%;z-index: 9999" bind:this={contentEl}>
{@render floating() }
</div>
</div>

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import type {Snippet} from 'svelte';
const { children, level = 1, mt, mb }: {
children: Snippet;
level: 1 | 2 | 3 | 4 | 5 | 6;
mt?: string;
mb?: string;
} = $props();
</script>
<svelte:element this={`h${level}`} class="tf-heading" style="margin-top:{mt || '0'};margin-bottom:{mb||'0'}">
{@render children?.()}
</svelte:element>

View File

@@ -0,0 +1,27 @@
import Button from './button.svelte';
import Checkbox from './checkbox.svelte';
import Chosen from './chosen.svelte';
import Input from './input.svelte';
import Textarea from './textarea.svelte';
import Tabs from './tabs.svelte';
import Collapse from './collapse.svelte';
import Render from './render.svelte';
import Select from './select.svelte';
import FloatingTrigger from './floating-trigger.svelte';
import Heading from './heading.svelte';
import MenuButton from './menu-button.svelte';
export {
Button,
Checkbox,
Chosen,
Input,
Textarea,
Tabs,
Collapse,
Render,
Select,
FloatingTrigger,
Heading,
MenuButton
};

View File

@@ -0,0 +1,6 @@
<script lang="ts">
import type {MyHTMLInputAttributes} from './types';
const { ...rest }: MyHTMLInputAttributes = $props();
</script>
<input type="text" spellcheck="false" {...rest} class="tf-input nopan nodrag {rest.class}" />

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import Button from './button.svelte';
import type {MyHTMLButtonAttributes} from './types';
const { ...rest }: MyHTMLButtonAttributes = $props();
</script>
<Button {...rest} class="input-btn-more {rest.class}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M4.5 10.5C3.675 10.5 3 11.175 3 12C3 12.825 3.675 13.5 4.5 13.5C5.325 13.5 6 12.825 6 12C6 11.175 5.325 10.5 4.5 10.5ZM19.5 10.5C18.675 10.5 18 11.175 18 12C18 12.825 18.675 13.5 19.5 13.5C20.325 13.5 21 12.825 21 12C21 11.175 20.325 10.5 19.5 10.5ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z"></path>
</svg>
</Button>
<style>
:global(.input-btn-more) {
border: 1px solid transparent;
padding: 3px;
&:hover {
background: #eee;
border: 1px solid transparent;
}
}
</style>

View File

@@ -0,0 +1,12 @@
<script lang="ts">
let { target } = $props();
if (typeof target === 'undefined') target = "undefined";
</script>
{#if typeof target === 'function'}
{@render target?.()}
{:else }
{@html target}
{/if}

View File

@@ -0,0 +1,117 @@
<script lang="ts">
import {FloatingTrigger, Render} from './index';
import type {SelectItem} from '#types';
let {
items,
onSelect,
value = [],
defaultValue = [],
expandAll = true,
multiple = false,
expandValue = [],
placeholder,
...rest
}: {
items: SelectItem[],
onSelect?: (item: SelectItem) => void,
value?: (any)[],
defaultValue?: (number | string | undefined)[],
expandAll?: boolean,
expandValue?: (number | string)[],
multiple?: boolean
placeholder?: string
[key: string]: any
} = $props();
let activeItemsState = $derived.by(() => {
const resultItems: SelectItem[] = [];
const fillResult = (items: SelectItem[]) => {
for (let item of items) {
if (value.length > 0) {
if (value.includes(item.value)) {
resultItems.push(item);
}
} else {
if (defaultValue.includes(item.value)) {
resultItems.push(item);
}
}
if (item.children && item.children.length > 0) {
fillResult(item.children);
}
}
};
fillResult(items);
return resultItems;
});
let triggerObject: any;
function handlerOnSelect(item: SelectItem) {
triggerObject?.hide();
onSelect?.(item);
}
</script>
{#snippet selectItems(items: SelectItem[])}
{#each items as item, index (`${index}_${item.value}`)}
<button class="tf-select-content-item"
onclick={() => handlerOnSelect(item)}
>
<span>
{#if item.children && item.children.length > 0}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path
d="M12 14L8 10H16L12 14Z"></path></svg>
{/if}
</span>
<Render target={item.label} />
</button>
{#if (item.children && item.children.length > 0 && (expandAll || expandValue.includes(item.value)))}
<div class="tf-select-content-children">
{@render selectItems(item.children)}
</div>
{/if}
{/each}
{/snippet}
<div {...rest} class="tf-select {rest['class']}">
<FloatingTrigger bind:this={triggerObject}>
<button class="tf-select-input nopan nodrag" {...rest}>
<div class="tf-select-input-value">
{#each activeItemsState as item, index (`${index}_${item.value}`)}
{#if !multiple}
{#if index === 0}
<Render target={item.label} />
{/if}
{:else}
<Render target={item.label} />
{#if index < activeItemsState.length - 1}
,
{/if}
{/if}
{:else}
<div class="tf-select-input-placeholder">
{placeholder}
</div>
{/each}
</div>
<div class="tf-select-input-arrow">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"></path>
</svg>
</div>
</button>
{#snippet floating()}
<div class="tf-select-content nopan nodrag nowheel ">
{@render selectItems(items)}
</div>
{/snippet}
</FloatingTrigger>
</div>

View File

@@ -0,0 +1,40 @@
<script lang="ts">
import type {Snippet} from 'svelte';
interface Item {
value: number | string;
label: string | Snippet;
}
export let items: Item[] = [];
export let onChange: ((item: Item, index: number) => void) = () => {
};
export let activeIndex: number = 0;
function handlerOnChange(item: Item, index: number) {
activeIndex = index;
onChange?.(item, index);
}
</script>
<div {...$$restProps} class="tf-tabs {$$restProps.class}">
{#each items as item, index}
<div class="tf-tabs-item {index === activeIndex ? 'active' : ''}" role="button" tabindex={index}
onclick={() => handlerOnChange(item,index)}
onkeydown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handlerOnChange(item,index);
}
}}
>
{#if typeof item.label === 'string'}
{item.label}
{:else }
{@render item.label?.()}
{/if}
</div>
{/each}
</div>

View File

@@ -0,0 +1,81 @@
<script lang="ts">
import type {MyHTMLTextareaAttributes} from './types';
const { value, height, autoHeight = true, maxHeight, onHeightChange, ...rest }: MyHTMLTextareaAttributes & {
value?: any;
height?: string | number;
autoHeight?: boolean;
rows?: number;
maxHeight?: string | number;
onHeightChange?: (height: string) => void;
} = $props();
let textareaEl: HTMLTextAreaElement;
let defaultHeight: number;
// 统一的高度调整函数
function adjustHeight() {
if (textareaEl && autoHeight) {
// 如果外部指定了固定高度,则使用指定高度
if (height !== undefined) {
const fixedHeight = typeof height === 'number' ? `${height}px` : height;
textareaEl.style.height = fixedHeight;
textareaEl.style.overflowY = 'hidden';
onHeightChange?.(fixedHeight);
return;
}
textareaEl.style.height = 'auto';
textareaEl.style.overflowY = 'hidden';
let newHeight = textareaEl.scrollHeight;
// defaultHeight 只计算一次
if (!defaultHeight) {
const defaultRows = rest.rows || 1;
const computedStyle = getComputedStyle(textareaEl);
const lineHeight = parseFloat(computedStyle.fontSize) * 1.2;
defaultHeight = (lineHeight * defaultRows + parseFloat(computedStyle.paddingTop)
+ parseFloat(computedStyle.paddingBottom)) + parseFloat(computedStyle.borderTopWidth);
}
if (newHeight < defaultHeight) {
newHeight = defaultHeight;
}
// 应用最大高度限制
if (maxHeight) {
const max_height = typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight;
if (newHeight > parseInt(max_height)) {
textareaEl.style.height = max_height;
textareaEl.style.overflowY = 'auto';
} else {
textareaEl.style.height = `${newHeight}px`;
}
} else {
textareaEl.style.height = `${newHeight}px`;
}
onHeightChange?.(textareaEl.style.height);
}
}
$effect(() => {
adjustHeight();
});
</script>
<textarea
bind:this={textareaEl}
spellcheck="false"
{...rest}
oninput={(e)=>{
adjustHeight();
rest.oninput?.(e);
}}
onchange={(e)=>{
adjustHeight();
rest.onchange?.(e);
}}
class="tf-textarea nodrag nowheel {rest.class}"
>{value || ""}</textarea>

View File

@@ -0,0 +1,14 @@
import type {
HTMLAttributes,
HTMLButtonAttributes,
HTMLInputAttributes,
HTMLTextareaAttributes,
} from 'svelte/elements';
export interface MyHTMLButtonAttributes extends HTMLButtonAttributes {}
export interface MyHTMLInputAttributes extends HTMLInputAttributes {}
export interface MyHTMLTextareaAttributes extends HTMLTextareaAttributes {}
export interface MyHTMLAttributes extends HTMLAttributes {}

View File

@@ -0,0 +1,178 @@
<script lang="ts">
import {Input, MenuButton, Textarea} from '../base';
import {Button, FloatingTrigger, Select} from '../base/index.js';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
import {useRefOptions} from '../utils/useRefOptions.svelte';
import type {Parameter} from '#types';
import {confirmFormTypes, contentTypes} from '#consts';
const { parameter, index, dataKeyName, useChildrenOnly }: {
parameter: Parameter,
index: number,
dataKeyName: string,
useChildrenOnly?: boolean,
} = $props();
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let param = $derived.by(() => {
return {
...parameter,
...(node?.current?.data?.[dataKeyName] as Array<Parameter>)[index]
};
});
const { updateNodeData } = useSvelteFlow();
const updateParam = (key: string, value: any) => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
parameters[index] = {
...parameters[index],
[key]: value
};
return {
[dataKeyName]: parameters
};
});
};
const updateParamByEvent = (name: string, event: Event) => {
const newValue = (event.target as any).value;
updateParam(name, newValue);
};
const updateRef = (item: any) => {
const newValue = item.value;
updateParam('ref', newValue);
};
const updateFormType = (item: any) => {
const newValue = item.value;
updateParam('formType', newValue);
};
const updateContentType = (item: any) => {
const newValue = item.value;
updateParam('contentType', newValue);
};
// const updateRequired = (item: any) => {
// const newValue = item.target.checked;
// updateParam('required', newValue);
// };
let triggerObject: any;
const handleDelete = () => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
parameters.splice(index, 1);
return {
[dataKeyName]: [...parameters]
};
});
triggerObject?.hide();
};
let selectItems = useRefOptions(useChildrenOnly);
</script>
<div class="input-item">
<Input style="width: 100%;" value={param.name} placeholder="请输入参数名称"
disabled={param.nameDisabled === true}
oninput={(event)=>updateParamByEvent('name', event)} />
</div>
<div class="input-item">
{#if param.refType === 'fixed'}
<Input value={param.value} placeholder="请输入参数值" oninput={(event)=>updateParamByEvent('value', event)} />
{:else if (param.refType !== 'input')}
<Select items={selectItems.current} style="width: 100%" defaultValue={["ref"]} value={[param.ref]}
expandAll
onSelect={updateRef} />
{/if}
</div>
<div class="input-item">
<FloatingTrigger placement="bottom" bind:this={triggerObject}>
<MenuButton />
{#snippet floating()}
<div class="input-more-setting">
<div class="input-more-item">
数据内容:
<Select items={contentTypes} style="width: 100%" defaultValue={["text"]}
value={param.contentType ? [param.contentType] : []}
onSelect={updateContentType}
/>
</div>
<div class="input-more-item">
确认方式:
<Select items={confirmFormTypes} style="width: 100%" defaultValue={["single"]}
value={param.formType ? [param.formType] : []}
onSelect={updateFormType}
/>
</div>
<div class="input-more-item">
数据标题:
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
updateParamByEvent('formLabel', event)
}} value={param.formLabel} />
</div>
<div class="input-more-item">
数据描述:
<Textarea rows={2} style="width: 100%;" onchange={(event)=>{
updateParamByEvent('formDescription', event)
}} value={param.formDescription} />
</div>
<!-- <label class="input-item-inline">-->
<!-- <span>是否必填:</span>-->
<!-- <input type="checkbox" checked={false} onchange={updateRequired} />-->
<!-- </label>-->
<div class="input-more-item">
<Button onclick={handleDelete}>删除</Button>
</div>
</div>
{/snippet}
</FloatingTrigger>
</div>
<style lang="less">
.input-item {
display: flex;
align-items: center;
}
.input-more-setting {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
width: 200px;
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
.input-more-item {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,66 @@
<script lang="ts">
import {useNodesData} from '@xyflow/svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import ConfirmParameterItem from './ConfirmParameterItem.svelte';
const {
noneParameterText = '无确认数据',
dataKeyName = 'parameters',
useChildrenOnly,
}: {
noneParameterText?: string;
dataKeyName?: string;
useChildrenOnly?: boolean,
} = $props();
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let parameters = $derived.by(() => {
return [...node?.current?.data?.[dataKeyName] as Array<any> || []];
});
</script>
<div class="input-container">
{#if (parameters.length !== 0)}
<div class="input-header">参数名称</div>
<div class="input-header">参数值</div>
<div class="input-header"></div>
{/if}
{#each parameters as param, index (param.id)}
<ConfirmParameterItem parameter={param} index={index} {dataKeyName} {useChildrenOnly}/>
{:else }
<div class="none-params">{noneParameterText}</div>
{/each}
</div>
<style lang="less">
.input-container {
display: grid;
grid-template-columns: 40% 50% 10%;
row-gap: 5px;
column-gap: 3px;
.none-params {
font-size: 12px;
background: #f8f8f8;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
width: calc(100% - 5px);
grid-column: 1 / -1; /* 从第一列开始到最后一列结束 */
}
.input-header {
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,184 @@
<script lang="ts">
import {Input, Textarea} from '../base';
import {Button, Checkbox, FloatingTrigger, Select} from '../base/index.js';
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
import {contentTypes, startFormTypes} from '#consts';
import type {Parameter} from '#types';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
const { parameter, index }: {
parameter: Parameter,
index: number
} = $props();
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let param = $derived.by(() => {
return {
...parameter,
...(node?.current?.data?.parameters as Array<Parameter>)[index]
};
});
const { updateNodeData } = useSvelteFlow();
const updateParameter = (key: string, value: any) => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data.parameters as Array<Parameter>;
(parameters[index] as any)[key] = value;
return {
parameters
};
});
};
const updateParamByEvent = (name: string, event: Event) => {
const newValue = (event.target as any).value;
updateParameter(name, newValue);
};
const updateName = (event: Event) => {
const newValue = (event.target as any).value;
updateParameter('name', newValue);
};
const updateRequired = (event: Event) => {
const checked = (event.target as any).checked;
updateParameter('required', checked);
};
const updateFormType = (item: any) => {
const newValue = item.value;
updateParameter('formType', newValue);
};
const updateContentType = (item: any) => {
const newValue = item.value;
updateParameter('contentType', newValue);
};
let triggerObject: any;
const handleDelete = () => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data.parameters as Array<Parameter>;
parameters.splice(index, 1);
return {
parameters: [...parameters]
};
});
triggerObject?.hide();
};
</script>
<div class="input-item">
<Input style="width: 100%;" value={param.name} placeholder="请输入参数名称"
oninput={updateName} />
</div>
<div class="input-item">
<Checkbox checked={param.required} onchange={updateRequired} />
</div>
<div class="input-item">
<FloatingTrigger placement="bottom" bind:this={triggerObject}>
<Button class="input-btn-more">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M4.5 10.5C3.675 10.5 3 11.175 3 12C3 12.825 3.675 13.5 4.5 13.5C5.325 13.5 6 12.825 6 12C6 11.175 5.325 10.5 4.5 10.5ZM19.5 10.5C18.675 10.5 18 11.175 18 12C18 12.825 18.675 13.5 19.5 13.5C20.325 13.5 21 12.825 21 12C21 11.175 20.325 10.5 19.5 10.5ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z"></path>
</svg>
</Button>
{#snippet floating()}
<div class="input-more-setting">
<div class="input-more-item">
数据内容:
<Select items={contentTypes} style="width: 100%" defaultValue={["text"]}
value={param.contentType ? [param.contentType] : []}
onSelect={updateContentType}
/>
</div>
<div class="input-more-item">
输入方式:
<Select items={startFormTypes} style="width: 100%" defaultValue={["input"]}
value={param.formType ? [param.formType] : []}
onSelect={updateFormType}
/>
</div>
{#if param.formType === "radio" || param.formType === "checkbox" || param.formType === "select" }
<div class="input-more-item">
数据选项:
<Textarea rows={3} style="width: 100%;" onchange={(event)=>{
updateParameter('enums', event.target?.value.trim().split("\n"))
}} value={param.enums?.join("\n")} placeholder="一行一个选项" />
</div>
{/if}
<div class="input-more-item">
数据标题:
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
updateParamByEvent('formLabel', event)
}} value={param.formLabel} />
</div>
<div class="input-more-item">
数据描述:
<Textarea rows={2} style="width: 100%;" onchange={(event)=>{
updateParamByEvent('formDescription', event)
}} value={param.formDescription} />
</div>
<div class="input-more-item">
占位符:
<Textarea rows={2} style="width: 100%;" onchange={(event)=>{
updateParamByEvent('formPlaceholder', event)
}} value={param.formPlaceholder} />
</div>
<div class="input-more-item">
<Button onclick={handleDelete}>删除</Button>
</div>
</div>
{/snippet}
</FloatingTrigger>
</div>
<style lang="less">
.input-item {
display: flex;
align-items: center;
}
.input-item-inline {
display: flex;
align-items: center;
font-size: 12px;
color: #666;
}
.input-more-setting {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
width: 200px;
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
.input-more-item {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,56 @@
<script lang="ts">
import DefinedParameterItem from './DefinedParameterItem.svelte';
import {useNodesData} from '@xyflow/svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let parameters = $derived.by(() => {
return [...node?.current?.data?.parameters as Array<any> || []];
});
</script>
<div class="input-container">
{#if (parameters.length !== 0)}
<div class="input-header">参数名称</div>
<div class="input-header">必填</div>
<div class="input-header"></div>
{/if}
{#each parameters as param, index (param.id)}
<DefinedParameterItem parameter={param} index={index} />
{:else }
<div class="none-params">无输入参数</div>
{/each}
</div>
<style lang="less">
.input-container {
display: grid;
grid-template-columns: 80% 10% 10%;
row-gap: 5px;
column-gap: 3px;
.none-params {
font-size: 12px;
background: #f8f8f8;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
width: calc(100% - 5px);
grid-column: 1 / -1; /* 从第一列开始到最后一列结束 */
}
.input-header {
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import {Button} from '../base/';
import {type Node} from '@xyflow/svelte';
const { icon, title, type, description, extra }: {
icon: string,
title: string,
type: string,
description: string,
extra?: Partial<Node['data']>,
} = $props();
const onDragStart = (event: DragEvent) => {
if (!event.dataTransfer) {
return null;
}
const node = {
type,
data: {
title,
description,
...extra
}
};
event.dataTransfer.setData('application/tinyflow', JSON.stringify(node));
event.dataTransfer.effectAllowed = 'move';
};
</script>
<Button draggable ondragstart={onDragStart} data-node-type={type}>{@html icon} {title}</Button>

View File

@@ -0,0 +1,299 @@
<script lang="ts">
import {Handle, type NodeProps, NodeToolbar, Position, useSvelteFlow} from '@xyflow/svelte';
import {Button, Collapse, FloatingTrigger, Input, Textarea} from '../base';
import {type Snippet} from 'svelte';
import {useDeleteNode} from '../utils/useDeleteNode.svelte';
import {useCopyNode} from '../utils/useCopyNode.svelte';
import {getOptions} from '../utils/NodeUtils';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
const {
data,
id = '',
icon,
handle,
children,
allowExecute = true,
allowCopy = true,
allowDelete = true,
allowSetting = true,
allowSettingOfCondition = true,
showSourceHandle = true,
showTargetHandle = true,
onCollapse
}: {
data: NodeProps['data'],
id?: NodeProps['id'],
icon?: Snippet,
handle?: Snippet,
children: Snippet,
allowExecute?: boolean,
allowCopy?: boolean,
allowDelete?: boolean,
allowSetting?: boolean,
allowSettingOfCondition?: boolean,
showSourceHandle?: boolean,
showTargetHandle?: boolean,
onCollapse?: (key: string) => void,
} = $props();
let activeKeys = data.expand ? ['key'] : [];
const { updateNodeData, getNode } = useSvelteFlow();
const items = $derived.by(() => {
return [{
key: 'key',
icon,
title: data.title as string,
description: data.description as string,
content: children
}];
});
const { deleteNode } = useDeleteNode();
const { copyNode } = useCopyNode();
const options = getOptions();
const executeNode = () => {
options.onNodeExecute?.(getNode(id)!);
};
let currentNodeId = getCurrentNodeId();
</script>
{#if allowExecute || allowCopy || allowDelete}
<NodeToolbar position={Position.Top} align="start">
<div class="tf-node-toolbar">
{#if allowDelete}
<Button class="tf-node-toolbar-item" onclick={()=>{ deleteNode(id) }}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M17 6H22V8H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V8H2V6H7V3C7 2.44772 7.44772 2 8 2H16C16.5523 2 17 2.44772 17 3V6ZM18 8H6V20H18V8ZM9 11H11V17H9V11ZM13 11H15V17H13V11ZM9 4V6H15V4H9Z"></path>
</svg>
</Button>
{/if}
{#if allowCopy}
<Button class="tf-node-toolbar-item" onclick={()=>{copyNode(id)}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M6.9998 6V3C6.9998 2.44772 7.44752 2 7.9998 2H19.9998C20.5521 2 20.9998 2.44772 20.9998 3V17C20.9998 17.5523 20.5521 18 19.9998 18H16.9998V20.9991C16.9998 21.5519 16.5499 22 15.993 22H4.00666C3.45059 22 3 21.5554 3 20.9991L3.0026 7.00087C3.0027 6.44811 3.45264 6 4.00942 6H6.9998ZM5.00242 8L5.00019 20H14.9998V8H5.00242ZM8.9998 6H16.9998V16H18.9998V4H8.9998V6Z"></path>
</svg>
</Button>
{/if}
{#if allowExecute}
<Button class="tf-node-toolbar-item" onclick={executeNode}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M8 18.3915V5.60846L18.2264 12L8 18.3915ZM6 3.80421V20.1957C6 20.9812 6.86395 21.46 7.53 21.0437L20.6432 12.848C21.2699 12.4563 21.2699 11.5436 20.6432 11.152L7.53 2.95621C6.86395 2.53993 6 3.01878 6 3.80421Z"></path>
</svg>
</Button>
{/if}
{#if allowSetting}
<FloatingTrigger placement="bottom">
<Button class="tf-node-toolbar-item">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M3.33946 17.0002C2.90721 16.2515 2.58277 15.4702 2.36133 14.6741C3.3338 14.1779 3.99972 13.1668 3.99972 12.0002C3.99972 10.8345 3.3348 9.824 2.36353 9.32741C2.81025 7.71651 3.65857 6.21627 4.86474 4.99001C5.7807 5.58416 6.98935 5.65534 7.99972 5.072C9.01009 4.48866 9.55277 3.40635 9.4962 2.31604C11.1613 1.8846 12.8847 1.90004 14.5031 2.31862C14.4475 3.40806 14.9901 4.48912 15.9997 5.072C17.0101 5.65532 18.2187 5.58416 19.1346 4.99007C19.7133 5.57986 20.2277 6.25151 20.66 7.00021C21.0922 7.7489 21.4167 8.53025 21.6381 9.32628C20.6656 9.82247 19.9997 10.8336 19.9997 12.0002C19.9997 13.166 20.6646 14.1764 21.6359 14.673C21.1892 16.2839 20.3409 17.7841 19.1347 19.0104C18.2187 18.4163 17.0101 18.3451 15.9997 18.9284C14.9893 19.5117 14.4467 20.5941 14.5032 21.6844C12.8382 22.1158 11.1148 22.1004 9.49633 21.6818C9.55191 20.5923 9.00929 19.5113 7.99972 18.9284C6.98938 18.3451 5.78079 18.4162 4.86484 19.0103C4.28617 18.4205 3.77172 17.7489 3.33946 17.0002ZM8.99972 17.1964C10.0911 17.8265 10.8749 18.8227 11.2503 19.9659C11.7486 20.0133 12.2502 20.014 12.7486 19.9675C13.1238 18.8237 13.9078 17.8268 14.9997 17.1964C16.0916 16.5659 17.347 16.3855 18.5252 16.6324C18.8146 16.224 19.0648 15.7892 19.2729 15.334C18.4706 14.4373 17.9997 13.2604 17.9997 12.0002C17.9997 10.74 18.4706 9.5632 19.2729 8.6665C19.1688 8.4405 19.0538 8.21822 18.9279 8.00021C18.802 7.78219 18.667 7.57148 18.5233 7.36842C17.3457 7.61476 16.0911 7.43414 14.9997 6.80405C13.9083 6.17395 13.1246 5.17768 12.7491 4.03455C12.2509 3.98714 11.7492 3.98646 11.2509 4.03292C10.8756 5.17671 10.0916 6.17364 8.99972 6.80405C7.9078 7.43447 6.65245 7.61494 5.47428 7.36803C5.18485 7.77641 4.93463 8.21117 4.72656 8.66637C5.52881 9.56311 5.99972 10.74 5.99972 12.0002C5.99972 13.2604 5.52883 14.4372 4.72656 15.3339C4.83067 15.5599 4.94564 15.7822 5.07152 16.0002C5.19739 16.2182 5.3324 16.4289 5.47612 16.632C6.65377 16.3857 7.90838 16.5663 8.99972 17.1964ZM11.9997 15.0002C10.3429 15.0002 8.99972 13.6571 8.99972 12.0002C8.99972 10.3434 10.3429 9.00021 11.9997 9.00021C13.6566 9.00021 14.9997 10.3434 14.9997 12.0002C14.9997 13.6571 13.6566 15.0002 11.9997 15.0002ZM11.9997 13.0002C12.552 13.0002 12.9997 12.5525 12.9997 12.0002C12.9997 11.4479 12.552 11.0002 11.9997 11.0002C11.4474 11.0002 10.9997 11.4479 10.9997 12.0002C10.9997 12.5525 11.4474 13.0002 11.9997 13.0002Z"></path>
</svg>
</Button>
{#snippet floating()}
<div class="settings">
<div class="input-item">
节点名称:
<Input style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
title: value
})
}} value={data.title} />
</div>
<div class="input-item">
参数描述:
<Textarea rows={3} style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
description: value
})
}} value={data.description} />
</div>
{#if allowSettingOfCondition}
<div class="input-item">
执行条件:
<Textarea rows={2} style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
condition: value
})
}} value={data.condition} />
</div>
{/if}
<label class="input-item-inline">
<span>异步执行:</span>
<input type="checkbox" checked={!!data.async} onchange={(event)=>{
const value = (event.target as any).checked;
updateNodeData(currentNodeId,{
async: value
})
}} />
</label>
<label class="input-item-inline">
<span>循环执行:</span>
<input type="checkbox" checked={!!data.loopEnable} onchange={(event)=>{
const value = (event.target as any).checked;
updateNodeData(currentNodeId,{
loopEnable: value
})
}} />
</label>
{#if !!data.loopEnable}
<div class="input-item">
循环间隔时间(单位:毫秒):
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
loopIntervalMs: value
})
}} value={data.loopIntervalMs || '1000'} />
</div>
<div class="input-item">
最大循环次数0 表示不限制):
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
maxLoopCount: value
})
}} value={data.maxLoopCount || '0'} />
</div>
<div class="input-item">
退出条件:
<Textarea rows={2} style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
loopBreakCondition: value
})
}} value={data.loopBreakCondition} />
</div>
{/if}
<label class="input-item-inline">
<span>错误重试:</span>
<input type="checkbox" checked={!!data.retryEnable} onchange={(event)=>{
const value = (event.target as any).checked;
updateNodeData(currentNodeId,{
retryEnable: value
})
}} />
</label>
{#if !!data.retryEnable}
<div class="input-item">
错误重试间隔时间(单位:毫秒):
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
retryIntervalMs: value
})
}} value={data.retryIntervalMs || '1000'} />
</div>
<div class="input-item">
最大重试次数:
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
maxRetryCount: value
})
}} value={data.maxRetryCount || '3'} />
</div>
<label class="input-item-inline">
<span>正常后重置重试次数记录:</span>
<input type="checkbox" checked={!!data.resetRetryCountAfterNormal} onchange={(event)=>{
const value = (event.target as any).checked;
updateNodeData(currentNodeId,{
resetRetryCountAfterNormal: value
})
}} />
</label>
{/if}
</div>
{/snippet}
</FloatingTrigger>
{/if}
</div>
</NodeToolbar>
{/if}
<div class="tf-node-wrapper">
<div class="tf-node-wrapper-body">
<Collapse {items} activeKeys={activeKeys} onChange={(_,actionKeys) => {
updateNodeData(id, {expand: actionKeys?.includes('key')})
onCollapse?.(actionKeys?.includes('key') ? 'key' : '')
}} />
</div>
</div>
{#if showTargetHandle}
<Handle type="target" position={Position.Left} style=" left: -12px;top: 20px" />
{/if}
{#if showSourceHandle}
<Handle type="source" position={Position.Right} style="right: -12px;top: 20px" />
{/if}
{@render handle?.()}
<style lang="less">
.tf-node-toolbar {
display: flex;
gap: 5px;
padding: 5px;
border-radius: 5px;
background: #fff;
border: 1px solid #eee;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
:global(.tf-node-toolbar-item) {
border: 1px solid transparent;
}
.settings {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
width: 200px;
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
.input-item {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 12px;
color: #666;
}
.input-item-inline {
display: flex;
align-items: center;
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,225 @@
<script lang="ts">
import {Input, MenuButton, Textarea} from '../base';
import {Button, FloatingTrigger, Select} from '../base/index.js';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
import {parameterDataTypes} from '#consts';
import {genShortId} from '../utils/IdGen';
import type {Parameter} from '#types';
import {deepClone} from '../utils/deepClone';
const { parameter, position, dataKeyName, placeholder = '请输入参数值' }: {
parameter: Parameter,
position: number[],
dataKeyName: string,
placeholder?: string,
} = $props();
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let currentParameter = $derived.by(() => {
let parameters = node?.current?.data?.[dataKeyName] as Parameter[];
let param;
if (parameters && position.length > 0) {
let params = parameters as Array<Parameter>;
for (let i = 0; i < position.length; i++) {
const pos = position[i];
if (i == position.length - 1) {
param = params[pos];
} else {
params = params[pos].children!;
}
}
}
return {
...parameter,
...param
};
});
const { updateNodeData } = useSvelteFlow();
const updateAttribute = (key: string, value: any) => {
updateNodeData(currentNodeId, (node) => {
const parameters = node.data?.[dataKeyName] as Array<Parameter>;
if (parameters && position.length > 0) {
let params = parameters as Parameter[];
for (let i = 0; i < position.length; i++) {
const pos = position[i];
if (i == position.length - 1) {
params[pos] = {
...params[pos],
[key]: value
};
} else {
params = params[pos].children!;
}
}
}
return {
[dataKeyName]: [...deepClone(parameters)]
};
});
};
const updateByEvent = (name: string, event: Event) => {
const newValue = (event.target as any).value;
updateAttribute(name, newValue);
};
const updateDataType = (item: any) => {
const newValue = item.value;
updateAttribute('dataType', newValue);
};
let triggerObject: any;
const handleDelete = () => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
if (parameters && position.length > 0) {
let params = parameters as Array<Parameter>;
for (let i = 0; i < position.length; i++) {
const pos = position[i];
if (i == position.length - 1) {
params.splice(pos, 1);
} else {
params = params[pos].children!;
}
}
}
return {
[dataKeyName]: [...deepClone(parameters)]
};
});
triggerObject?.hide();
};
const handleAddChildParameter = () => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
if (parameters && position.length > 0) {
let params = parameters as Array<Parameter>;
for (let i = 0; i < position.length; i++) {
const pos = position[i];
if (i == position.length - 1) {
if (params[pos].children) {
params[pos].children.push({
id: genShortId(),
name: 'newParam',
dataType: 'String'
});
} else {
params[pos].children = [
{
id: genShortId(),
name: 'newParam',
dataType: 'String'
}
];
}
} else {
params = params[pos].children!;
}
}
}
return {
[dataKeyName]: [...deepClone(parameters)]
};
});
};
</script>
<div class="input-item">
{#if (position.length > 1)}
{#each position as p} &nbsp;{/each}
{/if}
<Input style="width: 100%;" value={currentParameter.name} placeholder={placeholder}
oninput={(e)=>{updateByEvent('name',e)}} disabled={currentParameter.nameDisabled === true} />
</div>
<div class="input-item">
<Select items={currentParameter.dataTypeItems || parameterDataTypes} style="width: 100%" defaultValue={["String"]}
value={currentParameter.dataType ? [currentParameter.dataType]:[]}
disabled={currentParameter.dataTypeDisabled === true}
onSelect={updateDataType} />
{#if (currentParameter.dataType === "Object" || currentParameter.dataType === "Array") && currentParameter.addChildDisabled !== true}
<Button class="input-btn-more" style="margin-left: auto" onclick={handleAddChildParameter}>
<svg style="transform: scaleY(-1)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="currentColor">
<path
d="M13 8V16C13 17.6569 11.6569 19 10 19H7.82929C7.41746 20.1652 6.30622 21 5 21C3.34315 21 2 19.6569 2 18C2 16.3431 3.34315 15 5 15C6.30622 15 7.41746 15.8348 7.82929 17H10C10.5523 17 11 16.5523 11 16V8C11 6.34315 12.3431 5 14 5H17V2L22 6L17 10V7H14C13.4477 7 13 7.44772 13 8ZM5 19C5.55228 19 6 18.5523 6 18C6 17.4477 5.55228 17 5 17C4.44772 17 4 17.4477 4 18C4 18.5523 4.44772 19 5 19Z"></path>
</svg>
</Button>
{/if}
</div>
<div class="input-item">
<FloatingTrigger placement="bottom" bind:this={triggerObject}>
<MenuButton />
{#snippet floating()}
<div class="input-more-setting">
<div class="input-more-item">
默认值:
<Textarea rows={1} style="width: 100%;"
value={currentParameter.defaultValue||''}
onchange={(event)=>{
updateByEvent( 'defaultValue', event)
}} />
</div>
<div class="input-more-item">
参数描述:
<Textarea rows={3} style="width: 100%;"
value={currentParameter.description||''}
onchange={(event)=>{
updateByEvent( 'description', event)
}} />
</div>
{#if currentParameter.deleteDisabled !== true}
<div class="input-more-item">
<Button onclick={handleDelete}>删除</Button>
</div>
{/if}
</div>
{/snippet}
</FloatingTrigger>
</div>
<style lang="less">
.input-item {
display: flex;
align-items: center;
gap: 2px;
}
.input-more-setting {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
width: 200px;
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
.input-more-item {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,76 @@
<script lang="ts">
import {useNodesData} from '@xyflow/svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import type {Parameter} from '#types';
import OutputDefItem from './OutputDefItem.svelte';
const {
noneParameterText = '无输出参数',
dataKeyName = 'outputDefs',
placeholder = '请输入参数名称',
}: {
noneParameterText?: string;
dataKeyName?: string;
placeholder?: string
} = $props();
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let parameters = $derived.by(() => {
return [...node?.current?.data?.[dataKeyName] as Array<Parameter> || []];
});
</script>
{#snippet parameterList(params: Parameter[], position: number[])}
{#each params as param, index (`${param.id}_${param.children ? param.children.length : 0}`)}
<OutputDefItem parameter={param} position={[...position, index]} {dataKeyName} {placeholder} />
{#if param.children}
{@render parameterList(param.children, [...position, index])}
{/if}
{:else }
{#if position.length === 0}
<div class="none-params">{noneParameterText}</div>
{/if}
{/each}
{/snippet}
<div class="input-container">
{#if (parameters.length !== 0)}
<div class="input-header">参数名称</div>
<div class="input-header">参数类型</div>
<div class="input-header"></div>
{/if}
{@render parameterList(parameters || [], [])}
</div>
<style lang="less">
.input-container {
display: grid;
grid-template-columns: 40% 50% 10%;
row-gap: 5px;
column-gap: 3px;
.none-params {
font-size: 12px;
background: #f8f8f8;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
width: calc(100% - 5px);
grid-column: 1 / -1; /* 从第一列开始到最后一列结束 */
}
.input-header {
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,175 @@
<script lang="ts">
import {Input, MenuButton, Textarea} from '../base';
import {Button, FloatingTrigger, Select} from '../base/index.js';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
import {contentTypes, parameterRefTypes} from '#consts';
import {useRefOptions} from '../utils/useRefOptions.svelte';
import {onMount} from 'svelte';
import type {Parameter} from '#types';
onMount(() => {
if (!param.refType) {
updateRefType({ value: 'ref' }); // 设置数据来源默认值
}
});
const { parameter, index, dataKeyName, useChildrenOnly, showContentType = false }: {
parameter: Parameter,
index: number,
dataKeyName: string,
useChildrenOnly?: boolean,
showContentType?: boolean,
} = $props();
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let param = $derived.by(() => {
return {
...parameter,
...(node?.current?.data?.[dataKeyName] as Array<Parameter>)[index]
};
});
const { updateNodeData } = useSvelteFlow();
const updateParam = (key: string, value: any) => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
parameters[index] = {
...parameters[index],
[key]: value
};
return {
[dataKeyName]: parameters
};
});
};
const updateParamByEvent = (name: string, event: Event) => {
const newValue = (event.target as any).value;
updateParam(name, newValue);
};
const updateRef = (item: any) => {
const newValue = item.value;
updateParam('ref', newValue);
};
const updateRefType = (item: any) => {
const newValue = item.value;
updateParam('refType', newValue);
};
const updateContentType = (item: any) => {
const newValue = item.value;
updateParam('contentType', newValue);
};
let triggerObject: any;
const handleDelete = () => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
parameters.splice(index, 1);
return {
[dataKeyName]: [...parameters]
};
});
triggerObject?.hide();
};
let selectItems = useRefOptions(useChildrenOnly);
</script>
<div class="input-item">
<Input style="width: 100%;" value={param.name} placeholder="请输入参数名称"
disabled={param.nameDisabled === true}
oninput={(event)=>updateParamByEvent('name', event)} />
</div>
<div class="input-item">
{#if param.refType === 'fixed'}
<Input value={param.value} placeholder="请输入参数值" oninput={(event)=>updateParamByEvent('value', event)} />
{:else if (param.refType !== 'input')}
<Select items={selectItems.current} style="width: 100%" defaultValue={["ref"]} value={[param.ref]}
expandAll
onSelect={updateRef} />
{/if}
</div>
<div class="input-item">
<FloatingTrigger placement="bottom" bind:this={triggerObject}>
<MenuButton />
{#snippet floating()}
<div class="input-more-setting">
<div class="input-more-item">
数据来源:
<Select items={parameterRefTypes} style="width: 100%" defaultValue={["ref"]}
value={param.refType ? [param.refType] : []}
onSelect={updateRefType}
/>
</div>
{#if showContentType }
<div class="input-more-item">
数据内容:
<Select items={contentTypes} style="width: 100%" defaultValue={["text"]}
value={param.contentType ? [param.contentType] : []}
onSelect={updateContentType}
/>
</div>
{/if}
<div class="input-more-item">
默认值:
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
updateParamByEvent('defaultValue', event)
}} value={param.defaultValue} placeholder="请输入参数默认值" />
</div>
<div class="input-more-item">
参数描述:
<Textarea rows={3} style="width: 100%;" onchange={(event)=>{
updateParamByEvent('description', event)
}} value={param.description} placeholder="请输入参数描述"/>
</div>
<div class="input-more-item">
<Button onclick={handleDelete}>删除</Button>
</div>
</div>
{/snippet}
</FloatingTrigger>
</div>
<style lang="less">
.input-item {
display: flex;
align-items: center;
}
.input-more-setting {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
width: 200px;
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
.input-more-item {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,68 @@
<script lang="ts">
import {useNodesData} from '@xyflow/svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import RefParameterItem from './RefParameterItem.svelte';
const {
noneParameterText = '无输入参数',
dataKeyName = 'parameters',
useChildrenOnly,
showContentType = false
}: {
noneParameterText?: string;
dataKeyName?: string;
useChildrenOnly?: boolean,
showContentType?: boolean,
} = $props();
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let parameters = $derived.by(() => {
return [...node?.current?.data?.[dataKeyName] as Array<any> || []];
});
</script>
<div class="input-container">
{#if (parameters.length !== 0)}
<div class="input-header">参数名称</div>
<div class="input-header">参数值</div>
<div class="input-header"></div>
{/if}
{#each parameters as param, index (param.id)}
<RefParameterItem parameter={param} index={index} {dataKeyName} {useChildrenOnly} {showContentType} />
{:else }
<div class="none-params">{noneParameterText}</div>
{/each}
</div>
<style lang="less">
.input-container {
display: grid;
grid-template-columns: 40% 50% 10%;
row-gap: 5px;
column-gap: 3px;
.none-params {
font-size: 12px;
background: #f8f8f8;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
width: calc(100% - 5px);
grid-column: 1 / -1; /* 从第一列开始到最后一列结束 */
}
.input-header {
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,129 @@
<script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
import {Button, Heading, Select} from '../base';
import {Textarea} from '../base/index.js';
import RefParameterList from '../core/RefParameterList.svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useAddParameter} from '../utils/useAddParameter.svelte';
import OutputDefList from '../core/OutputDefList.svelte';
// 添加生命周期函数
import {onMount} from 'svelte';
const { data, ...rest }: {
data: NodeProps['data'],
[key: string]: any
} = $props();
// 在组件挂载时检查并设置默认值
onMount(() => {
if (!data.engine) {
updateNodeData(currentNodeId, () => {
return {
engine: 'qlexpress'
};
});
}
});
const currentNodeId = getCurrentNodeId();
const { addParameter } = useAddParameter();
const { updateNodeData } = useSvelteFlow();
const engines = [
{ label: 'JavaScript', value: 'js' },
{ label: 'Groovy', value: 'groovy' },
{ label: 'QLExpress', value: 'qlexpress' }
];
</script>
<NodeWrapper {data} {...rest}>
{#snippet 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>
{/snippet}
<div class="heading">
<Heading level={3}>输入参数</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId)
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<RefParameterList />
<Heading level={3} mt="10px">代码</Heading>
<div class="setting-title">执行引擎</div>
<div class="setting-item">
<Select items={engines} style="width: 100%" placeholder="请选择执行引擎" onSelect={(item)=>{
const newValue = item.value;
updateNodeData(currentNodeId, ()=>{
return {
engine: newValue
}
})
}} value={data.engine ? [data.engine] : ['qlexpress']} />
</div>
<div class="setting-title">执行代码</div>
<div class="setting-item">
<Textarea rows={10}
placeholder="请输入执行代码输出内容需添加到_result中_result['key'] = value 或者 _result.key = value"
style="width: 100%" onchange={(e:any)=>{
updateNodeData(currentNodeId, ()=>{
return {
code: e.target.value
}
})
}} value={data.code as string||""} />
</div>
<div class="heading">
<Heading level={3} mt="10px">输出参数</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId,'outputDefs')
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<OutputDefList />
</NodeWrapper>
<style>
.heading {
display: flex;
margin-bottom: 10px;
}
.setting-title {
font-size: 12px;
color: #999;
margin-bottom: 4px;
margin-top: 10px;
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
gap: 10px;
}
</style>

View File

@@ -0,0 +1,132 @@
<script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
import {Button, Heading} from '../base';
import {Textarea} from '../base/index.js';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useAddParameter} from '../utils/useAddParameter.svelte';
import OutputDefList from '../core/OutputDefList.svelte';
import ConfirmParameterList from '../core/ConfirmParameterList.svelte';
import type {Parameter} from '#types';
import {deepEqual} from '#components/utils/deepEqual';
const { data, ...rest }: {
data: NodeProps['data'],
[key: string]: any
} = $props();
const currentNodeId = getCurrentNodeId();
const { addParameter } = useAddParameter();
const { updateNodeData } = useSvelteFlow();
$effect(() => {
if (data.confirms) {
const outputDefs = data.confirms.map((confirm: Parameter) => {
return {
// id?: string;
// name?: string;
// nameDisabled?: boolean;
// dataType?: string;
// dataTypeDisabled?: boolean;
// ref?: string;
// refType?: string;
// value?: string;
// description?: string;
// required?: boolean;
// defaultValue?: string;
// deleteDisabled?: boolean;
// addChildDisabled?: boolean;
// children?: Parameter[];
...confirm,
nameDisabled: true,
dataTypeDisabled: true,
dataType: confirm.formType === 'checkbox' || confirm.formType === 'select' ? 'Array' : 'String',
addChildDisabled: true
} as Parameter;
});
// 判断 outputDefs 与 data.outputDefs 是否完全一致
// 如果不判断,则会造成死循环更新
if (!deepEqual(outputDefs, data.outputDefs)) {
updateNodeData(currentNodeId, () => {
return {
outputDefs
};
});
}
}
});
</script>
<NodeWrapper {data} {...rest}>
{#snippet 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>
{/snippet}
<div class="heading">
<Heading level={3}>确认数据</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId, 'confirms')
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<ConfirmParameterList dataKeyName="confirms" noneParameterText="无确认数据" />
<Heading level={3} mt="10px">确认消息</Heading>
<div class="setting-title">消息内容</div>
<div class="setting-item">
<Textarea rows={5} placeholder="请输入用户需要确认的消息内容"
style="width: 100%" onchange={(e:any)=>{
updateNodeData(currentNodeId, ()=>{
return {
message: e.target.value
}
})
}} value={data.message as string||""} />
</div>
<div class="heading">
<Heading level={3} mt="10px">输出参数</Heading>
</div>
<OutputDefList placeholder="" />
</NodeWrapper>
<style>
.heading {
display: flex;
margin-bottom: 10px;
}
.setting-title {
font-size: 12px;
color: #999;
margin-bottom: 4px;
margin-top: 10px;
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
gap: 10px;
}
</style>

View File

@@ -0,0 +1,250 @@
<script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte';
import {type Node, type NodeProps, useSvelteFlow} from '@xyflow/svelte';
import {Button, Chosen, Heading, Input, Select, Textarea} from '../base';
import RefParameterList from '../core/RefParameterList.svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useAddParameter} from '../utils/useAddParameter.svelte';
import {getOptions} from '../utils/NodeUtils';
import OutputDefList from '../core/OutputDefList.svelte';
import {fillParameterId} from '../utils/useAddParameter.svelte.js';
const { data, ...rest }: {
data: NodeProps['data'],
[key: string]: any
} = $props();
const currentNodeId = getCurrentNodeId();
const { addParameter } = useAddParameter();
const flowInstance = useSvelteFlow();
const { updateNodeData: updateNodeDataInner } = flowInstance;
const updateNodeData = (data: Record<string, any>) => {
updateNodeDataInner(currentNodeId, data);
};
const updateNodeDataByEvent = (name: string, event: Event) => {
updateNodeData({
[name]: (event.target as any)?.value
});
};
const node = {
...rest,
id: currentNodeId,
data
} as Node;
const externalElement = document.createElement('div') as HTMLElement;
const options = getOptions();
const customNode = options.customNodes![rest.type as string];
customNode.render?.(externalElement, node, flowInstance);
const forms = customNode.forms;
let container: HTMLElement;
$effect(() => {
// 注意:由于 $effect 的 state 自动追踪问题,需要 data.expand 方在 if 里的最前面
if (data.expand && container) {
container.append(externalElement);
}
});
$effect(() => {
if (data) {
customNode.onUpdate?.(externalElement, { ...node, data });
}
});
$effect(() => {
if (!data.parameters && customNode.parameters) {
updateNodeData({
parameters: fillParameterId(JSON.parse(JSON.stringify(customNode.parameters)))
});
}
});
$effect(() => {
if (!data.outputDefs && customNode.outputDefs) {
updateNodeData({
outputDefs: fillParameterId(JSON.parse(JSON.stringify(customNode.outputDefs)))
});
}
});
</script>
<NodeWrapper data={{...data, description: customNode.description}} {...rest}>
{#snippet icon()}
{@html customNode.icon}
{/snippet}
{#if customNode.parametersEnable !== false}
<div class="heading">
<Heading level={3}>输入参数</Heading>
{#if customNode.parametersAddEnable !== false}
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId)
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
{/if}
</div>
<RefParameterList />
{/if}
{#if forms}
{#each forms as form}
{#if form.type === 'input'}
<div class="setting-title">{form.label}</div>
<div class="setting-item">
<Input
placeholder={form.placeholder}
style="width: 100%"
value={data[form.name] || form.defaultValue}
{...form.attrs}
onchange={(e)=>{
updateNodeDataByEvent(form.name,e)
}}
/>
</div>
{:else if form.type === 'textarea'}
<div class="setting-title">{form.label}</div>
<div class="setting-item">
<Textarea
rows={3}
placeholder={form.placeholder}
style="width: 100%"
value={data[form.name] || form.defaultValue}
{...form.attrs}
onchange={(e)=>{
updateNodeDataByEvent(form.name,e)
}}
/>
</div>
{:else if form.type === 'slider'}
<div class="setting-title">{form.label}</div>
<div class="setting-item">
<div class="slider-container">
<span>{form.description}: {data[form.name] ?? form.defaultValue}</span>
<input
class="nodrag"
type="range"
{...form.attrs}
value={data[form.name] ?? form.defaultValue}
oninput={(e) => updateNodeData({ [form.name]: parseFloat(e.target.value) })}
/>
</div>
</div>
{:else if form.type === 'select'}
<div class="setting-title">{form.label}</div>
<div class="setting-item">
<Select items={form.options||[]} style="width: 100%" placeholder={form.placeholder} onSelect={(item)=>{
const newValue = item.value;
updateNodeData({
[form.name]: newValue
})
}} value={data[form.name] ? [data[form.name]] : [form.defaultValue]} />
</div>
{:else if form.type === 'chosen'}
<div class="setting-title">{form.label}</div>
<div class="setting-item">
<Chosen style="width: 100%" placeholder={form.placeholder}
buttonText={form.chosen?.buttonText} onChosen={(value,label,event)=>{
form.chosen?.onChosen?.(updateNodeData,value,label,event);
}} value={data[form.chosen?.valueDataKey||""]} label={data[form.chosen?.labelDataKey||""]} />
</div>
{:else if form.type === 'heading'}
<Heading level={3} mt="10px" {...form.attrs}>{form.label}</Heading>
{/if}
{/each}
{/if}
<div bind:this={container} style={customNode.rootStyle||""} class={customNode.rootClass}></div>
{#if customNode.outputDefsEnable !== false}
<div class="heading">
<Heading level={3} mt="10px">输出参数</Heading>
{#if customNode.outputDefsAddEnable !== false}
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId,'outputDefs')
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
{/if}
</div>
<OutputDefList />
{/if}
</NodeWrapper>
<style>
.heading {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.setting-title {
font-size: 12px;
color: #999;
margin-bottom: 4px;
margin-top: 10px;
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
gap: 10px;
}
/* 新增样式 */
.slider-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 4px;
}
.slider-container span {
font-size: 12px;
color: #666;
display: flex;
justify-content: space-between;
align-items: center;
}
input[type="range"] {
width: 100%;
height: 4px;
background: #ddd;
border-radius: 2px;
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: #007bff;
border-radius: 50%;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,50 @@
<script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps} from '@xyflow/svelte';
import {Button, Heading} from '../base';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import RefParameterList from '../core/RefParameterList.svelte';
import {useAddParameter} from '../utils/useAddParameter.svelte';
const { data, ...rest }: {
data: NodeProps['data'],
[key: string]: any
} = $props();
const currentNodeId = getCurrentNodeId();
const { addParameter } = useAddParameter();
</script>
<NodeWrapper {data} {...rest} allowExecute={false} showSourceHandle={false}>
{#snippet 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>
{/snippet}
<div class="heading">
<Heading level={3}>输出参数</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId,'outputDefs',{name:"output"})
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<RefParameterList noneParameterText="无输出参数" dataKeyName="outputDefs" showContentType={true} />
</NodeWrapper>
<style lang="less">
.heading {
display: flex;
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,263 @@
<script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
import {Button, Heading, Select} from '../base';
import {Input, Textarea} from '../base/index.js';
import RefParameterList from '../core/RefParameterList.svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useAddParameter} from '../utils/useAddParameter.svelte';
import OutputDefList from '../core/OutputDefList.svelte';
// 添加生命周期函数
import {onMount} from 'svelte';
const { data, ...rest }: {
data: NodeProps['data'],
[key: string]: any
} = $props();
// 在组件挂载时检查并设置默认值
onMount(() => {
if (!data.method) {
updateNodeData(currentNodeId, () => {
return {
method: 'get'
};
});
}
if (!data.outputDefs) {
addParameter(currentNodeId, 'outputDefs', [
{
name: 'headers',
nameDisabled: true,
dataType: 'Object',
dataTypeDisabled: true,
deleteDisabled: true
},
{
name: 'body',
nameDisabled: true,
dataType: 'String',
deleteDisabled: true
},
{
name: 'statusCode',
nameDisabled: true,
dataType: 'Number',
dataTypeDisabled: true,
deleteDisabled: true
}
]);
}
});
const method = [
{ value: 'get', label: 'GET' },
{ value: 'post', label: 'POST' },
{ value: 'put', label: 'PUT' },
{ value: 'delete', label: 'DELETE' },
{ value: 'head', label: 'HEAD' },
{ value: 'patch', label: 'PATCH' }
];
const currentNodeId = getCurrentNodeId();
const { addParameter } = useAddParameter();
const { updateNodeData } = useSvelteFlow();
</script>
<NodeWrapper {data} {...rest}>
{#snippet 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>
{/snippet}
<div class="heading">
<Heading level={3}>输入参数</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId)
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<RefParameterList />
<Heading level={3} mt="10px">URL 地址</Heading>
<div style="display: flex;gap: 2px;width: 100%;padding: 10px 0">
<div>
<Select items={method} style="width: 100%" placeholder="请选择请求方式" onSelect={(item)=>{
const newValue = item.value;
updateNodeData(currentNodeId, ()=>{
return {
method: newValue
}
})
}} value={data.method ? [data.method] : ['get']} />
</div>
<div style="width: 100%">
<Input placeholder="请输入url" style="width: 100%" onchange={(e:any)=>{
updateNodeData(currentNodeId, ()=>{
return {
url: e.target.value
}
})
}} value={data.url as string||""} />
</div>
</div>
<div class="heading">
<Heading level={3}>Http 头信息</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId,'headers')
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<RefParameterList dataKeyName="headers" />
{#if data.method === 'post' || data.method === 'put' || data.method === 'delete' || data.method === 'patch'}
<Heading level={3} mt="10px">Body</Heading>
<div class="radio-group">
<label><Input type="radio" value="" checked={!data.bodyType || data.bodyType === ''}
onchange={(e:any)=>{
if (e.target?.checked){
updateNodeData(currentNodeId,{
bodyType: '',
})
}
}} />none</label>
<label><Input type="radio" value="form-data" checked={data.bodyType === 'form-data'}
onchange={(e:any)=>{
if (e.target?.checked){
updateNodeData(currentNodeId,{
bodyType: 'form-data',
})
}
}} />form-data</label>
<label><Input type="radio" value="x-www-form-urlencoded"
checked={data.bodyType === 'x-www-form-urlencoded'} onchange={(e:any)=>{
if (e.target?.checked){
updateNodeData(currentNodeId,{
bodyType: 'x-www-form-urlencoded',
})
}
}} />x-www-form-urlencoded</label>
<label><Input type="radio" value="json" checked={data.bodyType === 'json'}
onchange={(e:any)=>{
if (e.target?.checked){
updateNodeData(currentNodeId,{
bodyType: 'json',
})
}
}} />json</label>
<label><Input type="radio" value="raw" checked={data.bodyType === 'raw'}
onchange={(e:any)=>{
if (e.target?.checked){
updateNodeData(currentNodeId,{
bodyType: 'raw',
})
}
}} />raw</label>
</div>
{/if}
{#if data.bodyType === 'form-data'}
<div class="heading" style="padding-top: 10px">
<Heading level={3}>参数</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId,'formData')
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<RefParameterList dataKeyName="formData" />
{/if}
{#if data.bodyType === 'x-www-form-urlencoded'}
<div class="heading" style="padding-top: 10px">
<Heading level={3}>Body 参数</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId,'formUrlencoded')
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<RefParameterList dataKeyName="formUrlencoded" />
{/if}
{#if data.bodyType === 'json'}
<div style="width: 100%">
<Textarea rows={5} style="width: 100%" placeholder="请输入 json 信息" value={data.bodyJson}
oninput={(e:any)=>{
updateNodeData(currentNodeId,{
bodyJson: e.target.value,
})
}} />
</div>
{/if}
{#if data.bodyType === 'raw'}
<div style="width: 100%">
<Textarea rows={5} style="width: 100%" placeholder="请输入请求信息" value={data.bodyRaw}
oninput={(e:any)=>{
updateNodeData(currentNodeId,{
bodyRaw: e.target.value,
})
}} />
</div>
{/if}
<div class="heading">
<Heading level={3} mt="10px">输出参数</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId,'outputDefs')
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<OutputDefList />
</NodeWrapper>
<style lang="less">
.heading {
display: flex;
margin-bottom: 10px;
}
.radio-group {
display: flex;
margin: 10px 0;
flex-wrap: wrap;
label {
display: flex;
font-size: 14px;
box-sizing: border-box;
}
}
</style>

View File

@@ -0,0 +1,173 @@
<script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
import {Button, Heading, Input, Select} from '../base';
import RefParameterList from '../core/RefParameterList.svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useAddParameter} from '../utils/useAddParameter.svelte';
import {getOptions} from '../utils/NodeUtils';
import {onMount} from 'svelte';
import OutputDefList from '../core/OutputDefList.svelte';
import type {SelectItem} from '#types';
const { data, ...rest }: {
data: NodeProps['data'],
[key: string]: any
} = $props();
const currentNodeId = getCurrentNodeId();
const { addParameter } = useAddParameter();
const options = getOptions();
let knowledgeArray = $state<SelectItem[]>([]);
onMount(async () => {
const newLLMs = await options.provider?.knowledge?.();
knowledgeArray.push(...(newLLMs || []));
});
const { updateNodeData } = useSvelteFlow();
$effect(() => {
if (!data.outputDefs || data.outputDefs.length === 0) {
addParameter(currentNodeId, 'outputDefs',
{
name: 'documents',
dataType: 'Array',
nameDisabled: true,
dataTypeDisabled: true,
addChildDisabled: true,
deleteDisabled: true,
children: [
{
name: 'title',
dataType: 'String',
nameDisabled: true,
dataTypeDisabled: true,
deleteDisabled: true
},
{
name: 'content',
dataType: 'String',
nameDisabled: true,
dataTypeDisabled: true,
deleteDisabled: true
},
{
name: 'documentId',
dataType: 'Number',
nameDisabled: true,
dataTypeDisabled: true,
deleteDisabled: true
},
{
name: 'knowledgeId',
dataType: 'Number',
nameDisabled: true,
dataTypeDisabled: true,
deleteDisabled: true
}
]
}
);
}
});
</script>
<NodeWrapper {data} {...rest}>
{#snippet 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>
{/snippet}
<div class="heading">
<Heading level={3}>输入参数</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId)
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<RefParameterList />
<Heading level={3} mt="10px">知识库设置</Heading>
<div class="setting-title">知识库</div>
<div class="setting-item">
<Select items={knowledgeArray} style="width: 100%" placeholder="请选择知识库" onSelect={(item)=>{
const newValue = item.value;
updateNodeData(currentNodeId, ()=>{
return {
knowledgeId: newValue
}
})
}} value={data.knowledgeId ? [data.knowledgeId] : []} />
</div>
<div class="setting-title">关键字</div>
<div class="setting-item">
<Input placeholder="请输入关键字" style="width: 100%"
value={data.keyword}
onchange={(e)=>{
const newValue = e.target.value;
updateNodeData(currentNodeId, ()=>{
return {
keyword: newValue
}
})
}}
/>
</div>
<div class="setting-title">获取数据量</div>
<div class="setting-item">
<Input placeholder="搜索的数据条数" style="width: 100%" onchange={(e)=>{
const newValue = e.target.value;
updateNodeData(currentNodeId, ()=>{
return {
limit: newValue
}
})
}} value={data.limit || ''} />
</div>
<div class="heading">
<Heading level={3} mt="10px">输出参数</Heading>
</div>
<OutputDefList />
</NodeWrapper>
<style>
.heading {
display: flex;
margin-bottom: 10px;
}
.setting-title {
font-size: 12px;
color: #999;
margin-bottom: 4px;
margin-top: 10px;
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
gap: 10px;
}
</style>

View File

@@ -0,0 +1,292 @@
<script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
import {Button, FloatingTrigger, Heading, Select} from '../base';
import {MenuButton, Textarea} from '../base/index.js';
import RefParameterList from '../core/RefParameterList.svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useAddParameter} from '../utils/useAddParameter.svelte';
import {getOptions} from '../utils/NodeUtils';
import {onMount} from 'svelte';
import OutputDefList from '../core/OutputDefList.svelte';
import type {SelectItem} from '#types';
const { data, ...rest }: {
data: NodeProps['data'],
[key: string]: any
} = $props();
const currentNodeId = getCurrentNodeId();
const { addParameter } = useAddParameter();
const options = getOptions();
let llmArray = $state<SelectItem[]>([]);
onMount(async () => {
const newLLMs = await options.provider?.llm?.();
llmArray.push(...(newLLMs || []));
});
const { updateNodeData } = useSvelteFlow();
const setOutType = (value: string) => {
updateNodeData(currentNodeId, () => {
return {
outType: value
};
});
if (value === 'text') {
updateNodeData(currentNodeId, {
outputDefs: [{
name: 'output',
dataType: 'String',
dataTypeDisabled: true,
deleteDisabled: true
}]
});
} else {
updateNodeData(currentNodeId, {
outputDefs: [{
name: 'root',
dataType: 'Object',
dataTypeItems: [
{
value: 'Object',
label: 'Object'
},
{
value: 'Array',
label: 'Array'
}],
deleteDisabled: true
}]
});
}
};
$effect(() => {
if (!data.outType) {
setOutType('text');
}
});
</script>
<NodeWrapper {data} {...rest}>
{#snippet 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>
{/snippet}
<div class="heading">
<Heading level={3}>输入参数</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId)
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<RefParameterList />
<div class="heading" style="padding-top: 10px">
<Heading level={3}>图片识别</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId, "images")
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<RefParameterList dataKeyName="images" noneParameterText="无图片参数" />
<Heading level={3} mt="10px">模型设置</Heading>
<div class="setting-title">模型</div>
<div class="setting-item">
<Select items={llmArray} style="width: 100%" placeholder="请选择模型" onSelect={(item)=>{
const newValue = item.value;
updateNodeData(currentNodeId, ()=>{
return {
llmId: newValue
}
})
}} value={data.llmId ? [data.llmId] : []} />
<FloatingTrigger placement="bottom">
<MenuButton />
{#snippet floating()}
<div class="llm-setting">
<!-- 在原有采样参数部分添加事件阻止 -->
<div class="setting-title">采样参数</div>
<div class="setting-item">
<div class="slider-container">
<span>Temperature: {data.temperature ?? 0.7}</span>
<input
class="nodrag"
type="range"
min="0"
max="1"
step="0.1"
value={data.temperature ?? 0.7}
oninput={(e) => updateNodeData(currentNodeId, { temperature: parseFloat(e.target.value) })}
/>
</div>
</div>
<div class="setting-item">
<div class="slider-container">
<span>Top P: {data.topP ?? 0.9}</span>
<input
class="nodrag"
type="range"
min="0"
max="1"
step="0.1"
value={data.topP ?? 0.9}
oninput={(e) => updateNodeData(currentNodeId, { topP: parseFloat(e.target.value) })}
/>
</div>
</div>
<div class="setting-item">
<div class="slider-container">
<span>Top K: {data.topK ?? 50}</span>
<input
class="nodrag"
type="range"
min="0"
max="100"
step="1"
value={data.topK ?? 50}
oninput={(e) => updateNodeData(currentNodeId, { topK: parseInt(e.target.value) })}
/>
</div>
</div>
</div>
{/snippet}
</FloatingTrigger>
</div>
<div class="setting-title">系统提示词</div>
<div class="setting-item">
<Textarea rows={5}
placeholder="请输入系统提示词"
style="width: 100%"
value={data.systemPrompt || ''}
oninput={(e)=>{
updateNodeData(currentNodeId, {
systemPrompt: e.target.value
});
}}
/>
</div>
<div class="setting-title">用户提示词</div>
<div class="setting-item">
<Textarea
rows={5}
placeholder="请输入用户提示词"
style="width: 100%"
value={data.userPrompt || ''}
oninput={(e)=>{
updateNodeData(currentNodeId, {
userPrompt: e.target.value
});
}}
/>
</div>
<div class="heading">
<Heading level={3}>输出参数</Heading>
<Select items={[{
label: '文本',
value: 'text'
}, {
label: 'JSON',
value: 'json'
}]} style="width: 100px;margin-left: auto" onSelect={(item)=>{
setOutType(item.value);
}} value={data.outType ? [data.outType] : []} />
</div>
<OutputDefList />
</NodeWrapper>
<style>
.heading {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.setting-title {
font-size: 12px;
color: #999;
margin-bottom: 4px;
margin-top: 10px;
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
gap: 10px;
}
/* 新增样式 */
.slider-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 4px;
}
.slider-container span {
font-size: 12px;
color: #666;
display: flex;
justify-content: space-between;
align-items: center;
}
.llm-setting {
width: 200px;
background: #fff;
padding: 10px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border: 1px solid #ddd;
}
input[type="range"] {
width: 100%;
height: 4px;
background: #ddd;
border-radius: 2px;
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: #007bff;
border-radius: 50%;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,95 @@
<script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte';
import {Handle, type NodeProps, Position} from '@xyflow/svelte';
import {Button, Heading} from '../base';
import RefParameterList from '../core/RefParameterList.svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useAddParameter} from '../utils/useAddParameter.svelte';
const { data, ...rest }: {
data: NodeProps['data'],
[key: string]: any
} = $props();
const currentNodeId = getCurrentNodeId();
const { addParameter } = useAddParameter();
$effect(() => {
if (!data.loopVars || data.loopVars.length === 0) {
addParameter(currentNodeId, 'loopVars', {
name: 'loopVar',
nameDisabled: true,
deleteDisabled: true
});
}
});
</script>
<NodeWrapper {data} {...rest}>
{#snippet 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>
{/snippet}
{#snippet handle()}
<Handle type="source" position={Position.Bottom} id="loop_handle" style="bottom: -12px;width: 100px"
class="loop_handle_wrapper" />
{/snippet}
<div class="heading">
<Heading level={3}>循环变量</Heading>
<!-- <Button class="input-btn-more" style="margin-left: auto" onclick={()=>{-->
<!-- addParameter(currentNodeId)-->
<!-- }}>-->
<!-- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">-->
<!-- <path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>-->
<!-- </svg>-->
<!-- </Button>-->
</div>
<RefParameterList dataKeyName="loopVars" />
<div class="heading">
<Heading level={3}>输出参数</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId,'outputDefs')
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<RefParameterList noneParameterText="无输出参数" dataKeyName="outputDefs" useChildrenOnly={true} />
</NodeWrapper>
<style lang="less">
.heading {
display: flex;
margin: 10px 0;
align-items: center;
}
:global(.loop_handle_wrapper ) {
&::after {
//display: none;
content: '循环体';
width: 100px;
height: 20px;
background: #000;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,160 @@
<script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
import {Button, Heading, Input, Select} from '../base';
import RefParameterList from '../core/RefParameterList.svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useAddParameter} from '../utils/useAddParameter.svelte';
import {getOptions} from '../utils/NodeUtils';
import {onMount} from 'svelte';
import OutputDefList from '../core/OutputDefList.svelte';
import type {SelectItem} from '#types';
const { data, ...rest }: {
data: NodeProps['data'],
[key: string]: any
} = $props();
const currentNodeId = getCurrentNodeId();
const { addParameter } = useAddParameter();
const options = getOptions();
let searchEngines = $state<SelectItem[]>([]);
onMount(async () => {
const newLLMs = await options.provider?.searchEngine?.();
searchEngines.push(...(newLLMs || []));
});
const { updateNodeData } = useSvelteFlow();
$effect(() => {
if (!data.outputDefs || data.outputDefs.length === 0) {
addParameter(currentNodeId, 'outputDefs',
{
name: 'documents',
dataType: 'Array',
nameDisabled: true,
dataTypeDisabled: true,
addChildDisabled: true,
deleteDisabled: true,
children: [
{
name: 'title',
dataType: 'String',
nameDisabled: true,
dataTypeDisabled: true,
deleteDisabled: true
},
{
name: 'content',
dataType: 'String',
nameDisabled: true,
dataTypeDisabled: true,
deleteDisabled: true
}
]
}
);
}
});
</script>
<NodeWrapper {data} {...rest}>
{#snippet 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>
{/snippet}
<div class="heading">
<Heading level={3}>输入参数</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId)
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<RefParameterList />
<Heading level={3} mt="10px">搜索引擎设置</Heading>
<div class="setting-title">搜索引擎</div>
<div class="setting-item">
<Select items={searchEngines} style="width: 100%" placeholder="请选择搜索引擎" onSelect={(item)=>{
const newValue = item.value;
updateNodeData(currentNodeId, ()=>{
return {
engine: newValue
}
})
}} value={data.engine ? [data.engine] : []} />
</div>
<div class="setting-title">关键字</div>
<div class="setting-item">
<Input placeholder="请输入关键字" style="width: 100%"
value={data.keyword}
onchange={(e)=>{
const newValue = e.target.value;
updateNodeData(currentNodeId, ()=>{
return {
keyword: newValue
}
})
}}
/>
</div>
<div class="setting-title">搜索数据量</div>
<div class="setting-item">
<Input placeholder="搜索的数据条数" style="width: 100%" value={data.limit}
onchange={(e)=>{
const newValue = e.target.value;
updateNodeData(currentNodeId, ()=>{
return {
limit: newValue
}
})
}} />
</div>
<div class="heading">
<Heading level={3} mt="10px">输出参数</Heading>
</div>
<OutputDefList />
</NodeWrapper>
<style>
.heading {
display: flex;
margin-bottom: 10px;
}
.setting-title {
font-size: 12px;
color: #999;
margin-bottom: 4px;
margin-top: 10px;
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
gap: 10px;
}
</style>

View File

@@ -0,0 +1,59 @@
<script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte';
import {Heading} from '../base';
import {Button} from '../base/index.js';
import {type NodeProps} from '@xyflow/svelte';
import DefinedParameterList from '../core/DefinedParameterList.svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useAddParameter} from '../utils/useAddParameter.svelte';
const { data, ...rest }: {
data: NodeProps['data'],
[key: string]: any
} = $props();
const currentNodeId = getCurrentNodeId();
const { addParameter } = useAddParameter();
</script>
<NodeWrapper {...rest} {data} allowExecute={false} showTargetHandle={false} allowSettingOfCondition={false}>
{#snippet 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>
{/snippet}
<div class="heading">
<Heading level={3}>输入参数</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId, "parameters", {refType: "input", name: "newParam"});
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<DefinedParameterList />
</NodeWrapper>
<style lang="less">
.heading {
display: flex;
margin-bottom: 10px;
}
:global(.input-btn-more) {
border: 1px solid transparent;
padding: 3px;
&:hover {
background: #eee;
border: 1px solid transparent;
}
}
</style>

View File

@@ -0,0 +1,93 @@
<script lang="ts">
import NodeWrapper from '../core/NodeWrapper.svelte';
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
import {Button, Heading} from '../base';
import {Textarea} from '../base/index.js';
import RefParameterList from '../core/RefParameterList.svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useAddParameter} from '../utils/useAddParameter.svelte';
import OutputDefList from '../core/OutputDefList.svelte';
const { data, ...rest }: {
data: NodeProps['data'],
[key: string]: any
} = $props();
const currentNodeId = getCurrentNodeId();
const { addParameter } = useAddParameter();
const { updateNodeData } = useSvelteFlow();
$effect(() => {
if (!data.outputDefs || data.outputDefs.length === 0) {
addParameter(currentNodeId, 'outputDefs', {
name: 'output',
dataType: 'String',
dataTypeDisabled: true,
deleteDisabled: true
});
}
});
</script>
<NodeWrapper {data} {...rest}>
{#snippet 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>
{/snippet}
<div class="heading">
<Heading level={3}>输入参数</Heading>
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
addParameter(currentNodeId)
}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
</svg>
</Button>
</div>
<RefParameterList />
<Heading level={3} mt="10px" mb="10px">模板内容</Heading>
<div class="setting-item">
<Textarea rows={10} placeholder="请输入模板内容" style="width: 100%" onchange={(e:any)=>{
updateNodeData(currentNodeId, ()=>{
return {
template: e.target.value
}
})
}} value={data.template ||""} />
</div>
<div class="heading">
<Heading level={3} mt="10px">输出参数</Heading>
</div>
<OutputDefList />
</NodeWrapper>
<style>
.heading {
display: flex;
margin-bottom: 10px;
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
gap: 10px;
}
</style>

View File

@@ -0,0 +1,27 @@
import type {NodeTypes} from '@xyflow/svelte';
import StartNode from './StartNode.svelte';
import EndNode from './EndNode.svelte';
import LLMNode from './LLMNode.svelte';
import CodeNode from './CodeNode.svelte';
import TemplateNode from './TemplateNode.svelte';
import HttpNode from './HttpNode.svelte';
import KnowledgeNode from './KnowledgeNode.svelte';
import SearchEngineNode from './SearchEngineNode.svelte';
import LoopNode from './LoopNode.svelte';
import ConfirmNode from './ConfirmNode.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,
endNode: EndNode
} as any as NodeTypes;

View File

@@ -0,0 +1,12 @@
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('');
};
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)
);
};

View File

@@ -0,0 +1,26 @@
import {getContext} from 'svelte';
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
import type {TinyflowOptions} from '#types';
export const getCurrentNodeId = () => {
return getContext<string>('svelteflow__node_id');
};
export const getOptions = () => {
return getContext<TinyflowOptions>('tinyflow_options');
};
export const useCurrentNodeData = () => {
const currentNodeId = getCurrentNodeId();
return useNodesData<any>(currentNodeId);
};
export const useUpdateNodeData = () => {
const { updateNodeData } = useSvelteFlow();
const currentNodeId = getCurrentNodeId();
return {
updateNodeData: (data: any) => {
updateNodeData(currentNodeId, data);
}
};
};

View File

@@ -0,0 +1,193 @@
import {
arrow,
computePosition,
flip,
type FlipOptions,
offset,
type OffsetOptions,
type Placement,
shift,
type ShiftOptions
} 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;
};
export type FloatingInstance = {
destroy: () => void;
hide: () => void;
isVisible: () => boolean;
};
export const createFloating = ({
trigger,
triggerEvent,
floatContent,
placement = 'bottom',
offsetOptions,
flipOptions,
shiftOptions,
interactive,
showArrow
}: 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;
}
} else {
floating = floatContent 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);
}
function updatePosition() {
computePosition(trigger as Element, floating, {
placement: placement,
middleware: [
offset(offsetOptions), // 手动偏移配置
flip(flipOptions), //自动翻转
shift(shiftOptions), //自动偏移(使得浮动元素能够进入视野)
...(showArrow ? [arrow({ element: arrowElement })] : [])
]
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(floating.style, {
left: `${x}px`,
top: `${y}px`
});
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'
});
}
});
}
let visible = false;
function showTooltip() {
floating.style.display = 'block';
floating.style.visibility = 'block';
floating.style.position = 'absolute';
if (showArrow) {
arrowElement.style.display = 'block';
}
visible = true;
updatePosition();
}
function hideTooltip() {
floating.style.display = 'none';
if (showArrow) {
arrowElement.style.display = 'none';
}
visible = false;
}
function onTrigger(event: any) {
event.stopPropagation();
if (!visible) {
showTooltip();
} else {
hideTooltip();
}
}
function hideTooltipCompute(event: any) {
if (floating.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;
}
};
};

View File

@@ -0,0 +1,3 @@
export const deepClone = <T>(obj: T): T => {
return JSON.parse(JSON.stringify(obj));
};

View File

@@ -0,0 +1,37 @@
export const deepEqual = <T>(obj1: T, obj2: T) => {
if (obj1 === obj2) return true;
// 处理 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;
}
return true;
}
};

View File

@@ -0,0 +1,16 @@
/**
* 判断当前焦点是否位于可编辑元素中(如 input、textarea 或 contenteditable 区域)。
* 适用于快捷键、全局事件监听等需要避免干扰用户输入的场景。
*/
export const isInEditableElement = () => {
const el = document.activeElement;
if (!el || !(el instanceof HTMLElement)) {
return false;
}
return (
el instanceof HTMLInputElement ||
el instanceof HTMLTextAreaElement ||
el.isContentEditable
);
};

View File

@@ -0,0 +1,63 @@
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);
});
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);
}
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));
}
updateNodeData(nodeId, (node) => {
let parameters = node.data[dataKey] as Array<any>;
if (parameters) {
parameters.push(...newParameters);
} else {
parameters = [...newParameters];
}
return {
[dataKey]: [...parameters]
};
});
}
};
};

View File

@@ -0,0 +1,27 @@
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
}
};
store.updateNodes((nodes) => {
const newNodes = nodes.map((n) => ({ ...n, selected: false }));
return [...newNodes, newNode];
});
}
};
return {
copyNode
};
};

View File

@@ -0,0 +1,282 @@
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;
}
/**
* 清理节点中不可序列化的字段,确保可安全 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 })
};
}
/**
* 对 nodes 数组排序,确保每个父节点出现在其所有子节点之前。
* 使用 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
// 初始化
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);
}
}
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);
// 将该节点的所有直接子节点入队(如果其父已处理)
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);
}
}
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)) : {}
};
}
/**
* 递归遍历对象,仅当遇到 { 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直接返回
return obj;
}
/**
* 复制粘贴处理器 Hook
*/
export const useCopyPasteHandler = () => {
const svelteFlow = useSvelteFlow();
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 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 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 内容
}
if (!parsed?.tinyflowNodes || !Array.isArray(parsed.tinyflowNodes)) {
return;
}
event.preventDefault();
const pastedNodes = sortNodesForSvelteFlow(parsed.tinyflowNodes);
const pastedEdges = parsed.tinyflowEdges || [];
// 创建新节点(带新 ID 和偏移)
const newNodeIdMap = new Map<string, string>();
const newNodes: Node[] = [];
for (const node of pastedNodes) {
const newId = `node_${genShortId()}`;
newNodeIdMap.set(node.id, newId);
}
// 构建新节点(含重写后的 data
for (const node of pastedNodes) {
const newId = newNodeIdMap.get(node.id)!;
const newParentId =
node.parentId !== undefined
? newNodeIdMap.get(node.parentId) // 安全:即使父不在粘贴范围内,也会是 undefined
: undefined;
const newData = rewriteRefsInData(node.data, newNodeIdMap);
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取消其他节点选中添加新内容
store.updateNodes((nodes) => {
const unselected = nodes.map((n) => ({ ...n, selected: false }));
return [...unselected, ...newNodes];
});
store.updateEdges((edges) => {
const unselected = edges.map((n) => ({ ...n, selected: false }));
return [...unselected, ...newEdges];
});
};
return {
copyHandler,
pasteHandler
};
};

View File

@@ -0,0 +1,10 @@
import {store} from '#store/stores.svelte';
export const useDeleteEdge = () => {
const deleteEdge = (id: string) => {
store.removeEdge( id);
};
return {
deleteEdge
};
};

View File

@@ -0,0 +1,11 @@
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
};
};

View File

@@ -0,0 +1,43 @@
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;
}
}
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;
}
}
if (firstChildIndex == -1) {
return nodeArray;
}
const parentNode = nodeArray[parentIndex];
for (let i = parentIndex; i > firstChildIndex; i--) {
nodeArray[i] = nodeArray[i - 1];
}
nodeArray[firstChildIndex] = parentNode;
return nodeArray;
});
};
return {
ensureParentInNodesBefore
};
};

View File

@@ -0,0 +1,11 @@
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
};
};

View File

@@ -0,0 +1,11 @@
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
};
};

View File

@@ -0,0 +1,10 @@
import {store} from '#store/stores.svelte';
export const useGetNode = () => {
const getNode = (id: string) => {
return store.getNode(id);
};
return {
getNode
};
};

View File

@@ -0,0 +1,22 @@
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;
if (node.parentId) {
node = store.getNode(node.parentId);
} else {
node = undefined;
}
}
return position;
};
return {
getNodeRelativePosition
};
};

View File

@@ -0,0 +1,31 @@
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 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;
}
return result;
};
return {
getNodesFromSource
};
};

View File

@@ -0,0 +1,117 @@
import {type Edge, type Node, useNodesData, useStore} from '@xyflow/svelte';
import type {Parameter} from '#types';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
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) => {
if (!params || params.length === 0) return [];
return params.map((param: any) => ({
label:
param.name +
(nodeIsChildren
? ` (Array<${param.dataType || 'String'}>)`
: ` (${param.dataType || 'String'})`),
value: parentId + '.' + param.name,
children: getChildren(param.children, parentId + '.' + param.name, nodeIsChildren)
}));
};
const nodeToOptions = (node: Node, nodeIsChildren: boolean, currentNode: Node) => {
if (node.type === 'startNode') {
const parameters = node.data.parameters as Array<Parameter>;
const children = [];
if (parameters)
for (const parameter of parameters) {
children.push({
label:
parameter.name +
(nodeIsChildren
? ` (Array<${parameter.dataType || 'String'}>)`
: ` (${parameter.dataType || 'String'})`),
value: node.id + '.' + parameter.name
});
}
return {
label: node.data.title,
value: node.id,
children
};
} else if (node.type === 'loopNode' && currentNode.parentId) {
return {
label: node.data.title,
value: node.id,
children: [
{
label: 'loopItem',
value: node.id + '.loopItem'
},
{
label: 'index (Number)',
value: node.id + '.index'
}
]
};
} else {
const outputDefs = node.data.outputDefs;
if (outputDefs) {
return {
label: node.data.title,
value: node.id,
children: getChildren(outputDefs, node.id, nodeIsChildren)
};
}
}
};
export const useRefOptions: any = (useChildrenOnly: boolean = false) => {
const currentNodeId = getCurrentNodeId();
const currentNode = useNodesData(currentNodeId);
const { nodes, edges, nodeLookup } = $derived(useStore());
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);
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 {
get current() {
return selectItems;
}
};
};

View File

@@ -0,0 +1,25 @@
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 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
};
};

View File

@@ -0,0 +1,65 @@
export const componentName = 'tinyflow-component';
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'
}
];
export const parameterRefTypes = [
{
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' }
];
export const startFormTypes = [
{ label: '单行输入框', value: 'input' },
{ label: '多行输入框', value: 'textarea' },
{ label: '下拉菜单', value: 'select' },
{ label: '单选', value: 'radio' },
{ label: '多选', value: 'checkbox' }
];
export const confirmFormTypes = [
{ label: '单选', value: 'radio' },
{ label: '多选', value: 'checkbox' }
];

View File

@@ -0,0 +1,3 @@
export * from './types';
export * from './Tinyflow';
export * from './components/TinyflowComponent.svelte';

View File

@@ -0,0 +1,74 @@
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);
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;
},
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();

View File

@@ -0,0 +1,245 @@
.tf-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
background: #fff;
border: 1px solid #ccc;
cursor: pointer;
border-radius: 5px;
padding: 5px;
margin: 0;
height: fit-content;
width: fit-content;
svg {
fill: currentColor;
width: 16px;
height: 16px;
}
&:hover {
border: 1px solid var(--tf-primary-color);
}
}
.tf-btn.tf-btn-primary {
background: var(--tf-primary-color);
color: #fff;
&:hover {
border: 1px solid #ccc;
background: #3a6fe3;
}
}
.tf-input, .tf-textarea {
display: flex;
border-radius: 5px;
border: 1px solid #ccc;
padding: 5px 8px;
box-sizing: border-box;
resize: vertical;
outline: none;
line-height: normal;
overflow-y: hidden;
&::placeholder {
color: #ccc;
font-size: 12px;
}
// 获得焦点时的样式
&:focus {
border-color: var(--tf-primary-color);
box-shadow: 0 0 5px rgba(81, 203, 238, .2);
}
// 禁用时的样式
&[disabled] {
background-color: #f0f0f0;
cursor: not-allowed;
color: #aaa;
}
}
.tf-select {
&-input {
display: flex;
border: 1px solid #ccc;
padding: 3px 10px;
border-radius: 5px;
font-size: 14px;
justify-content: space-between;
align-items: center;
cursor: pointer;
background: #fff;
height: 27px;
&:focus {
border-color: var(--tf-primary-color);
box-shadow: 0 0 5px rgba(81, 203, 238, .2);
}
&-value {
height: 21px;
min-width: 10px;
font-size: 12px;
display: flex;
align-items: center;
}
&-arrow {
display: block;
width: 16px;
height: 16px;
color: #666;
}
&-placeholder {
color: #ccc;
}
}
&-content {
display: flex;
flex-direction: column;
background: #fff;
margin-top: 5px;
border: 1px solid #ccc;
border-radius: 5px;
padding: 5px;
width: max-content;
min-width: 100%;
z-index: 999;
box-sizing: border-box;
max-height: 220px;
overflow: auto;
&-item {
display: flex;
align-items: center;
padding: 5px 10px;
border: none;
background: #fff;
border-radius: 5px;
cursor: pointer;
line-height: 100%;
gap: 2px;
span {
width: 16px;
display: flex;
}
svg {
width: 16px;
height: 16px;
margin: auto;
}
&:hover {
background: #f0f0f0;
}
}
&-children {
padding-left: 14px;
}
}
}
.tf-checkbox {
width: 14px;
height: 14px;
}
.tf-tabs {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
padding: 5px;
border-radius: 5px;
border: none;
background: #F4F4F5;
.tf-tabs-item {
flex-grow: 1;
padding: 5px 10px;
cursor: pointer;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #808088;
}
.tf-tabs-item.active {
background: #fff;
color: #333;
font-weight: 500;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.15);
}
}
h3.tf-heading {
font-weight: 700;
font-size: 14px;
margin-top: 2px;
margin-bottom: 3px;
color: #333;
}
.tf-collapse {
border: none;
border-radius: 5px;
//padding: 5px;
&-item {
&-title {
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
&-icon {
display: flex;
width: 26px;
height: 26px;
color: #2563EB;
background: #cedafb;
border-radius: 5px;
padding: 3px;
justify-content: center;
align-items: center;
margin-right: 10px;
svg {
width: 22px;
height: 22px;
color: #3474ff;
}
}
&-arrow {
display: block;
width: 16px;
height: 16px;
margin-left: auto;
}
}
&-description {
font-size: 12px;
margin: 10px 0;
color: #999;
}
}
}

View File

@@ -0,0 +1,3 @@
import './variable.less';
import './base.less';
import './tinyflow.less';

View File

@@ -0,0 +1,160 @@
.svelte-flow__nodes {
.svelte-flow__node {
border: 3px solid transparent;
border-radius: 5px;
box-sizing: border-box;
.svelte-flow__handle {
width: 16px;
height: 16px;
background: transparent;
display: flex;
justify-content: center;
align-items: center;
border: none;
&::after {
content: ' ';
background: #2563EB;
width: 8px;
height: 8px;
border-radius: 100%;
transition: width 0.1s, height 0.1s;
}
&:hover {
&::after {
width: 16px;
height: 16px;
}
}
}
div.loop_handle_wrapper {
&::after {
content: '循环体';
background: #2563EB;
width: 100px;
height: 20px;
border-radius: 0;
display: flex;
color: #fff;
justify-content: center;
align-items: center;
}
&:hover {
&::after {
width: 100px;
height: 20px;
}
}
}
&:after {
content: ' ';
position: absolute;
border-radius: 5px;
top: -2px;
left: -2px;
border: 1px solid #ccc;
height: calc(100% + 2px);
width: calc(100% + 2px);
}
&:hover {
border: 3px solid #bacaef7d;
}
&.selectable.selected {
border: 3px solid #bacaef7d;
box-shadow: var(--xy-node-boxshadow-selected);
}
&:hover:after {
display: none;
}
&.selectable.selected:after {
display: none;
}
}
}
.tf-node-wrapper {
border-radius: 5px;
min-width: 300px;
background: #fff;
&-body {
padding: 10px;
}
}
.svelte-flow__attribution a {
display: none;
}
.tf-toolbar {
z-index: 200;
position: absolute;
top: 10px;
left: 10px;
display: flex;
gap: 5px;
transition: transform 0.5s ease, opacity 0.5s ease;
transform: translateX(calc(-100% + 20px)); /* 完全移出视口 */
&.show {
transform: translateX(0);
}
&-container {
background: #fff;
border: 1px solid #eee;
border-radius: 5px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
padding: 10px;
width: fit-content;
&-header {
display: flex;
}
&-body {
display: flex;
margin-top: 20px;
.tf-toolbar-container-base, .tf-toolbar-container-tools {
display: flex;
flex-direction: column;
gap: 4px;
flex-grow: 1;
.tf-btn {
border: none;
width: 100%;
justify-content: flex-start;
height: 40px;
gap: 10px;
cursor: grabbing;
border-radius: 5px;
svg {
width: 20px;
height: 20px;
fill: #2563EB;
}
&:hover {
background: #f1f1f1;
}
}
}
}
}
}

View File

@@ -0,0 +1,14 @@
:root,
:root .tf-theme-light {
--tf-primary-color: #2563EB;
--xy-node-boxshadow-selected: 0 0 0 1px var(--tf-primary-color);
--xy-handle-background-color: var(--tf-primary-color);
}
//the dark theme variables
:root .tf-theme-dark {
}

View File

@@ -0,0 +1,113 @@
import type {Snippet} from 'svelte';
import type {Node, useSvelteFlow} from '@xyflow/svelte';
export type TinyflowData = Partial<ReturnType<ReturnType<typeof useSvelteFlow>['toObject']>>;
export type SelectItem = {
value: number | string;
label: string | Snippet;
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[];
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[];
};
export type TinyflowOptions = {
element: string | Element;
data?: TinyflowData | string;
provider?: {
llm?: () => SelectItem[] | Promise<SelectItem[]>;
knowledge?: () => SelectItem[] | Promise<SelectItem[]>;
searchEngine?: () => SelectItem[] | Promise<SelectItem[]>;
} & Record<string, () => SelectItem[] | Promise<SelectItem[]>>;
//type : node
customNodes?: Record<string, CustomNode>;
onNodeExecute?: (node: Node) => 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;
};

View File

@@ -0,0 +1,15 @@
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
}
};

View File

@@ -0,0 +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"]
}

View File

@@ -0,0 +1,4 @@
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,24 @@
{
"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,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,44 @@
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') // 替换为生产环境
})
]
}
},
plugins: [
svelte({ emitCss: false }),
dts({
rollupTypes: true,
tsconfigPath: './tsconfig.app.json'
})
]
});

View File

@@ -0,0 +1,5 @@
module.exports = {
"rules": {
"@typescript-eslint/no-explicit-any": ["off"],
}
};

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,74 @@
{
"name": "@tinyflow-ai/vue",
"version": "1.2.2",
"type": "module",
"keywords": [
"tinyflow",
"ai",
"ai flow",
"agent flow",
"agents flow"
],
"types": "./dist/index.d.ts",
"main": "./dist/index.js",
"module": "./dist/index.js",
"browser": "./dist/index.umd.js",
"license": "LGPL-3.0-or-later",
"files": [
"dist",
"LICENSE",
"README.md"
],
"scripts": {
"dev": "vite",
"build": "vite build",
"build:watch": "vite build --watch",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
},
"dependencies": {
"@tinyflow-ai/ui": "workspace:*"
},
"peerDependencies": {
"vue": "^3.5.13"
},
"devDependencies": {
"@types/node": "^22.18.12",
"@vitejs/plugin-vue": "catalog:",
"autoprefixer": "^10.4.21",
"less": "^4.2.2",
"typescript": "^5.6.2",
"vite": "^7.0.4",
"vite-plugin-dts": "^4.5.3",
"vue": "^3.5.13"
},
"imports": {
"#*": [
"./src/*.ts",
"./src/*.vue"
]
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.js"
},
"./dist/index.css": {
"import": "./dist/index.css",
"require": "./dist/index.css"
}
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "https://github.com/tinyflow-ai/tinyflow"
},
"bugs": {
"url": "https://github.com/tinyflow-ai/tinyflow/issues"
},
"homepage": "https://github.com/tinyflow-ai/tinyflow#readme"
}

View File

@@ -0,0 +1,78 @@
<template>
<div ref="divRef" :class="['tinyflow', className]" :style="style" />
</template>
<script setup lang="ts">
import {Tinyflow as TinyflowNative, TinyflowOptions} from '@tinyflow-ai/ui';
import '@tinyflow-ai/ui/dist/index.css';
import {onMounted, onUnmounted, ref} from 'vue';
const props = defineProps<
{
className?: string;
style?: Record<string, string>;
} & Omit<TinyflowOptions, 'element'>
>();
const divRef = ref<HTMLDivElement | null>(null);
let tinyflow: TinyflowNative | null = null;
// 安全深拷贝工具函数
function safeDeepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') return obj;
try {
return structuredClone(obj);
} catch {
try {
return JSON.parse(JSON.stringify(obj));
} catch {
console.warn('Failed to clone object, returning original (may cause issues)', obj);
return obj;
}
}
}
onMounted(() => {
if (divRef.value) {
// 净化 props.data避免响应式对象或函数污染
const cleanedProps = { ...props } as any;
if ('data' in cleanedProps && cleanedProps.data != null) {
cleanedProps.data = safeDeepClone(cleanedProps.data);
}
tinyflow = new TinyflowNative({
...cleanedProps,
element: divRef.value
});
}
});
onUnmounted(() => {
if (tinyflow) {
tinyflow.destroy();
tinyflow = null;
}
});
const getData = () => {
if (tinyflow) {
return tinyflow.getData();
}
console.warn('Tinyflow instance is not initialized');
return null;
};
const getInstance = () => {
if (tinyflow) {
return tinyflow;
}
console.warn('Tinyflow instance is not initialized');
return null;
};
defineExpose({
getData,
getInstance
});
</script>

View File

@@ -0,0 +1,3 @@
import Tinyflow from './Tinyflow.vue';
export { Tinyflow };

View File

@@ -0,0 +1,4 @@
import {createApp} from 'vue'
import App from './Tinyflow.vue'
createApp(App).mount('#app')

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@@ -0,0 +1,4 @@
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "node",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,39 @@
import {defineConfig} from 'vite';
import {resolve} from 'path';
import vue from '@vitejs/plugin-vue';
import dts from 'vite-plugin-dts';
// https://vitejs.dev/config/
export default defineConfig({
build: {
outDir: 'dist',
emptyOutDir: true,
minify: true,
sourcemap: true,
cssCodeSplit: 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: {
external: ['vue'], // 排除 Vue 作为外部依赖
output: {
globals: {
vue: 'Vue'
}
}
}
},
plugins: [
vue(),
dts({
rollupTypes: true,
tsconfigPath: './tsconfig.app.json'
})
]
});

View File

@@ -490,6 +490,12 @@ importers:
.:
devDependencies:
'@changesets/changelog-github':
specifier: 'catalog:'
version: 0.5.1
'@changesets/cli':
specifier: 'catalog:'
version: 2.29.7(@types/node@24.10.1)
'@easyflow/commitlint-config':
specifier: workspace:*
version: link:internal/lint-configs/commitlint-config
@@ -517,12 +523,6 @@ importers:
'@easyflow/vsh':
specifier: workspace:*
version: link:scripts/vsh
'@changesets/changelog-github':
specifier: 'catalog:'
version: 0.5.1
'@changesets/cli':
specifier: 'catalog:'
version: 2.29.7(@types/node@24.10.1)
'@playwright/test':
specifier: 'catalog:'
version: 1.56.1
@@ -638,8 +638,8 @@ importers:
specifier: ^2.3.2
version: 2.3.2(vue@3.5.24(typescript@5.9.3))
'@tinyflow-ai/vue':
specifier: ^1.2.2
version: 1.2.2(svelte@5.44.0)(vue@3.5.24(typescript@5.9.3))
specifier: workspace:*
version: link:../packages/tinyflow-vue
'@vueuse/core':
specifier: 'catalog:'
version: 13.9.0(vue@3.5.24(typescript@5.9.3))
@@ -692,15 +692,15 @@ importers:
internal/lint-configs/commitlint-config:
dependencies:
'@easyflow/node-utils':
specifier: workspace:*
version: link:../../node-utils
'@commitlint/cli':
specifier: 'catalog:'
version: 19.8.1(@types/node@24.10.1)(typescript@5.9.3)
'@commitlint/config-conventional':
specifier: 'catalog:'
version: 19.8.1
'@easyflow/node-utils':
specifier: workspace:*
version: link:../../node-utils
commitlint-plugin-function-rules:
specifier: 'catalog:'
version: 4.1.1(@commitlint/lint@19.8.1)
@@ -1580,7 +1580,7 @@ importers:
version: 3.0.4(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))
pinia-plugin-persistedstate:
specifier: 'catalog:'
version: 4.7.1(@nuxt/kit@4.2.1)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))
version: 4.7.1(@nuxt/kit@3.20.0)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)))
secure-ls:
specifier: 'catalog:'
version: 2.0.0
@@ -1597,6 +1597,80 @@ importers:
specifier: workspace:*
version: link:../@core/base/design
packages/tinyflow-ui:
dependencies:
'@floating-ui/dom':
specifier: ^1.7.4
version: 1.7.4
'@xyflow/svelte':
specifier: ^1.4.2
version: 1.4.2(svelte@5.53.3)
devDependencies:
'@rollup/plugin-replace':
specifier: ^6.0.2
version: 6.0.3(rollup@4.53.2)
'@sveltejs/vite-plugin-svelte':
specifier: ^6.2.1
version: 6.2.4(svelte@5.53.3)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))
'@tsconfig/svelte':
specifier: ^5.0.5
version: 5.0.8
'@types/node':
specifier: ^22.18.12
version: 22.19.11
autoprefixer:
specifier: ^10.4.21
version: 10.4.22(postcss@8.5.6)
less:
specifier: ^4.2.2
version: 4.4.2
svelte:
specifier: ^5.45.2
version: 5.53.3
svelte-check:
specifier: ^4.3.4
version: 4.4.3(picomatch@4.0.3)(svelte@5.53.3)(typescript@5.9.3)
typescript:
specifier: ^5.6.2
version: 5.9.3
vite:
specifier: ^7.2.4
version: 7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vite-plugin-dts:
specifier: ^4.5.4
version: 4.5.4(@types/node@22.19.11)(rollup@4.53.2)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))
packages/tinyflow-vue:
dependencies:
'@tinyflow-ai/ui':
specifier: workspace:*
version: link:../tinyflow-ui
devDependencies:
'@types/node':
specifier: ^22.18.12
version: 22.19.11
'@vitejs/plugin-vue':
specifier: 'catalog:'
version: 6.0.1(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))
autoprefixer:
specifier: ^10.4.21
version: 10.4.22(postcss@8.5.6)
less:
specifier: ^4.2.2
version: 4.4.2
typescript:
specifier: ^5.6.2
version: 5.9.3
vite:
specifier: ^7.0.4
version: 7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vite-plugin-dts:
specifier: ^4.5.3
version: 4.5.4(@types/node@22.19.11)(rollup@4.53.2)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))
vue:
specifier: ^3.5.17
version: 3.5.24(typescript@5.9.3)
packages/types:
dependencies:
'@easyflow-core/typings':
@@ -1623,12 +1697,12 @@ importers:
scripts/turbo-run:
dependencies:
'@easyflow/node-utils':
specifier: workspace:*
version: link:../../internal/node-utils
'@clack/prompts':
specifier: 'catalog:'
version: 0.10.1
'@easyflow/node-utils':
specifier: workspace:*
version: link:../../internal/node-utils
cac:
specifier: 'catalog:'
version: 6.7.14
@@ -3360,10 +3434,6 @@ packages:
resolution: {integrity: sha512-EoF1Gf0SPj9vxgAIcGEH+a4PRLC7Dwsy21K6f5+POzylT8DgssN8zL5pwXC+X7OcfzBrwYFh7mM7phvh7ubgeg==}
engines: {node: '>=18.12.0'}
'@nuxt/kit@4.2.1':
resolution: {integrity: sha512-lLt8KLHyl7IClc3RqRpRikz15eCfTRlAWL9leVzPyg5N87FfKE/7EWgWvpiL/z4Tf3dQCIqQb88TmHE0JTIDvA==}
engines: {node: '>=18.12.0'}
'@one-ini/wasm@0.1.1':
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
@@ -3842,6 +3912,21 @@ packages:
peerDependencies:
acorn: ^8.9.0
'@sveltejs/vite-plugin-svelte-inspector@5.0.2':
resolution: {integrity: sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==}
engines: {node: ^20.19 || ^22.12 || >=24}
peerDependencies:
'@sveltejs/vite-plugin-svelte': ^6.0.0-next.0
svelte: ^5.0.0
vite: ^6.3.0 || ^7.0.0
'@sveltejs/vite-plugin-svelte@6.2.4':
resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==}
engines: {node: ^20.19 || ^22.12 || >=24}
peerDependencies:
svelte: ^5.0.0
vite: ^6.3.0 || ^7.0.0
'@swc/helpers@0.5.17':
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
@@ -3878,13 +3963,8 @@ packages:
peerDependencies:
vue: ^3.5.17
'@tinyflow-ai/ui@1.2.2':
resolution: {integrity: sha512-SwKucKqoR5A3CMyTk/KWK+c33d2D8uhyDtKhl61Wj/LAWuJdaxtZiUqQN+tjGGH0QqwgtgpuYK7h4UYWlUhqoQ==}
'@tinyflow-ai/vue@1.2.2':
resolution: {integrity: sha512-e+zkeyOrSu/sO2s0kBkQI46jMJjP0D1USswMJrlaOX6+uBujQhaZt5wLkKO8VKGiTsRaPY9PF79ecLUmiP+IRQ==}
peerDependencies:
vue: ^3.5.17
'@tsconfig/svelte@5.0.8':
resolution: {integrity: sha512-UkNnw1/oFEfecR8ypyHIQuWYdkPvHiwcQ78sh+ymIiYoF+uc5H1UBetbjyqT+vgGJ3qQN6nhucJviX6HesWtKQ==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -3979,6 +4059,9 @@ packages:
'@types/node@12.20.55':
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
'@types/node@22.19.11':
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
'@types/node@24.10.1':
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
@@ -4588,8 +4671,8 @@ packages:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'}
aria-query@5.3.2:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
aria-query@5.3.1:
resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
engines: {node: '>= 0.4'}
array-buffer-byte-length@1.0.2:
@@ -5488,8 +5571,8 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
devalue@5.5.0:
resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==}
devalue@5.6.3:
resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==}
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
@@ -5945,8 +6028,8 @@ packages:
resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
engines: {node: '>=0.10'}
esrap@2.1.3:
resolution: {integrity: sha512-T/Dhhv/QH+yYmiaLz9SA3PW+YyenlnRKDNdtlYJrSOBmNsH4nvPux+mTwx7p+wAedlJrGoZtXNI0a0MjQ2QkVg==}
esrap@2.2.3:
resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==}
esrecurse@4.3.0:
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
@@ -7689,6 +7772,9 @@ packages:
resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==}
engines: {node: '>= 0.4'}
obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
ofetch@1.5.1:
resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==}
@@ -9308,8 +9394,16 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
svelte@5.44.0:
resolution: {integrity: sha512-R7387No2zEGw4CtYtI2rgsui6BqjFARzoZFGLiLN5OPla0Pq4Ra2WwcP/zBomP3MYalhSNvF1fzDMuU0P0zPJw==}
svelte-check@4.4.3:
resolution: {integrity: sha512-4HtdEv2hOoLCEsSXI+RDELk9okP/4sImWa7X02OjMFFOWeSdFF3NFy3vqpw0z+eH9C88J9vxZfUXz/Uv2A1ANw==}
engines: {node: '>= 18.0.0'}
hasBin: true
peerDependencies:
svelte: ^4.0.0 || ^5.0.0-next.0
typescript: '>=5.0.0'
svelte@5.53.3:
resolution: {integrity: sha512-pRUBr6j6uQDgBi208gHnGRMykw0Rf2Yr1HmLyRucsvcaYgIUxswJkT93WZJflsmezu5s8Lq+q78EoyLv2yaFCg==}
engines: {node: '>=18'}
sver@1.8.4:
@@ -9604,6 +9698,9 @@ packages:
unctx@2.4.1:
resolution: {integrity: sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
@@ -9924,6 +10021,54 @@ packages:
yaml:
optional: true
vite@7.3.1:
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
jiti: '>=1.21.0'
less: ^4.0.0
lightningcss: ^1.21.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
vitefu@1.1.2:
resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==}
peerDependencies:
vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0
peerDependenciesMeta:
vite:
optional: true
vitest@3.2.4:
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -12263,6 +12408,14 @@ snapshots:
- encoding
- supports-color
'@microsoft/api-extractor-model@7.31.3(@types/node@22.19.11)':
dependencies:
'@microsoft/tsdoc': 0.15.1
'@microsoft/tsdoc-config': 0.17.1
'@rushstack/node-core-library': 5.18.0(@types/node@22.19.11)
transitivePeerDependencies:
- '@types/node'
'@microsoft/api-extractor-model@7.31.3(@types/node@24.10.1)':
dependencies:
'@microsoft/tsdoc': 0.15.1
@@ -12271,6 +12424,25 @@ snapshots:
transitivePeerDependencies:
- '@types/node'
'@microsoft/api-extractor@7.54.0(@types/node@22.19.11)':
dependencies:
'@microsoft/api-extractor-model': 7.31.3(@types/node@22.19.11)
'@microsoft/tsdoc': 0.15.1
'@microsoft/tsdoc-config': 0.17.1
'@rushstack/node-core-library': 5.18.0(@types/node@22.19.11)
'@rushstack/rig-package': 0.6.0
'@rushstack/terminal': 0.19.3(@types/node@22.19.11)
'@rushstack/ts-command-line': 5.1.3(@types/node@22.19.11)
diff: 8.0.2
lodash: 4.17.21
minimatch: 10.0.3
resolve: 1.22.11
semver: 7.5.4
source-map: 0.6.1
typescript: 5.8.2
transitivePeerDependencies:
- '@types/node'
'@microsoft/api-extractor@7.54.0(@types/node@24.10.1)':
dependencies:
'@microsoft/api-extractor-model': 7.31.3(@types/node@24.10.1)
@@ -12349,32 +12521,6 @@ snapshots:
- magicast
optional: true
'@nuxt/kit@4.2.1':
dependencies:
c12: 3.3.1(magicast@0.5.1)
consola: 3.4.2
defu: 6.1.4
destr: 2.0.5
errx: 0.1.0
exsolve: 1.0.7
ignore: 7.0.5
jiti: 2.6.1
klona: 2.0.6
mlly: 1.8.0
ohash: 2.0.11
pathe: 2.0.3
pkg-types: 2.3.0
rc9: 2.1.2
scule: 1.3.0
semver: 7.7.3
tinyglobby: 0.2.15
ufo: 1.6.1
unctx: 2.4.1
untyped: 2.0.0
transitivePeerDependencies:
- magicast
optional: true
'@one-ini/wasm@0.1.1': {}
'@parcel/watcher-android-arm64@2.5.1':
@@ -12682,6 +12828,19 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.53.2':
optional: true
'@rushstack/node-core-library@5.18.0(@types/node@22.19.11)':
dependencies:
ajv: 8.13.0
ajv-draft-04: 1.0.0(ajv@8.13.0)
ajv-formats: 3.0.1(ajv@8.13.0)
fs-extra: 11.3.2
import-lazy: 4.0.0
jju: 1.4.0
resolve: 1.22.11
semver: 7.5.4
optionalDependencies:
'@types/node': 22.19.11
'@rushstack/node-core-library@5.18.0(@types/node@24.10.1)':
dependencies:
ajv: 8.13.0
@@ -12695,6 +12854,10 @@ snapshots:
optionalDependencies:
'@types/node': 24.10.1
'@rushstack/problem-matcher@0.1.1(@types/node@22.19.11)':
optionalDependencies:
'@types/node': 22.19.11
'@rushstack/problem-matcher@0.1.1(@types/node@24.10.1)':
optionalDependencies:
'@types/node': 24.10.1
@@ -12704,6 +12867,14 @@ snapshots:
resolve: 1.22.11
strip-json-comments: 3.1.1
'@rushstack/terminal@0.19.3(@types/node@22.19.11)':
dependencies:
'@rushstack/node-core-library': 5.18.0(@types/node@22.19.11)
'@rushstack/problem-matcher': 0.1.1(@types/node@22.19.11)
supports-color: 8.1.1
optionalDependencies:
'@types/node': 22.19.11
'@rushstack/terminal@0.19.3(@types/node@24.10.1)':
dependencies:
'@rushstack/node-core-library': 5.18.0(@types/node@24.10.1)
@@ -12712,6 +12883,15 @@ snapshots:
optionalDependencies:
'@types/node': 24.10.1
'@rushstack/ts-command-line@5.1.3(@types/node@22.19.11)':
dependencies:
'@rushstack/terminal': 0.19.3(@types/node@22.19.11)
'@types/argparse': 1.0.38
argparse: 1.0.10
string-argv: 0.3.2
transitivePeerDependencies:
- '@types/node'
'@rushstack/ts-command-line@5.1.3(@types/node@24.10.1)':
dependencies:
'@rushstack/terminal': 0.19.3(@types/node@24.10.1)
@@ -12786,14 +12966,31 @@ snapshots:
magic-string: 0.25.9
string.prototype.matchall: 4.0.12
'@svelte-put/shortcut@4.1.0(svelte@5.44.0)':
'@svelte-put/shortcut@4.1.0(svelte@5.53.3)':
dependencies:
svelte: 5.44.0
svelte: 5.53.3
'@sveltejs/acorn-typescript@1.0.7(acorn@8.15.0)':
dependencies:
acorn: 8.15.0
'@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.3)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)))(svelte@5.53.3)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))':
dependencies:
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.3)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))
obug: 2.1.1
svelte: 5.53.3
vite: 7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
'@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.3)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))':
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.3)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)))(svelte@5.53.3)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))
deepmerge: 4.3.1
magic-string: 0.30.21
obug: 2.1.1
svelte: 5.53.3
vite: 7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vitefu: 1.1.2(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))
'@swc/helpers@0.5.17':
dependencies:
tslib: 2.8.1
@@ -12825,19 +13022,7 @@ snapshots:
'@tanstack/virtual-core': 3.13.12
vue: 3.5.24(typescript@5.9.3)
'@tinyflow-ai/ui@1.2.2(svelte@5.44.0)':
dependencies:
'@floating-ui/dom': 1.7.4
'@xyflow/svelte': 1.4.2(svelte@5.44.0)
transitivePeerDependencies:
- svelte
'@tinyflow-ai/vue@1.2.2(svelte@5.44.0)(vue@3.5.24(typescript@5.9.3))':
dependencies:
'@tinyflow-ai/ui': 1.2.2(svelte@5.44.0)
vue: 3.5.24(typescript@5.9.3)
transitivePeerDependencies:
- svelte
'@tsconfig/svelte@5.0.8': {}
'@tybys/wasm-util@0.10.1':
dependencies:
@@ -12859,7 +13044,7 @@ snapshots:
'@types/conventional-commits-parser@5.0.2':
dependencies:
'@types/node': 24.10.1
'@types/node': 22.19.11
'@types/d3-color@3.1.3': {}
@@ -12939,6 +13124,10 @@ snapshots:
'@types/node@12.20.55': {}
'@types/node@22.19.11':
dependencies:
undici-types: 6.21.0
'@types/node@24.10.1':
dependencies:
undici-types: 7.16.0
@@ -12959,7 +13148,7 @@ snapshots:
'@types/readdir-glob@1.1.5':
dependencies:
'@types/node': 24.10.1
'@types/node': 22.19.11
'@types/resolve@1.20.2': {}
@@ -13210,6 +13399,12 @@ snapshots:
vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vue: 3.5.24(typescript@5.9.3)
'@vitejs/plugin-vue@6.0.1(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.29
vite: 7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vue: 3.5.24(typescript@5.9.3)
'@vitest/expect@3.2.4':
dependencies:
'@types/chai': 5.2.3
@@ -13218,13 +13413,13 @@ snapshots:
chai: 5.3.3
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))':
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vite: 7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
'@vitest/pretty-format@3.2.4':
dependencies:
@@ -13510,11 +13705,11 @@ snapshots:
vue: 3.5.24(typescript@5.9.3)
xe-utils: 3.7.9
'@xyflow/svelte@1.4.2(svelte@5.44.0)':
'@xyflow/svelte@1.4.2(svelte@5.53.3)':
dependencies:
'@svelte-put/shortcut': 4.1.0(svelte@5.44.0)
'@svelte-put/shortcut': 4.1.0(svelte@5.53.3)
'@xyflow/system': 0.0.73
svelte: 5.44.0
svelte: 5.53.3
'@xyflow/system@0.0.73':
dependencies:
@@ -13657,7 +13852,7 @@ snapshots:
dependencies:
tslib: 2.8.1
aria-query@5.3.2: {}
aria-query@5.3.1: {}
array-buffer-byte-length@1.0.2:
dependencies:
@@ -14194,7 +14389,6 @@ snapshots:
copy-anything@2.0.6:
dependencies:
is-what: 3.14.1
optional: true
copy-anything@4.0.5:
dependencies:
@@ -14611,7 +14805,7 @@ snapshots:
detect-libc@2.1.2: {}
devalue@5.5.0: {}
devalue@5.6.3: {}
devlop@1.1.0:
dependencies:
@@ -15198,7 +15392,7 @@ snapshots:
dependencies:
estraverse: 5.3.0
esrap@2.1.3:
esrap@2.2.3:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -16147,8 +16341,7 @@ snapshots:
call-bound: 1.0.4
get-intrinsic: 1.3.0
is-what@3.14.1:
optional: true
is-what@3.14.1: {}
is-what@5.5.0: {}
@@ -16356,7 +16549,6 @@ snapshots:
mime: 1.6.0
needle: 3.3.1
source-map: 0.6.1
optional: true
leven@3.1.0: {}
@@ -17236,6 +17428,8 @@ snapshots:
has-symbols: 1.1.0
object-keys: 1.1.1
obug@2.1.1: {}
ofetch@1.5.1:
dependencies:
destr: 2.0.5
@@ -17387,8 +17581,7 @@ snapshots:
parse-ms@4.0.0: {}
parse-node-version@1.0.1:
optional: true
parse-node-version@1.0.1: {}
parse-passwd@1.0.0: {}
@@ -17464,11 +17657,11 @@ snapshots:
pify@4.0.1: {}
pinia-plugin-persistedstate@4.7.1(@nuxt/kit@4.2.1)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))):
pinia-plugin-persistedstate@4.7.1(@nuxt/kit@3.20.0)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))):
dependencies:
defu: 6.1.4
optionalDependencies:
'@nuxt/kit': 4.2.1
'@nuxt/kit': 3.20.0
pinia: 3.0.4(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))
pinia@3.0.4(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)):
@@ -18955,19 +19148,32 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
svelte@5.44.0:
svelte-check@4.4.3(picomatch@4.0.3)(svelte@5.53.3)(typescript@5.9.3):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
chokidar: 4.0.3
fdir: 6.5.0(picomatch@4.0.3)
picocolors: 1.1.1
sade: 1.8.1
svelte: 5.53.3
typescript: 5.9.3
transitivePeerDependencies:
- picomatch
svelte@5.53.3:
dependencies:
'@jridgewell/remapping': 2.3.5
'@jridgewell/sourcemap-codec': 1.5.5
'@sveltejs/acorn-typescript': 1.0.7(acorn@8.15.0)
'@types/estree': 1.0.8
'@types/trusted-types': 2.0.7
acorn: 8.15.0
aria-query: 5.3.2
aria-query: 5.3.1
axobject-query: 4.1.0
clsx: 2.1.1
devalue: 5.5.0
devalue: 5.6.3
esm-env: 1.2.2
esrap: 2.1.3
esrap: 2.2.3
is-reference: 3.0.3
locate-character: 3.0.0
magic-string: 0.30.21
@@ -19301,6 +19507,8 @@ snapshots:
magic-string: 0.30.21
unplugin: 2.3.10
undici-types@6.21.0: {}
undici-types@7.16.0: {}
undici@7.16.0: {}
@@ -19550,7 +19758,7 @@ snapshots:
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vite: 7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -19574,6 +19782,25 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite-plugin-dts@4.5.4(@types/node@22.19.11)(rollup@4.53.2)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)):
dependencies:
'@microsoft/api-extractor': 7.54.0(@types/node@22.19.11)
'@rollup/pluginutils': 5.3.0(rollup@4.53.2)
'@volar/typescript': 2.4.23
'@vue/language-core': 2.2.0(typescript@5.9.3)
compare-versions: 6.1.1
debug: 4.4.3
kolorist: 1.8.0
local-pkg: 1.1.2
magic-string: 0.30.21
typescript: 5.9.3
optionalDependencies:
vite: 7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
transitivePeerDependencies:
- '@types/node'
- rollup
- supports-color
vite-plugin-dts@4.5.4(@types/node@24.10.1)(rollup@4.53.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)):
dependencies:
'@microsoft/api-extractor': 7.54.0(@types/node@24.10.1)
@@ -19691,11 +19918,49 @@ snapshots:
terser: 5.44.1
yaml: 2.8.1
vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
dependencies:
esbuild: 0.25.3
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.53.2
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 22.19.11
fsevents: 2.3.3
jiti: 2.6.1
less: 4.4.2
sass: 1.94.0
terser: 5.44.1
yaml: 2.8.1
vite@7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
dependencies:
esbuild: 0.25.3
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.53.2
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.10.1
fsevents: 2.3.3
jiti: 2.6.1
less: 4.4.2
sass: 1.94.0
terser: 5.44.1
yaml: 2.8.1
vitefu@1.1.2(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)):
optionalDependencies:
vite: 7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
@@ -19713,7 +19978,7 @@ snapshots:
tinyglobby: 0.2.15
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vite: 7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
vite-node: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
why-is-node-running: 2.3.0
optionalDependencies:

View File

@@ -1,2 +1,11 @@
# Admin
正在进行中...
## Tinyflow Vendor Source
- Vendor scope: `packages/tinyflow-ui`, `packages/tinyflow-vue`
- Upstream repository: `https://github.com/tinyflow-ai/tinyflow`
- Upstream commit: `1873200bd38b5d8765246b5afad18bb22e8faaf1`
Notes:
- Local workspace keeps package names as `@tinyflow-ai/ui` and `@tinyflow-ai/vue`.
- Source is maintained locally in this repository.