mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-23 06:04:46 +08:00
Surface the usage_metadata that PR #1218 added to the streaming API. A compact indicator in the chat header shows cumulative tokens consumed per thread, with a tooltip breakdown of input/output/total counts. Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
158 lines
5.9 KiB
TypeScript
158 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback } from "react";
|
|
|
|
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
|
import { ArtifactTrigger } from "@/components/workspace/artifacts";
|
|
import {
|
|
ChatBox,
|
|
useSpecificChatMode,
|
|
useThreadChat,
|
|
} from "@/components/workspace/chats";
|
|
import { ExportTrigger } from "@/components/workspace/export-trigger";
|
|
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 { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
|
|
import { Welcome } from "@/components/workspace/welcome";
|
|
import { useI18n } from "@/core/i18n/hooks";
|
|
import { useNotification } from "@/core/notification/hooks";
|
|
import { useLocalSettings } from "@/core/settings";
|
|
import { useThreadStream } from "@/core/threads/hooks";
|
|
import { textOfMessage } from "@/core/threads/utils";
|
|
import { env } from "@/env";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export default function ChatPage() {
|
|
const { t } = useI18n();
|
|
const [settings, setSettings] = useLocalSettings();
|
|
|
|
const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat();
|
|
useSpecificChatMode();
|
|
|
|
const { showNotification } = useNotification();
|
|
|
|
const [thread, sendMessage, isUploading] = useThreadStream({
|
|
threadId: isNewThread ? undefined : threadId,
|
|
context: settings.context,
|
|
isMock,
|
|
onStart: () => {
|
|
setIsNewThread(false);
|
|
// ! Important: Never use next.js router for navigation in this case, otherwise it will cause the thread to re-mount and lose all states. Use native history API instead.
|
|
history.replaceState(null, "", `/workspace/chats/${threadId}`);
|
|
},
|
|
onFinish: (state) => {
|
|
if (document.hidden || !document.hasFocus()) {
|
|
let body = "Conversation finished";
|
|
const lastMessage = state.messages.at(-1);
|
|
if (lastMessage) {
|
|
const textContent = textOfMessage(lastMessage);
|
|
if (textContent) {
|
|
body =
|
|
textContent.length > 200
|
|
? textContent.substring(0, 200) + "..."
|
|
: textContent;
|
|
}
|
|
}
|
|
showNotification(state.title, { body });
|
|
}
|
|
},
|
|
});
|
|
|
|
const handleSubmit = useCallback(
|
|
(message: PromptInputMessage) => {
|
|
void sendMessage(threadId, message);
|
|
},
|
|
[sendMessage, threadId],
|
|
);
|
|
const handleStop = useCallback(async () => {
|
|
await thread.stop();
|
|
}, [thread]);
|
|
|
|
return (
|
|
<ThreadContext.Provider value={{ thread, isMock }}>
|
|
<ChatBox threadId={threadId}>
|
|
<div className="relative flex size-full min-h-0 justify-between">
|
|
<header
|
|
className={cn(
|
|
"absolute top-0 right-0 left-0 z-30 flex h-12 shrink-0 items-center px-4",
|
|
isNewThread
|
|
? "bg-background/0 backdrop-blur-none"
|
|
: "bg-background/80 shadow-xs backdrop-blur",
|
|
)}
|
|
>
|
|
<div className="flex w-full items-center text-sm font-medium">
|
|
<ThreadTitle threadId={threadId} thread={thread} />
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<TokenUsageIndicator messages={thread.messages} />
|
|
<ExportTrigger threadId={threadId} />
|
|
<ArtifactTrigger />
|
|
</div>
|
|
</header>
|
|
<main className="flex min-h-0 max-w-full grow flex-col">
|
|
<div className="flex size-full justify-center">
|
|
<MessageList
|
|
className={cn("size-full", !isNewThread && "pt-10")}
|
|
threadId={threadId}
|
|
thread={thread}
|
|
/>
|
|
</div>
|
|
<div className="absolute right-0 bottom-0 left-0 z-30 flex justify-center px-4">
|
|
<div
|
|
className={cn(
|
|
"relative w-full",
|
|
isNewThread && "-translate-y-[calc(50vh-96px)]",
|
|
isNewThread
|
|
? "max-w-(--container-width-sm)"
|
|
: "max-w-(--container-width-md)",
|
|
)}
|
|
>
|
|
<div className="absolute -top-4 right-0 left-0 z-0">
|
|
<div className="absolute right-0 bottom-0 left-0">
|
|
<TodoList
|
|
className="bg-background/5"
|
|
todos={thread.values.todos ?? []}
|
|
hidden={
|
|
!thread.values.todos || thread.values.todos.length === 0
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<InputBox
|
|
className={cn("bg-background/5 w-full -translate-y-4")}
|
|
isNewThread={isNewThread}
|
|
threadId={threadId}
|
|
autoFocus={isNewThread}
|
|
status={
|
|
thread.error
|
|
? "error"
|
|
: thread.isLoading
|
|
? "streaming"
|
|
: "ready"
|
|
}
|
|
context={settings.context}
|
|
extraHeader={
|
|
isNewThread && <Welcome mode={settings.context.mode} />
|
|
}
|
|
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || isUploading}
|
|
onContextChange={(context) => setSettings("context", context)}
|
|
onSubmit={handleSubmit}
|
|
onStop={handleStop}
|
|
/>
|
|
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && (
|
|
<div className="text-muted-foreground/67 w-full translate-y-12 text-center text-xs">
|
|
{t.common.notAvailableInDemoMode}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</ChatBox>
|
|
</ThreadContext.Provider>
|
|
);
|
|
}
|