Enhance workflow routing and agent configuration management
- Introduce WorkflowLLMRouter for pre-response LLM routing, allowing agents to determine the appropriate function to call based on user input. - Implement UserTurnRoutingProcessor to manage user turns before reaching the LLM, ensuring proper routing and handling of user messages. - Refactor WorkflowBrain to integrate new routing logic and enhance agent stage configuration, including entry modes and resource management. - Update service factory to support dynamic LLM resource configuration based on workflow settings. - Add tests for new routing functionality and ensure proper handling of user messages in various scenarios.
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import { Settings2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { KnowledgeRetrievalConfig } from "@/lib/api";
|
||||
|
||||
export const DEFAULT_KNOWLEDGE_RETRIEVAL_CONFIG: KnowledgeRetrievalConfig = {
|
||||
mode: "automatic",
|
||||
topN: 5,
|
||||
scoreThreshold: 0,
|
||||
};
|
||||
|
||||
export function KnowledgeRetrievalConfigDialog({
|
||||
disabled,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
value: KnowledgeRetrievalConfig;
|
||||
onChange: (config: KnowledgeRetrievalConfig) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [draft, setDraft] = useState(value);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function openDialog() {
|
||||
setDraft(value);
|
||||
setError(null);
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
function saveDraft() {
|
||||
if (draft.topN === 0 || draft.topN < -1 || !Number.isInteger(draft.topN)) {
|
||||
setError("Top N 必须为 -1 或大于 0 的整数");
|
||||
return;
|
||||
}
|
||||
if (draft.scoreThreshold < 0 || draft.scoreThreshold > 1) {
|
||||
setError("最低相关度必须在 0 到 1 之间");
|
||||
return;
|
||||
}
|
||||
onChange(draft);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={openDialog}
|
||||
aria-label="打开知识库高级配置"
|
||||
title={
|
||||
disabled
|
||||
? "请先选择知识库"
|
||||
: `${value.mode === "automatic" ? "自动检索" : "模型主动检索"} · Top N ${value.topN === -1 ? "不限" : value.topN} · 最低相关度 ${value.scoreThreshold}`
|
||||
}
|
||||
className="flex h-5 w-5 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
<Settings2 size={14} />
|
||||
</button>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>知识库高级配置</DialogTitle>
|
||||
<DialogDescription>
|
||||
设置检索触发方式、返回数量和相关度过滤条件。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-5 py-2">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium text-foreground">检索方式</div>
|
||||
<Select
|
||||
value={draft.mode}
|
||||
onValueChange={(mode: "automatic" | "on_demand") =>
|
||||
setDraft({ ...draft, mode })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full border-hairline-strong bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="automatic">自动检索</SelectItem>
|
||||
<SelectItem value="on_demand">模型主动检索</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{draft.mode === "automatic"
|
||||
? "每轮用户提问后自动检索,响应行为更稳定。"
|
||||
: "由大模型判断是否调用知识库,依赖模型的工具调用能力。"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-medium text-foreground">
|
||||
最多返回片段数
|
||||
</span>
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
min="-1"
|
||||
value={draft.topN}
|
||||
onChange={(event) =>
|
||||
setDraft({ ...draft, topN: Number(event.target.value) })
|
||||
}
|
||||
/>
|
||||
<span className="mt-1.5 block text-xs text-muted-foreground">
|
||||
填写 -1 时保留所有达到阈值的结果。
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-medium text-foreground">
|
||||
最低相关度
|
||||
</span>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="1"
|
||||
value={draft.scoreThreshold}
|
||||
onChange={(event) =>
|
||||
setDraft({
|
||||
...draft,
|
||||
scoreThreshold: Number(event.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span className="mt-1.5 block text-xs text-muted-foreground">
|
||||
仅保留相关度达到该值的片段,范围 0–1。
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="button" onClick={saveDraft}>
|
||||
保存配置
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
92
frontend/src/components/editor/section-card.tsx
Normal file
92
frontend/src/components/editor/section-card.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Compact section chrome shared by assistant editors and workflow node panels.
|
||||
* Density matches the debug preview drawer (text-sm titles, tight padding).
|
||||
*/
|
||||
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function SectionCard({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
icon?: ReactNode;
|
||||
title?: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const hasHeader = Boolean(title);
|
||||
|
||||
return (
|
||||
<Card
|
||||
size="sm"
|
||||
className={cn(
|
||||
"gap-3 rounded-2xl border border-hairline bg-card py-3.5 text-card-foreground shadow-sm ring-0",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{hasHeader && (
|
||||
<CardHeader className="gap-0 px-4">
|
||||
<div className="flex items-center gap-2.5">
|
||||
{icon && (
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<CardTitle className="text-sm font-medium leading-none">
|
||||
{title}
|
||||
</CardTitle>
|
||||
{description && <HelpHint text={description} />}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
)}
|
||||
<CardContent className={cn("px-4", hasHeader && "space-y-3")}>
|
||||
{children}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function HelpHint({ text }: { text: string }) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="查看说明"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
|
||||
>
|
||||
<HelpCircle size={13} />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-72 text-sm leading-6 text-muted-foreground"
|
||||
>
|
||||
{text}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
Save,
|
||||
Mic,
|
||||
Send,
|
||||
HelpCircle,
|
||||
Waypoints,
|
||||
AudioLines,
|
||||
Terminal,
|
||||
@@ -87,12 +86,6 @@ import { PageHeader } from "@/components/ui/page-header";
|
||||
import { FilterPills } from "@/components/ui/filter-pills";
|
||||
import { SearchInput } from "@/components/ui/search-input";
|
||||
import { ListToolbar } from "@/components/ui/list-toolbar";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
@@ -124,6 +117,7 @@ import {
|
||||
WorkflowEditor,
|
||||
type WorkflowSettings,
|
||||
} from "@/components/workflow/WorkflowEditor";
|
||||
import { HelpHint, SectionCard } from "@/components/editor/section-card";
|
||||
import {
|
||||
defaultGraph,
|
||||
type WorkflowGraph,
|
||||
@@ -362,7 +356,7 @@ type AssistantTypeOption = {
|
||||
label: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
/** 提示词、Dify、FastGPT 类型已落地,工作流暂时显示占位页 */
|
||||
/** 提示词、工作流、Dify、FastGPT 已落地;OpenCode 暂时显示即将上线 */
|
||||
available: boolean;
|
||||
};
|
||||
|
||||
@@ -379,7 +373,7 @@ const assistantTypeOptions: AssistantTypeOption[] = [
|
||||
label: "使用工作流构建",
|
||||
description: "用可视化编排串联多个节点,适合多步骤、带分支的复杂流程。",
|
||||
icon: <Workflow size={20} />,
|
||||
available: false,
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
type: "Dify",
|
||||
@@ -400,7 +394,7 @@ const assistantTypeOptions: AssistantTypeOption[] = [
|
||||
label: "使用 OpenCode 构建",
|
||||
description: "对接 OpenCode 服务,通过提示词驱动代码助手并支持实时语音对话。",
|
||||
icon: <Terminal size={20} />,
|
||||
available: true,
|
||||
available: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -472,12 +466,23 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
);
|
||||
const [workflowSettings, setWorkflowSettings] = useState<WorkflowSettings>({
|
||||
globalPrompt: defaultGraph().settings.globalPrompt,
|
||||
llm: defaultGraph().settings.defaultLlmResourceId,
|
||||
asr: defaultGraph().settings.defaultAsrResourceId,
|
||||
tts: defaultGraph().settings.defaultTtsResourceId,
|
||||
toolIds: defaultGraph().settings.toolIds,
|
||||
knowledgeBaseId: defaultGraph().settings.knowledgeBaseId,
|
||||
knowledgeRetrievalConfig: {
|
||||
mode: defaultGraph().settings.knowledgeMode,
|
||||
topN: defaultGraph().settings.knowledgeTopN,
|
||||
scoreThreshold: defaultGraph().settings.knowledgeScoreThreshold,
|
||||
},
|
||||
allowInterrupt: true,
|
||||
turnConfig: defaultTurnConfig(),
|
||||
});
|
||||
const [workflowDynamicVariableDefinitions, setWorkflowDynamicVariableDefinitions] =
|
||||
useState<Record<string, DynamicVariableDefinition>>({});
|
||||
const [workflowDebugOpen, setWorkflowDebugOpen] = useState(false);
|
||||
const [workflowSettingsOpen, setWorkflowSettingsOpen] = useState(false);
|
||||
const [workflowEditingNodeId, setWorkflowEditingNodeId] = useState<string | null>(null);
|
||||
const [workflowEditingEdgeId, setWorkflowEditingEdgeId] = useState<string | null>(null);
|
||||
const [activeNodeId, setActiveNodeId] = useState<string | null>(null);
|
||||
@@ -847,9 +852,25 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
? (assistant.graph as WorkflowGraph)
|
||||
: defaultGraph();
|
||||
const wfSettings: WorkflowSettings = {
|
||||
llm: assistant.modelResourceIds.LLM,
|
||||
llm:
|
||||
graph.settings?.defaultLlmResourceId ||
|
||||
assistant.modelResourceIds.LLM,
|
||||
asr: graph.settings?.defaultAsrResourceId || assistant.modelResourceIds.ASR,
|
||||
tts: graph.settings?.defaultTtsResourceId || assistant.modelResourceIds.TTS,
|
||||
toolIds: graph.settings?.toolIds ?? [],
|
||||
knowledgeBaseId:
|
||||
graph.settings?.knowledgeBaseId || assistant.knowledgeBaseId || "",
|
||||
knowledgeRetrievalConfig: {
|
||||
mode:
|
||||
graph.settings?.knowledgeMode ||
|
||||
assistant.knowledgeRetrievalConfig.mode,
|
||||
topN:
|
||||
graph.settings?.knowledgeTopN ??
|
||||
assistant.knowledgeRetrievalConfig.topN,
|
||||
scoreThreshold:
|
||||
graph.settings?.knowledgeScoreThreshold ??
|
||||
assistant.knowledgeRetrievalConfig.scoreThreshold,
|
||||
},
|
||||
globalPrompt: graph.settings?.globalPrompt ?? "",
|
||||
allowInterrupt: assistant.enableInterrupt,
|
||||
turnConfig: assistant.turnConfig,
|
||||
@@ -916,6 +937,9 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
...(workflowSettings.asr ? { ASR: workflowSettings.asr } : {}),
|
||||
...(workflowSettings.tts ? { TTS: workflowSettings.tts } : {}),
|
||||
},
|
||||
knowledgeBaseId: workflowSettings.knowledgeBaseId || null,
|
||||
knowledgeRetrievalConfig: workflowSettings.knowledgeRetrievalConfig,
|
||||
toolIds: workflowSettings.toolIds,
|
||||
graph: workflowGraph as unknown as Record<string, unknown>,
|
||||
dynamicVariableDefinitions: workflowDynamicVariableDefinitions,
|
||||
}),
|
||||
@@ -1386,7 +1410,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
<span
|
||||
role="alert"
|
||||
title={saveError}
|
||||
className="line-clamp-2 max-w-[min(42vw,560px)] self-center text-right text-sm leading-5 text-destructive"
|
||||
className="line-clamp-1 max-w-[min(42vw,560px)] self-center text-right text-sm leading-5 text-destructive"
|
||||
>
|
||||
{saveError}
|
||||
</span>
|
||||
@@ -1396,6 +1420,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
className="gap-2 border-hairline-strong text-foreground hover:bg-surface-strong"
|
||||
disabled={!editingId}
|
||||
onClick={() => {
|
||||
setWorkflowSettingsOpen(false);
|
||||
setWorkflowEditingNodeId(null);
|
||||
setWorkflowEditingEdgeId(null);
|
||||
setWorkflowDebugOpen(true);
|
||||
@@ -1430,6 +1455,8 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
onEditingNodeIdChange={setWorkflowEditingNodeId}
|
||||
editingEdgeId={workflowEditingEdgeId}
|
||||
onEditingEdgeIdChange={setWorkflowEditingEdgeId}
|
||||
settingsOpen={workflowSettingsOpen}
|
||||
onSettingsOpenChange={setWorkflowSettingsOpen}
|
||||
debugOpen={workflowDebugOpen}
|
||||
onDebugOpenChange={(open) => {
|
||||
setWorkflowDebugOpen(open);
|
||||
@@ -1508,10 +1535,10 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1 gap-6">
|
||||
<div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
|
||||
<div className="flex min-h-0 flex-1 gap-4">
|
||||
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
|
||||
<SectionCard
|
||||
icon={<Boxes size={18} />}
|
||||
icon={<Boxes size={15} />}
|
||||
title="Dify 应用配置"
|
||||
description="从「模型资源」中选择 Dify 应用。开场白、知识库、提示词等对话编排请在 Dify 平台配置,本页不重复设置。"
|
||||
>
|
||||
@@ -1525,7 +1552,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<Brain size={18} />}
|
||||
icon={<Brain size={15} />}
|
||||
title="语音配置"
|
||||
description="从「模型资源」中选择语音识别与语音合成。大模型、知识库与开场白由 Dify 应用提供,请前往 Dify 平台配置。"
|
||||
>
|
||||
@@ -1546,7 +1573,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<Sparkles size={18} />}
|
||||
icon={<Sparkles size={15} />}
|
||||
title="交互策略"
|
||||
description="设置实时视频对话时的交互体验"
|
||||
>
|
||||
@@ -1604,10 +1631,10 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1 gap-6">
|
||||
<div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
|
||||
<div className="flex min-h-0 flex-1 gap-4">
|
||||
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
|
||||
<SectionCard
|
||||
icon={<Database size={18} />}
|
||||
icon={<Database size={15} />}
|
||||
title="FastGPT 应用配置"
|
||||
description="从「模型资源」中选择 FastGPT 应用。开场白、知识库、提示词等对话编排请在 FastGPT 平台配置,本页不重复设置。"
|
||||
>
|
||||
@@ -1621,7 +1648,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<Brain size={18} />}
|
||||
icon={<Brain size={15} />}
|
||||
title="语音配置"
|
||||
description="从「模型资源」中选择语音识别与语音合成。大模型、知识库与开场白由 FastGPT 应用提供,请前往 FastGPT 平台配置。"
|
||||
>
|
||||
@@ -1642,7 +1669,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<Sparkles size={18} />}
|
||||
icon={<Sparkles size={15} />}
|
||||
title="交互策略"
|
||||
description="设置实时视频对话时的交互体验"
|
||||
>
|
||||
@@ -1704,10 +1731,10 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1 gap-6">
|
||||
<div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
|
||||
<div className="flex min-h-0 flex-1 gap-4">
|
||||
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
|
||||
<SectionCard
|
||||
icon={<Terminal size={18} />}
|
||||
icon={<Terminal size={15} />}
|
||||
title="OpenCode 服务配置"
|
||||
description="从「模型资源」中选择 OpenCode 服务资源。"
|
||||
>
|
||||
@@ -1721,7 +1748,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<MessageSquareText size={18} />}
|
||||
icon={<MessageSquareText size={15} />}
|
||||
title="提示词"
|
||||
description="描述助手的角色、能力和回答要求"
|
||||
>
|
||||
@@ -1734,7 +1761,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<Brain size={18} />}
|
||||
icon={<Brain size={15} />}
|
||||
title="模型与语音配置"
|
||||
description="配置 OpenCode 使用的大语言模型、语音识别与语音合成资源。"
|
||||
>
|
||||
@@ -1779,7 +1806,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<Sparkles size={18} />}
|
||||
icon={<Sparkles size={15} />}
|
||||
title="交互策略"
|
||||
description="设置实时视频对话时的交互体验"
|
||||
>
|
||||
@@ -1843,10 +1870,10 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex min-h-0 flex-1 gap-6">
|
||||
<div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
|
||||
<div className="flex min-h-0 flex-1 gap-4">
|
||||
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
|
||||
<SectionCard>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -1858,25 +1885,25 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
}
|
||||
}}
|
||||
className={[
|
||||
"cursor-pointer rounded-2xl border p-5 text-left transition-colors",
|
||||
"cursor-pointer rounded-xl border p-3.5 text-left transition-colors",
|
||||
form.runtimeMode === "pipeline"
|
||||
? "border-primary bg-primary/5 ring-1 ring-primary"
|
||||
: "border-hairline bg-canvas-soft hover:border-hairline-strong",
|
||||
].join(" ")}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
|
||||
<Waypoints size={18} />
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
|
||||
<Waypoints size={15} />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium text-foreground">Pipeline 模式</span>
|
||||
<span className="text-sm font-medium text-foreground">Pipeline 模式</span>
|
||||
<HelpHint text="通过 ASR、LLM 和 TTS 级联组成语音管线,灵活选配各模块。" />
|
||||
</div>
|
||||
</div>
|
||||
{form.runtimeMode === "pipeline" && (
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
||||
<Check size={14} />
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
||||
<Check size={12} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -1893,25 +1920,25 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
}
|
||||
}}
|
||||
className={[
|
||||
"cursor-pointer rounded-2xl border p-5 text-left transition-colors",
|
||||
"cursor-pointer rounded-xl border p-3.5 text-left transition-colors",
|
||||
form.runtimeMode === "realtime"
|
||||
? "border-primary bg-primary/5 ring-1 ring-primary"
|
||||
: "border-hairline bg-canvas-soft hover:border-hairline-strong",
|
||||
].join(" ")}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
|
||||
<AudioLines size={18} />
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
|
||||
<AudioLines size={15} />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium text-foreground">Realtime 模式</span>
|
||||
<span className="text-sm font-medium text-foreground">Realtime 模式</span>
|
||||
<HelpHint text="使用原生实时语音模型,模型直接处理音频输入并生成语音回复。" />
|
||||
</div>
|
||||
</div>
|
||||
{form.runtimeMode === "realtime" && (
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
||||
<Check size={14} />
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
||||
<Check size={12} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -1920,7 +1947,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<MessageSquareText size={18} />}
|
||||
icon={<MessageSquareText size={15} />}
|
||||
title="提示词"
|
||||
description="描述助手的角色、能力和回答要求"
|
||||
>
|
||||
@@ -1938,7 +1965,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
|
||||
{form.runtimeMode === "pipeline" ? (
|
||||
<SectionCard
|
||||
icon={<Brain size={18} />}
|
||||
icon={<Brain size={15} />}
|
||||
title="模型配置"
|
||||
description="从「模型资源」中选择大语言模型、语音识别与语音合成"
|
||||
>
|
||||
@@ -1983,7 +2010,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
</SectionCard>
|
||||
) : (
|
||||
<SectionCard
|
||||
icon={<Brain size={18} />}
|
||||
icon={<Brain size={15} />}
|
||||
title="模型配置"
|
||||
description="当前模式下 ASR 与 TTS 由 Realtime 模型内置完成"
|
||||
>
|
||||
@@ -1998,7 +2025,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
)}
|
||||
|
||||
<SectionCard
|
||||
icon={<Bot size={18} />}
|
||||
icon={<Bot size={15} />}
|
||||
title="开场白"
|
||||
description="助手与用户首次对话时的开场语"
|
||||
>
|
||||
@@ -2015,7 +2042,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
|
||||
{form.runtimeMode === "pipeline" && (
|
||||
<SectionCard
|
||||
icon={<Database size={18} />}
|
||||
icon={<Database size={15} />}
|
||||
title="知识库配置"
|
||||
description="选择助手回答时可检索的业务知识来源"
|
||||
>
|
||||
@@ -2041,7 +2068,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
)}
|
||||
|
||||
<SectionCard
|
||||
icon={<Wrench size={18} />}
|
||||
icon={<Wrench size={15} />}
|
||||
title="工具"
|
||||
description="配置该提示词助手可以调用的工具"
|
||||
>
|
||||
@@ -2053,7 +2080,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<Sparkles size={18} />}
|
||||
icon={<Sparkles size={15} />}
|
||||
title="交互策略"
|
||||
description="设置实时视频对话时的交互体验"
|
||||
>
|
||||
@@ -3377,29 +3404,6 @@ function EditableTitle({
|
||||
);
|
||||
}
|
||||
|
||||
function HelpHint({ text }: { text: string }) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="查看说明"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className="flex h-5 w-5 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
|
||||
>
|
||||
<HelpCircle size={14} />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-72 text-sm leading-6 text-muted-foreground"
|
||||
>
|
||||
{text}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function DynamicVariableEditorHint({
|
||||
count,
|
||||
onOpen,
|
||||
@@ -3657,45 +3661,6 @@ function DynamicVariablesDialog({
|
||||
);
|
||||
}
|
||||
|
||||
function SectionCard({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
}: {
|
||||
icon?: React.ReactNode;
|
||||
title?: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const hasHeader = Boolean(title);
|
||||
|
||||
return (
|
||||
<Card className="rounded-2xl border-hairline bg-card text-card-foreground shadow-sm">
|
||||
{hasHeader && (
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
{icon && (
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-surface-strong text-foreground">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<CardTitle className="text-base font-medium">{title}</CardTitle>
|
||||
{description && <HelpHint text={description} />}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
)}
|
||||
|
||||
<CardContent className={hasHeader ? "space-y-4" : undefined}>
|
||||
{children}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function TextAreaField({
|
||||
label,
|
||||
value,
|
||||
@@ -3712,7 +3677,7 @@ function TextAreaField({
|
||||
return (
|
||||
<label className="block">
|
||||
{label && (
|
||||
<div className="mb-2 text-sm font-medium text-foreground">{label}</div>
|
||||
<div className="mb-1.5 text-sm font-medium text-foreground">{label}</div>
|
||||
)}
|
||||
<Textarea
|
||||
value={value}
|
||||
@@ -3721,7 +3686,7 @@ function TextAreaField({
|
||||
rows={rows}
|
||||
// Override ui/textarea's field-sizing-content so `rows` sets a real height
|
||||
// instead of collapsing to min-h-16 when the value is short.
|
||||
className="field-sizing-fixed min-h-32 resize-y border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
|
||||
className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
@@ -3747,7 +3712,7 @@ function ResourceSelectField({
|
||||
return (
|
||||
<div className="block">
|
||||
{label && (
|
||||
<div className="mb-2 text-sm font-medium text-foreground">{label}</div>
|
||||
<div className="mb-1.5 text-sm font-medium text-foreground">{label}</div>
|
||||
)}
|
||||
|
||||
<Select
|
||||
@@ -4056,18 +4021,18 @@ function ToggleRow({
|
||||
<div
|
||||
className={[
|
||||
"flex items-center justify-between border border-hairline bg-canvas-soft",
|
||||
hasIcon ? "rounded-2xl p-5" : "rounded-xl p-4",
|
||||
hasIcon ? "rounded-xl p-3.5" : "rounded-xl px-3.5 py-3",
|
||||
].join(" ")}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={[
|
||||
"flex items-center font-medium text-foreground",
|
||||
hasIcon ? "gap-3" : "gap-1.5",
|
||||
"flex items-center text-sm font-medium text-foreground",
|
||||
hasIcon ? "gap-2.5" : "gap-1.5",
|
||||
].join(" ")}
|
||||
>
|
||||
{icon && (
|
||||
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
@@ -4077,7 +4042,7 @@ function ToggleRow({
|
||||
</span>
|
||||
</div>
|
||||
{description && (
|
||||
<div className="mt-1 text-sm text-muted-foreground">
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { HelpCircle, Settings2 } from "lucide-react";
|
||||
import { Settings2 } from "lucide-react";
|
||||
|
||||
import { HelpHint } from "@/components/editor/section-card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
@@ -13,11 +14,6 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -59,7 +55,7 @@ export function TurnConfigEditor({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 rounded-2xl border border-hairline bg-card p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between gap-3 rounded-xl border border-hairline bg-canvas-soft px-3.5 py-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
允许用户打断
|
||||
@@ -72,7 +68,7 @@ export function TurnConfigEditor({
|
||||
aria-label="打开允许用户打断高级配置"
|
||||
className="flex h-5 w-5 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
|
||||
>
|
||||
<Settings2 size={14} />
|
||||
<Settings2 size={13} />
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[calc(100vh-3rem)] overflow-y-auto sm:max-w-6xl lg:max-w-[88rem] lg:overflow-hidden">
|
||||
@@ -161,29 +157,6 @@ function ConfigSection({ title, children }: { title: string; children: ReactNode
|
||||
);
|
||||
}
|
||||
|
||||
function HelpHint({ text }: { text: string }) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="查看允许用户打断说明"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className="flex h-5 w-5 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
|
||||
>
|
||||
<HelpCircle size={14} />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-72 text-sm leading-6 text-muted-foreground"
|
||||
>
|
||||
{text}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function NumberField({ label, value, min, max, step, onChange }: { label: string; value: number; min: number; max: number; step: number; onChange: (value: number) => void }) {
|
||||
return (
|
||||
<label className="block space-y-2">
|
||||
|
||||
@@ -30,14 +30,24 @@ export function GenericNode({ id, type, data, selected }: NodeProps) {
|
||||
const preview = (nodeData.greeting || nodeData.prompt || nodeData.message || "")
|
||||
.toString()
|
||||
.trim();
|
||||
const entryModeLabel = {
|
||||
wait_user: "等待用户",
|
||||
generate: "立即回复",
|
||||
fixed_speech: "固定进入语",
|
||||
}[nodeData.entryMode ?? "wait_user"];
|
||||
const inheritsGlobal = nodeData.inheritGlobalConfig !== false;
|
||||
const meta = type === "agent"
|
||||
? [
|
||||
nodeData.contextPolicy === "fresh" ? "独立上下文" : "继承上下文",
|
||||
`${nodeData.toolIds?.length ?? 0} 工具`,
|
||||
nodeData.knowledgeBaseId ? "知识库" : null,
|
||||
nodeData.asrResourceId ? "独立 ASR" : null,
|
||||
nodeData.ttsResourceId ? "独立 TTS" : null,
|
||||
].filter(Boolean)
|
||||
? inheritsGlobal
|
||||
? [entryModeLabel, "继承全局配置"]
|
||||
: [
|
||||
entryModeLabel,
|
||||
"自定义配置",
|
||||
nodeData.llmResourceId ? "独立 LLM" : null,
|
||||
`${nodeData.toolIds?.length ?? 0} 工具`,
|
||||
nodeData.knowledgeBaseId ? "知识库" : null,
|
||||
nodeData.asrResourceId ? "独立 ASR" : null,
|
||||
nodeData.ttsResourceId ? "独立 TTS" : null,
|
||||
].filter(Boolean)
|
||||
: type === "action" && nodeData.toolId
|
||||
? ["确定性工具"]
|
||||
: [];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ import type { NodeSpecDto } from "@/lib/api";
|
||||
export type WorkflowNodeType = "start" | "agent" | "action" | "handoff" | "end";
|
||||
export type ContextPolicy = "inherit" | "fresh";
|
||||
export type KnowledgeMode = "automatic" | "on_demand" | "disabled";
|
||||
export type AgentEntryMode = "wait_user" | "generate" | "fixed_speech";
|
||||
export type EdgeMode = "llm" | "expression" | "always";
|
||||
export type ExpressionOperator =
|
||||
| "eq"
|
||||
@@ -25,11 +26,15 @@ export type WorkflowNodeData = {
|
||||
greeting?: string;
|
||||
prompt?: string;
|
||||
contextPolicy?: ContextPolicy;
|
||||
inheritGlobalConfig?: boolean;
|
||||
entryMode?: AgentEntryMode;
|
||||
entrySpeech?: string;
|
||||
toolIds?: string[];
|
||||
knowledgeBaseId?: string;
|
||||
knowledgeMode?: KnowledgeMode;
|
||||
knowledgeTopN?: number;
|
||||
knowledgeScoreThreshold?: number;
|
||||
llmResourceId?: string;
|
||||
asrResourceId?: string;
|
||||
ttsResourceId?: string;
|
||||
toolId?: string;
|
||||
@@ -129,8 +134,14 @@ export type WorkflowGraph = {
|
||||
specVersion: 3;
|
||||
settings: {
|
||||
globalPrompt: string;
|
||||
defaultLlmResourceId: string;
|
||||
defaultAsrResourceId: string;
|
||||
defaultTtsResourceId: string;
|
||||
toolIds: string[];
|
||||
knowledgeBaseId: string;
|
||||
knowledgeMode: "automatic" | "on_demand";
|
||||
knowledgeTopN: number;
|
||||
knowledgeScoreThreshold: number;
|
||||
};
|
||||
nodes: Array<{
|
||||
id: string;
|
||||
@@ -153,8 +164,14 @@ export function defaultGraph(): WorkflowGraph {
|
||||
settings: {
|
||||
globalPrompt:
|
||||
"你是一个友好、专业的语音助手。请使用简短、自然、适合口语表达的句子。",
|
||||
defaultLlmResourceId: "",
|
||||
defaultAsrResourceId: "",
|
||||
defaultTtsResourceId: "",
|
||||
toolIds: [],
|
||||
knowledgeBaseId: "",
|
||||
knowledgeMode: "automatic",
|
||||
knowledgeTopN: 5,
|
||||
knowledgeScoreThreshold: 0,
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
@@ -174,10 +191,9 @@ export function defaultGraph(): WorkflowGraph {
|
||||
name: "Agent",
|
||||
prompt: "了解用户需求并提供清晰、准确的帮助。",
|
||||
contextPolicy: "inherit",
|
||||
toolIds: [],
|
||||
knowledgeMode: "disabled",
|
||||
knowledgeTopN: 5,
|
||||
knowledgeScoreThreshold: 0,
|
||||
inheritGlobalConfig: true,
|
||||
entryMode: "wait_user",
|
||||
entrySpeech: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user