"use client"; import type { UseStream } from "@langchain/langgraph-sdk/react"; import { FilesIcon, ListTodoIcon, XIcon } from "lucide-react"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ConversationEmptyState } from "@/components/ai-elements/conversation"; import { usePromptInputController } from "@/components/ai-elements/prompt-input"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { DevDialog, DevDialogContent, DevDialogFooter, DevDialogHeader, DevDialogTitle, } from "@/components/ui/dev-dialog"; import { useSidebar } from "@/components/ui/sidebar"; import { ArtifactFileDetail, ArtifactFileList, useArtifacts, } from "@/components/workspace/artifacts"; import { DevTodoList } from "@/components/workspace/dev-todo-list"; import { IframeTestPanel } from "@/components/workspace/iframe-test-panel"; import { InputBox } from "@/components/workspace/input-box"; import { MessageList } from "@/components/workspace/messages"; import { ThreadContext } from "@/components/workspace/messages/context"; import { ThreadTitle } from "@/components/workspace/thread-title"; import { TodoList } from "@/components/workspace/todo-list"; import { Tooltip } from "@/components/workspace/tooltip"; import { useSpecificChatMode } from "@/components/workspace/use-chat-mode"; import { Welcome } from "@/components/workspace/welcome"; import { useI18n } from "@/core/i18n/hooks"; import { useNotification } from "@/core/notification/hooks"; import { useLocalSettings } from "@/core/settings"; import { bootstrapRemoteSkill } from "@/core/skills"; import { type AgentThread, type AgentThreadState } from "@/core/threads"; import { useSubmitThread, useThreadStream } from "@/core/threads/hooks"; import { pathOfThread, textOfMessage, titleOfThread, } from "@/core/threads/utils"; import { uuid } from "@/core/utils/uuid"; import { env } from "@/env"; import { useSelectedSkillListener } from "@/hooks/use-selected-skill-listener"; import { cn } from "@/lib/utils"; export default function ChatPage() { const { t } = useI18n(); const router = useRouter(); useSpecificChatMode(); const [settings, setSettings] = useLocalSettings(); const { setOpen: setSidebarOpen } = useSidebar(); const { artifacts, open: artifactsOpen, setOpen: setArtifactsOpen, setArtifacts, select: selectArtifact, selectedArtifact, fullscreen, } = useArtifacts(); const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>(); const searchParams = useSearchParams(); const promptInputController = usePromptInputController(); // UI mode depends only on route: /workspace/chats/new is always "new page" mode. const isNewThread = useMemo( () => threadIdFromPath === "new", [threadIdFromPath], ); // Submission strategy is controlled by `isnew` query param only. // - isnew=false: reuse existing thread // - otherwise: create/start a new session const createNewSession = useMemo(() => { if (threadIdFromPath !== "new") { return false; } return searchParams.get("isnew")?.trim().toLowerCase() !== "false"; }, [threadIdFromPath, searchParams]); const uploadTarget = useMemo(() => { const target = searchParams.get("upload_target")?.trim().toLowerCase(); return target === "skill" ? "skill" : undefined; }, [searchParams]); const [threadId, setThreadId] = useState(null); useEffect(() => { if (threadIdFromPath !== "new") { setThreadId(threadIdFromPath); } else { const queryThreadId = searchParams.get("thread_id")?.trim(); setThreadId(queryThreadId ?? uuid()); } }, [threadIdFromPath, searchParams]); // Runtime strategy for /new page: // - UI remains new-page mode // - if isnew=false, execute against existing thread_id without creating a new one const reuseExistingThread = useMemo( () => threadIdFromPath === "new" && !createNewSession && !!threadId, [threadIdFromPath, createNewSession, threadId], ); const { showNotification } = useNotification(); // 监听宿主页 selectedSkill 消息 const { selectedSkill, skillError: selectedSkillError, clearSkillError: clearSelectedSkillError, isBootstrapping: isSelectedSkillBootstrapping, } = useSelectedSkillListener({ threadId }); const [finalState, setFinalState] = useState(null); const thread = useThreadStream({ // Keep UI in new-page mode, but runtime may reuse existing thread isNewThread: reuseExistingThread ? false : isNewThread, threadId, fetchStateHistory: true, onFinish: (state) => { setFinalState(state); // 新对话完成后导航到对话页面 if (isNewThread && threadId) { router.push(pathOfThread(threadId)); } if (document.hidden || !document.hasFocus()) { let body = "Conversation finished"; const lastMessage = state.messages[state.messages.length - 1]; if (lastMessage) { const textContent = textOfMessage(lastMessage); if (textContent) { if (textContent.length > 200) { body = textContent.substring(0, 200) + "..."; } else { body = textContent; } } } showNotification(state.title, { body, }); } }, }) as unknown as UseStream; useEffect(() => { if (thread.isLoading) setFinalState(null); }, [thread.isLoading]); const title = useMemo(() => { let result = isNewThread ? "" : titleOfThread(thread as unknown as AgentThread); if (result === "Untitled") { result = ""; } return result; }, [thread, isNewThread]); const [hasSubmitted, setHasSubmitted] = useState(false); const suppressExistingThreadPrefetchUi = reuseExistingThread && !hasSubmitted; useEffect(() => { const pageTitle = isNewThread ? t.pages.newChat : thread.values?.title && thread.values.title !== "Untitled" ? thread.values.title : t.pages.untitled; if (thread.isThreadLoading && !suppressExistingThreadPrefetchUi) { document.title = `Loading... - ${t.pages.appName}`; } else { document.title = `${pageTitle} - ${t.pages.appName}`; } }, [ isNewThread, t.pages.newChat, t.pages.untitled, t.pages.appName, thread.values.title, thread.isThreadLoading, suppressExistingThreadPrefetchUi, ]); const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true); useEffect(() => { setArtifacts(thread.values.artifacts); if ( env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && autoSelectFirstArtifact ) { if (thread?.values?.artifacts?.length > 0) { setAutoSelectFirstArtifact(false); selectArtifact(thread.values.artifacts[0]!); } } }, [ autoSelectFirstArtifact, selectArtifact, setArtifacts, thread.values.artifacts, ]); const artifactPanelOpen = useMemo(() => { if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true") { return artifactsOpen && artifacts?.length > 0; } return artifactsOpen; }, [artifactsOpen, artifacts]); const [todoListCollapsed, setTodoListCollapsed] = useState(true); const [showExitDialog, setShowExitDialog] = useState(false); const submitThread = useSubmitThread({ isNewThread, createNewSession, threadId, thread, uploadTarget, threadContext: { ...settings.context, thinking_enabled: settings.context.mode !== "flash", is_plan_mode: settings.context.mode === "pro" || settings.context.mode === "ultra", subagent_enabled: settings.context.mode === "ultra", }, afterSubmit() { // 导航已在 onFinish 中处理,确保 stream 完成后再导航 }, }); const handleSubmit = useCallback( (message: Parameters[0]) => { if (isSelectedSkillBootstrapping) { return; } setHasSubmitted(true); void submitThread(message); }, [isSelectedSkillBootstrapping, submitThread], ); const handleStop = useCallback(async () => { await thread.stop(); }, [thread]); if (!threadId) { return null; } return (
{title !== "Untitled" && ( )}
{selectedArtifact ? ( ) : (
{thread.values.artifacts?.length === 0 ? ( } title="No artifact selected" description="Select an artifact to view its details" /> ) : (

{t.common.artifacts}

)}
)}
{/* Fixed 底部居中输入框容器 */}
{isNewThread && !hasSubmitted && ( )}
} disabled={ env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || isSelectedSkillBootstrapping } onContextChange={(context) => setSettings("context", context)} onSubmit={handleSubmit} onStop={handleStop} /> {/* {isSelectedSkillBootstrapping && (
正在初始化 Skill 文件...
)} */} {env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && (
{t.common.notAvailableInDemoMode}
)}
{/* 退出确认对话框 */} 提示

退出后,当前会话结束并销毁,请先下载保存当前结果!

{/* selectedSkill 失败:错误弹窗 */} { if (!open) clearSelectedSkillError(); }} > ⚠️ {selectedSkillError?.title ?? "技能加载失败"}

{selectedSkillError?.message ?? "发生了未知错误,请稍后重试。"}

{/* MARK: 开发测试:iframe 通信功能测试面板 */} {/* */}
); }