feat: add prompt startup actions and shared vision config
This commit is contained in:
@@ -29,8 +29,10 @@ import {
|
||||
} from "@/components/assistant-editor/editor-controls";
|
||||
import type { AssistantForm } from "@/components/assistant-editor/types";
|
||||
import { SectionCard } from "@/components/editor/section-card";
|
||||
import { VisionConfigSection } from "@/components/editor/vision-config-section";
|
||||
import { TurnConfigEditor } from "@/components/turn-config-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { DynamicVariableDefinition, Tool } from "@/lib/api";
|
||||
|
||||
type ResourceOption = { value: string; label: string };
|
||||
@@ -83,6 +85,81 @@ export function PromptEditor({
|
||||
handlePromptVisionEnabledChange,
|
||||
handlePromptModelChange,
|
||||
}: PromptEditorProps) {
|
||||
const openingMessage = form.startup.actions.find(
|
||||
(action) => action.id === "opening_message" && action.phase === "opening",
|
||||
);
|
||||
const openingArguments = openingMessage?.arguments ?? {};
|
||||
const openingButtons = Array.isArray(openingArguments.actions)
|
||||
? openingArguments.actions
|
||||
: [];
|
||||
const openingButton =
|
||||
openingButtons[0] && typeof openingButtons[0] === "object"
|
||||
? (openingButtons[0] as Record<string, unknown>)
|
||||
: {};
|
||||
const showMessageTools = tools
|
||||
.filter(
|
||||
(tool) =>
|
||||
tool.status === "active" &&
|
||||
tool.type === "client" &&
|
||||
tool.functionName === "show_message" &&
|
||||
tool.definition.type === "client" &&
|
||||
tool.definition.config.waitForResponse &&
|
||||
tool.definition.config.responseWaitMode === "session",
|
||||
)
|
||||
.map((tool) => ({ value: tool.id, label: tool.name }));
|
||||
|
||||
function setOpeningMessage(enabled: boolean) {
|
||||
const otherActions = form.startup.actions.filter(
|
||||
(action) => action.id !== "opening_message",
|
||||
);
|
||||
const defaultToolId = showMessageTools[0]?.value ?? "";
|
||||
updateForm("startup", {
|
||||
executionMode: "sequential",
|
||||
actions: enabled
|
||||
? [
|
||||
...otherActions,
|
||||
{
|
||||
id: "opening_message",
|
||||
phase: "opening",
|
||||
toolId: defaultToolId,
|
||||
required: true,
|
||||
arguments: {
|
||||
title: "重要提示",
|
||||
message: "请确认已阅读以上信息。",
|
||||
actions: [
|
||||
{ id: "confirmed", label: "确认", style: "primary" },
|
||||
],
|
||||
dismissible: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
: otherActions,
|
||||
});
|
||||
if (
|
||||
enabled &&
|
||||
defaultToolId &&
|
||||
!form.toolIds.includes(defaultToolId)
|
||||
) {
|
||||
updateForm("toolIds", [...form.toolIds, defaultToolId]);
|
||||
}
|
||||
}
|
||||
|
||||
function updateOpeningMessage(
|
||||
patch: Partial<NonNullable<typeof openingMessage>>,
|
||||
) {
|
||||
if (!openingMessage) return;
|
||||
updateForm("startup", {
|
||||
...form.startup,
|
||||
actions: form.startup.actions.map((action) =>
|
||||
action.id === openingMessage.id ? { ...action, ...patch } : action,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function updateOpeningArguments(patch: Record<string, unknown>) {
|
||||
updateOpeningMessage({ arguments: { ...openingArguments, ...patch } });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="-mt-6 flex h-full flex-col gap-4">
|
||||
<div className="flex shrink-0 items-center justify-between gap-6 border-b border-hairline pb-3 pt-1">
|
||||
@@ -161,11 +238,87 @@ export function PromptEditor({
|
||||
count={Object.keys(dynamicVariableDefinitions).length}
|
||||
onOpen={() => setDynamicVariablesOpen(true)}
|
||||
/>
|
||||
{form.runtimeMode === "pipeline" && (
|
||||
<div className="mt-4 space-y-3 border-t border-hairline pt-4">
|
||||
<ToggleRow
|
||||
title="开场确认弹窗"
|
||||
description="与开场白同时显示,确认且播报完成后才允许用户开始对话。"
|
||||
checked={Boolean(openingMessage)}
|
||||
onChange={setOpeningMessage}
|
||||
/>
|
||||
|
||||
{openingMessage && (
|
||||
<div className="space-y-3 rounded-xl border border-hairline bg-canvas-soft p-4">
|
||||
<ResourceSelectField
|
||||
label="消息工具"
|
||||
value={openingMessage.toolId}
|
||||
options={showMessageTools}
|
||||
noneLabel="请选择 show_message 工具"
|
||||
onChange={(toolId) => {
|
||||
updateOpeningMessage({ toolId });
|
||||
if (toolId && !form.toolIds.includes(toolId)) {
|
||||
updateForm("toolIds", [...form.toolIds, toolId]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{showMessageTools.length === 0 && (
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
请先在组件管理中创建 functionName 为 show_message、会话内等待的 Client Tool。
|
||||
</p>
|
||||
)}
|
||||
<label className="block">
|
||||
<span className="mb-1.5 block text-sm font-medium text-foreground">
|
||||
弹窗标题
|
||||
</span>
|
||||
<Input
|
||||
value={String(openingArguments.title ?? "")}
|
||||
onChange={(event) =>
|
||||
updateOpeningArguments({ title: event.target.value })
|
||||
}
|
||||
placeholder="重要提示"
|
||||
className="border-hairline-strong bg-background"
|
||||
/>
|
||||
</label>
|
||||
<TextAreaField
|
||||
label="重要信息"
|
||||
value={String(openingArguments.message ?? "")}
|
||||
onChange={(message) => updateOpeningArguments({ message })}
|
||||
placeholder="请输入需要用户确认的重要信息"
|
||||
rows={4}
|
||||
/>
|
||||
<label className="block">
|
||||
<span className="mb-1.5 block text-sm font-medium text-foreground">
|
||||
确认按钮
|
||||
</span>
|
||||
<Input
|
||||
value={String(openingButton.label ?? "")}
|
||||
onChange={(event) =>
|
||||
updateOpeningArguments({
|
||||
actions: [
|
||||
{
|
||||
id: "confirmed",
|
||||
label: event.target.value,
|
||||
style: "primary",
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
placeholder="确认"
|
||||
className="border-hairline-strong bg-background"
|
||||
/>
|
||||
</label>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
弹窗不可通过关闭按钮、Esc 或点击外部跳过;失败时将结束本次通话。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<Brain size={15} />}
|
||||
title="模型配置"
|
||||
title="模型与语音"
|
||||
description={
|
||||
form.runtimeMode === "pipeline"
|
||||
? "选择运行方式,以及大语言模型、语音识别与语音合成资源"
|
||||
@@ -174,49 +327,40 @@ export function PromptEditor({
|
||||
>
|
||||
<RuntimeModeSelector
|
||||
value={form.runtimeMode}
|
||||
onChange={(runtimeMode) => updateForm("runtimeMode", runtimeMode)}
|
||||
onChange={(runtimeMode) => {
|
||||
updateForm("runtimeMode", runtimeMode);
|
||||
if (runtimeMode === "realtime" && form.startup.actions.length) {
|
||||
updateForm("startup", {
|
||||
executionMode: "sequential",
|
||||
actions: [],
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{form.runtimeMode === "pipeline" ? (
|
||||
<>
|
||||
<ToggleRow
|
||||
title="视觉理解"
|
||||
hint="开启后,开始对话时会允许助手按需理解当前视频画面。视觉模型选「模型自己」时,大语言模型本身必须支持图片输入。"
|
||||
checked={form.visionEnabled}
|
||||
onChange={handlePromptVisionEnabledChange}
|
||||
/>
|
||||
{form.visionEnabled && (
|
||||
<ResourceSelectField
|
||||
label="视觉模型"
|
||||
value={form.visionModelResourceId}
|
||||
onChange={(value) =>
|
||||
updateForm("visionModelResourceId", value)
|
||||
}
|
||||
options={visionModelOptions}
|
||||
noneLabel="模型自己"
|
||||
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="大语言模型"
|
||||
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
|
||||
@@ -229,6 +373,21 @@ export function PromptEditor({
|
||||
)}
|
||||
</SectionCard>
|
||||
|
||||
{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
|
||||
icon={<Database size={15} />}
|
||||
@@ -299,4 +458,3 @@ export function PromptEditor({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user