feat(frontend): embed prompt dynamic variables section

This commit is contained in:
Xin Wang
2026-08-03 10:46:56 +08:00
parent b0f82142a5
commit cff591aa13
4 changed files with 60 additions and 72 deletions

View File

@@ -1,16 +1,9 @@
"use client"; "use client";
import { Braces, Check, Copy } from "lucide-react"; import { Check, Copy } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
Select, Select,
@@ -141,40 +134,6 @@ export function DynamicVariableEditorHint({
); );
} }
export function DynamicVariablesDialog({
open,
onOpenChange,
definitions,
onChange,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
definitions: Record<string, DynamicVariableDefinition>;
onChange: (definitions: Record<string, DynamicVariableDefinition>) => void;
}) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[78vh] overflow-hidden rounded-2xl border-hairline bg-card p-0 sm:max-w-[520px]">
<DialogHeader className="border-b border-hairline px-6 py-5 text-left">
<DialogTitle className="flex items-center gap-2 text-base font-medium">
<Braces size={17} />
</DialogTitle>
<DialogDescription className="text-xs leading-5">
</DialogDescription>
</DialogHeader>
<DynamicVariablesEditor
definitions={definitions}
onChange={onChange}
layout="dialog"
/>
</DialogContent>
</Dialog>
);
}
export function DynamicVariablesPanel({ export function DynamicVariablesPanel({
definitions, definitions,
onChange, onChange,
@@ -196,6 +155,27 @@ export function DynamicVariablesPanel({
); );
} }
export function DynamicVariablesSection({
definitions,
onChange,
}: {
definitions: Record<string, DynamicVariableDefinition>;
onChange: (definitions: Record<string, DynamicVariableDefinition>) => void;
}) {
return (
<div className="space-y-3">
<p className="text-xs leading-5 text-muted-foreground">
</p>
<DynamicVariablesEditor
definitions={definitions}
onChange={onChange}
layout="section"
/>
</div>
);
}
function DynamicVariablesEditor({ function DynamicVariablesEditor({
definitions, definitions,
onChange, onChange,
@@ -203,7 +183,7 @@ function DynamicVariablesEditor({
}: { }: {
definitions: Record<string, DynamicVariableDefinition>; definitions: Record<string, DynamicVariableDefinition>;
onChange: (definitions: Record<string, DynamicVariableDefinition>) => void; onChange: (definitions: Record<string, DynamicVariableDefinition>) => void;
layout: "dialog" | "panel"; layout: "panel" | "section";
}) { }) {
const entries = Object.entries(definitions); const entries = Object.entries(definitions);
const [copiedSystemVariable, setCopiedSystemVariable] = useState<string | null>( const [copiedSystemVariable, setCopiedSystemVariable] = useState<string | null>(
@@ -226,7 +206,7 @@ function DynamicVariablesEditor({
className={ className={
layout === "panel" layout === "panel"
? "flex min-h-0 flex-1 flex-col" ? "flex min-h-0 flex-1 flex-col"
: "min-h-0 px-6 pb-6" : "min-h-0 flex-none"
} }
> >
<TabsList className="grid w-full shrink-0 grid-cols-2 bg-surface-strong"> <TabsList className="grid w-full shrink-0 grid-cols-2 bg-surface-strong">
@@ -249,7 +229,7 @@ function DynamicVariablesEditor({
className={ className={
layout === "panel" layout === "panel"
? "scrollbar-subtle min-h-0 flex-1 overflow-y-auto pt-3" ? "scrollbar-subtle min-h-0 flex-1 overflow-y-auto pt-3"
: "max-h-[calc(78vh-168px)] overflow-y-auto pt-3" : "flex-none pt-3"
} }
> >
{entries.length === 0 ? ( {entries.length === 0 ? (
@@ -371,7 +351,7 @@ function DynamicVariablesEditor({
className={ className={
layout === "panel" layout === "panel"
? "scrollbar-subtle min-h-0 flex-1 space-y-3 overflow-y-auto pt-3" ? "scrollbar-subtle min-h-0 flex-1 space-y-3 overflow-y-auto pt-3"
: "max-h-[calc(78vh-168px)] space-y-3 overflow-y-auto pt-3" : "flex-none space-y-3 pt-3"
} }
> >
<p className="text-[11px] leading-5 text-muted-foreground"> <p className="text-[11px] leading-5 text-muted-foreground">

View File

@@ -2,6 +2,7 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { import {
Braces,
Bot, Bot,
Brain, Brain,
Database, Database,
@@ -15,7 +16,7 @@ import {
import { DebugDrawer } from "@/components/assistant-editor/debug-preview"; import { DebugDrawer } from "@/components/assistant-editor/debug-preview";
import { import {
DynamicVariableEditorHint, DynamicVariableEditorHint,
DynamicVariablesDialog, DynamicVariablesSection,
} from "@/components/assistant-editor/dynamic-variables"; } from "@/components/assistant-editor/dynamic-variables";
import { import {
AssistantIdentity, AssistantIdentity,
@@ -43,6 +44,7 @@ const promptSections = [
{ id: "models", label: "模型与语音" }, { id: "models", label: "模型与语音" },
{ id: "capabilities", label: "知识与工具" }, { id: "capabilities", label: "知识与工具" },
{ id: "interaction", label: "交互策略" }, { id: "interaction", label: "交互策略" },
{ id: "variables", label: "动态变量" },
] as const; ] as const;
type PromptSectionId = (typeof promptSections)[number]["id"]; type PromptSectionId = (typeof promptSections)[number]["id"];
@@ -50,7 +52,6 @@ type PromptSectionId = (typeof promptSections)[number]["id"];
type PromptEditorProps = { type PromptEditorProps = {
assistantId: string | null; assistantId: string | null;
form: AssistantForm; form: AssistantForm;
dynamicVariablesOpen: boolean;
dynamicVariableDefinitions: Record<string, DynamicVariableDefinition>; dynamicVariableDefinitions: Record<string, DynamicVariableDefinition>;
saving: boolean; saving: boolean;
dirty: boolean; dirty: boolean;
@@ -64,7 +65,6 @@ type PromptEditorProps = {
tools: Tool[]; tools: Tool[];
onBack: () => void; onBack: () => void;
onSave: () => void; onSave: () => void;
setDynamicVariablesOpen: (open: boolean) => void;
updateForm: <K extends keyof AssistantForm>( updateForm: <K extends keyof AssistantForm>(
key: K, key: K,
value: AssistantForm[K], value: AssistantForm[K],
@@ -76,7 +76,6 @@ type PromptEditorProps = {
export function PromptEditor({ export function PromptEditor({
assistantId, assistantId,
form, form,
dynamicVariablesOpen,
dynamicVariableDefinitions, dynamicVariableDefinitions,
saving, saving,
dirty, dirty,
@@ -90,7 +89,6 @@ export function PromptEditor({
tools, tools,
onBack, onBack,
onSave, onSave,
setDynamicVariablesOpen,
updateForm, updateForm,
handlePromptVisionEnabledChange, handlePromptVisionEnabledChange,
handlePromptModelChange, handlePromptModelChange,
@@ -102,6 +100,7 @@ export function PromptEditor({
models: null, models: null,
capabilities: null, capabilities: null,
interaction: null, interaction: null,
variables: null,
}); });
const selectedAnchorRef = useRef<PromptSectionId | null>(null); const selectedAnchorRef = useRef<PromptSectionId | null>(null);
const [activeSection, setActiveSection] = const [activeSection, setActiveSection] =
@@ -217,7 +216,7 @@ export function PromptEditor({
} }
return ( return (
<div className="-mt-6 flex h-full flex-col gap-4"> <div className="-mt-6 flex h-full flex-col gap-4 overflow-hidden bg-background">
<div className="flex shrink-0 items-center justify-between gap-6 border-b border-hairline pb-3 pt-1"> <div className="flex shrink-0 items-center justify-between gap-6 border-b border-hairline pb-3 pt-1">
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<EditorBackButton onClick={onBack} /> <EditorBackButton onClick={onBack} />
@@ -249,25 +248,13 @@ export function PromptEditor({
</div> </div>
</div> </div>
<DynamicVariablesDialog
open={dynamicVariablesOpen}
onOpenChange={setDynamicVariablesOpen}
definitions={dynamicVariableDefinitions}
onChange={(dynamicVariableDefinitions) =>
updateForm(
"dynamicVariableDefinitions",
dynamicVariableDefinitions,
)
}
/>
<div className="flex min-h-0 flex-1 gap-4"> <div className="flex min-h-0 flex-1 gap-4">
<div className="flex min-h-0 min-w-0 flex-1 flex-col gap-3"> <div className="flex min-h-0 min-w-0 flex-1 flex-col gap-3">
<nav <nav
aria-label="提示词配置分区" aria-label="提示词配置分区"
className="shrink-0 overflow-x-auto" className="shrink-0 overflow-x-auto overscroll-none"
> >
<div className="grid min-w-[22rem] grid-cols-4 border-b border-hairline"> <div className="grid min-w-[28rem] grid-cols-5 border-b border-hairline">
{promptSections.map((section) => { {promptSections.map((section) => {
const active = activeSection === section.id; const active = activeSection === section.id;
@@ -293,7 +280,7 @@ export function PromptEditor({
<div <div
ref={scrollContainerRef} ref={scrollContainerRef}
className="scrollbar-subtle min-h-0 min-w-0 flex-1 space-y-3 overflow-y-auto pr-1" className="scrollbar-subtle min-h-0 min-w-0 flex-1 space-y-3 overflow-y-auto overscroll-none bg-background pr-1"
> >
<section <section
ref={(element) => { ref={(element) => {
@@ -314,7 +301,7 @@ export function PromptEditor({
/> />
<DynamicVariableEditorHint <DynamicVariableEditorHint
count={Object.keys(dynamicVariableDefinitions).length} count={Object.keys(dynamicVariableDefinitions).length}
onOpen={() => setDynamicVariablesOpen(true)} onOpen={() => scrollToSection("variables")}
/> />
</SectionCard> </SectionCard>
@@ -330,7 +317,7 @@ export function PromptEditor({
/> />
<DynamicVariableEditorHint <DynamicVariableEditorHint
count={Object.keys(dynamicVariableDefinitions).length} count={Object.keys(dynamicVariableDefinitions).length}
onOpen={() => setDynamicVariablesOpen(true)} onOpen={() => scrollToSection("variables")}
/> />
{form.runtimeMode === "pipeline" && ( {form.runtimeMode === "pipeline" && (
<div className="mt-4 space-y-3 border-t border-hairline pt-4"> <div className="mt-4 space-y-3 border-t border-hairline pt-4">
@@ -550,6 +537,29 @@ export function PromptEditor({
)} )}
</SectionCard> </SectionCard>
</section> </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>

View File

@@ -8,7 +8,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
return ( return (
<div className="flex h-screen overflow-hidden bg-background text-foreground"> <div className="fixed inset-0 flex overflow-hidden bg-background text-foreground">
<Sidebar collapsed={collapsed} onToggle={() => setCollapsed((v) => !v)} /> <Sidebar collapsed={collapsed} onToggle={() => setCollapsed((v) => !v)} />
<main className="flex min-w-0 flex-1 flex-col overflow-hidden"> <main className="flex min-w-0 flex-1 flex-col overflow-hidden">

View File

@@ -1565,7 +1565,6 @@ export function AssistantPage(props: AssistantPageProps) {
<PromptEditor <PromptEditor
assistantId={editingId} assistantId={editingId}
form={form} form={form}
dynamicVariablesOpen={dynamicVariablesOpen}
dynamicVariableDefinitions={effectiveDynamicVariableDefinitions} dynamicVariableDefinitions={effectiveDynamicVariableDefinitions}
saving={saving} saving={saving}
dirty={dirty} dirty={dirty}
@@ -1579,7 +1578,6 @@ export function AssistantPage(props: AssistantPageProps) {
tools={tools} tools={tools}
onBack={() => router.push("/assistants")} onBack={() => router.push("/assistants")}
onSave={() => void handleSavePrompt()} onSave={() => void handleSavePrompt()}
setDynamicVariablesOpen={setDynamicVariablesOpen}
updateForm={updateForm} updateForm={updateForm}
handlePromptVisionEnabledChange={handlePromptVisionEnabledChange} handlePromptVisionEnabledChange={handlePromptVisionEnabledChange}
handlePromptModelChange={handlePromptModelChange} handlePromptModelChange={handlePromptModelChange}