From 7a769fab14d11564e8f5c49765af56e5a08393e0 Mon Sep 17 00:00:00 2001
From: root <953994191@qq.com>
Date: Sun, 26 Apr 2026 16:05:44 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=20SSE=20=E4=BA=8B?=
=?UTF-8?q?=E4=BB=B6=E7=B1=BB=E5=9E=8B=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=AE=8C?=
=?UTF-8?q?=E6=95=B4=20React=20=E7=BB=84=E4=BB=B6=E6=94=AF=E6=8C=81?=
=?UTF-8?q?=E6=80=9D=E8=80=83=E8=BF=87=E7=A8=8B=E3=80=81=E5=B7=A5=E5=85=B7?=
=?UTF-8?q?=E8=B0=83=E7=94=A8=E3=80=81=E4=BA=BA=E5=B7=A5=E5=AE=A1=E6=A0=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/app/agent/service.py | 105 +++++-
frontend/FRONTEND_GUIDE.md | 413 ++++++++++++++++++++++
frontend/src/components/ChatContainer.tsx | 379 ++++++++++++++++++++
frontend/src/components/useChat.ts | 272 ++++++++++++++
4 files changed, 1157 insertions(+), 12 deletions(-)
create mode 100644 frontend/FRONTEND_GUIDE.md
create mode 100644 frontend/src/components/ChatContainer.tsx
create mode 100644 frontend/src/components/useChat.ts
diff --git a/backend/app/agent/service.py b/backend/app/agent/service.py
index 8c0a5dc..0c86c90 100644
--- a/backend/app/agent/service.py
+++ b/backend/app/agent/service.py
@@ -109,6 +109,9 @@ class AIAgentService:
input_state = {"messages": [{"role": "user", "content": message}]}
context = GraphContext(user_id=user_id)
+ current_node = None
+ tool_calls_in_progress = {}
+
async for chunk in graph.astream(
input_state,
config=config,
@@ -123,35 +126,113 @@ class AIAgentService:
if chunk_type == "messages":
message_chunk, metadata = chunk["data"]
node_name = metadata.get("langgraph_node", "unknown")
+
+ # 检测节点变化,发送节点开始事件
+ if node_name != current_node:
+ if current_node:
+ yield {
+ "type": "node_end",
+ "node": current_node
+ }
+ yield {
+ "type": "node_start",
+ "node": node_name
+ }
+ current_node = node_name
+
+ # 处理消息内容
token_content = getattr(message_chunk, 'content', str(message_chunk))
reasoning_token = ""
if hasattr(message_chunk, 'additional_kwargs'):
reasoning_token = message_chunk.additional_kwargs.get("reasoning_content", "")
- processed_event = {
- "type": "llm_token",
- "node": node_name,
- "token": token_content,
- "reasoning_token": reasoning_token,
- "metadata": metadata
- }
+ # 处理思考过程
+ if reasoning_token:
+ processed_event = {
+ "type": "reasoning",
+ "node": node_name,
+ "content": reasoning_token
+ }
+ # 处理工具调用
+ elif hasattr(message_chunk, 'tool_calls') and message_chunk.tool_calls:
+ for tool_call in message_chunk.tool_calls:
+ tool_call_id = tool_call.get("id", "")
+ tool_name = tool_call.get("name", "")
+ tool_args = tool_call.get("args", {})
+
+ # 记录工具调用开始
+ if tool_call_id not in tool_calls_in_progress:
+ tool_calls_in_progress[tool_call_id] = {
+ "name": tool_name,
+ "args": tool_args
+ }
+ yield {
+ "type": "tool_call_start",
+ "tool": tool_name,
+ "args": tool_args,
+ "id": tool_call_id
+ }
+ # 处理普通 token
+ elif token_content:
+ processed_event = {
+ "type": "llm_token",
+ "node": node_name,
+ "content": token_content
+ }
+
elif chunk_type == "updates":
updates_data = chunk["data"]
serialized_data = self._serialize_value(updates_data)
+
+ # 检查是否有人工审核请求
+ if "review_pending" in serialized_data and serialized_data["review_pending"]:
+ review_id = serialized_data.get("review_id", "")
+ content_to_review = serialized_data.get("content_to_review", "")
+ yield {
+ "type": "human_review_request",
+ "review_id": review_id,
+ "content": content_to_review
+ }
+
+ # 检查是否有工具结果
+ if "messages" in serialized_data:
+ for msg in serialized_data["messages"]:
+ # 检测工具结果消息
+ if msg.get("role") == "tool":
+ tool_call_id = msg.get("tool_call_id", "")
+ tool_name = msg.get("name", "")
+ tool_output = msg.get("content", "")
+
+ if tool_call_id in tool_calls_in_progress:
+ yield {
+ "type": "tool_call_end",
+ "tool": tool_name,
+ "id": tool_call_id,
+ "result": tool_output
+ }
+ del tool_calls_in_progress[tool_call_id]
+
processed_event = {
"type": "state_update",
"data": serialized_data
}
- if "messages" in serialized_data:
- processed_event["messages"] = serialized_data["messages"]
+
elif chunk_type == "custom":
serialized_data = self._serialize_value(chunk["data"])
processed_event = {
"type": "custom",
"data": serialized_data
}
- else:
- continue
if processed_event:
- yield processed_event
\ No newline at end of file
+ yield processed_event
+
+ # 发送结束事件
+ if current_node:
+ yield {
+ "type": "node_end",
+ "node": current_node
+ }
+ yield {
+ "type": "done"
+ }
\ No newline at end of file
diff --git a/frontend/FRONTEND_GUIDE.md b/frontend/FRONTEND_GUIDE.md
new file mode 100644
index 0000000..257c4de
--- /dev/null
+++ b/frontend/FRONTEND_GUIDE.md
@@ -0,0 +1,413 @@
+# AI Agent 前端展示 - 完整实现文档
+
+## 一、概述
+
+本文档描述了 AI Agent 对话系统的前端展示实现,包括:
+- 思考过程展示
+- 工具调用与结果展示
+- 最终回答流式渲染
+- 人工介入确认显示
+
+## 二、后端 SSE 事件类型
+
+### 事件类型汇总
+
+| 事件类型 | 说明 | 数据结构 |
+|---------|------|---------|
+| `node_start` | 节点开始执行 | `{ type: "node_start", node: string }` |
+| `node_end` | 节点执行结束 | `{ type: "node_end", node: string }` |
+| `reasoning` | 思考过程 token | `{ type: "reasoning", node: string, content: string }` |
+| `tool_call_start` | 工具调用开始 | `{ type: "tool_call_start", tool: string, args: any, id: string }` |
+| `tool_call_end` | 工具调用结束 | `{ type: "tool_call_end", tool: string, id: string, result: string }` |
+| `llm_token` | 最终回答 token | `{ type: "llm_token", node: string, content: string }` |
+| `human_review_request` | 人工审核请求 | `{ type: "human_review_request", review_id: string, content: string }` |
+| `state_update` | 状态更新 | `{ type: "state_update", data: any }` |
+| `custom` | 自定义事件 | `{ type: "custom", data: any }` |
+| `done` | 对话完成 | `{ type: "done" }` |
+
+### 事件处理流程
+
+```
+用户消息
+ ↓
+[node_start] llm_call
+ ↓
+[reasoning] 思考过程流式输出
+ ↓
+[tool_call_start] 工具调用开始
+ ↓
+[node_end] llm_call
+ ↓
+[node_start] tool_node
+ ↓
+[tool_call_end] 工具调用完成,返回结果
+ ↓
+[node_end] tool_node
+ ↓
+[node_start] llm_call
+ ↓
+[llm_token] 最终回答流式输出
+ ↓
+[node_end] llm_call
+ ↓
+[human_review_request] 人工审核请求(如有)
+ ↓
+[done]
+```
+
+## 三、前端组件结构
+
+### 组件树
+
+```
+ChatContainer (主容器)
+├── useChat (自定义 Hook)
+│ ├── ApiClient (API 客户端)
+│ └── 状态管理
+├── UserMessage (用户消息)
+└── AssistantMessage (AI 消息)
+ ├── ReasoningSection (思考过程)
+ ├── ToolCallCard[] (工具调用卡片)
+ ├── HumanReviewCard (人工审核卡片)
+ └── 最终回答内容
+```
+
+## 四、视觉设计规范
+
+### 1. 思考区(Reasoning Section)
+
+```tsx
+// 设计规范
+{
+ icon: '💭',
+ style: {
+ background: 'bg-gray-50',
+ border: 'border-gray-200',
+ text: 'text-gray-600 italic',
+ },
+ interaction: {
+ collapsible: true,
+ streaming: true,
+ }
+}
+```
+
+### 2. 工具调用区(Tool Call Card)
+
+```tsx
+// 设计规范
+{
+ icon: '⚙️',
+ statusIcons: {
+ pending: '⏳',
+ running: '🔄',
+ success: '✅',
+ error: '❌',
+ },
+ colors: {
+ pending: 'border-gray-300 bg-gray-50',
+ running: 'border-blue-300 bg-blue-50',
+ success: 'border-green-300 bg-green-50',
+ error: 'border-red-300 bg-red-50',
+ },
+ features: {
+ argsCollapsible: true,
+ resultCollapsible: true,
+ }
+}
+```
+
+### 3. 最终回答区(Final Answer)
+
+```tsx
+// 设计规范
+{
+ style: {
+ background: 'bg-blue-50',
+ border: 'border-blue-100',
+ },
+ interaction: {
+ streaming: true,
+ cursorBlink: true,
+ },
+ actions: {
+ copy: true,
+ feedback: true,
+ }
+}
+```
+
+### 4. 人工审核区(Human Review Card)
+
+```tsx
+// 设计规范
+{
+ icon: '👤',
+ style: {
+ background: 'bg-yellow-50',
+ border: 'border-yellow-300',
+ },
+ actions: {
+ approve: true,
+ reject: true,
+ modify: true,
+ },
+ fields: {
+ contentToReview: true,
+ comment: true,
+ modifiedContent: true,
+ }
+}
+```
+
+## 五、使用示例
+
+### 基本使用
+
+```tsx
+import React from 'react';
+import ChatContainer from './components/ChatContainer';
+
+function App() {
+ return (
+
+
+
+ );
+}
+
+export default App;
+```
+
+### 自定义 API 客户端
+
+```tsx
+import { ApiClient } from './components/useChat';
+
+const customClient = new ApiClient('http://my-custom-backend:8080');
+
+// 流式对话
+async function streamChat() {
+ for await (const event of customClient.chatStream(
+ '你好',
+ 'thread-1',
+ 'zhipu'
+ )) {
+ console.log('Event:', event);
+ }
+}
+
+// 审核操作
+await customClient.approveReview('review-123', 'user@example.com', '内容正确');
+await customClient.rejectReview('review-123', 'user@example.com', '内容有误');
+await customClient.modifyReview(
+ 'review-123',
+ 'user@example.com',
+ '修改后的内容',
+ '调整了措辞'
+);
+```
+
+### 自定义样式
+
+```tsx
+import { AssistantMessage } from './components/ChatContainer';
+
+// 自定义组件
+const CustomAssistantMessage = ({ message }) => (
+
+ {/* 自定义思考区 */}
+
+
+ {/* 自定义工具卡片 */}
+ {message.toolCalls.map(tc => (
+
+ ))}
+
+ {/* 自定义审核卡片 */}
+ {message.humanReview && (
+
+ )}
+
+ {/* 自定义回答 */}
+
{message.content}
+
+);
+```
+
+## 六、后端修改说明
+
+### 修改的文件
+
+1. `backend/app/agent/service.py` - 补充完整 SSE 事件类型
+
+### 新增的事件处理逻辑
+
+```python
+# 节点开始/结束事件
+if node_name != current_node:
+ if current_node:
+ yield { "type": "node_end", "node": current_node }
+ yield { "type": "node_start", "node": node_name }
+ current_node = node_name
+
+# 思考过程事件
+if reasoning_token:
+ yield { "type": "reasoning", "node": node_name, "content": reasoning_token }
+
+# 工具调用开始事件
+if tool_call_id not in tool_calls_in_progress:
+ yield {
+ "type": "tool_call_start",
+ "tool": tool_name,
+ "args": tool_args,
+ "id": tool_call_id
+ }
+
+# 工具调用结束事件
+if msg.get("role") == "tool":
+ yield {
+ "type": "tool_call_end",
+ "tool": tool_name,
+ "id": tool_call_id,
+ "result": tool_output
+ }
+
+# 人工审核请求事件
+if "review_pending" in serialized_data and serialized_data["review_pending"]:
+ yield {
+ "type": "human_review_request",
+ "review_id": review_id,
+ "content": content_to_review
+ }
+```
+
+## 七、状态管理
+
+### Message 接口
+
+```typescript
+interface Message {
+ id: string;
+ role: 'user' | 'assistant';
+ content: string;
+ reasoning: string;
+ toolCalls: ToolCall[];
+ humanReview?: HumanReview;
+ isLoading: boolean;
+ timestamp: Date;
+}
+```
+
+### ToolCall 接口
+
+```typescript
+interface ToolCall {
+ id: string;
+ tool: string;
+ args: any;
+ status: 'pending' | 'running' | 'success' | 'error';
+ result?: string;
+}
+```
+
+### HumanReview 接口
+
+```typescript
+interface HumanReview {
+ reviewId: string;
+ content: string;
+ status: 'pending' | 'approved' | 'rejected' | 'modified';
+ comment?: string;
+ modifiedContent?: string;
+}
+```
+
+## 八、文件清单
+
+### 后端文件
+
+| 文件 | 说明 |
+|------|------|
+| `backend/app/agent/service.py` | 补充完整 SSE 事件类型 |
+| `backend/app/agent_subgraphs/common/human_review.py` | 人工审核功能(已有) |
+
+### 前端文件
+
+| 文件 | 说明 |
+|------|------|
+| `frontend/src/components/useChat.ts` | 自定义 Hook + API 客户端 |
+| `frontend/src/components/ChatContainer.tsx` | 完整 UI 组件 |
+
+### 文档文件
+
+| 文件 | 说明 |
+|------|------|
+| `backend/docs/RAG_EVALUATION_GUIDE.md` | RAG 评估指南 |
+| `frontend/FRONTEND_GUIDE.md` | 本文档 |
+
+## 九、测试方法
+
+### 测试用例 1:简单对话
+
+输入:
+```
+你好,请介绍一下你自己
+```
+
+预期输出:
+```
+[思考过程] 我需要介绍自己...
+[最终回答] 你好!我是 AI Agent...
+```
+
+### 测试用例 2:工具调用
+
+输入:
+```
+请帮我查询北京的天气
+```
+
+预期输出:
+```
+[思考过程] 用户要查询天气...
+[tool_call_start] get_weather { city: "北京" }
+[tool_call_end] 结果: "北京, 晴天, 25°C"
+[最终回答] 北京今天天气是晴,气温 25°C...
+```
+
+### 测试用例 3:人工审核
+
+输入:
+```
+请生成一份重要邮件内容
+```
+
+预期输出:
+```
+[思考过程] 用户要生成邮件...
+[最终回答] 邮件内容...
+[human_review_request] 需要审核...
+[审核卡片] 通过 / 拒绝 / 修改
+```
+
+## 十、常见问题
+
+### Q: 如何自定义样式?
+
+A: 复制组件文件,修改 className 或样式对象即可。
+
+### Q: 如何添加新的事件类型?
+
+A: 1. 在后端添加 yield 语句,2. 在前端 useChat 中添加 case 分支,3. 在 UI 中添加展示组件。
+
+### Q: 如何处理多个工具调用?
+
+A: ToolCallCard 组件支持数组,每个工具调用独立显示。
+
+### Q: 如何集成现有项目?
+
+A: 1. 复制后端修改,2. 复制前端组件,3. 根据项目调整 API 端点。
\ No newline at end of file
diff --git a/frontend/src/components/ChatContainer.tsx b/frontend/src/components/ChatContainer.tsx
new file mode 100644
index 0000000..8b6d138
--- /dev/null
+++ b/frontend/src/components/ChatContainer.tsx
@@ -0,0 +1,379 @@
+import React, { useState } from 'react';
+import { Message, useChat, ToolCall, HumanReview } from './useChat';
+
+interface ReasoningSectionProps {
+ content: string;
+}
+
+export const ReasoningSection: React.FC = ({ content }) => {
+ const [isExpanded, setIsExpanded] = useState(true);
+
+ if (!content) return null;
+
+ return (
+
+
+ {isExpanded && (
+
+ )}
+
+ );
+};
+
+interface ToolCallCardProps {
+ toolCall: ToolCall;
+}
+
+export const ToolCallCard: React.FC = ({ toolCall }) => {
+ const [showArgs, setShowArgs] = useState(false);
+ const [showResult, setShowResult] = useState(false);
+
+ const getStatusIcon = () => {
+ switch (toolCall.status) {
+ case 'pending': return '⏳';
+ case 'running': return '🔄';
+ case 'success': return '✅';
+ case 'error': return '❌';
+ default: return '⚙️';
+ }
+ };
+
+ const getStatusColor = () => {
+ switch (toolCall.status) {
+ case 'success': return 'border-green-300 bg-green-50';
+ case 'error': return 'border-red-300 bg-red-50';
+ case 'running': return 'border-blue-300 bg-blue-50';
+ default: return 'border-gray-300 bg-gray-50';
+ }
+ };
+
+ return (
+
+
+
+ ⚙️
+ {toolCall.tool}
+ {getStatusIcon()}
+
+
+ {Object.keys(toolCall.args).length > 0 && (
+
+ )}
+ {toolCall.result && (
+
+ )}
+
+
+
+ {showArgs && Object.keys(toolCall.args).length > 0 && (
+
+
+ {JSON.stringify(toolCall.args, null, 2)}
+
+
+ )}
+
+ {showResult && toolCall.result && (
+
+
+ {toolCall.result}
+
+
+ )}
+
+ );
+};
+
+interface HumanReviewCardProps {
+ review: HumanReview;
+ onAction: (action: 'approve' | 'reject' | 'modify', comment?: string, modifiedContent?: string) => void;
+}
+
+export const HumanReviewCard: React.FC = ({ review, onAction }) => {
+ const [comment, setComment] = useState('');
+ const [modifiedContent, setModifiedContent] = useState(review.content);
+ const [showModify, setShowModify] = useState(false);
+
+ const getStatusIcon = () => {
+ switch (review.status) {
+ case 'pending': return '⏳';
+ case 'approved': return '✅';
+ case 'rejected': return '❌';
+ case 'modified': return '✏️';
+ default: return '👤';
+ }
+ };
+
+ const getStatusText = () => {
+ switch (review.status) {
+ case 'pending': return '待审核';
+ case 'approved': return '已通过';
+ case 'rejected': return '已拒绝';
+ case 'modified': return '已修改';
+ default: return '未知';
+ }
+ };
+
+ return (
+
+
+
+ 👤
+ 人工审核确认
+ {getStatusIcon()}
+ {getStatusText()}
+
+
+
+
+
待审核内容:
+
{review.content}
+
+
+ {review.status === 'pending' && (
+
+
+
+
+
+
+
+
+
+
+
+ {showModify && (
+
+
+
+ )}
+
+ )}
+
+ {(review.status === 'approved' || review.status === 'rejected' || review.status === 'modified') && (
+
+
审核结果:
+ {review.comment &&
意见:{review.comment}
}
+ {review.modifiedContent && (
+
修改后:{review.modifiedContent}
+ )}
+
+ )}
+
+ );
+};
+
+interface AssistantMessageProps {
+ message: Message;
+ onReviewAction?: (review: HumanReview, action: 'approve' | 'reject' | 'modify', comment?: string, modifiedContent?: string) => void;
+}
+
+export const AssistantMessage: React.FC = ({ message, onReviewAction }) => {
+ return (
+
+
+ AI
+
+
+
+
+ {message.toolCalls.map(toolCall => (
+
+ ))}
+
+ {message.humanReview && onReviewAction && (
+
+ onReviewAction(message.humanReview!, action, comment, modifiedContent)
+ }
+ />
+ )}
+
+ {message.content && (
+
+
{message.content}
+ {message.isLoading && (
+
+ )}
+
+ )}
+
+
+ );
+};
+
+interface UserMessageProps {
+ message: Message;
+}
+
+export const UserMessage: React.FC = ({ message }) => {
+ return (
+
+ );
+};
+
+interface ChatInputProps {
+ onSend: (text: string) => void;
+ disabled: boolean;
+}
+
+export const ChatInput: React.FC = ({ onSend, disabled }) => {
+ const [input, setInput] = useState('');
+
+ const handleSend = () => {
+ if (input.trim() && !disabled) {
+ onSend(input.trim());
+ setInput('');
+ }
+ };
+
+ return (
+
+
+ setInput(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && handleSend()}
+ disabled={disabled}
+ placeholder="发消息或按住说话..."
+ className="flex-1 border rounded-lg p-3 disabled:bg-gray-100"
+ />
+
+
+
+
+
+
+
+ );
+};
+
+interface ChatContainerProps {
+ model?: string;
+ threadId?: string;
+}
+
+export const ChatContainer: React.FC = ({ model = 'zhipu', threadId: propThreadId }) => {
+ const [threadId] = useState(() => propThreadId || Date.now().toString());
+ const { messages, isLoading, sendMessage, handleReviewAction } = useChat();
+
+ const handleSend = (text: string) => {
+ sendMessage(text, threadId, model);
+ };
+
+ const handleReview = (
+ review: HumanReview,
+ action: 'approve' | 'reject' | 'modify',
+ comment?: string,
+ modifiedContent?: string
+ ) => {
+ handleReviewAction(review.reviewId, action, 'user', comment, modifiedContent);
+ };
+
+ return (
+
+
+
AI Agent 对话
+
模型:{model}
+
+
+
+ {messages.map(message => (
+ message.role === 'user' ? (
+
+ ) : (
+
+ )
+ ))}
+ {messages.length === 0 && (
+
+
开始与 AI Agent 对话吧!
+
支持思考过程展示、工具调用、人工审核确认
+
+ )}
+
+
+
+
+ );
+};
+
+export default ChatContainer;
\ No newline at end of file
diff --git a/frontend/src/components/useChat.ts b/frontend/src/components/useChat.ts
new file mode 100644
index 0000000..59834e5
--- /dev/null
+++ b/frontend/src/components/useChat.ts
@@ -0,0 +1,272 @@
+import { useState, useEffect, useRef, useCallback } from 'react';
+
+export interface SSEEvent {
+ type: string;
+ [key: string]: any;
+}
+
+export interface ToolCall {
+ id: string;
+ tool: string;
+ args: any;
+ status: 'pending' | 'running' | 'success' | 'error';
+ result?: string;
+}
+
+export interface HumanReview {
+ reviewId: string;
+ content: string;
+ status: 'pending' | 'approved' | 'rejected' | 'modified';
+ comment?: string;
+ modifiedContent?: string;
+}
+
+export interface Message {
+ id: string;
+ role: 'user' | 'assistant';
+ content: string;
+ reasoning: string;
+ toolCalls: ToolCall[];
+ humanReview?: HumanReview;
+ isLoading: boolean;
+ timestamp: Date;
+}
+
+const API_BASE = 'http://localhost:8079';
+
+export class ApiClient {
+ private baseUrl: string;
+
+ constructor(baseUrl: string = API_BASE) {
+ this.baseUrl = baseUrl;
+ }
+
+ async* chatStream(message: string, threadId: string, model: string, userId: string = 'default_user'): AsyncGenerator {
+ const response = await fetch(`${this.baseUrl}/chat/stream`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ message, thread_id: threadId, model, user_id: userId }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const reader = response.body?.getReader();
+ if (!reader) {
+ throw new Error('No reader available');
+ }
+
+ const decoder = new TextDecoder();
+ let buffer = '';
+
+ 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.startsWith('data: ')) {
+ const data = line.slice(6);
+ if (data === '[DONE]') return;
+
+ try {
+ const event = JSON.parse(data);
+ yield event;
+ } catch (e) {
+ console.error('Failed to parse SSE event:', e);
+ }
+ }
+ }
+ }
+ }
+
+ async approveReview(reviewId: string, reviewer: string, comment: string = ''): Promise {
+ const response = await fetch(`${this.baseUrl}/reviews/${reviewId}/approve`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ review_id: reviewId, reviewer, comment }),
+ });
+ return response.ok;
+ }
+
+ async rejectReview(reviewId: string, reviewer: string, comment: string = ''): Promise {
+ const response = await fetch(`${this.baseUrl}/reviews/${reviewId}/reject`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ review_id: reviewId, reviewer, comment }),
+ });
+ return response.ok;
+ }
+
+ async modifyReview(reviewId: string, reviewer: string, modifiedContent: string, comment: string = ''): Promise {
+ const response = await fetch(`${this.baseUrl}/reviews/${reviewId}/modify`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ review_id: reviewId, reviewer, modified_content: modifiedContent, comment }),
+ });
+ return response.ok;
+ }
+}
+
+export function useChat() {
+ const [messages, setMessages] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [apiClient] = useState(() => new ApiClient());
+ const currentMessageRef = useRef(null);
+
+ const addMessage = useCallback((role: 'user' | 'assistant', content: string = '') => {
+ const message: Message = {
+ id: Date.now().toString(),
+ role,
+ content,
+ reasoning: '',
+ toolCalls: [],
+ isLoading: role === 'assistant',
+ timestamp: new Date(),
+ };
+ setMessages(prev => [...prev, message]);
+ currentMessageRef.current = message;
+ return message;
+ }, []);
+
+ const updateCurrentMessage = useCallback((updates: Partial) => {
+ if (!currentMessageRef.current) return;
+
+ setMessages(prev => prev.map(msg =>
+ msg.id === currentMessageRef.current!.id
+ ? { ...msg, ...updates }
+ : msg
+ ));
+
+ currentMessageRef.current = { ...currentMessageRef.current, ...updates };
+ }, []);
+
+ const sendMessage = useCallback(async (text: string, threadId: string, model: string = 'zhipu') => {
+ setIsLoading(true);
+ addMessage('user', text);
+ addMessage('assistant', '');
+
+ try {
+ for await (const event of apiClient.chatStream(text, threadId, model)) {
+ switch (event.type) {
+ case 'node_start':
+ console.log('Node started:', event.node);
+ break;
+
+ case 'node_end':
+ console.log('Node ended:', event.node);
+ break;
+
+ case 'reasoning':
+ updateCurrentMessage({
+ reasoning: (currentMessageRef.current?.reasoning || '') + event.content
+ });
+ break;
+
+ case 'tool_call_start':
+ updateCurrentMessage({
+ toolCalls: [
+ ...(currentMessageRef.current?.toolCalls || []),
+ {
+ id: event.id,
+ tool: event.tool,
+ args: event.args,
+ status: 'running' as const
+ }
+ ]
+ });
+ break;
+
+ case 'tool_call_end':
+ updateCurrentMessage({
+ toolCalls: (currentMessageRef.current?.toolCalls || []).map(tc =>
+ tc.id === event.id
+ ? { ...tc, status: 'success' as const, result: event.result }
+ : tc
+ )
+ });
+ break;
+
+ case 'llm_token':
+ updateCurrentMessage({
+ content: (currentMessageRef.current?.content || '') + event.content
+ });
+ break;
+
+ case 'human_review_request':
+ updateCurrentMessage({
+ humanReview: {
+ reviewId: event.review_id,
+ content: event.content,
+ status: 'pending' as const
+ }
+ });
+ break;
+
+ case 'done':
+ updateCurrentMessage({ isLoading: false });
+ break;
+ }
+ }
+ } catch (error) {
+ console.error('Chat stream error:', error);
+ updateCurrentMessage({
+ content: '抱歉,发生了错误,请重试。',
+ isLoading: false
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ }, [addMessage, updateCurrentMessage, apiClient]);
+
+ const handleReviewAction = useCallback(async (
+ reviewId: string,
+ action: 'approve' | 'reject' | 'modify',
+ reviewer: string,
+ comment?: string,
+ modifiedContent?: string
+ ) => {
+ try {
+ let success = false;
+ switch (action) {
+ case 'approve':
+ success = await apiClient.approveReview(reviewId, reviewer, comment);
+ break;
+ case 'reject':
+ success = await apiClient.rejectReview(reviewId, reviewer, comment);
+ break;
+ case 'modify':
+ if (modifiedContent) {
+ success = await apiClient.modifyReview(reviewId, reviewer, modifiedContent, comment);
+ }
+ break;
+ }
+
+ if (success) {
+ updateCurrentMessage({
+ humanReview: {
+ ...currentMessageRef.current!.humanReview!,
+ status: action as any,
+ comment,
+ modifiedContent
+ }
+ });
+ }
+ } catch (error) {
+ console.error('Review action failed:', error);
+ }
+ }, [updateCurrentMessage, apiClient]);
+
+ return {
+ messages,
+ isLoading,
+ sendMessage,
+ handleReviewAction,
+ };
+}
\ No newline at end of file