feat(frontend): compact theme selector, adaptive prompt editor layout
- Theme dropdown: 亮色/暗色/随系统 as one row of three icons - Prompt editor: sections centered (max-w-7xl) when debug drawer is closed, full-width aligned with tab edges when open - Debug drawer opens by default - Prompt textarea: 8 → 16 rows Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -43,11 +43,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import type {
|
import type { DynamicVariableDefinition, StartupConfig, Tool } from "@/lib/api";
|
||||||
DynamicVariableDefinition,
|
|
||||||
StartupConfig,
|
|
||||||
Tool,
|
|
||||||
} from "@/lib/api";
|
|
||||||
|
|
||||||
type ResourceOption = { value: string; label: string };
|
type ResourceOption = { value: string; label: string };
|
||||||
type PromptOpeningMode = StartupConfig["openingMode"];
|
type PromptOpeningMode = StartupConfig["openingMode"];
|
||||||
@@ -153,7 +149,7 @@ export function PromptEditor({
|
|||||||
const selectedAnchorRef = useRef<PromptSectionId | null>(null);
|
const selectedAnchorRef = useRef<PromptSectionId | null>(null);
|
||||||
const [activeSection, setActiveSection] =
|
const [activeSection, setActiveSection] =
|
||||||
useState<PromptSectionId>("conversation");
|
useState<PromptSectionId>("conversation");
|
||||||
const [debugOpen, setDebugOpen] = useState(false);
|
const [debugOpen, setDebugOpen] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = scrollContainerRef.current;
|
const container = scrollContainerRef.current;
|
||||||
@@ -245,13 +241,14 @@ export function PromptEditor({
|
|||||||
updateForm("startup", {
|
updateForm("startup", {
|
||||||
...form.startup,
|
...form.startup,
|
||||||
openingMode: nextMode,
|
openingMode: nextMode,
|
||||||
openingMessage: nextMode === "confirmation"
|
openingMessage:
|
||||||
? {
|
nextMode === "confirmation"
|
||||||
title: openingMessage?.title ?? "重要提示",
|
? {
|
||||||
message: openingMessage?.message ?? "请确认已阅读以上信息。",
|
title: openingMessage?.title ?? "重要提示",
|
||||||
confirmLabel: openingMessage?.confirmLabel ?? "确认",
|
message: openingMessage?.message ?? "请确认已阅读以上信息。",
|
||||||
}
|
confirmLabel: openingMessage?.confirmLabel ?? "确认",
|
||||||
: null,
|
}
|
||||||
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,377 +319,395 @@ export function PromptEditor({
|
|||||||
aria-label="提示词配置分区"
|
aria-label="提示词配置分区"
|
||||||
className="shrink-0 overflow-x-auto overscroll-none bg-background px-4 pt-3 sm:px-6 sm:pt-4 lg:px-8"
|
className="shrink-0 overflow-x-auto overscroll-none bg-background px-4 pt-3 sm:px-6 sm:pt-4 lg:px-8"
|
||||||
>
|
>
|
||||||
<div className="grid min-w-[28rem] grid-cols-5 border-b border-hairline">
|
<div className={debugOpen ? "w-full" : "mx-auto w-full max-w-7xl"}>
|
||||||
{promptSections.map((section) => {
|
<div className="grid min-w-[28rem] grid-cols-5 border-b border-hairline">
|
||||||
const active = activeSection === section.id;
|
{promptSections.map((section) => {
|
||||||
|
const active = activeSection === section.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={section.id}
|
key={section.id}
|
||||||
type="button"
|
type="button"
|
||||||
aria-current={active ? "location" : undefined}
|
aria-current={active ? "location" : undefined}
|
||||||
onClick={() => scrollToSection(section.id)}
|
onClick={() => scrollToSection(section.id)}
|
||||||
className={[
|
className={[
|
||||||
"-mb-px whitespace-nowrap border-b-2 px-2 py-2.5 text-center text-xs transition-colors sm:px-3 sm:text-sm",
|
"-mb-px whitespace-nowrap border-b-2 px-2 py-2.5 text-center text-xs transition-colors sm:px-3 sm:text-sm",
|
||||||
active
|
active
|
||||||
? "border-primary text-foreground"
|
? "border-primary text-foreground"
|
||||||
: "border-transparent text-muted-foreground hover:bg-surface-strong/50 hover:text-foreground",
|
: "border-transparent text-muted-foreground hover:bg-surface-strong/50 hover:text-foreground",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{section.label}
|
{section.label}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={scrollContainerRef}
|
ref={scrollContainerRef}
|
||||||
className="scrollbar-subtle min-h-0 min-w-0 flex-1 space-y-3 overflow-y-auto overscroll-none bg-background px-4 pb-6 pt-3 sm:px-6 sm:pb-8 lg:px-8 lg:pb-10"
|
className="scrollbar-subtle min-h-0 min-w-0 flex-1 overflow-y-auto overscroll-none bg-background px-4 pb-6 pt-3 sm:px-6 sm:pb-8 lg:px-8 lg:pb-10"
|
||||||
>
|
>
|
||||||
<section
|
<div
|
||||||
ref={(element) => {
|
className={
|
||||||
sectionRefs.current.conversation = element;
|
debugOpen ? "space-y-3" : "mx-auto max-w-7xl space-y-3"
|
||||||
}}
|
}
|
||||||
className="scroll-mt-3 space-y-3"
|
|
||||||
>
|
>
|
||||||
<SectionCard
|
<section
|
||||||
icon={<MessageSquareText size={15} />}
|
ref={(element) => {
|
||||||
title="提示词"
|
sectionRefs.current.conversation = element;
|
||||||
description="描述助手的角色、能力和回答要求"
|
}}
|
||||||
|
className="scroll-mt-3 space-y-3"
|
||||||
>
|
>
|
||||||
<TextAreaField
|
|
||||||
value={form.prompt}
|
|
||||||
onChange={(value) => updateForm("prompt", value)}
|
|
||||||
placeholder="请输入提示词,描述助手的角色、能力和回答要求"
|
|
||||||
rows={8}
|
|
||||||
/>
|
|
||||||
<DynamicVariableEditorHint
|
|
||||||
count={Object.keys(dynamicVariableDefinitions).length}
|
|
||||||
onOpen={() => scrollToSection("variables")}
|
|
||||||
/>
|
|
||||||
</SectionCard>
|
|
||||||
|
|
||||||
<SectionCard
|
|
||||||
icon={<Bot size={15} />}
|
|
||||||
title="开场白"
|
|
||||||
description="助手与用户首次对话时的开场语"
|
|
||||||
>
|
|
||||||
<TextAreaField
|
|
||||||
value={form.greeting}
|
|
||||||
onChange={(value) => updateForm("greeting", value)}
|
|
||||||
placeholder="请输入助手开场白"
|
|
||||||
/>
|
|
||||||
<DynamicVariableEditorHint
|
|
||||||
count={Object.keys(dynamicVariableDefinitions).length}
|
|
||||||
onOpen={() => scrollToSection("variables")}
|
|
||||||
/>
|
|
||||||
{form.runtimeMode === "pipeline" && (
|
|
||||||
<div className="mt-4 space-y-3 border-t border-hairline pt-4">
|
|
||||||
<div>
|
|
||||||
<div className="mb-1.5 text-sm font-medium text-foreground">
|
|
||||||
开场白继续方式
|
|
||||||
</div>
|
|
||||||
<Select
|
|
||||||
value={openingMode}
|
|
||||||
onValueChange={(value: PromptOpeningMode) =>
|
|
||||||
setOpeningMode(value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{OPENING_MODE_OPTIONS.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="mt-1.5 text-xs leading-5 text-muted-foreground">
|
|
||||||
{openingModeDescription}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{openingMode === "confirmation" && openingMessage && (
|
|
||||||
<div className="space-y-3 rounded-xl border border-hairline bg-canvas-soft p-4">
|
|
||||||
<label className="block">
|
|
||||||
<span className="mb-1.5 block text-sm font-medium text-foreground">
|
|
||||||
弹窗标题
|
|
||||||
</span>
|
|
||||||
<Input
|
|
||||||
value={openingMessage.title}
|
|
||||||
onChange={(event) =>
|
|
||||||
updateOpeningMessage({ title: event.target.value })
|
|
||||||
}
|
|
||||||
placeholder="重要提示"
|
|
||||||
className="border-hairline-strong bg-background"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<TextAreaField
|
|
||||||
label="重要信息"
|
|
||||||
value={openingMessage.message}
|
|
||||||
onChange={(message) =>
|
|
||||||
updateOpeningMessage({ message })
|
|
||||||
}
|
|
||||||
placeholder="请输入需要用户确认的重要信息"
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
<label className="block">
|
|
||||||
<span className="mb-1.5 block text-sm font-medium text-foreground">
|
|
||||||
确认按钮
|
|
||||||
</span>
|
|
||||||
<Input
|
|
||||||
value={openingMessage.confirmLabel}
|
|
||||||
onChange={(event) =>
|
|
||||||
updateOpeningMessage({
|
|
||||||
confirmLabel: event.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="确认"
|
|
||||||
className="border-hairline-strong bg-background"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<p className="text-xs leading-5 text-muted-foreground">
|
|
||||||
确认前关闭语音、文字和图片输入;确认后立即停止剩余开场白并进入 Agent。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="border-t border-hairline pt-3">
|
|
||||||
<div className="mb-1.5 text-sm font-medium text-foreground">
|
|
||||||
Agent 进入行为
|
|
||||||
</div>
|
|
||||||
<Select
|
|
||||||
value={form.startup.entryMode}
|
|
||||||
onValueChange={(entryMode: PromptEntryMode) =>
|
|
||||||
updateForm("startup", {
|
|
||||||
...form.startup,
|
|
||||||
entryMode,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{ENTRY_MODE_OPTIONS.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="mt-1.5 text-xs leading-5 text-muted-foreground">
|
|
||||||
该设置仅控制开场阶段正常完成后的 Agent 首轮行为。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SectionCard>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
|
||||||
ref={(element) => {
|
|
||||||
sectionRefs.current.models = element;
|
|
||||||
}}
|
|
||||||
className="scroll-mt-3 space-y-3"
|
|
||||||
>
|
|
||||||
<SectionCard
|
|
||||||
icon={<Brain size={15} />}
|
|
||||||
title="模型与语音"
|
|
||||||
description={
|
|
||||||
form.runtimeMode === "pipeline"
|
|
||||||
? "选择运行方式,以及大语言模型、语音识别与语音合成资源"
|
|
||||||
: "选择运行方式;Realtime 模型内置语音识别与语音合成"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<RuntimeModeSelector
|
|
||||||
value={form.runtimeMode}
|
|
||||||
onChange={(runtimeMode) => {
|
|
||||||
updateForm("runtimeMode", runtimeMode);
|
|
||||||
if (
|
|
||||||
runtimeMode === "realtime" &&
|
|
||||||
(form.startup.actions.length ||
|
|
||||||
form.startup.openingMode !== "interruptible" ||
|
|
||||||
form.startup.entryMode !== "wait_user" ||
|
|
||||||
form.startup.openingMessage)
|
|
||||||
) {
|
|
||||||
updateForm("startup", {
|
|
||||||
executionMode: "sequential",
|
|
||||||
openingMode: "interruptible",
|
|
||||||
entryMode: "wait_user",
|
|
||||||
actions: [],
|
|
||||||
openingMessage: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (runtimeMode === "realtime") {
|
|
||||||
updateForm(
|
|
||||||
"toolIds",
|
|
||||||
form.toolIds.filter(
|
|
||||||
(id) =>
|
|
||||||
tools.find((tool) => tool.id === id)?.type !==
|
|
||||||
"system",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{form.runtimeMode === "pipeline" ? (
|
|
||||||
<>
|
|
||||||
<ResourceSelectField
|
|
||||||
label="大语言模型"
|
|
||||||
value={form.model}
|
|
||||||
onChange={handlePromptModelChange}
|
|
||||||
options={llmOptions}
|
|
||||||
noneLabel="无"
|
|
||||||
/>
|
|
||||||
<ResourceSelectField
|
|
||||||
label="语音识别"
|
|
||||||
value={form.asr}
|
|
||||||
onChange={(value) => updateForm("asr", value)}
|
|
||||||
options={asrOptions}
|
|
||||||
noneLabel="无"
|
|
||||||
/>
|
|
||||||
<ResourceSelectField
|
|
||||||
label="语音合成"
|
|
||||||
value={form.voice}
|
|
||||||
onChange={(value) => updateForm("voice", value)}
|
|
||||||
options={ttsOptions}
|
|
||||||
noneLabel="无"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<ResourceSelectField
|
|
||||||
label="Realtime 模型"
|
|
||||||
value={form.realtimeModel}
|
|
||||||
onChange={(value) => updateForm("realtimeModel", value)}
|
|
||||||
options={realtimeOptions}
|
|
||||||
noneLabel="无"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SectionCard>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
|
||||||
ref={(element) => {
|
|
||||||
sectionRefs.current.capabilities = element;
|
|
||||||
}}
|
|
||||||
className="scroll-mt-3 space-y-3"
|
|
||||||
>
|
|
||||||
{form.runtimeMode === "pipeline" && (
|
|
||||||
<VisionConfigSection
|
|
||||||
description="配置提示词助手是否可以按需理解用户摄像头画面"
|
|
||||||
hint="开启后,助手会获得读取当前视频画面的工具。选择「模型自己」时,大语言模型必须支持图片输入。"
|
|
||||||
enabled={form.visionEnabled}
|
|
||||||
modelResourceId={form.visionModelResourceId}
|
|
||||||
mainModelResourceId={form.model}
|
|
||||||
modelOptions={visionModelOptions}
|
|
||||||
onEnabledChange={handlePromptVisionEnabledChange}
|
|
||||||
onModelResourceIdChange={(value) =>
|
|
||||||
updateForm("visionModelResourceId", value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{form.runtimeMode === "pipeline" && (
|
|
||||||
<SectionCard
|
<SectionCard
|
||||||
icon={<Database size={15} />}
|
icon={<MessageSquareText size={15} />}
|
||||||
title="知识库配置"
|
title="提示词"
|
||||||
description="选择助手回答时可检索的业务知识来源"
|
description="描述助手的角色、能力和回答要求"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1.5">
|
<TextAreaField
|
||||||
<span className="text-sm font-medium text-foreground">
|
value={form.prompt}
|
||||||
知识库选择
|
onChange={(value) => updateForm("prompt", value)}
|
||||||
</span>
|
placeholder="请输入提示词,描述助手的角色、能力和回答要求"
|
||||||
<KnowledgeRetrievalConfigDialog
|
rows={16}
|
||||||
disabled={!form.knowledgeBase}
|
/>
|
||||||
value={form.knowledgeRetrievalConfig}
|
<DynamicVariableEditorHint
|
||||||
onChange={(config) =>
|
count={Object.keys(dynamicVariableDefinitions).length}
|
||||||
updateForm("knowledgeRetrievalConfig", config)
|
onOpen={() => scrollToSection("variables")}
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ResourceSelectField
|
|
||||||
value={form.knowledgeBase}
|
|
||||||
onChange={(value) => updateForm("knowledgeBase", value)}
|
|
||||||
options={kbOptions}
|
|
||||||
noneLabel="无"
|
|
||||||
/>
|
/>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
)}
|
|
||||||
|
|
||||||
<SectionCard
|
<SectionCard
|
||||||
icon={<Wrench size={15} />}
|
icon={<Bot size={15} />}
|
||||||
title="工具"
|
title="开场白"
|
||||||
description="配置该提示词助手可以调用的工具"
|
description="助手与用户首次对话时的开场语"
|
||||||
>
|
>
|
||||||
<ToolPicker
|
<TextAreaField
|
||||||
tools={tools.filter(
|
value={form.greeting}
|
||||||
(tool) =>
|
onChange={(value) => updateForm("greeting", value)}
|
||||||
tool.status === "active" &&
|
placeholder="请输入助手开场白"
|
||||||
(form.runtimeMode === "pipeline" || tool.type !== "system"),
|
/>
|
||||||
|
<DynamicVariableEditorHint
|
||||||
|
count={Object.keys(dynamicVariableDefinitions).length}
|
||||||
|
onOpen={() => scrollToSection("variables")}
|
||||||
|
/>
|
||||||
|
{form.runtimeMode === "pipeline" && (
|
||||||
|
<div className="mt-4 space-y-3 border-t border-hairline pt-4">
|
||||||
|
<div>
|
||||||
|
<div className="mb-1.5 text-sm font-medium text-foreground">
|
||||||
|
开场白继续方式
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={openingMode}
|
||||||
|
onValueChange={(value: PromptOpeningMode) =>
|
||||||
|
setOpeningMode(value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{OPENING_MODE_OPTIONS.map((option) => (
|
||||||
|
<SelectItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="mt-1.5 text-xs leading-5 text-muted-foreground">
|
||||||
|
{openingModeDescription}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{openingMode === "confirmation" && openingMessage && (
|
||||||
|
<div className="space-y-3 rounded-xl border border-hairline bg-canvas-soft p-4">
|
||||||
|
<label className="block">
|
||||||
|
<span className="mb-1.5 block text-sm font-medium text-foreground">
|
||||||
|
弹窗标题
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
value={openingMessage.title}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateOpeningMessage({
|
||||||
|
title: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="重要提示"
|
||||||
|
className="border-hairline-strong bg-background"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<TextAreaField
|
||||||
|
label="重要信息"
|
||||||
|
value={openingMessage.message}
|
||||||
|
onChange={(message) =>
|
||||||
|
updateOpeningMessage({ message })
|
||||||
|
}
|
||||||
|
placeholder="请输入需要用户确认的重要信息"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
<label className="block">
|
||||||
|
<span className="mb-1.5 block text-sm font-medium text-foreground">
|
||||||
|
确认按钮
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
value={openingMessage.confirmLabel}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateOpeningMessage({
|
||||||
|
confirmLabel: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="确认"
|
||||||
|
className="border-hairline-strong bg-background"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p className="text-xs leading-5 text-muted-foreground">
|
||||||
|
确认前关闭语音、文字和图片输入;确认后立即停止剩余开场白并进入
|
||||||
|
Agent。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="border-t border-hairline pt-3">
|
||||||
|
<div className="mb-1.5 text-sm font-medium text-foreground">
|
||||||
|
Agent 进入行为
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={form.startup.entryMode}
|
||||||
|
onValueChange={(entryMode: PromptEntryMode) =>
|
||||||
|
updateForm("startup", {
|
||||||
|
...form.startup,
|
||||||
|
entryMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full border-hairline-strong bg-background text-foreground">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{ENTRY_MODE_OPTIONS.map((option) => (
|
||||||
|
<SelectItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="mt-1.5 text-xs leading-5 text-muted-foreground">
|
||||||
|
该设置仅控制开场阶段正常完成后的 Agent 首轮行为。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
selectedIds={form.toolIds}
|
</SectionCard>
|
||||||
onChange={(toolIds) => updateForm("toolIds", toolIds)}
|
</section>
|
||||||
/>
|
|
||||||
</SectionCard>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
<section
|
||||||
ref={(element) => {
|
ref={(element) => {
|
||||||
sectionRefs.current.interaction = element;
|
sectionRefs.current.models = element;
|
||||||
}}
|
}}
|
||||||
className="scroll-mt-3 space-y-3"
|
className="scroll-mt-3 space-y-3"
|
||||||
>
|
|
||||||
<SectionCard
|
|
||||||
icon={<Sparkles size={15} />}
|
|
||||||
title="交互策略"
|
|
||||||
description="设置实时视频对话时的交互体验"
|
|
||||||
>
|
>
|
||||||
{form.runtimeMode === "pipeline" ? (
|
<SectionCard
|
||||||
<TurnConfigEditor
|
icon={<Brain size={15} />}
|
||||||
enabled={form.enableInterrupt}
|
title="模型与语音"
|
||||||
config={form.turnConfig}
|
description={
|
||||||
onEnabledChange={(checked) =>
|
form.runtimeMode === "pipeline"
|
||||||
updateForm("enableInterrupt", checked)
|
? "选择运行方式,以及大语言模型、语音识别与语音合成资源"
|
||||||
}
|
: "选择运行方式;Realtime 模型内置语音识别与语音合成"
|
||||||
onConfigChange={(config) =>
|
}
|
||||||
updateForm("turnConfig", config)
|
>
|
||||||
|
<RuntimeModeSelector
|
||||||
|
value={form.runtimeMode}
|
||||||
|
onChange={(runtimeMode) => {
|
||||||
|
updateForm("runtimeMode", runtimeMode);
|
||||||
|
if (
|
||||||
|
runtimeMode === "realtime" &&
|
||||||
|
(form.startup.actions.length ||
|
||||||
|
form.startup.openingMode !== "interruptible" ||
|
||||||
|
form.startup.entryMode !== "wait_user" ||
|
||||||
|
form.startup.openingMessage)
|
||||||
|
) {
|
||||||
|
updateForm("startup", {
|
||||||
|
executionMode: "sequential",
|
||||||
|
openingMode: "interruptible",
|
||||||
|
entryMode: "wait_user",
|
||||||
|
actions: [],
|
||||||
|
openingMessage: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (runtimeMode === "realtime") {
|
||||||
|
updateForm(
|
||||||
|
"toolIds",
|
||||||
|
form.toolIds.filter(
|
||||||
|
(id) =>
|
||||||
|
tools.find((tool) => tool.id === id)?.type !==
|
||||||
|
"system",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.runtimeMode === "pipeline" ? (
|
||||||
|
<>
|
||||||
|
<ResourceSelectField
|
||||||
|
label="大语言模型"
|
||||||
|
value={form.model}
|
||||||
|
onChange={handlePromptModelChange}
|
||||||
|
options={llmOptions}
|
||||||
|
noneLabel="无"
|
||||||
|
/>
|
||||||
|
<ResourceSelectField
|
||||||
|
label="语音识别"
|
||||||
|
value={form.asr}
|
||||||
|
onChange={(value) => updateForm("asr", value)}
|
||||||
|
options={asrOptions}
|
||||||
|
noneLabel="无"
|
||||||
|
/>
|
||||||
|
<ResourceSelectField
|
||||||
|
label="语音合成"
|
||||||
|
value={form.voice}
|
||||||
|
onChange={(value) => updateForm("voice", value)}
|
||||||
|
options={ttsOptions}
|
||||||
|
noneLabel="无"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ResourceSelectField
|
||||||
|
label="Realtime 模型"
|
||||||
|
value={form.realtimeModel}
|
||||||
|
onChange={(value) => updateForm("realtimeModel", value)}
|
||||||
|
options={realtimeOptions}
|
||||||
|
noneLabel="无"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SectionCard>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
ref={(element) => {
|
||||||
|
sectionRefs.current.capabilities = element;
|
||||||
|
}}
|
||||||
|
className="scroll-mt-3 space-y-3"
|
||||||
|
>
|
||||||
|
{form.runtimeMode === "pipeline" && (
|
||||||
|
<VisionConfigSection
|
||||||
|
description="配置提示词助手是否可以按需理解用户摄像头画面"
|
||||||
|
hint="开启后,助手会获得读取当前视频画面的工具。选择「模型自己」时,大语言模型必须支持图片输入。"
|
||||||
|
enabled={form.visionEnabled}
|
||||||
|
modelResourceId={form.visionModelResourceId}
|
||||||
|
mainModelResourceId={form.model}
|
||||||
|
modelOptions={visionModelOptions}
|
||||||
|
onEnabledChange={handlePromptVisionEnabledChange}
|
||||||
|
onModelResourceIdChange={(value) =>
|
||||||
|
updateForm("visionModelResourceId", value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
高级抢话与轮次检测当前仅支持 Pipeline 模式。
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</SectionCard>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
{form.runtimeMode === "pipeline" && (
|
||||||
ref={(element) => {
|
<SectionCard
|
||||||
sectionRefs.current.variables = element;
|
icon={<Database size={15} />}
|
||||||
}}
|
title="知识库配置"
|
||||||
className="scroll-mt-3 space-y-3"
|
description="选择助手回答时可检索的业务知识来源"
|
||||||
>
|
>
|
||||||
<SectionCard
|
<div className="flex items-center gap-1.5">
|
||||||
icon={<Braces size={15} />}
|
<span className="text-sm font-medium text-foreground">
|
||||||
title="动态变量"
|
知识库选择
|
||||||
description="编辑提示词和开场白中引用的自定义变量"
|
</span>
|
||||||
|
<KnowledgeRetrievalConfigDialog
|
||||||
|
disabled={!form.knowledgeBase}
|
||||||
|
value={form.knowledgeRetrievalConfig}
|
||||||
|
onChange={(config) =>
|
||||||
|
updateForm("knowledgeRetrievalConfig", config)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ResourceSelectField
|
||||||
|
value={form.knowledgeBase}
|
||||||
|
onChange={(value) => updateForm("knowledgeBase", value)}
|
||||||
|
options={kbOptions}
|
||||||
|
noneLabel="无"
|
||||||
|
/>
|
||||||
|
</SectionCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SectionCard
|
||||||
|
icon={<Wrench size={15} />}
|
||||||
|
title="工具"
|
||||||
|
description="配置该提示词助手可以调用的工具"
|
||||||
|
>
|
||||||
|
<ToolPicker
|
||||||
|
tools={tools.filter(
|
||||||
|
(tool) =>
|
||||||
|
tool.status === "active" &&
|
||||||
|
(form.runtimeMode === "pipeline" ||
|
||||||
|
tool.type !== "system"),
|
||||||
|
)}
|
||||||
|
selectedIds={form.toolIds}
|
||||||
|
onChange={(toolIds) => updateForm("toolIds", toolIds)}
|
||||||
|
/>
|
||||||
|
</SectionCard>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
ref={(element) => {
|
||||||
|
sectionRefs.current.interaction = element;
|
||||||
|
}}
|
||||||
|
className="scroll-mt-3 space-y-3"
|
||||||
>
|
>
|
||||||
<DynamicVariablesSection
|
<SectionCard
|
||||||
definitions={dynamicVariableDefinitions}
|
icon={<Sparkles size={15} />}
|
||||||
onChange={(dynamicVariableDefinitions) =>
|
title="交互策略"
|
||||||
updateForm(
|
description="设置实时视频对话时的交互体验"
|
||||||
"dynamicVariableDefinitions",
|
>
|
||||||
dynamicVariableDefinitions,
|
{form.runtimeMode === "pipeline" ? (
|
||||||
)
|
<TurnConfigEditor
|
||||||
}
|
enabled={form.enableInterrupt}
|
||||||
/>
|
config={form.turnConfig}
|
||||||
</SectionCard>
|
onEnabledChange={(checked) =>
|
||||||
</section>
|
updateForm("enableInterrupt", checked)
|
||||||
|
}
|
||||||
|
onConfigChange={(config) =>
|
||||||
|
updateForm("turnConfig", config)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
高级抢话与轮次检测当前仅支持 Pipeline 模式。
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</SectionCard>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
ref={(element) => {
|
||||||
|
sectionRefs.current.variables = element;
|
||||||
|
}}
|
||||||
|
className="scroll-mt-3 space-y-3"
|
||||||
|
>
|
||||||
|
<SectionCard
|
||||||
|
icon={<Braces size={15} />}
|
||||||
|
title="动态变量"
|
||||||
|
description="编辑提示词和开场白中引用的自定义变量"
|
||||||
|
>
|
||||||
|
<DynamicVariablesSection
|
||||||
|
definitions={dynamicVariableDefinitions}
|
||||||
|
onChange={(dynamicVariableDefinitions) =>
|
||||||
|
updateForm(
|
||||||
|
"dynamicVariableDefinitions",
|
||||||
|
dynamicVariableDefinitions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SectionCard>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Check, Laptop, Moon, Sun } from "lucide-react";
|
import { Laptop, Moon, Sun } from "lucide-react";
|
||||||
|
|
||||||
export type ThemeMode = "light" | "dark" | "system";
|
export type ThemeMode = "light" | "dark" | "system";
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ export function ThemeModeSelector() {
|
|||||||
<div
|
<div
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
aria-label="界面主题"
|
aria-label="界面主题"
|
||||||
className="space-y-1"
|
className="grid grid-cols-3 gap-1"
|
||||||
>
|
>
|
||||||
{themeOptions.map((option) => {
|
{themeOptions.map((option) => {
|
||||||
const Icon = option.icon;
|
const Icon = option.icon;
|
||||||
@@ -72,17 +72,17 @@ export function ThemeModeSelector() {
|
|||||||
type="button"
|
type="button"
|
||||||
role="radio"
|
role="radio"
|
||||||
aria-checked={selected}
|
aria-checked={selected}
|
||||||
|
aria-label={option.label}
|
||||||
|
title={option.label}
|
||||||
onClick={() => selectMode(option.value)}
|
onClick={() => selectMode(option.value)}
|
||||||
className={[
|
className={[
|
||||||
"flex h-10 w-full items-center gap-2.5 rounded-xl px-3 text-sm font-normal transition-colors",
|
"flex h-9 items-center justify-center rounded-xl transition-colors",
|
||||||
selected
|
selected
|
||||||
? "bg-surface-strong text-foreground shadow-sm"
|
? "bg-surface-strong text-foreground shadow-sm"
|
||||||
: "text-muted-foreground hover:bg-surface-strong/60 hover:text-foreground",
|
: "text-muted-foreground hover:bg-surface-strong/60 hover:text-foreground",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<Icon size={16} />
|
<Icon size={16} />
|
||||||
<span className="min-w-0 flex-1 text-left">{option.label}</span>
|
|
||||||
{selected && <Check size={15} className="shrink-0" />}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user