import * as React from "react" import { ChevronDown, ChevronUp, CircleStop, Copy, Edit3, FileText, GitFork, MessageSquare, Save, Send, Settings, SlidersHorizontal, Trash2, X, } from "lucide-react" import { MarkdownMessage } from "@/components/markdown-message" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Select } from "@/components/ui/select" import { Textarea } from "@/components/ui/textarea" import { LabelledField } from "@/components/app/shared" import { ASK_QUESTIONS_TOOL_NAME, parseAskQuestionsArguments, } from "@/lib/tools" import { formatMessageCost } from "@/lib/pricing" import { cn } from "@/lib/utils" import type { AskQuestionsAnswer, AskQuestionsPayload, ChatMessage, ChatSession, ChatToolCall, ModelConfig, PromptTemplate, RequestState, ReasoningEffort, RuntimeConfig, ServiceConfig, } from "@/types" const STATUS_TEXT: Record = { idle: "就绪", requesting: "请求中", generating: "生成中", failed: "失败", stopped: "已停止", done: "完成", } const REASONING_EFFORT_OPTIONS: { value: ReasoningEffort; label: string }[] = [ { value: "default", label: "默认" }, { value: "minimal", label: "minimal" }, { value: "low", label: "low" }, { value: "medium", label: "medium" }, { value: "high", label: "high" }, ] function formatMessageTime(value: string) { return new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", }).format(new Date(value)) } function formatDuration(ms?: number) { if (!ms) { return "" } if (ms < 1000) { return `${Math.round(ms)}ms` } return `${(ms / 1000).toFixed(1)}s` } function formatUsage(message: ChatMessage) { if (!message.usage) { return "" } const cachedPromptTokens = message.usage.promptCacheHitTokens ?? message.usage.cachedPromptTokens const parts = [ message.usage.promptTokens !== undefined ? `In: ${message.usage.promptTokens}` : "", cachedPromptTokens !== undefined ? `(Cached: ${cachedPromptTokens})` : "", message.usage.completionTokens !== undefined ? `Out: ${message.usage.completionTokens}` : "", message.usage.totalTokens !== undefined ? `All: ${message.usage.totalTokens}` : "", ].filter(Boolean) return parts.length ? parts.join(" ") : "" } function getMessageCopyContent(message: ChatMessage) { if (message.role !== "assistant" || !message.reasoningContent?.trim()) { return message.content } return [ `reasoning_content:\n${message.reasoningContent.trim()}`, message.content.trim() ? `content:\n${message.content.trim()}` : "", ] .filter(Boolean) .join("\n\n") } type ChatViewProps = { currentSession: ChatSession runtimeConfig: RuntimeConfig services: ServiceConfig[] models: ModelConfig[] prompts: PromptTemplate[] chatInput: string editingMessageId: string | null editingContent: string showPromptEditor: boolean headerOpen: boolean quickConfigOpen: boolean isBusy: boolean configIsValid: boolean onInputChange: (value: string) => void onSend: () => void onStop: () => void onCopy: (text: string, label?: string) => void onDeleteMessage: (messageId: string) => void onEditMessage: (message: ChatMessage) => void onForkMessage: (messageId: string) => void onSubmitAskQuestions: ( messageId: string, toolCall: ChatToolCall, answers: AskQuestionsAnswer[] ) => void onCancelEdit: () => void onEditingContentChange: (value: string) => void onSaveEditOnly: () => void onSaveEditAndSend: () => void onServiceChange: (serviceId: string) => void onModelChange: (modelId: string) => void onPromptChange: (promptId: string) => void onHeaderOpenChange: (open: boolean) => void onQuickConfigOpenChange: (open: boolean) => void onPromptEditorToggle: () => void onSystemPromptChange: (value: string) => void onReasoningEffortChange: (value: ReasoningEffort) => void onOpenSettings: () => void } export function ChatView({ currentSession, runtimeConfig, services, models, prompts, chatInput, editingMessageId, editingContent, showPromptEditor, headerOpen, quickConfigOpen, isBusy, configIsValid, onInputChange, onSend, onStop, onCopy, onDeleteMessage, onEditMessage, onForkMessage, onSubmitAskQuestions, onCancelEdit, onEditingContentChange, onSaveEditOnly, onSaveEditAndSend, onServiceChange, onModelChange, onPromptChange, onHeaderOpenChange, onQuickConfigOpenChange, onPromptEditorToggle, onSystemPromptChange, onReasoningEffortChange, onOpenSettings, }: ChatViewProps) { const canSend = chatInput.trim().length > 0 && !isBusy const messagesScrollRef = React.useRef(null) const reasoningMenuRef = React.useRef(null) const [reasoningMenuOpen, setReasoningMenuOpen] = React.useState(false) const selectedReasoningLabel = REASONING_EFFORT_OPTIONS.find( (option) => option.value === runtimeConfig.parameters.reasoningEffort )?.label ?? runtimeConfig.parameters.reasoningEffort React.useEffect(() => { const element = messagesScrollRef.current if (!element) { return } // Keep session switching predictable by jumping to the latest message. element.scrollTop = element.scrollHeight }, [currentSession.id]) React.useEffect(() => { if (!reasoningMenuOpen) { return undefined } const handlePointerDown = (event: PointerEvent) => { if ( reasoningMenuRef.current && !reasoningMenuRef.current.contains(event.target as Node) ) { setReasoningMenuOpen(false) } } const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { setReasoningMenuOpen(false) } } document.addEventListener("pointerdown", handlePointerDown) document.addEventListener("keydown", handleKeyDown) return () => { document.removeEventListener("pointerdown", handlePointerDown) document.removeEventListener("keydown", handleKeyDown) } }, [reasoningMenuOpen]) return (
{headerOpen ? (

{currentSession.title}

{currentSession.temporary ? ( 临时 ) : null}
{runtimeConfig.service?.name || "未选择服务"} {runtimeConfig.modelName || "未选择模型"} {currentSession.messages.length} 条消息
) : (
)} {quickConfigOpen ? (
{!configIsValid ? ( ) : null}
{showPromptEditor ? (