Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server
This commit is contained in:
commit
6cc28c7edd
15 files changed
+1373
-998
No files matched your search
@@ -2,10 +2,7 @@
|
||||
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
BotIcon,
|
||||
CheckCircleIcon,
|
||||
InfoIcon,
|
||||
MoreHorizontalIcon,
|
||||
SaveIcon,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -18,17 +15,14 @@ import {
|
||||
PromptInputSubmit,
|
||||
PromptInputTextarea,
|
||||
} from "@/components/ai-elements/prompt-input";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ArtifactsProvider } from "@/components/workspace/artifacts";
|
||||
import { MessageList } from "@/components/workspace/messages";
|
||||
import {
|
||||
MessageList,
|
||||
type SetupAgentApprovalPayload,
|
||||
} from "@/components/workspace/messages";
|
||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||
import { getAPIClient } from "@/core/api";
|
||||
import type { Agent } from "@/core/agents";
|
||||
@@ -37,19 +31,15 @@ import {
|
||||
checkAgentName,
|
||||
createAgent,
|
||||
getAgent,
|
||||
updateAgent,
|
||||
} from "@/core/agents/api";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
import type { Skill } from "@/core/skills";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
import { useThreadStream } from "@/core/threads/hooks";
|
||||
import { uuid } from "@/core/utils/uuid";
|
||||
import { isIMEComposing } from "@/lib/ime";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Step = "name" | "chat";
|
||||
type SetupAgentStatus = "idle" | "requested" | "completed";
|
||||
|
||||
const SAVE_HINT_STORAGE_KEY = "deerflow.agent-create.save-hint-seen";
|
||||
const AGENT_READ_RETRY_DELAYS_MS = [200, 500, 1_000, 2_000];
|
||||
const DEFAULT_SELECTED_SKILL_COUNT = 3;
|
||||
|
||||
@@ -62,14 +52,12 @@ async function getAgentWithRetry(agentId: string) {
|
||||
if (delay > 0) {
|
||||
await wait(delay);
|
||||
}
|
||||
|
||||
try {
|
||||
return await getAgent(agentId);
|
||||
} catch {
|
||||
// Retry until the write settles or the attempts are exhausted.
|
||||
// Retry until the file/database write settles.
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -90,60 +78,56 @@ function getCreateAgentErrorMessage(
|
||||
export default function NewAgentPage() {
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const threadId = useMemo(() => uuid(), []);
|
||||
|
||||
const [step, setStep] = useState<Step>("name");
|
||||
const [nameInput, setNameInput] = useState("");
|
||||
const [nameError, setNameError] = useState("");
|
||||
const [isCheckingName, setIsCheckingName] = useState(false);
|
||||
const [isCreatingAgent, setIsCreatingAgent] = useState(false);
|
||||
const [descriptionInput, setDescriptionInput] = useState("");
|
||||
const [soulInput, setSoulInput] = useState("");
|
||||
const [formError, setFormError] = useState("");
|
||||
const [agentId, setAgentId] = useState("");
|
||||
const [agent, setAgent] = useState<Agent | null>(null);
|
||||
const [showSaveHint, setShowSaveHint] = useState(false);
|
||||
const [setupAgentStatus, setSetupAgentStatus] =
|
||||
useState<SetupAgentStatus>("idle");
|
||||
const [isCreatingAgent, setIsCreatingAgent] = useState(false);
|
||||
const [isSavingDraft, setIsSavingDraft] = useState(false);
|
||||
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
|
||||
const [isSkillSelectionTouched, setIsSkillSelectionTouched] = useState(false);
|
||||
const [threadPrepared, setThreadPrepared] = useState(false);
|
||||
const { skills, isLoading: isSkillsLoading } = useSkills();
|
||||
|
||||
const enabledSkills = useMemo(
|
||||
() => skills.filter((skill) => skill.enabled),
|
||||
[skills],
|
||||
);
|
||||
|
||||
const threadId = useMemo(() => uuid(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSkillSelectionTouched) {
|
||||
return;
|
||||
}
|
||||
const defaults = enabledSkills
|
||||
.slice(0, DEFAULT_SELECTED_SKILL_COUNT)
|
||||
.map((skill) => skill.name);
|
||||
setSelectedSkills(defaults);
|
||||
setSelectedSkills(
|
||||
enabledSkills
|
||||
.slice(0, DEFAULT_SELECTED_SKILL_COUNT)
|
||||
.map((skill) => skill.name),
|
||||
);
|
||||
}, [enabledSkills, isSkillSelectionTouched]);
|
||||
|
||||
const [thread, sendMessage] = useThreadStream({
|
||||
threadId: step === "chat" ? threadId : undefined,
|
||||
threadId,
|
||||
assistantId: agentId || undefined,
|
||||
context: {
|
||||
mode: "flash",
|
||||
is_bootstrap: true,
|
||||
},
|
||||
onFinish() {
|
||||
if (!agent && setupAgentStatus === "requested") {
|
||||
setSetupAgentStatus("idle");
|
||||
}
|
||||
},
|
||||
onToolEnd({ name }) {
|
||||
if (name !== "setup_agent" || !agentId) return;
|
||||
setSetupAgentStatus("completed");
|
||||
if (name !== "setup_agent" || !agentId) {
|
||||
return;
|
||||
}
|
||||
void getAgentWithRetry(agentId).then((fetched) => {
|
||||
if (fetched) {
|
||||
setAgent(fetched);
|
||||
return;
|
||||
setDescriptionInput(fetched.description ?? "");
|
||||
setSoulInput(fetched.soul ?? "");
|
||||
setSelectedSkills(fetched.skills ?? []);
|
||||
toast.success("专家配置已创建");
|
||||
}
|
||||
|
||||
toast.error(t.agents.agentCreatedPendingRefresh);
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -152,362 +136,339 @@ export default function NewAgentPage() {
|
||||
if (threadPrepared) {
|
||||
return;
|
||||
}
|
||||
await getAPIClient().threads.create({
|
||||
threadId,
|
||||
});
|
||||
await getAPIClient().threads.create({ threadId });
|
||||
setThreadPrepared(true);
|
||||
}, [threadPrepared, threadId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined" || step !== "chat") {
|
||||
return;
|
||||
}
|
||||
if (window.localStorage.getItem(SAVE_HINT_STORAGE_KEY) === "1") {
|
||||
return;
|
||||
}
|
||||
setShowSaveHint(true);
|
||||
window.localStorage.setItem(SAVE_HINT_STORAGE_KEY, "1");
|
||||
}, [step]);
|
||||
|
||||
const handleConfirmName = useCallback(async () => {
|
||||
const trimmed = nameInput.trim();
|
||||
if (!trimmed) {
|
||||
setNameError(t.agents.nameStepInvalidError);
|
||||
return;
|
||||
}
|
||||
}, [threadId, threadPrepared]);
|
||||
|
||||
const validateForm = useCallback(() => {
|
||||
// Name is intentionally NOT required here so the user can start chatting
|
||||
// immediately. If they leave it blank, ensureAgent() generates a placeholder
|
||||
// and writes it back into the input so they can rename it later.
|
||||
if (enabledSkills.length > 0 && selectedSkills.length === 0) {
|
||||
setNameError("请至少选择一个技能");
|
||||
return;
|
||||
setFormError("请至少选择一个技能");
|
||||
return null;
|
||||
}
|
||||
setFormError("");
|
||||
return nameInput.trim();
|
||||
}, [enabledSkills.length, nameInput, selectedSkills.length]);
|
||||
|
||||
const ensureAgent = useCallback(async () => {
|
||||
const provided = validateForm();
|
||||
if (provided === null) {
|
||||
return null;
|
||||
}
|
||||
let name = provided;
|
||||
if (!name) {
|
||||
// Names are display-only and may collide, so a timestamped placeholder is
|
||||
// safe. Reflect it in the form so the user sees what got created.
|
||||
name = `未命名专家-${Date.now().toString(36)}`;
|
||||
setNameInput(name);
|
||||
}
|
||||
|
||||
setNameError("");
|
||||
setIsCheckingName(true);
|
||||
try {
|
||||
await checkAgentName(trimmed);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof AgentNameCheckError &&
|
||||
err.reason === "backend_unreachable"
|
||||
) {
|
||||
setNameError(t.agents.nameStepNetworkError);
|
||||
} else {
|
||||
setNameError(t.agents.nameStepCheckError);
|
||||
if (agentId) {
|
||||
setIsSavingDraft(true);
|
||||
try {
|
||||
const updated = await updateAgent(agentId, {
|
||||
name,
|
||||
description: descriptionInput,
|
||||
soul: soulInput,
|
||||
skills: selectedSkills,
|
||||
});
|
||||
setAgent(updated);
|
||||
return updated;
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : String(error));
|
||||
return null;
|
||||
} finally {
|
||||
setIsSavingDraft(false);
|
||||
}
|
||||
return;
|
||||
} finally {
|
||||
setIsCheckingName(false);
|
||||
}
|
||||
|
||||
let createdId: string;
|
||||
let createdName: string;
|
||||
setIsCreatingAgent(true);
|
||||
try {
|
||||
await checkAgentName(name);
|
||||
const created = await createAgent({
|
||||
name: trimmed,
|
||||
description: "",
|
||||
name,
|
||||
description: descriptionInput,
|
||||
soul: soulInput,
|
||||
skills: selectedSkills,
|
||||
soul: "",
|
||||
});
|
||||
createdId = created.id;
|
||||
createdName = created.name;
|
||||
} catch (err) {
|
||||
setNameError(
|
||||
getCreateAgentErrorMessage(
|
||||
err,
|
||||
t.agents.nameStepNetworkError,
|
||||
t.agents.nameStepCheckError,
|
||||
),
|
||||
);
|
||||
return;
|
||||
setAgentId(created.id);
|
||||
setAgent(created);
|
||||
return created;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof AgentNameCheckError &&
|
||||
error.reason === "backend_unreachable"
|
||||
) {
|
||||
setFormError(t.agents.nameStepNetworkError);
|
||||
} else {
|
||||
setFormError(
|
||||
getCreateAgentErrorMessage(
|
||||
error,
|
||||
t.agents.nameStepNetworkError,
|
||||
t.agents.nameStepCheckError,
|
||||
),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
setIsCreatingAgent(false);
|
||||
}
|
||||
|
||||
setAgentId(createdId);
|
||||
setStep("chat");
|
||||
await ensureThreadExists();
|
||||
await sendMessage(threadId, {
|
||||
text: t.agents.nameStepBootstrapMessage.replace("{name}", createdName),
|
||||
files: [],
|
||||
});
|
||||
}, [
|
||||
nameInput,
|
||||
sendMessage,
|
||||
t.agents.nameStepNetworkError,
|
||||
t.agents.nameStepBootstrapMessage,
|
||||
t.agents.nameStepCheckError,
|
||||
t.agents.nameStepInvalidError,
|
||||
threadId,
|
||||
enabledSkills.length,
|
||||
agentId,
|
||||
descriptionInput,
|
||||
selectedSkills,
|
||||
ensureThreadExists,
|
||||
soulInput,
|
||||
t.agents.nameStepCheckError,
|
||||
t.agents.nameStepNetworkError,
|
||||
validateForm,
|
||||
]);
|
||||
|
||||
const toggleSkill = useCallback((skill: Skill) => {
|
||||
setIsSkillSelectionTouched(true);
|
||||
setNameError("");
|
||||
setSelectedSkills((prev) => {
|
||||
if (prev.includes(skill.name)) {
|
||||
return prev.filter((name) => name !== skill.name);
|
||||
}
|
||||
return [...prev, skill.name];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleNameKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" && !isIMEComposing(e)) {
|
||||
e.preventDefault();
|
||||
void handleConfirmName();
|
||||
const handleSaveAgent = useCallback(async () => {
|
||||
const saved = await ensureAgent();
|
||||
if (!saved) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
toast.success(`专家「${saved.name}」已保存`);
|
||||
}, [ensureAgent]);
|
||||
|
||||
const handleChatSubmit = useCallback(
|
||||
async (text: string) => {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed || thread.isLoading) return;
|
||||
if (!trimmed || thread.isLoading) {
|
||||
return;
|
||||
}
|
||||
// Lazily create the agent on first message so the user can just start
|
||||
// typing — ensureAgent generates a placeholder name when none is provided.
|
||||
let activeAgentId = agentId;
|
||||
if (!activeAgentId) {
|
||||
const created = await ensureAgent();
|
||||
if (!created) {
|
||||
return;
|
||||
}
|
||||
activeAgentId = created.id;
|
||||
}
|
||||
await ensureThreadExists();
|
||||
await sendMessage(
|
||||
threadId,
|
||||
{ text: trimmed, files: [] },
|
||||
{ agent_id: agentId },
|
||||
{ agent_id: activeAgentId },
|
||||
);
|
||||
},
|
||||
[agentId, sendMessage, thread.isLoading, threadId, ensureThreadExists],
|
||||
[agentId, ensureAgent, ensureThreadExists, sendMessage, thread.isLoading, threadId],
|
||||
);
|
||||
|
||||
const handleSaveAgent = useCallback(async () => {
|
||||
if (
|
||||
!agentId ||
|
||||
agent ||
|
||||
thread.isLoading ||
|
||||
setupAgentStatus !== "idle"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const handleApproveSetupAgent = useCallback(
|
||||
async (payload: SetupAgentApprovalPayload) => {
|
||||
if (!agentId) {
|
||||
return;
|
||||
}
|
||||
// Approval persists directly to the backend — no LLM round-trip. The
|
||||
// backend's PUT /api/agents/{id} writes both the DB record and SOUL.md /
|
||||
// config.yaml on disk, which is exactly what setup_agent_tool would have
|
||||
// done if we'd let the model re-invoke it.
|
||||
const proposedName = payload.name?.trim() ?? "";
|
||||
const newDescription = payload.description ?? "";
|
||||
const newSoul = payload.soul ?? "";
|
||||
const newSkills = payload.skills ?? [];
|
||||
|
||||
setSetupAgentStatus("requested");
|
||||
setShowSaveHint(false);
|
||||
try {
|
||||
await sendMessage(
|
||||
threadId,
|
||||
{ text: t.agents.saveCommandMessage, files: [] },
|
||||
{ agent_id: agentId },
|
||||
{ additionalKwargs: { hide_from_ui: true } },
|
||||
);
|
||||
toast.success(t.agents.saveRequested);
|
||||
} catch (error) {
|
||||
setSetupAgentStatus("idle");
|
||||
toast.error(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}, [
|
||||
agent,
|
||||
agentId,
|
||||
sendMessage,
|
||||
setupAgentStatus,
|
||||
t.agents.saveCommandMessage,
|
||||
t.agents.saveRequested,
|
||||
thread.isLoading,
|
||||
threadId,
|
||||
]);
|
||||
// Prefer the model's proposed name when present; fall back to whatever
|
||||
// the user already typed, or the existing agent record.
|
||||
const resolvedName = proposedName || nameInput.trim() || agent?.name || agentId;
|
||||
setNameInput(resolvedName);
|
||||
setDescriptionInput(newDescription);
|
||||
setSoulInput(newSoul);
|
||||
setSelectedSkills(newSkills);
|
||||
setIsSkillSelectionTouched(true);
|
||||
|
||||
const header = (
|
||||
<header className="flex shrink-0 items-center justify-between gap-3 border-b px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => router.push("/page/workspace/agents")}
|
||||
>
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<h1 className="text-sm font-semibold">{t.agents.createPageTitle}</h1>
|
||||
</div>
|
||||
|
||||
{step === "chat" ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon-sm" aria-label={t.agents.more}>
|
||||
<MoreHorizontalIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onSelect={() => void handleSaveAgent()}
|
||||
disabled={
|
||||
!!agent || thread.isLoading || setupAgentStatus !== "idle"
|
||||
}
|
||||
>
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
{setupAgentStatus === "requested"
|
||||
? t.agents.saving
|
||||
: t.agents.save}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</header>
|
||||
try {
|
||||
const updated = await updateAgent(agentId, {
|
||||
name: resolvedName,
|
||||
description: newDescription,
|
||||
soul: newSoul,
|
||||
skills: newSkills,
|
||||
});
|
||||
setAgent(updated);
|
||||
toast.success("专家配置已保存");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "保存失败");
|
||||
}
|
||||
},
|
||||
[agent, agentId, nameInput],
|
||||
);
|
||||
|
||||
if (step === "name") {
|
||||
return (
|
||||
<div className="flex size-full flex-col">
|
||||
{header}
|
||||
<main className="flex flex-1 flex-col items-center justify-center px-4">
|
||||
<div className="w-full max-w-sm space-y-8">
|
||||
<div className="space-y-3 text-center">
|
||||
<div className="bg-primary/10 mx-auto flex h-14 w-14 items-center justify-center rounded-full">
|
||||
<BotIcon className="text-primary h-7 w-7" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{t.agents.nameStepTitle}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t.agents.nameStepHint}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
const handleRejectSetupAgent = useCallback(
|
||||
async () => {
|
||||
await handleChatSubmit("我暂时不批准创建这个专家配置,请继续帮我调整。");
|
||||
},
|
||||
[handleChatSubmit],
|
||||
);
|
||||
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder={t.agents.nameStepPlaceholder}
|
||||
value={nameInput}
|
||||
onChange={(e) => {
|
||||
setNameInput(e.target.value);
|
||||
setNameError("");
|
||||
}}
|
||||
onKeyDown={handleNameKeyDown}
|
||||
className={cn(nameError && "border-destructive")}
|
||||
/>
|
||||
{nameError ? (
|
||||
<p className="text-destructive text-sm">{nameError}</p>
|
||||
) : null}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-medium">默认技能(可自定义)</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
已选 {selectedSkills.length} 个
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-md border p-2">
|
||||
{isSkillsLoading ? (
|
||||
<p className="text-muted-foreground text-sm">技能加载中...</p>
|
||||
) : enabledSkills.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
当前没有可用技能,将创建为无技能智能体。
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{enabledSkills.map((skill) => {
|
||||
const checked = selectedSkills.includes(skill.name);
|
||||
return (
|
||||
<Button
|
||||
key={skill.name}
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={checked ? "default" : "outline"}
|
||||
onClick={() => toggleSkill(skill)}
|
||||
className={cn(
|
||||
"h-8",
|
||||
checked ? "" : "text-muted-foreground",
|
||||
)}
|
||||
title={skill.description}
|
||||
>
|
||||
{skill.name}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => void handleConfirmName()}
|
||||
disabled={
|
||||
!nameInput.trim() ||
|
||||
isCheckingName ||
|
||||
isCreatingAgent ||
|
||||
isSkillsLoading
|
||||
}
|
||||
>
|
||||
{t.agents.nameStepContinue}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
const toggleSkill = useCallback((skill: Skill) => {
|
||||
setIsSkillSelectionTouched(true);
|
||||
setFormError("");
|
||||
setSelectedSkills((prev) =>
|
||||
prev.includes(skill.name)
|
||||
? prev.filter((name) => name !== skill.name)
|
||||
: [...prev, skill.name],
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const busy = isCreatingAgent || isSavingDraft || thread.isLoading;
|
||||
|
||||
return (
|
||||
<ThreadContext.Provider value={{ thread }}>
|
||||
<ArtifactsProvider>
|
||||
<div className="flex size-full flex-col">
|
||||
{header}
|
||||
|
||||
<main className="flex min-h-0 flex-1 flex-col">
|
||||
{showSaveHint ? (
|
||||
<div className="px-4 pt-4">
|
||||
<div className="mx-auto w-full max-w-(--container-width-md)">
|
||||
<Alert>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertDescription>{t.agents.saveHint}</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
<header className="flex shrink-0 items-center justify-between gap-3 border-b px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => router.push("/workspace/agents")}
|
||||
>
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<h1 className="text-sm font-semibold">创建专家</h1>
|
||||
</div>
|
||||
{agent ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/workspace/agents/${agent.id}/chats/new`)}
|
||||
>
|
||||
<CheckCircleIcon className="h-4 w-4" />
|
||||
开始对话
|
||||
</Button>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<div className="flex min-h-0 flex-1 justify-center">
|
||||
<MessageList
|
||||
className={cn("size-full", showSaveHint ? "pt-4" : "pt-10")}
|
||||
threadId={threadId}
|
||||
thread={thread}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-background flex shrink-0 justify-center border-t px-4 py-4">
|
||||
<div className="w-full max-w-(--container-width-md)">
|
||||
{agent ? (
|
||||
<div className="flex flex-col items-center gap-4 rounded-2xl border py-8 text-center">
|
||||
<CheckCircleIcon className="text-primary h-10 w-10" />
|
||||
<p className="font-semibold">{t.agents.agentCreated}</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/page/workspace/agents/${agentId}/chats/new`,
|
||||
)
|
||||
}
|
||||
>
|
||||
{t.agents.startChatting}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => router.push("/page/workspace/agents")}
|
||||
>
|
||||
{t.agents.backToGallery}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PromptInput
|
||||
onSubmit={({ text }) => void handleChatSubmit(text)}
|
||||
>
|
||||
<PromptInputTextarea
|
||||
autoFocus
|
||||
placeholder={t.agents.createPageSubtitle}
|
||||
disabled={thread.isLoading}
|
||||
/>
|
||||
<PromptInputFooter className="justify-end">
|
||||
<PromptInputSubmit disabled={thread.isLoading} />
|
||||
</PromptInputFooter>
|
||||
</PromptInput>
|
||||
)}
|
||||
<main className="grid min-h-0 flex-1 grid-cols-1 md:grid-cols-[minmax(0,1fr)_420px]">
|
||||
<section className="flex min-h-0 flex-col border-r">
|
||||
<div className="min-h-0 flex-1">
|
||||
<MessageList
|
||||
className="size-full"
|
||||
threadId={threadId}
|
||||
thread={thread}
|
||||
onClarificationSubmit={handleChatSubmit}
|
||||
onSetupAgentApprove={handleApproveSetupAgent}
|
||||
onSetupAgentReject={handleRejectSetupAgent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-background shrink-0 border-t px-4 py-4">
|
||||
<PromptInput
|
||||
onSubmit={({ text }) => void handleChatSubmit(text)}
|
||||
>
|
||||
<PromptInputTextarea
|
||||
placeholder="直接告诉模型你想要什么样的专家,或回答模型的问题"
|
||||
disabled={thread.isLoading}
|
||||
/>
|
||||
<PromptInputFooter className="justify-end">
|
||||
<PromptInputSubmit disabled={thread.isLoading} />
|
||||
</PromptInputFooter>
|
||||
</PromptInput>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside className="bg-background min-h-0 overflow-y-auto">
|
||||
<div className="space-y-5 p-4">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold">专家配置</h2>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
这些内容会作为创建专家时的初始配置。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">名称</label>
|
||||
<Input
|
||||
placeholder="例如:合同审查专家"
|
||||
value={nameInput}
|
||||
onChange={(event) => {
|
||||
setNameInput(event.currentTarget.value);
|
||||
setFormError("");
|
||||
}}
|
||||
className={cn(formError && "border-destructive")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">描述</label>
|
||||
<Textarea
|
||||
placeholder="简要说明这个专家擅长什么"
|
||||
value={descriptionInput}
|
||||
onChange={(event) =>
|
||||
setDescriptionInput(event.currentTarget.value)
|
||||
}
|
||||
className="min-h-24 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">SOUL.md</label>
|
||||
<Textarea
|
||||
placeholder="写下专家的人设、边界、工作方式和输出要求"
|
||||
value={soulInput}
|
||||
onChange={(event) => setSoulInput(event.currentTarget.value)}
|
||||
className="min-h-48 resize-y font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium">技能</label>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
已选 {selectedSkills.length} 个
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded-md border p-2">
|
||||
{isSkillsLoading ? (
|
||||
<p className="text-muted-foreground text-sm">技能加载中...</p>
|
||||
) : enabledSkills.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
当前没有可用技能。
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{enabledSkills.map((skill) => {
|
||||
const checked = selectedSkills.includes(skill.name);
|
||||
return (
|
||||
<Button
|
||||
key={skill.name}
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={checked ? "default" : "outline"}
|
||||
onClick={() => toggleSkill(skill)}
|
||||
title={skill.description}
|
||||
className={cn(
|
||||
"h-8 max-w-full",
|
||||
!checked && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{skill.name}</span>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formError ? (
|
||||
<p className="text-destructive text-sm">{formError}</p>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => void handleSaveAgent()}
|
||||
disabled={busy || isSkillsLoading}
|
||||
>
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
{agentId ? "保存专家" : "创建专家"}
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
</div>
|
||||
</ArtifactsProvider>
|
||||
|
||||
Reference in new issue
Block a user