- 新增 ParamTokenEditor,支持参数选择插入、token 高亮、整段删除与光标避让 - 参数候选改为动态监测,未映射参数可选择并在下拉与输入框顶部告警 - 接入知识库/搜索引擎/LLM/动态代码/HTTP Body 及 SQL、查询数据自定义节点 - 优化 Http 节点布局并补充参数解析工具与单测
283 lines
9.8 KiB
Svelte
283 lines
9.8 KiB
Svelte
<script lang="ts">
|
|
import NodeWrapper from '../core/NodeWrapper.svelte';
|
|
import {type Node, type NodeProps, useNodesData, 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';
|
|
import ParamTokenEditor from '../core/ParamTokenEditor.svelte';
|
|
|
|
const { data, ...rest }: {
|
|
data: NodeProps['data'],
|
|
[key: string]: any
|
|
} = $props();
|
|
|
|
const currentNodeId = getCurrentNodeId();
|
|
let currentNode = useNodesData(currentNodeId);
|
|
const { addParameter } = useAddParameter();
|
|
const flowInstance = useSvelteFlow();
|
|
const { updateNodeData: updateNodeDataInner } = flowInstance;
|
|
const editorParameters = $derived.by(() => {
|
|
return (currentNode?.current?.data?.parameters as Array<any>) || data.parameters || [];
|
|
});
|
|
|
|
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">
|
|
{#if form.templateSupport}
|
|
<ParamTokenEditor
|
|
mode="input"
|
|
placeholder={form.placeholder}
|
|
style="width: 100%"
|
|
value={data[form.name] || form.defaultValue}
|
|
parameters={editorParameters}
|
|
{...form.attrs}
|
|
oninput={(e)=>{
|
|
updateNodeDataByEvent(form.name,e)
|
|
}}
|
|
/>
|
|
{:else}
|
|
<Input
|
|
placeholder={form.placeholder}
|
|
style="width: 100%"
|
|
value={data[form.name] || form.defaultValue}
|
|
{...form.attrs}
|
|
onchange={(e)=>{
|
|
updateNodeDataByEvent(form.name,e)
|
|
}}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
{:else if form.type === 'textarea'}
|
|
<div class="setting-title">{form.label}</div>
|
|
<div class="setting-item">
|
|
{#if form.templateSupport}
|
|
<ParamTokenEditor
|
|
mode="textarea"
|
|
rows={3}
|
|
placeholder={form.placeholder}
|
|
style="width: 100%"
|
|
value={data[form.name] || form.defaultValue}
|
|
parameters={editorParameters}
|
|
{...form.attrs}
|
|
oninput={(e)=>{
|
|
updateNodeDataByEvent(form.name,e)
|
|
}}
|
|
/>
|
|
{:else}
|
|
<Textarea
|
|
rows={3}
|
|
placeholder={form.placeholder}
|
|
style="width: 100%"
|
|
value={data[form.name] || form.defaultValue}
|
|
{...form.attrs}
|
|
onchange={(e)=>{
|
|
updateNodeDataByEvent(form.name,e)
|
|
}}
|
|
/>
|
|
{/if}
|
|
</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>
|
|
|