visuddhinanda 7 months ago
parent
commit
fb3dbd3526

+ 125 - 0
dashboard-v4/dashboard/src/components/chat/AssistantMessage.tsx

@@ -0,0 +1,125 @@
+import React from "react";
+import { Button, Space } from "antd";
+import {
+  RedoOutlined,
+  LikeOutlined,
+  DislikeOutlined,
+  CopyOutlined,
+  ShareAltOutlined,
+} from "@ant-design/icons";
+import { AssistantMessageProps } from "../../types/chat";
+
+const AssistantMessage = ({
+  messages,
+  onRefresh,
+  onEdit,
+  isPending,
+  onLike,
+  onDislike,
+  onCopy,
+  onShare,
+}: AssistantMessageProps) => {
+  const mainMessage = messages.find((m) => m.role === "assistant" && m.content);
+  const toolMessages = messages.filter((m) => m.role === "tool");
+
+  const handleCopy = () => {
+    if (mainMessage?.content && onCopy) {
+      onCopy(mainMessage.uid);
+    }
+  };
+
+  const handleShare = async () => {
+    if (mainMessage && onShare) {
+      try {
+        const shareUrl = await onShare(mainMessage.uid);
+        // 可以显示分享链接或复制到剪贴板
+        navigator.clipboard.writeText(shareUrl);
+      } catch (err) {
+        console.error("分享失败:", err);
+      }
+    }
+  };
+
+  return (
+    <div className="assistant-message">
+      <div className="message-header">
+        <span className="role-label">Assistant</span>
+        {mainMessage?.model_id && (
+          <span className="model-info">{mainMessage.model_id}</span>
+        )}
+
+        {!isPending && (
+          <div className="message-actions">
+            <Space size="small">
+              <Button
+                size="small"
+                type="text"
+                icon={<RedoOutlined />}
+                onClick={onRefresh}
+              />
+              <Button
+                size="small"
+                type="text"
+                icon={<LikeOutlined />}
+                onClick={() => mainMessage && onLike && onLike(mainMessage.uid)}
+              />
+              <Button
+                size="small"
+                type="text"
+                icon={<DislikeOutlined />}
+                onClick={() =>
+                  mainMessage && onDislike && onDislike(mainMessage.uid)
+                }
+              />
+              <Button
+                size="small"
+                type="text"
+                icon={<CopyOutlined />}
+                onClick={handleCopy}
+              />
+              <Button
+                size="small"
+                type="text"
+                icon={<ShareAltOutlined />}
+                onClick={handleShare}
+              />
+            </Space>
+          </div>
+        )}
+      </div>
+
+      <div className="message-content">
+        {/* Tool calls 显示 */}
+        {toolMessages.length > 0 && (
+          <div className="tool-calls">
+            {toolMessages.map((toolMsg, index) => (
+              <div key={toolMsg.uid} className="tool-result">
+                <span className="tool-label">Tool {index + 1}</span>
+                <div className="tool-content">{toolMsg.content}</div>
+              </div>
+            ))}
+          </div>
+        )}
+
+        {/* 主要回答内容 */}
+        {mainMessage?.content && (
+          <div className="message-text">
+            {mainMessage.content}
+            {isPending && (
+              <span className="status-indicator pending">生成中...</span>
+            )}
+          </div>
+        )}
+
+        {/* Token 使用信息 */}
+        {mainMessage?.metadata?.token_usage && (
+          <div className="token-info">
+            Token: {mainMessage.metadata.token_usage.total_tokens}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+};
+
+export default AssistantMessage;

+ 60 - 0
dashboard-v4/dashboard/src/components/chat/ChatContainer.tsx

@@ -0,0 +1,60 @@
+import React, { useEffect, useState } from "react";
+import { useChatData } from "../../hooks/useChatData";
+import { SessionGroup } from "./SessionGroup";
+import { ChatInput } from "./ChatInput";
+import { StreamingMessage } from "./StreamingMessage";
+import { mockChatState } from "../../hooks/mockChatData";
+
+import "./style.css";
+
+interface ChatContainerProps {
+  chatId: string;
+}
+
+export function ChatContainer({ chatId }: ChatContainerProps) {
+  const { chatState, actions } = useChatData(chatId);
+  const [chatStateMock] = useState(mockChatState);
+  useEffect(() => {
+    actions.loadMessages();
+  }, [chatId, actions.loadMessages, actions]);
+
+  return (
+    <div className="chat-container">
+      <div className="messages-area">
+        {chatStateMock.session_groups.map((session) => (
+          <SessionGroup
+            key={session.session_id}
+            session={session}
+            onVersionSwitch={actions.switchVersion}
+            onRefresh={actions.refreshResponse}
+            onEdit={actions.editMessage}
+            onRetry={actions.retryMessage}
+            onLike={actions.likeMessage}
+            onDislike={actions.dislikeMessage}
+            onCopy={actions.copyMessage}
+            onShare={actions.shareMessage}
+          />
+        ))}
+
+        {/* 流式消息显示 */}
+        {chatState.streaming_message && (
+          <StreamingMessage
+            content={chatState.streaming_message}
+            sessionId={chatState.streaming_session_id}
+          />
+        )}
+
+        {/* 错误提示 */}
+        {chatState.error && (
+          <div className="error-message">{chatState.error}</div>
+        )}
+      </div>
+
+      <ChatInput
+        onSend={(content) => actions.editMessage("new", content)}
+        disabled={chatState.is_loading}
+        placeholder="输入你的问题..."
+      />
+    </div>
+  );
+}

+ 59 - 0
dashboard-v4/dashboard/src/components/chat/ChatInput.tsx

@@ -0,0 +1,59 @@
+import React, { useState, useCallback } from "react";
+import { Button, Input, Space } from "antd";
+import { SendOutlined, PaperClipOutlined } from "@ant-design/icons";
+import { ChatInputProps } from "../../types/chat";
+
+const { TextArea } = Input;
+
+export function ChatInput({ onSend, disabled, placeholder }: ChatInputProps) {
+  const [inputValue, setInputValue] = useState("");
+
+  const handleSend = useCallback(() => {
+    if (!inputValue.trim() || disabled) return;
+
+    onSend(inputValue.trim());
+    setInputValue("");
+  }, [inputValue, disabled, onSend]);
+
+  const handleKeyPress = useCallback(
+    (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+      if (e.key === "Enter" && !e.shiftKey) {
+        e.preventDefault();
+        handleSend();
+      }
+    },
+    [handleSend]
+  );
+
+  return (
+    <div className="chat-input">
+      <div className="input-area">
+        <TextArea
+          value={inputValue}
+          onChange={(e) => setInputValue(e.target.value)}
+          onKeyPress={handleKeyPress}
+          placeholder={placeholder || "输入你的问题..."}
+          autoSize={{ minRows: 1, maxRows: 6 }}
+          disabled={disabled}
+        />
+
+        <div className="input-actions">
+          <Space>
+            <Button
+              size="small"
+              type="text"
+              icon={<PaperClipOutlined />}
+              disabled={disabled}
+            />
+            <Button
+              type="primary"
+              icon={<SendOutlined />}
+              onClick={handleSend}
+              disabled={!inputValue.trim() || disabled}
+            />
+          </Space>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 82 - 0
dashboard-v4/dashboard/src/components/chat/SessionGroup.tsx

@@ -0,0 +1,82 @@
+import React from "react";
+import { Button } from "antd";
+
+import UserMessage from "./UserMessage";
+import { SessionGroupProps } from "../../types/chat";
+import { VersionSwitcher } from "./VersionSwitcher";
+import AssistantMessage from "./AssistantMessage";
+
+export function SessionGroup({
+  session,
+  onVersionSwitch,
+  onRefresh,
+  onEdit,
+  onRetry,
+  onLike,
+  onDislike,
+  onCopy,
+  onShare,
+}: SessionGroupProps) {
+  const hasFailed = session.messages.some((m) => m.save_status === "failed");
+  const isPending = session.messages.some((m) => m.save_status === "pending");
+  const hasMultipleVersions = session.versions.length > 1;
+
+  return (
+    <div
+      className={`session-group ${isPending ? "pending" : ""} ${
+        hasFailed ? "failed" : ""
+      }`}
+    >
+      {/* 用户消息 */}
+      {session.user_message && (
+        <UserMessage
+          message={session.user_message}
+          onEdit={(content) => onEdit && onEdit(session.session_id, content)}
+          onCopy={() => onCopy && onCopy(session.user_message!.uid)}
+        />
+      )}
+
+      {/* AI回答区域 */}
+      <div className="ai-response">
+        {/* 失败重试提示 */}
+        {hasFailed && onRetry && (
+          <div className="retry-section">
+            <span className="error-message">回答生成失败</span>
+            <Button
+              size="small"
+              type="primary"
+              onClick={() => onRetry(session.messages[0].temp_id!)}
+            >
+              重试
+            </Button>
+          </div>
+        )}
+
+        {/* 版本切换器 */}
+        {hasMultipleVersions && !isPending && !hasFailed && (
+          <VersionSwitcher
+            versions={session.versions}
+            currentVersion={session.current_version}
+            onSwitch={(versionIndex) =>
+              onVersionSwitch(session.session_id, versionIndex)
+            }
+          />
+        )}
+
+        {/* AI消息内容 */}
+        {!hasFailed && session.ai_messages.length > 0 && (
+          <AssistantMessage
+            messages={session.ai_messages}
+            onRefresh={() => onRefresh && onRefresh(session.session_id)}
+            onEdit={(content) => onEdit && onEdit(session.session_id, content)}
+            isPending={isPending}
+            onLike={onLike}
+            onDislike={onDislike}
+            onCopy={onCopy}
+            onShare={onShare}
+          />
+        )}
+      </div>
+    </div>
+  );
+}

+ 24 - 0
dashboard-v4/dashboard/src/components/chat/StreamingMessage.tsx

@@ -0,0 +1,24 @@
+import React from "react";
+
+interface StreamingMessageProps {
+  content: string;
+  sessionId?: string;
+}
+
+export function StreamingMessage({ content }: StreamingMessageProps) {
+  return (
+    <div className="streaming-message">
+      <div className="message-header">
+        <span className="role-label">Assistant</span>
+        <span className="streaming-indicator">正在生成中...</span>
+      </div>
+
+      <div className="message-content">
+        <div className="message-text">
+          {content}
+          <span className="cursor">|</span>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 82 - 0
dashboard-v4/dashboard/src/components/chat/UserMessage.tsx

@@ -0,0 +1,82 @@
+import React, { useState } from "react";
+import { Button, Input } from "antd";
+import { EditOutlined, CopyOutlined } from "@ant-design/icons";
+import { UserMessageProps } from "../../types/chat";
+
+const { TextArea } = Input;
+
+const UserMessage = ({ message, onEdit, onCopy }: UserMessageProps) => {
+  const [isEditing, setIsEditing] = useState(false);
+  const [editContent, setEditContent] = useState(message.content || "");
+
+  const handleEdit = () => {
+    if (onEdit && editContent.trim()) {
+      onEdit(editContent.trim());
+      setIsEditing(false);
+    }
+  };
+
+  const handleCancel = () => {
+    setEditContent(message.content || "");
+    setIsEditing(false);
+  };
+
+  return (
+    <div className="user-message">
+      <div className="message-header">
+        <span className="role-label">You</span>
+        <div className="message-actions">
+          {!isEditing && (
+            <>
+              <Button
+                size="small"
+                type="text"
+                icon={<EditOutlined />}
+                onClick={() => setIsEditing(true)}
+              />
+              <Button
+                size="small"
+                type="text"
+                icon={<CopyOutlined />}
+                onClick={onCopy}
+              />
+            </>
+          )}
+        </div>
+      </div>
+
+      <div className="message-content">
+        {isEditing ? (
+          <div className="edit-area">
+            <TextArea
+              value={editContent}
+              onChange={(e) => setEditContent(e.target.value)}
+              autoSize={{ minRows: 2, maxRows: 8 }}
+              autoFocus
+            />
+            <div className="edit-actions">
+              <Button size="small" onClick={handleCancel}>
+                取消
+              </Button>
+              <Button size="small" type="primary" onClick={handleEdit}>
+                保存
+              </Button>
+            </div>
+          </div>
+        ) : (
+          <div className="message-text">
+            {message.content}
+            {message.save_status === "pending" && (
+              <span className="status-indicator pending">发送中...</span>
+            )}
+            {message.save_status === "failed" && (
+              <span className="status-indicator failed">发送失败</span>
+            )}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+};
+
+export default UserMessage;

+ 60 - 0
dashboard-v4/dashboard/src/components/chat/VersionSwitcher.tsx

@@ -0,0 +1,60 @@
+import React from "react";
+import { Button, Space, Tooltip } from "antd";
+import { LeftOutlined, RightOutlined } from "@ant-design/icons";
+import { VersionSwitcherProps } from "../../types/chat";
+
+export function VersionSwitcher({
+  versions,
+  currentVersion,
+  onSwitch,
+}: VersionSwitcherProps) {
+  if (versions.length <= 1) return null;
+
+  const canGoPrev = currentVersion > 0;
+  const canGoNext = currentVersion < versions.length - 1;
+
+  const currentVersionInfo = versions[currentVersion];
+
+  return (
+    <div className="version-switcher">
+      <Space align="center">
+        <Button
+          size="small"
+          type="text"
+          icon={<LeftOutlined />}
+          disabled={!canGoPrev}
+          onClick={() => canGoPrev && onSwitch(currentVersion - 1)}
+        />
+
+        <Tooltip
+          title={
+            <div>
+              <div>
+                版本 {currentVersion + 1} / {versions.length}
+              </div>
+              <div>模型: {currentVersionInfo.model_id || "未知"}</div>
+              <div>
+                创建: {new Date(currentVersionInfo.created_at).toLocaleString()}
+              </div>
+              {currentVersionInfo.token_usage && (
+                <div>Token: {currentVersionInfo.token_usage}</div>
+              )}
+            </div>
+          }
+        >
+          <span className="version-info">
+            {currentVersion + 1} / {versions.length}
+          </span>
+        </Tooltip>
+
+        <Button
+          size="small"
+          type="text"
+          icon={<RightOutlined />}
+          disabled={!canGoNext}
+          onClick={() => canGoNext && onSwitch(currentVersion + 1)}
+        />
+      </Space>
+    </div>
+  );
+}

+ 194 - 0
dashboard-v4/dashboard/src/components/chat/style.css

@@ -0,0 +1,194 @@
+.chat-container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+}
+
+.messages-area {
+  flex: 1;
+  overflow-y: auto;
+  padding: 16px;
+}
+
+.session-group {
+  margin-bottom: 24px;
+  border-radius: 8px;
+  padding: 16px;
+}
+
+.session-group.pending {
+  opacity: 0.7;
+}
+
+.session-group.failed {
+  border: 1px solid #ff4d4f;
+  background-color: #fff2f0;
+}
+
+.user-message,
+.assistant-message {
+  margin-bottom: 12px;
+}
+
+.message-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.role-label {
+  font-weight: bold;
+  color: #1890ff;
+}
+
+.message-content {
+  padding: 12px;
+  border-radius: 6px;
+  background-color: #f5f5f5;
+}
+
+.version-switcher {
+  margin: 8px 0;
+  text-align: center;
+}
+
+.chat-input {
+  border-top: 1px solid #d9d9d9;
+  padding: 16px;
+}
+
+.streaming-message .cursor {
+  animation: blink 1s infinite;
+}
+
+@keyframes blink {
+  0%,
+  50% {
+    opacity: 1;
+  }
+  51%,
+  100% {
+    opacity: 0;
+  }
+}
+
+.retry-section {
+  padding: 12px;
+  background-color: #fff2f0;
+  border: 1px solid #ffccc7;
+  border-radius: 6px;
+  margin-bottom: 12px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.error-message {
+  color: #ff4d4f;
+  font-size: 14px;
+}
+
+.status-indicator {
+  margin-left: 8px;
+  font-size: 12px;
+}
+
+.status-indicator.pending {
+  color: #1890ff;
+}
+
+.status-indicator.failed {
+  color: #ff4d4f;
+}
+
+.tool-calls {
+  margin-bottom: 12px;
+  padding: 8px;
+  background-color: #f0f0f0;
+  border-radius: 4px;
+}
+
+.tool-result {
+  margin-bottom: 8px;
+}
+
+.tool-label {
+  font-size: 12px;
+  color: #666;
+  font-weight: bold;
+}
+
+.tool-content {
+  margin-top: 4px;
+  font-family: monospace;
+  font-size: 12px;
+  padding: 4px;
+  background-color: #fff;
+  border-radius: 2px;
+}
+
+.token-info {
+  margin-top: 8px;
+  font-size: 12px;
+  color: #666;
+  text-align: right;
+}
+
+.edit-area {
+  margin-top: 8px;
+}
+
+.edit-actions {
+  margin-top: 8px;
+  text-align: right;
+}
+
+.edit-actions .ant-btn {
+  margin-left: 8px;
+}
+
+.input-area {
+  position: relative;
+}
+
+.input-actions {
+  position: absolute;
+  right: 8px;
+  bottom: 8px;
+  display: flex;
+  align-items: center;
+}
+
+.version-info {
+  font-size: 12px;
+  color: #666;
+  padding: 0 8px;
+  user-select: none;
+}
+
+.model-info {
+  font-size: 12px;
+  color: #666;
+  background-color: #f0f0f0;
+  padding: 2px 6px;
+  border-radius: 4px;
+}
+
+.streaming-indicator {
+  font-size: 12px;
+  color: #1890ff;
+  animation: pulse 1.5s infinite;
+}
+
+@keyframes pulse {
+  0% {
+    opacity: 0.6;
+  }
+  50% {
+    opacity: 1;
+  }
+  100% {
+    opacity: 0.6;
+  }
+}

+ 392 - 0
dashboard-v4/dashboard/src/hooks/mockChatData.ts

@@ -0,0 +1,392 @@
+import {
+  MessageNode,
+  ChatState,
+  SessionInfo,
+  VersionInfo,
+} from "../types/chat";
+
+// 模拟的消息数据 - 包含完整的对话场景
+export const mockMessages: MessageNode[] = [
+  // System 消息 (根节点)
+  {
+    id: 1,
+    uid: "msg-system-001",
+    chat_id: "chat-001",
+    session_id: "system-session",
+    role: "system",
+    content:
+      "你是一个巴利语专家和佛教术语解释助手。当用户询问佛教术语时,你可以调用 searchTerm 函数来查询详细信息。",
+    is_active: true,
+    created_at: "2025-01-15T10:00:00Z",
+    updated_at: "2025-01-15T10:00:00Z",
+  },
+
+  // 第一轮对话 - 用户询问佛教术语
+  {
+    id: 2,
+    uid: "msg-user-001",
+    chat_id: "chat-001",
+    parent_id: "msg-system-001",
+    session_id: "session-001",
+    role: "user",
+    content: "什么是 dhamma?请详细解释一下。",
+    is_active: true,
+    created_at: "2025-01-15T10:01:00Z",
+    updated_at: "2025-01-15T10:01:00Z",
+  },
+
+  // AI 回答 - 带 Function Call
+  {
+    id: 3,
+    uid: "msg-assistant-001",
+    chat_id: "chat-001",
+    parent_id: "msg-user-001",
+    session_id: "session-001",
+    role: "assistant",
+    model_id: "gpt-4",
+    tool_calls: [
+      {
+        id: "call_dhamma_001",
+        function: "searchTerm",
+        arguments: { term: "dhamma" },
+      },
+    ],
+    is_active: true,
+    metadata: {
+      generation_params: {
+        temperature: 0.7,
+        max_tokens: 2048,
+      },
+    },
+    created_at: "2025-01-15T10:01:30Z",
+    updated_at: "2025-01-15T10:01:30Z",
+  },
+
+  // Tool 调用结果
+  {
+    id: 4,
+    uid: "msg-tool-001",
+    chat_id: "chat-001",
+    parent_id: "msg-assistant-001",
+    session_id: "session-001",
+    role: "tool",
+    content:
+      '{"term":"dhamma","definition":"法;教法;正义;真理","etymology":"来自梵语dharma","category":"佛教基本概念","explanation":"Dhamma是佛教中最核心的概念之一,指佛陀的教导、宇宙的法则以及存在的真理。"}',
+    tool_call_id: "call_dhamma_001",
+    is_active: true,
+    created_at: "2025-01-15T10:01:35Z",
+    updated_at: "2025-01-15T10:01:35Z",
+  },
+
+  // AI 最终回答
+  {
+    id: 5,
+    uid: "msg-assistant-002",
+    chat_id: "chat-001",
+    parent_id: "msg-tool-001",
+    session_id: "session-001",
+    role: "assistant",
+    content:
+      'Dhamma(法)是佛教中最重要的概念之一。根据巴利语词典,Dhamma有以下几层含义:\n\n1. **佛陀的教导**:指佛陀所传授的教法和智慧\n2. **宇宙法则**:指支配宇宙运行的自然规律\n3. **正义与真理**:指正确的行为准则和道德标准\n\nDhamma来自梵语"dharma",在不同语境下可以指代教法、法则、正义、真理等。对于佛教修行者来说,学习和实践Dhamma是走向解脱的根本途径。',
+    model_id: "gpt-4",
+    is_active: true,
+    metadata: {
+      generation_params: {
+        temperature: 0.7,
+        max_tokens: 2048,
+      },
+      token_usage: {
+        prompt_tokens: 180,
+        completion_tokens: 220,
+        total_tokens: 400,
+      },
+      performance: {
+        response_time_ms: 1500,
+        first_token_time_ms: 600,
+      },
+    },
+    created_at: "2025-01-15T10:01:45Z",
+    updated_at: "2025-01-15T10:01:45Z",
+  },
+
+  // 第二轮对话 - 用户追问
+  {
+    id: 6,
+    uid: "msg-user-002",
+    chat_id: "chat-001",
+    parent_id: "msg-assistant-002",
+    session_id: "session-002",
+    role: "user",
+    content: "那 karma 和 dhamma 有什么关系呢?",
+    is_active: true,
+    created_at: "2025-01-15T10:02:00Z",
+    updated_at: "2025-01-15T10:02:00Z",
+  },
+
+  // AI 第二次回答(第一个版本)
+  {
+    id: 7,
+    uid: "msg-assistant-003",
+    chat_id: "chat-001",
+    parent_id: "msg-user-002",
+    session_id: "session-002",
+    role: "assistant",
+    content:
+      "Karma(业)和 Dhamma(法)是密切相关的佛教概念:\n\n**Karma(业力法则)**是 Dhamma 的重要组成部分。Karma 描述了行为与后果之间的因果关系,而这个因果法则本身就是宇宙运行的 Dhamma 之一。\n\n简单来说:\n- Dhamma 是更大的框架,包含了所有佛教教义和宇宙法则\n- Karma 是 Dhamma 中的具体法则之一,专门解释行为的因果关系\n\n通过理解 Karma,我们能更好地实践 Dhamma;通过实践 Dhamma,我们能更好地净化 Karma。",
+    model_id: "gpt-4",
+    is_active: true,
+    metadata: {
+      generation_params: {
+        temperature: 0.7,
+        max_tokens: 2048,
+      },
+      token_usage: {
+        prompt_tokens: 420,
+        completion_tokens: 150,
+        total_tokens: 570,
+      },
+    },
+    created_at: "2025-01-15T10:02:15Z",
+    updated_at: "2025-01-15T10:02:15Z",
+  },
+
+  // 第二个版本的AI回答(用户点了刷新)
+  {
+    id: 8,
+    uid: "msg-assistant-004",
+    chat_id: "chat-001",
+    parent_id: "msg-user-002",
+    session_id: "session-002",
+    role: "assistant",
+    content:
+      "Karma 和 Dhamma 的关系可以这样理解:\n\n1. **包含关系**:Karma 是 Dhamma 的一部分。Dhamma 是整个佛教教法体系,而 Karma 是其中的核心法则\n\n2. **实践关系**:\n   - 学习 Dhamma → 了解 Karma 的运作原理\n   - 正确理解 Karma → 能够更好地实践 Dhamma\n\n3. **目标一致**:两者都指向同一目标 - 通过正确的认知和行为获得解脱\n\n可以说,Karma 是 Dhamma 在日常生活中的具体体现,而 Dhamma 为理解 Karma 提供了更深层的智慧框架。",
+    model_id: "claude-3-sonnet",
+    is_active: false, // 这是第二个版本,当前不激活
+    metadata: {
+      generation_params: {
+        temperature: 0.8,
+        max_tokens: 2048,
+      },
+      token_usage: {
+        prompt_tokens: 420,
+        completion_tokens: 180,
+        total_tokens: 600,
+      },
+    },
+    created_at: "2025-01-15T10:03:00Z",
+    updated_at: "2025-01-15T10:03:00Z",
+  },
+
+  // 第三轮对话 - 用户编辑了第一个问题
+  {
+    id: 9,
+    uid: "msg-user-003",
+    chat_id: "chat-001",
+    parent_id: "msg-system-001", // 回到系统消息后
+    session_id: "session-003",
+    role: "user",
+    content: "请解释一下佛教中的 nirvana 概念,它与 dhamma 有什么关系?", // 用户编辑了第一个问题
+    is_active: false, // 用户当前查看的是第一条线路,这条是备选线路
+    created_at: "2025-01-15T10:04:00Z",
+    updated_at: "2025-01-15T10:04:00Z",
+  },
+
+  {
+    id: 10,
+    uid: "msg-assistant-005",
+    chat_id: "chat-001",
+    parent_id: "msg-user-003",
+    session_id: "session-003",
+    role: "assistant",
+    content:
+      'Nirvana(涅槃)是佛教修行的最终目标,与 Dhamma 有着密不可分的关系:\n\n**Nirvana 的含义**:\n- 字面意思是"熄灭",指贪嗔痴等烦恼的彻底熄灭\n- 是痛苦和轮回的终结,达到究竟的解脱状态\n\n**与 Dhamma 的关系**:\n1. **Dhamma 是道路**:佛陀的教法为我们指明了通向 Nirvana 的道路\n2. **Nirvana 是目标**:修行 Dhamma 的最终目的就是证得 Nirvana\n3. **相互验证**:真正的 Dhamma 必然能导向 Nirvana;真正的 Nirvana 必然符合 Dhamma\n\n可以说,没有 Dhamma 就无法理解和达到 Nirvana,而 Nirvana 是 Dhamma 价值的最终体现。',
+    model_id: "gpt-4",
+    is_active: false,
+    metadata: {
+      generation_params: {
+        temperature: 0.7,
+      },
+      token_usage: {
+        prompt_tokens: 150,
+        completion_tokens: 200,
+        total_tokens: 350,
+      },
+    },
+    created_at: "2025-01-15T10:04:20Z",
+    updated_at: "2025-01-15T10:04:20Z",
+  },
+];
+
+// 模拟版本信息
+export const mockVersions: VersionInfo[] = [
+  {
+    version_index: 0,
+    model_id: "gpt-4",
+    model_name: "GPT-4",
+    created_at: "2025-01-15T10:02:15Z",
+    message_count: 1,
+    token_usage: 570,
+  },
+  {
+    version_index: 1,
+    model_id: "claude-3-sonnet",
+    model_name: "Claude 3 Sonnet",
+    created_at: "2025-01-15T10:03:00Z",
+    message_count: 1,
+    token_usage: 600,
+  },
+];
+
+// 模拟会话组信息
+export const mockSessionGroups: SessionInfo[] = [
+  {
+    session_id: "session-001",
+    messages: [
+      mockMessages[1], // user message
+      mockMessages[2], // assistant with tool calls
+      mockMessages[3], // tool result
+      mockMessages[4], // final assistant response
+    ],
+    versions: [
+      {
+        version_index: 0,
+        model_id: "gpt-4",
+        created_at: "2025-01-15T10:01:45Z",
+        message_count: 3,
+        token_usage: 400,
+      },
+    ],
+    current_version: 0,
+    user_message: mockMessages[1],
+    ai_messages: [mockMessages[2], mockMessages[3], mockMessages[4]],
+  },
+  {
+    session_id: "session-002",
+    messages: [
+      mockMessages[5], // user message
+      mockMessages[6], // assistant response (active version)
+    ],
+    versions: mockVersions,
+    current_version: 0,
+    user_message: mockMessages[5],
+    ai_messages: [mockMessages[6]],
+  },
+];
+
+// 模拟完整的聊天状态
+export const mockChatState: ChatState = {
+  chat_id: "chat-001",
+  title: "佛教术语学习对话",
+  raw_messages: mockMessages,
+  active_path: [
+    mockMessages[0], // system
+    mockMessages[1], // user 1
+    mockMessages[2], // assistant 1 (with tool calls)
+    mockMessages[3], // tool result
+    mockMessages[4], // assistant final
+    mockMessages[5], // user 2
+    mockMessages[6], // assistant 2 (active version)
+  ],
+  session_groups: mockSessionGroups,
+  pending_messages: [],
+  is_loading: false,
+  current_model: "gpt-4",
+  streaming_message: undefined,
+  streaming_session_id: undefined,
+};
+
+// 模拟带有待发送消息的状态
+export const mockChatStateWithPending: ChatState = {
+  ...mockChatState,
+  pending_messages: [
+    {
+      temp_id: "temp_123456",
+      session_id: "session_temp_001",
+      messages: [
+        {
+          id: 0,
+          uid: "temp_user_123456",
+          temp_id: "temp_123456",
+          chat_id: "chat-001",
+          parent_id: "msg-assistant-002",
+          session_id: "session_temp_001",
+          role: "user",
+          content: "佛教中的八正道具体是什么?",
+          is_active: true,
+          save_status: "pending",
+          created_at: new Date().toISOString(),
+          updated_at: new Date().toISOString(),
+        },
+      ],
+      retry_count: 0,
+      created_at: new Date().toISOString(),
+    },
+  ],
+  is_loading: true,
+};
+
+// 模拟失败状态的数据
+export const mockChatStateWithError: ChatState = {
+  ...mockChatState,
+  pending_messages: [
+    {
+      temp_id: "temp_failed_001",
+      session_id: "session_failed_001",
+      messages: [
+        {
+          id: 0,
+          uid: "temp_user_failed",
+          temp_id: "temp_failed_001",
+          chat_id: "chat-001",
+          parent_id: "msg-assistant-002",
+          session_id: "session_failed_001",
+          role: "user",
+          content: "什么是禅宗?",
+          is_active: true,
+          save_status: "failed",
+          created_at: new Date().toISOString(),
+          updated_at: new Date().toISOString(),
+        },
+      ],
+      retry_count: 2,
+      error: "API调用失败,请重试",
+      created_at: new Date().toISOString(),
+    },
+  ],
+  error: "API调用失败,请重试",
+};
+
+// 工具函数:根据 session_id 过滤消息
+export function getMessagesBySession(sessionId: string): MessageNode[] {
+  return mockMessages.filter(
+    (msg) => msg.session_id === sessionId && msg.is_active
+  );
+}
+
+// 工具函数:获取激活路径
+export function getActivePath(): MessageNode[] {
+  return mockMessages.filter((msg) => msg.is_active);
+}
+
+// 工具函数:模拟API响应格式
+export function createMockApiResponse<T>(data: T) {
+  return {
+    ok: true,
+    message: "success",
+    data,
+  };
+}
+
+// 导出所有mock数据
+const mock = {
+  messages: mockMessages,
+  sessions: mockSessionGroups,
+  chatState: mockChatState,
+  chatStateWithPending: mockChatStateWithPending,
+  chatStateWithError: mockChatStateWithError,
+  versions: mockVersions,
+  getMessagesBySession,
+  getActivePath,
+  createMockApiResponse,
+};
+export default mock;

+ 31 - 0
dashboard-v4/dashboard/src/hooks/useActivePath.ts

@@ -0,0 +1,31 @@
+import { useMemo, useCallback } from "react";
+import { MessageNode } from "../types/chat";
+
+export function useActivePath(rawMessages: MessageNode[]) {
+  const computeActivePath = useCallback(() => {
+    // 从system消息开始,沿着is_active=true的路径构建激活链
+    const messageMap = new Map(rawMessages.map((m) => [m.uid, m]));
+    const activePath: MessageNode[] = [];
+
+    // 找到system消息(根节点)
+    const systemMsg = rawMessages.find(
+      (m) => m.role === "system" && !m.parent_id && m.is_active
+    );
+    if (!systemMsg) return [];
+
+    // 沿着激活路径构建链
+    let current: MessageNode | undefined = systemMsg;
+    while (current) {
+      activePath.push(current);
+
+      // 找到当前消息的激活子消息
+      current = rawMessages.find(
+        (m) => m.parent_id === current?.uid && m.is_active
+      );
+    }
+
+    return activePath;
+  }, [rawMessages]);
+
+  return useMemo(() => computeActivePath(), [computeActivePath]);
+}

+ 539 - 0
dashboard-v4/dashboard/src/hooks/useChatData.ts

@@ -0,0 +1,539 @@
+import { useState, useCallback, useMemo } from "react";
+import {
+  MessageNode,
+  ChatState,
+  ChatActions,
+  PendingMessage,
+} from "../types/chat";
+
+import { useActivePath } from "./useActivePath";
+import { useSessionGroups } from "./useSessionGroups";
+import { messageApi } from "../services/messageApi";
+import { getModelAdapter } from "../services/modelAdapters";
+
+export function useChatData(chatId: string): {
+  chatState: ChatState;
+  actions: ChatActions;
+} {
+  const [rawMessages, setRawMessages] = useState<MessageNode[]>([]);
+  const [pendingMessages, setPendingMessages] = useState<PendingMessage[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [streamingMessage, setStreamingMessage] = useState<string>();
+  const [streamingSessionId, setStreamingSessionId] = useState<string>();
+  const [currentModel, setCurrentModel] = useState("gpt-4");
+  const [error, setError] = useState<string>();
+
+  // 合并已保存和待保存的消息用于显示
+  const allMessages = useMemo(() => {
+    const pending = pendingMessages.flatMap((p) => p.messages);
+    return [...rawMessages, ...pending];
+  }, [rawMessages, pendingMessages]);
+
+  const activePath = useActivePath(allMessages);
+  const sessionGroups = useSessionGroups(activePath, allMessages);
+
+  // 加载消息列表
+  const loadMessages = useCallback(async () => {
+    try {
+      setIsLoading(true);
+      const response = await messageApi.getMessages(chatId);
+      setRawMessages(response.data.rows);
+    } catch (err) {
+      setError(err instanceof Error ? err.message : "加载消息失败");
+    } finally {
+      setIsLoading(false);
+    }
+  }, [chatId]);
+
+  // 构建对话历史(用于AI API调用)
+  const buildConversationHistory = useCallback(
+    (baseMessages: MessageNode[], newUserMessage?: MessageNode) => {
+      const history = activePath
+        .filter((m) => m.role !== "tool") // 排除tool消息
+        .map((m) => ({
+          role: m.role as any,
+          content: m.content || "",
+          tool_calls: m.tool_calls,
+          tool_call_id: m.tool_call_id,
+        }));
+
+      if (newUserMessage) {
+        history.push({
+          role: "user",
+          content: newUserMessage.content || "",
+        });
+      }
+
+      return history;
+    },
+    [activePath]
+  );
+
+  // 发送消息给AI并处理响应
+  const sendMessageToAI = useCallback(
+    async (userMessage: MessageNode, pendingGroup: PendingMessage) => {
+      try {
+        setIsLoading(true);
+        setStreamingSessionId(pendingGroup.session_id);
+
+        const conversationHistory = buildConversationHistory(
+          rawMessages,
+          userMessage
+        );
+        const adapter = getModelAdapter(currentModel);
+
+        // 处理Function Call的循环逻辑
+        let currentMessages = conversationHistory;
+        let maxIterations = 10;
+        let allAiMessages: MessageNode[] = [];
+
+        while (maxIterations-- > 0) {
+          // 流式处理AI响应
+          let responseContent = "";
+          let functionCalls: any[] = [];
+          let metadata: any = {};
+
+          const streamResponse = await adapter.sendMessage(currentMessages, {
+            temperature: 0.7,
+            max_tokens: 2048,
+          });
+
+          // 模拟流式输出处理
+          await new Promise((resolve, reject) => {
+            const processStream = async () => {
+              try {
+                // 这里应该是实际的流处理逻辑
+                for await (const chunk of streamResponse) {
+                  const parsed = adapter.parseStreamChunk(chunk);
+                  if (parsed?.content) {
+                    responseContent += parsed.content;
+                    setStreamingMessage(responseContent);
+                  }
+                  if (parsed?.function_call) {
+                    // 处理function call
+                  }
+                }
+                resolve(undefined);
+              } catch (err) {
+                reject(err);
+              }
+            };
+            processStream();
+          });
+
+          // 创建AI响应消息
+          const aiMessage: MessageNode = {
+            id: 0,
+            uid: `temp_ai_${pendingGroup.temp_id}_${allAiMessages.length}`,
+            temp_id: pendingGroup.temp_id,
+            chat_id: chatId,
+            session_id: pendingGroup.session_id,
+            parent_id:
+              allAiMessages.length === 0
+                ? userMessage.uid
+                : allAiMessages[allAiMessages.length - 1].uid,
+            role: "assistant",
+            content: responseContent,
+            model_id: currentModel,
+            tool_calls: functionCalls.length > 0 ? functionCalls : undefined,
+            metadata,
+            is_active: true,
+            save_status: "pending",
+            created_at: new Date().toISOString(),
+            updated_at: new Date().toISOString(),
+          };
+
+          allAiMessages.push(aiMessage);
+
+          // 如果有function calls,处理它们
+          if (functionCalls.length > 0) {
+            const toolResults = await Promise.all(
+              functionCalls.map((call) => adapter.handleFunctionCall(call))
+            );
+
+            const toolMessages = functionCalls.map((call, index) => ({
+              id: 0,
+              uid: `temp_tool_${pendingGroup.temp_id}_${index}`,
+              temp_id: pendingGroup.temp_id,
+              chat_id: chatId,
+              session_id: pendingGroup.session_id,
+              parent_id: aiMessage.uid,
+              role: "tool" as const,
+              content: JSON.stringify(toolResults[index]),
+              tool_call_id: call.id,
+              is_active: true,
+              save_status: "pending" as const,
+              created_at: new Date().toISOString(),
+              updated_at: new Date().toISOString(),
+            }));
+
+            allAiMessages.push(...toolMessages);
+
+            // 更新对话历史,继续循环
+            currentMessages.push(
+              {
+                role: "assistant",
+                content: responseContent,
+                tool_calls: functionCalls,
+              },
+              ...toolMessages.map((tm) => ({
+                role: "tool" as const,
+                content: tm.content || "",
+                tool_call_id: tm.tool_call_id,
+              }))
+            );
+
+            continue;
+          }
+
+          // 没有function call,结束循环
+          break;
+        }
+
+        // 更新pending消息组
+        setPendingMessages((prev) =>
+          prev.map((p) =>
+            p.temp_id === pendingGroup.temp_id
+              ? { ...p, messages: [...p.messages, ...allAiMessages] }
+              : p
+          )
+        );
+
+        // 保存整个消息组到数据库
+        await saveMessageGroup(pendingGroup.temp_id, [
+          userMessage,
+          ...allAiMessages,
+        ]);
+      } catch (err) {
+        console.error("AI响应失败:", err);
+        setPendingMessages((prev) =>
+          prev.map((p) =>
+            p.temp_id === pendingGroup.temp_id
+              ? {
+                  ...p,
+                  error: err instanceof Error ? err.message : "未知错误",
+                  retry_count: p.retry_count + 1,
+                }
+              : p
+          )
+        );
+      } finally {
+        setIsLoading(false);
+        setStreamingMessage(undefined);
+        setStreamingSessionId(undefined);
+      }
+    },
+    [rawMessages, currentModel, chatId, buildConversationHistory]
+  );
+
+  // 保存消息组到数据库
+  const saveMessageGroup = useCallback(
+    async (tempId: string, messages: MessageNode[]) => {
+      try {
+        const savedMessages = await messageApi.createMessages(chatId, {
+          messages: messages.map((m) => ({
+            parent_id: m.parent_id,
+            role: m.role as any,
+            content: m.content,
+            model_id: m.model_id,
+            tool_calls: m.tool_calls,
+            tool_call_id: m.tool_call_id,
+            metadata: m.metadata,
+          })),
+        });
+
+        // 更新本地状态:移除pending,添加到已保存消息
+        setPendingMessages((prev) => prev.filter((p) => p.temp_id !== tempId));
+        setRawMessages((prev) => [...prev, ...savedMessages.data]);
+      } catch (err) {
+        console.error("保存消息组失败:", err);
+        setPendingMessages((prev) =>
+          prev.map((p) =>
+            p.temp_id === tempId
+              ? {
+                  ...p,
+                  error: err instanceof Error ? err.message : "保存失败",
+                  messages: p.messages.map((m) => ({
+                    ...m,
+                    save_status: "failed" as const,
+                  })),
+                }
+              : p
+          )
+        );
+      }
+    },
+    [chatId]
+  );
+
+  // 编辑消息 - 创建新版本
+  const editMessage = useCallback(
+    async (
+      sessionId: string,
+      content: string,
+      role: "user" | "assistant" = "user"
+    ) => {
+      const tempId = `temp_${Date.now()}`;
+
+      try {
+        // 找到要编辑的消息的父消息
+        let parentId: string | undefined;
+
+        if (sessionId === "new") {
+          // 新消息,找到最后一个激活消息作为父消息
+          const lastMessage = activePath[activePath.length - 1];
+          parentId = lastMessage?.uid;
+        } else {
+          // 编辑现有session,找到该session的父消息
+          const sessionMessages = activePath.filter(
+            (m) => m.session_id === sessionId
+          );
+          const firstMessage = sessionMessages[0];
+          parentId = firstMessage?.parent_id;
+        }
+
+        const newSessionId =
+          sessionId === "new" ? `session_${tempId}` : `session_${tempId}`;
+
+        // 创建新的用户消息
+        const newUserMessage: MessageNode = {
+          id: 0,
+          uid: `temp_user_${tempId}`,
+          temp_id: tempId,
+          chat_id: chatId,
+          parent_id: parentId,
+          session_id: newSessionId,
+          role: "user",
+          content,
+          is_active: true,
+          save_status: "pending",
+          created_at: new Date().toISOString(),
+          updated_at: new Date().toISOString(),
+        };
+
+        // 创建待保存消息组
+        const pendingGroup: PendingMessage = {
+          temp_id: tempId,
+          session_id: newSessionId,
+          messages: [newUserMessage],
+          retry_count: 0,
+          created_at: new Date().toISOString(),
+        };
+
+        setPendingMessages((prev) => [...prev, pendingGroup]);
+
+        // 如果是用户消息,发送给AI
+        if (role === "user") {
+          await sendMessageToAI(newUserMessage, pendingGroup);
+        }
+      } catch (err) {
+        console.error("编辑消息失败:", err);
+        setPendingMessages((prev) =>
+          prev.map((p) =>
+            p.temp_id === tempId
+              ? {
+                  ...p,
+                  messages: p.messages.map((m) => ({
+                    ...m,
+                    save_status: "failed" as const,
+                  })),
+                  error: err instanceof Error ? err.message : "编辑失败",
+                }
+              : p
+          )
+        );
+      }
+    },
+    [chatId, activePath, sendMessageToAI]
+  );
+
+  // 重试失败的消息
+  const retryMessage = useCallback(
+    async (tempId: string) => {
+      const pendingGroup = pendingMessages.find((p) => p.temp_id === tempId);
+      if (!pendingGroup) return;
+
+      const userMessage = pendingGroup.messages.find((m) => m.role === "user");
+      if (!userMessage) return;
+
+      // 重置状态并重试
+      setPendingMessages((prev) =>
+        prev.map((p) =>
+          p.temp_id === tempId
+            ? {
+                ...p,
+                messages: [{ ...userMessage, save_status: "pending" }],
+                error: undefined,
+              }
+            : p
+        )
+      );
+
+      await sendMessageToAI(userMessage, {
+        ...pendingGroup,
+        messages: [userMessage],
+      });
+    },
+    [pendingMessages, sendMessageToAI]
+  );
+
+  // 切换版本
+  const switchVersion = useCallback(
+    async (sessionId: string, versionIndex: number) => {
+      try {
+        // 找到指定版本的消息
+        const sessionMessages = rawMessages.filter(
+          (m) => m.session_id === sessionId
+        );
+        const versions =
+          sessionGroups.find((sg) => sg.session_id === sessionId)?.versions ||
+          [];
+
+        if (versionIndex >= versions.length) return;
+
+        const targetVersion = versions[versionIndex];
+        const versionMessages = sessionMessages.filter(
+          (m) => m.created_at === targetVersion.created_at
+        );
+
+        // 调用API更新激活状态
+        await messageApi.switchVersion(
+          chatId,
+          versionMessages.map((m) => m.uid)
+        );
+
+        // 重新加载数据
+        await loadMessages();
+      } catch (err) {
+        console.error("切换版本失败:", err);
+        setError(err instanceof Error ? err.message : "切换版本失败");
+      }
+    },
+    [rawMessages, sessionGroups, chatId, loadMessages]
+  );
+
+  // 刷新AI回答
+  const refreshResponse = useCallback(
+    async (sessionId: string, modelId?: string) => {
+      const session = sessionGroups.find((sg) => sg.session_id === sessionId);
+      if (!session?.user_message) return;
+
+      // 使用指定的模型或当前模型
+      const useModel = modelId || currentModel;
+      const tempId = `temp_refresh_${Date.now()}`;
+
+      try {
+        // 创建基于原用户消息的新AI回答
+        const userMsg = session.user_message;
+        const newSessionId = `session_${tempId}`;
+
+        const pendingGroup: PendingMessage = {
+          temp_id: tempId,
+          session_id: newSessionId,
+          messages: [
+            {
+              ...userMsg,
+              temp_id: tempId,
+              session_id: newSessionId,
+              save_status: "pending",
+            },
+          ],
+          retry_count: 0,
+          created_at: new Date().toISOString(),
+        };
+
+        setPendingMessages((prev) => [...prev, pendingGroup]);
+
+        // 发送给AI获取新回答
+        await sendMessageToAI(pendingGroup.messages[0], pendingGroup);
+      } catch (err) {
+        console.error("刷新回答失败:", err);
+        setError(err instanceof Error ? err.message : "刷新失败");
+      }
+    },
+    [sessionGroups, currentModel, sendMessageToAI]
+  );
+
+  // 消息操作功能
+  const likeMessage = useCallback(async (messageId: string) => {
+    try {
+      await messageApi.likeMessage(messageId);
+      // 可以添加本地状态更新
+    } catch (err) {
+      console.error("点赞失败:", err);
+    }
+  }, []);
+
+  const dislikeMessage = useCallback(async (messageId: string) => {
+    try {
+      await messageApi.dislikeMessage(messageId);
+      // 可以添加本地状态更新
+    } catch (err) {
+      console.error("点踩失败:", err);
+    }
+  }, []);
+
+  const copyMessage = useCallback(
+    (messageId: string) => {
+      const message = allMessages.find((m) => m.uid === messageId);
+      if (message?.content) {
+        navigator.clipboard.writeText(message.content);
+      }
+    },
+    [allMessages]
+  );
+
+  const shareMessage = useCallback(
+    async (messageId: string): Promise<string> => {
+      try {
+        const response = await messageApi.shareMessage(messageId);
+        return response.data.shareUrl;
+      } catch (err) {
+        console.error("分享失败:", err);
+        throw err;
+      }
+    },
+    []
+  );
+
+  const deleteMessage = useCallback(
+    async (messageId: string) => {
+      try {
+        await messageApi.deleteMessage(messageId);
+        await loadMessages(); // 重新加载数据
+      } catch (err) {
+        console.error("删除失败:", err);
+        setError(err instanceof Error ? err.message : "删除失败");
+      }
+    },
+    [loadMessages]
+  );
+
+  return {
+    chatState: {
+      chat_id: chatId,
+      title: "", // 可以从props传入或另行管理
+      raw_messages: rawMessages,
+      active_path: activePath,
+      session_groups: sessionGroups,
+      pending_messages: pendingMessages,
+      is_loading: isLoading,
+      streaming_message: streamingMessage,
+      streaming_session_id: streamingSessionId,
+      current_model: currentModel,
+      error,
+    },
+    actions: {
+      switchVersion,
+      editMessage,
+      retryMessage,
+      refreshResponse,
+      loadMessages,
+      likeMessage,
+      dislikeMessage,
+      copyMessage,
+      shareMessage,
+      deleteMessage,
+    },
+  };
+}

+ 114 - 0
dashboard-v4/dashboard/src/hooks/useSessionGroups.ts

@@ -0,0 +1,114 @@
+import { useMemo, useCallback } from "react";
+import { MessageNode, SessionInfo, VersionInfo } from "../types/chat";
+
+export function useSessionGroups(
+  activePath: MessageNode[],
+  rawMessages: MessageNode[]
+) {
+  const computeSessionVersions = useCallback(
+    (sessionId: string): VersionInfo[] => {
+      // 找到该session的所有消息
+      const sessionMessages = rawMessages.filter(
+        (m) => m.session_id === sessionId
+      );
+
+      // 按不同的创建时间和父消息分组,计算版本
+      const versionMap = new Map<string, MessageNode[]>();
+
+      sessionMessages.forEach((msg) => {
+        // 使用第一个AI消息的创建时间作为版本标识
+        const firstAiMsg = sessionMessages
+          .filter((m) => m.role === "assistant")
+          .sort((a, b) => a.id - b.id)[0];
+
+        const versionKey = firstAiMsg ? firstAiMsg.created_at : msg.created_at;
+
+        if (!versionMap.has(versionKey)) {
+          versionMap.set(versionKey, []);
+        }
+        versionMap.get(versionKey)!.push(msg);
+      });
+
+      // 转换为VersionInfo数组
+      const versions: VersionInfo[] = Array.from(versionMap.entries())
+        .map(([timestamp, messages], index) => {
+          const aiMessage = messages.find((m) => m.role === "assistant");
+          return {
+            version_index: index,
+            model_id: aiMessage?.model_id,
+            created_at: timestamp,
+            message_count: messages.length,
+            token_usage: aiMessage?.metadata?.token_usage?.total_tokens,
+          };
+        })
+        .sort(
+          (a, b) =>
+            new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
+        );
+
+      return versions;
+    },
+    [rawMessages]
+  );
+
+  const findCurrentVersion = useCallback(
+    (sessionMessages: MessageNode[], versions: VersionInfo[]): number => {
+      // 找到当前激活的AI消息
+      const activeAiMsg = sessionMessages.find(
+        (m) => m.role === "assistant" && m.is_active
+      );
+      if (!activeAiMsg) return 0;
+
+      // 根据创建时间找到对应的版本索引
+      const versionIndex = versions.findIndex(
+        (v) => v.created_at === activeAiMsg.created_at
+      );
+      return Math.max(0, versionIndex);
+    },
+    []
+  );
+
+  const computeSessionGroups = useCallback((): SessionInfo[] => {
+    const sessionMap = new Map<string, MessageNode[]>();
+
+    // 按session_id分组激活路径上的消息(排除system消息)
+    activePath.forEach((msg) => {
+      if (msg.role !== "system") {
+        const sessionId = msg.session_id;
+        if (!sessionMap.has(sessionId)) {
+          sessionMap.set(sessionId, []);
+        }
+        sessionMap.get(sessionId)!.push(msg);
+      }
+    });
+
+    // 为每个session计算版本信息
+    const sessionGroups: SessionInfo[] = [];
+
+    sessionMap.forEach((messages, sessionId) => {
+      const versions = computeSessionVersions(sessionId);
+      const currentVersion = findCurrentVersion(messages, versions);
+
+      const userMessage = messages.find((m) => m.role === "user");
+      const aiMessages = messages.filter((m) => m.role !== "user");
+
+      sessionGroups.push({
+        session_id: sessionId,
+        messages,
+        versions,
+        current_version: currentVersion,
+        user_message: userMessage,
+        ai_messages: aiMessages,
+      });
+    });
+
+    // 按消息ID排序,保证显示顺序
+    return sessionGroups.sort((a, b) => {
+      const aFirstId = Math.min(...a.messages.map((m) => m.id));
+      const bFirstId = Math.min(...b.messages.map((m) => m.id));
+      return aFirstId - bFirstId;
+    });
+  }, [activePath, computeSessionVersions, findCurrentVersion]);
+
+  return useMemo(() => computeSessionGroups(), [computeSessionGroups]);
+}

+ 38 - 0
dashboard-v4/dashboard/src/services/chatApi.ts

@@ -0,0 +1,38 @@
+import { CreateChatRequest, ChatResponse, ApiResponse } from "../types/chat";
+
+export const chatApi = {
+  async createChat(
+    request: CreateChatRequest
+  ): Promise<ApiResponse<ChatResponse>> {
+    const response = await fetch("/api/v2/chats", {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify(request),
+    });
+    return response.json();
+  },
+
+  async getChat(chatId: string): Promise<ApiResponse<ChatResponse>> {
+    const response = await fetch(`/api/v2/chats/${chatId}`);
+    return response.json();
+  },
+
+  async updateChat(
+    chatId: string,
+    updates: Partial<CreateChatRequest>
+  ): Promise<ApiResponse<ChatResponse>> {
+    const response = await fetch(`/api/v2/chats/${chatId}`, {
+      method: "PUT",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify(updates),
+    });
+    return response.json();
+  },
+
+  async deleteChat(chatId: string): Promise<ApiResponse<void>> {
+    const response = await fetch(`/api/v2/chats/${chatId}`, {
+      method: "DELETE",
+    });
+    return response.json();
+  },
+};

+ 73 - 0
dashboard-v4/dashboard/src/services/messageApi.ts

@@ -0,0 +1,73 @@
+import {
+  CreateMessageRequest,
+  MessageListResponse,
+  ApiResponse,
+  MessageNode,
+} from "../types/chat";
+
+export const messageApi = {
+  async getMessages(chatId: string): Promise<ApiResponse<MessageListResponse>> {
+    const response = await fetch(`/api/v2/chat-messages?chat=${chatId}`);
+    return response.json();
+  },
+
+  async createMessages(
+    chatId: string,
+    request: CreateMessageRequest
+  ): Promise<ApiResponse<MessageNode[]>> {
+    const response = await fetch("/api/v2/chat-messages", {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({
+        chat_id: chatId,
+        ...request,
+      }),
+    });
+    return response.json();
+  },
+
+  async switchVersion(
+    chatId: string,
+    messageUids: string[]
+  ): Promise<ApiResponse<void>> {
+    const response = await fetch("/api/v2/chat-messages/switch-version", {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({
+        chat_id: chatId,
+        message_uids: messageUids,
+      }),
+    });
+    return response.json();
+  },
+
+  async likeMessage(messageId: string): Promise<ApiResponse<void>> {
+    const response = await fetch(`/api/v2/chat-messages/${messageId}/like`, {
+      method: "POST",
+    });
+    return response.json();
+  },
+
+  async dislikeMessage(messageId: string): Promise<ApiResponse<void>> {
+    const response = await fetch(`/api/v2/chat-messages/${messageId}/dislike`, {
+      method: "POST",
+    });
+    return response.json();
+  },
+
+  async shareMessage(
+    messageId: string
+  ): Promise<ApiResponse<{ shareUrl: string }>> {
+    const response = await fetch(`/api/v2/chat-messages/${messageId}/share`, {
+      method: "POST",
+    });
+    return response.json();
+  },
+
+  async deleteMessage(messageId: string): Promise<ApiResponse<void>> {
+    const response = await fetch(`/api/v2/chat-messages/${messageId}`, {
+      method: "DELETE",
+    });
+    return response.json();
+  },
+};

+ 46 - 0
dashboard-v4/dashboard/src/services/modelAdapters/base.ts

@@ -0,0 +1,46 @@
+import {
+  ModelAdapter,
+  OpenAIMessage,
+  SendOptions,
+  ParsedChunk,
+  ToolCall,
+} from "../../types/chat";
+
+export abstract class BaseModelAdapter implements ModelAdapter {
+  abstract name: string;
+  abstract supportsFunctionCall: boolean;
+
+  abstract sendMessage(
+    messages: OpenAIMessage[],
+    options: SendOptions
+  ): Promise<AsyncIterable<string>>;
+  abstract parseStreamChunk(chunk: string): ParsedChunk | null;
+  abstract handleFunctionCall(functionCall: ToolCall): Promise<any>;
+
+  protected createStreamController() {
+    return {
+      addToken: (token: string) => {
+        // 流式输出控制逻辑
+      },
+      complete: () => {
+        // 完成处理逻辑
+      },
+    };
+  }
+
+  protected buildRequestPayload(
+    messages: OpenAIMessage[],
+    options: SendOptions
+  ) {
+    return {
+      model: this.name,
+      messages,
+      stream: true,
+      temperature: options.temperature || 0.7,
+      max_tokens: options.max_tokens || 2048,
+      top_p: options.top_p || 1,
+      functions: options.functions,
+      function_call: options.function_call || "auto",
+    };
+  }
+}

+ 20 - 0
dashboard-v4/dashboard/src/services/modelAdapters/index.ts

@@ -0,0 +1,20 @@
+import { ModelAdapter } from "../../types/chat";
+import { OpenAIAdapter } from "./openai";
+
+const adapters = new Map<string, ModelAdapter>();
+
+// 注册适配器
+adapters.set("gpt-4", new OpenAIAdapter());
+adapters.set("gpt-3.5-turbo", new OpenAIAdapter());
+
+export function getModelAdapter(modelId: string): ModelAdapter {
+  const adapter = adapters.get(modelId);
+  if (!adapter) {
+    throw new Error(`未找到模型适配器: ${modelId}`);
+  }
+  return adapter;
+}
+
+export function registerAdapter(modelId: string, adapter: ModelAdapter) {
+  adapters.set(modelId, adapter);
+}

+ 115 - 0
dashboard-v4/dashboard/src/services/modelAdapters/openai.ts

@@ -0,0 +1,115 @@
+import { BaseModelAdapter } from "./base";
+import {
+  OpenAIMessage,
+  SendOptions,
+  ParsedChunk,
+  ToolCall,
+} from "../../types/chat";
+
+export class OpenAIAdapter extends BaseModelAdapter {
+  name = "gpt-4";
+  supportsFunctionCall = true;
+
+  // 修改这个方法
+  async sendMessage(
+    messages: OpenAIMessage[],
+    options: SendOptions
+  ): Promise<AsyncIterable<string>> {
+    const payload = this.buildRequestPayload(messages, options);
+
+    return this.createStreamIterable(payload);
+  }
+
+  // 新增这个私有方法
+  private async *createStreamIterable(payload: any): AsyncIterable<string> {
+    const response = await fetch(process.env.REACT_APP_OPENAI_PROXY!, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+        Authorization: `Bearer ${process.env.REACT_APP_OPENAI_KEY}`,
+      },
+      body: JSON.stringify({
+        model_id: "gpt-4",
+        payload,
+      }),
+    });
+
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`);
+    }
+
+    const reader = response.body?.getReader();
+    if (!reader) {
+      throw new Error("无法获取响应流");
+    }
+
+    const decoder = new TextDecoder();
+    let buffer = "";
+
+    try {
+      while (true) {
+        const { done, value } = await reader.read();
+        if (done) break;
+
+        buffer += decoder.decode(value, { stream: true });
+        const lines = buffer.split("\n");
+        buffer = lines.pop() || "";
+
+        for (const line of lines) {
+          if (!line.trim() || !line.startsWith("data: ")) continue;
+          const data = line.slice(6);
+          if (data === "[DONE]") return;
+
+          yield data;
+        }
+      }
+    } finally {
+      reader.releaseLock();
+    }
+  }
+
+  // 其他方法保持不变
+  parseStreamChunk(chunk: string): ParsedChunk | null {
+    try {
+      const parsed = JSON.parse(chunk);
+      const delta = parsed.choices?.[0]?.delta;
+      const finishReason = parsed.choices?.[0]?.finish_reason;
+
+      return {
+        content: delta?.content,
+        function_call: delta?.function_call,
+        finish_reason: finishReason,
+      };
+    } catch {
+      return null;
+    }
+  }
+
+  async handleFunctionCall(functionCall: ToolCall): Promise<any> {
+    switch (functionCall.function) {
+      case "searchTerm":
+        return await this.searchTerm(functionCall.arguments.term);
+      case "getWeather":
+        return await this.getWeather(functionCall.arguments.city);
+      default:
+        throw new Error(`未知函数: ${functionCall.function}`);
+    }
+  }
+
+  private async searchTerm(term: string) {
+    const response = await fetch(
+      `/v2/search-pali-wbw?view=pali&key=${term}&limit=20&offset=0`
+    );
+    const result = await response.json();
+    return result.ok ? result.data.rows : { error: "搜索失败" };
+  }
+
+  private async getWeather(city: string) {
+    return {
+      city,
+      temperature: "25°C",
+      condition: "晴朗",
+      humidity: "60%",
+    };
+  }
+}

+ 256 - 0
dashboard-v4/dashboard/src/types/chat.ts

@@ -0,0 +1,256 @@
+// 工具调用相关类型
+export interface ToolCall {
+  id: string;
+  function: string;
+  arguments: Record<string, any>;
+}
+
+// 消息元数据
+export interface MessageMetadata {
+  generation_params?: {
+    temperature?: number;
+    max_tokens?: number;
+    top_p?: number;
+    frequency_penalty?: number;
+    presence_penalty?: number;
+  };
+  token_usage?: {
+    prompt_tokens?: number;
+    completion_tokens?: number;
+    total_tokens?: number;
+  };
+  performance?: {
+    response_time_ms?: number;
+    first_token_time_ms?: number;
+  };
+  tool_stats?: {
+    total_calls?: number;
+    successful_calls?: number;
+    execution_time_ms?: number;
+  };
+  custom_data?: Record<string, any>;
+}
+
+// 消息节点(对应数据库结构)
+export interface MessageNode {
+  id: number; // DB自增ID,用于版本排序
+  uid: string; // UUID
+  chat_id: string;
+  parent_id?: string;
+  session_id: string;
+  role: "system" | "user" | "assistant" | "tool";
+  content?: string;
+  model_id?: string;
+  tool_calls?: ToolCall[];
+  tool_call_id?: string;
+  metadata?: MessageMetadata;
+  is_active: boolean;
+  editor_id?: string;
+  created_at: string;
+  updated_at: string;
+  deleted_at?: string;
+
+  // 临时状态字段(前端使用)
+  save_status?: "saved" | "pending" | "failed";
+  temp_id?: string; // 临时ID,用于未保存消息
+}
+
+// 版本信息
+export interface VersionInfo {
+  version_index: number; // 版本索引(0,1,2...)
+  model_id?: string; // 该版本使用的模型
+  model_name?: string; // 模型显示名称
+  created_at: string; // 版本创建时间
+  message_count: number; // 该版本包含的消息数量
+  token_usage?: number; // 该版本的token使用量
+}
+
+// Session 信息
+export interface SessionInfo {
+  session_id: string;
+  messages: MessageNode[]; // 该session的所有消息(按激活路径过滤)
+  versions: VersionInfo[]; // 该session所有版本信息
+  current_version: number; // 当前显示的版本索引
+  user_message?: MessageNode; // 该session的用户消息(便于访问)
+  ai_messages: MessageNode[]; // 该session的AI消息列表
+}
+
+// 待保存消息组
+export interface PendingMessage {
+  temp_id: string;
+  session_id: string;
+  messages: MessageNode[]; // 待保存的消息组
+  retry_count: number;
+  error?: string;
+  created_at: string;
+}
+
+// 聊天状态
+export interface ChatState {
+  chat_id: string;
+  title: string;
+  raw_messages: MessageNode[]; // 从DB加载的原始线性数据
+  active_path: MessageNode[]; // 当前激活路径上的消息
+  session_groups: SessionInfo[]; // 按session分组的显示数据
+  pending_messages: PendingMessage[]; // 待保存的消息组
+  is_loading: boolean;
+  streaming_message?: string;
+  streaming_session_id?: string;
+  current_model: string;
+  error?: string;
+}
+
+// 聊天操作接口
+export interface ChatActions {
+  switchVersion: (sessionId: string, versionIndex: number) => Promise<void>;
+  editMessage: (
+    sessionId: string,
+    content: string,
+    role?: "user" | "assistant"
+  ) => Promise<void>;
+  retryMessage: (tempId: string) => Promise<void>;
+  refreshResponse: (sessionId: string, modelId?: string) => Promise<void>;
+  loadMessages: () => Promise<void>;
+  likeMessage: (messageId: string) => Promise<void>;
+  dislikeMessage: (messageId: string) => Promise<void>;
+  copyMessage: (messageId: string) => void;
+  shareMessage: (messageId: string) => Promise<string>;
+  deleteMessage: (messageId: string) => Promise<void>;
+}
+
+// API 请求类型
+export interface CreateMessageRequest {
+  messages: Array<{
+    parent_id?: string;
+    role: "user" | "assistant" | "tool";
+    content?: string;
+    model_id?: string;
+    tool_calls?: ToolCall[];
+    tool_call_id?: string;
+    metadata?: MessageMetadata;
+  }>;
+}
+
+export interface CreateChatRequest {
+  title: string;
+  user_id?: string;
+}
+
+// API 响应类型
+export interface ApiResponse<T> {
+  ok: boolean;
+  message: string;
+  data: T;
+}
+
+export interface ChatResponse {
+  id: string;
+  title: string;
+  user_id?: string;
+  created_at: string;
+  updated_at: string;
+}
+
+export interface MessageListResponse {
+  rows: MessageNode[];
+  total: number;
+}
+
+// 模型适配器相关类型
+/*
+export interface ModelAdapter {
+  name: string;
+  supportsFunctionCall: boolean;
+  sendMessage(
+    messages: OpenAIMessage[],
+    options: SendOptions
+  ): Promise<StreamResponse>;
+  parseStreamChunk(chunk: string): ParsedChunk | null;
+  handleFunctionCall(functionCall: ToolCall): Promise<any>;
+}
+*/
+export interface ModelAdapter {
+  name: string;
+  supportsFunctionCall: boolean;
+  sendMessage(
+    messages: OpenAIMessage[],
+    options: SendOptions
+  ): Promise<AsyncIterable<string>>; // 修改这里
+  parseStreamChunk(chunk: string): ParsedChunk | null;
+  handleFunctionCall(functionCall: ToolCall): Promise<any>;
+}
+export interface OpenAIMessage {
+  role: "system" | "user" | "assistant" | "function" | "tool";
+  content?: string;
+  name?: string;
+  tool_calls?: ToolCall[];
+  tool_call_id?: string;
+}
+
+export interface SendOptions {
+  temperature?: number;
+  max_tokens?: number;
+  top_p?: number;
+  functions?: Array<{
+    name: string;
+    description: string;
+    parameters: any;
+  }>;
+  function_call?: string | { name: string };
+}
+
+export interface StreamResponse {
+  messages: MessageNode[];
+  metadata?: MessageMetadata;
+}
+
+export interface ParsedChunk {
+  content?: string;
+  function_call?: {
+    name?: string;
+    arguments?: string;
+  };
+  finish_reason?: string;
+}
+
+// 组件 Props 类型
+export interface SessionGroupProps {
+  session: SessionInfo;
+  onVersionSwitch: (sessionId: string, versionIndex: number) => void;
+  onRefresh: (sessionId: string, modelId?: string) => void;
+  onEdit: (sessionId: string, content: string) => void;
+  onRetry?: (tempId: string) => void;
+  onLike?: (messageId: string) => void;
+  onDislike?: (messageId: string) => void;
+  onCopy?: (messageId: string) => void;
+  onShare?: (messageId: string) => Promise<string>;
+}
+
+export interface UserMessageProps {
+  message: MessageNode;
+  onEdit?: (content: string) => void;
+  onCopy?: () => void;
+}
+
+export interface AssistantMessageProps {
+  messages: MessageNode[];
+  onRefresh?: () => void;
+  onEdit?: (content: string) => void;
+  isPending?: boolean;
+  onLike?: (messageId: string) => void;
+  onDislike?: (messageId: string) => void;
+  onCopy?: (messageId: string) => void;
+  onShare?: (messageId: string) => Promise<string>;
+}
+
+export interface VersionSwitcherProps {
+  versions: VersionInfo[];
+  currentVersion: number;
+  onSwitch: (versionIndex: number) => void;
+}
+
+export interface ChatInputProps {
+  onSend: (content: string) => void;
+  disabled?: boolean;
+  placeholder?: string;
+}