"use client"; import { ArrowLeftIcon, BotIcon, CheckCircleIcon, InfoIcon, MoreHorizontalIcon, SaveIcon, } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { PromptInput, PromptInputFooter, 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 { ArtifactsProvider } from "@/components/workspace/artifacts"; import { MessageList } from "@/components/workspace/messages"; import { ThreadContext } from "@/components/workspace/messages/context"; import type { Agent } from "@/core/agents"; import { checkAgentName, getAgent } from "@/core/agents/api"; import { useI18n } from "@/core/i18n/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 NAME_RE = /^[A-Za-z0-9-]+$/; const SAVE_HINT_STORAGE_KEY = "deerflow.agent-create.save-hint-seen"; const AGENT_READ_RETRY_DELAYS_MS = [200, 500, 1_000, 2_000]; function wait(ms: number) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } async function getAgentWithRetry(agentName: string) { for (const delay of [0, ...AGENT_READ_RETRY_DELAYS_MS]) { if (delay > 0) { await wait(delay); } try { return await getAgent(agentName); } catch { // Retry until the write settles or the attempts are exhausted. } } return null; } export default function NewAgentPage() { const { t } = useI18n(); const router = useRouter(); const [step, setStep] = useState("name"); const [nameInput, setNameInput] = useState(""); const [nameError, setNameError] = useState(""); const [isCheckingName, setIsCheckingName] = useState(false); const [agentName, setAgentName] = useState(""); const [agent, setAgent] = useState(null); const [showSaveHint, setShowSaveHint] = useState(false); const [setupAgentStatus, setSetupAgentStatus] = useState("idle"); const threadId = useMemo(() => uuid(), []); const [thread, sendMessage] = useThreadStream({ threadId: step === "chat" ? threadId : undefined, context: { mode: "flash", is_bootstrap: true, }, onFinish() { if (!agent && setupAgentStatus === "requested") { setSetupAgentStatus("idle"); } }, onToolEnd({ name }) { if (name !== "setup_agent" || !agentName) return; setSetupAgentStatus("completed"); void getAgentWithRetry(agentName).then((fetched) => { if (fetched) { setAgent(fetched); return; } toast.error(t.agents.agentCreatedPendingRefresh); }); }, }); 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) return; if (!NAME_RE.test(trimmed)) { setNameError(t.agents.nameStepInvalidError); return; } setNameError(""); setIsCheckingName(true); try { const result = await checkAgentName(trimmed); if (!result.available) { setNameError(t.agents.nameStepAlreadyExistsError); return; } } catch (err) { if (err instanceof TypeError && err.message === "Failed to fetch") { setNameError(t.agents.nameStepNetworkError); } else { setNameError(t.agents.nameStepCheckError); } return; } finally { setIsCheckingName(false); } setAgentName(trimmed); setStep("chat"); await sendMessage(threadId, { text: t.agents.nameStepBootstrapMessage.replace("{name}", trimmed), files: [], }); }, [ nameInput, sendMessage, t.agents.nameStepAlreadyExistsError, t.agents.nameStepNetworkError, t.agents.nameStepBootstrapMessage, t.agents.nameStepCheckError, t.agents.nameStepInvalidError, threadId, ]); const handleNameKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !isIMEComposing(e)) { e.preventDefault(); void handleConfirmName(); } }; const handleChatSubmit = useCallback( async (text: string) => { const trimmed = text.trim(); if (!trimmed || thread.isLoading) return; await sendMessage( threadId, { text: trimmed, files: [] }, { agent_name: agentName }, ); }, [agentName, sendMessage, thread.isLoading, threadId], ); const handleSaveAgent = useCallback(async () => { if ( !agentName || agent || thread.isLoading || setupAgentStatus !== "idle" ) { return; } setSetupAgentStatus("requested"); setShowSaveHint(false); try { await sendMessage( threadId, { text: t.agents.saveCommandMessage, files: [] }, { agent_name: agentName }, { additionalKwargs: { hide_from_ui: true } }, ); toast.success(t.agents.saveRequested); } catch (error) { setSetupAgentStatus("idle"); toast.error(error instanceof Error ? error.message : String(error)); } }, [ agent, agentName, sendMessage, setupAgentStatus, t.agents.saveCommandMessage, t.agents.saveRequested, thread.isLoading, threadId, ]); const header = (

{t.agents.createPageTitle}

{step === "chat" ? ( void handleSaveAgent()} disabled={ !!agent || thread.isLoading || setupAgentStatus !== "idle" } > {setupAgentStatus === "requested" ? t.agents.saving : t.agents.save} ) : null}
); if (step === "name") { return (
{header}

{t.agents.nameStepTitle}

{t.agents.nameStepHint}

{ setNameInput(e.target.value); setNameError(""); }} onKeyDown={handleNameKeyDown} className={cn(nameError && "border-destructive")} /> {nameError ? (

{nameError}

) : null}
); } return (
{header}
{showSaveHint ? (
{t.agents.saveHint}
) : null}
{agent ? (

{t.agents.agentCreated}

) : ( void handleChatSubmit(text)} > )}
); }