Initial commit: 国土空间规划课程智能体 v1.0
单容器 Docker 架构的国土空间规划课程智能问答系统,集成 FastAPI 后端与 Next.js 前端。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
import { create } from "zustand";
|
||||
import { ChatSession, ChatMessage, SourceInfo, ThinkingStep } from "@/types";
|
||||
import { chatAPI } from "@/lib/api";
|
||||
|
||||
interface ChatState {
|
||||
sessions: ChatSession[];
|
||||
currentSession: ChatSession | null;
|
||||
messages: ChatMessage[];
|
||||
isLoading: boolean;
|
||||
isStreaming: boolean;
|
||||
error: string | null;
|
||||
abortController: AbortController | null;
|
||||
}
|
||||
|
||||
interface ChatActions {
|
||||
// 会话管理
|
||||
loadSessions: () => Promise<void>;
|
||||
createSession: (title: string) => Promise<ChatSession | null>;
|
||||
selectSession: (sessionId: number) => Promise<void>;
|
||||
deleteSession: (sessionId: number) => Promise<void>;
|
||||
renameSession: (sessionId: number, newTitle: string) => Promise<void>;
|
||||
exportSession: (sessionId: number, format?: string) => Promise<void>;
|
||||
|
||||
// 消息管理
|
||||
loadMessages: (sessionId: number) => Promise<void>;
|
||||
sendMessage: (message: string) => Promise<void>;
|
||||
streamMessage: (message: string, mode?: string, knowledgeBaseIds?: string[], model?: string) => Promise<void>;
|
||||
addMessage: (message: ChatMessage) => void;
|
||||
updateMessage: (messageId: number, content: string) => void;
|
||||
editMessage: (messageId: number, newContent: string) => Promise<void>;
|
||||
regenerateMessage: (messageId: number) => Promise<void>;
|
||||
feedbackMessage: (messageId: number, feedback: "like" | "dislike") => Promise<void>;
|
||||
stopGeneration: () => void;
|
||||
|
||||
// 状态管理
|
||||
setLoading: (loading: boolean) => void;
|
||||
setStreaming: (streaming: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
clearError: () => void;
|
||||
clearMessages: () => void;
|
||||
}
|
||||
|
||||
type ChatStore = ChatState & ChatActions;
|
||||
|
||||
export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
// 初始状态
|
||||
sessions: [],
|
||||
currentSession: null,
|
||||
messages: [],
|
||||
isLoading: false,
|
||||
isStreaming: false,
|
||||
error: null,
|
||||
abortController: null,
|
||||
|
||||
// 加载会话列表
|
||||
loadSessions: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const sessions = await chatAPI.getSessions();
|
||||
set({ sessions, isLoading: false });
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "加载会话失败";
|
||||
set({ error: errorMessage, isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 创建新会话
|
||||
createSession: async (title: string) => {
|
||||
try {
|
||||
// 直接调用创建会话API
|
||||
const response = await chatAPI.createSession(title);
|
||||
const newSession: ChatSession = {
|
||||
id: response.id,
|
||||
title: response.title,
|
||||
created_at: response.created_at,
|
||||
updated_at: response.updated_at || response.created_at,
|
||||
message_count: response.message_count || 0,
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
sessions: [newSession, ...state.sessions],
|
||||
currentSession: newSession,
|
||||
messages: [],
|
||||
}));
|
||||
|
||||
return newSession;
|
||||
} catch (error) {
|
||||
console.error("创建会话失败:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// 选择会话
|
||||
selectSession: async (sessionId: number) => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const session = get().sessions.find(s => s.id === sessionId);
|
||||
if (!session) {
|
||||
throw new Error("会话不存在");
|
||||
}
|
||||
|
||||
set({ currentSession: session });
|
||||
await get().loadMessages(sessionId);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "选择会话失败";
|
||||
set({ error: errorMessage, isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 删除会话
|
||||
deleteSession: async (sessionId: number) => {
|
||||
try {
|
||||
await chatAPI.deleteSession(sessionId);
|
||||
set((state) => ({
|
||||
sessions: state.sessions.filter(s => s.id !== sessionId),
|
||||
currentSession: state.currentSession?.id === sessionId ? null : state.currentSession,
|
||||
messages: state.currentSession?.id === sessionId ? [] : state.messages,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("删除会话失败:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "删除会话失败";
|
||||
set({ error: errorMessage });
|
||||
}
|
||||
},
|
||||
|
||||
// 重命名会话
|
||||
renameSession: async (sessionId: number, newTitle: string) => {
|
||||
try {
|
||||
await chatAPI.renameSession(sessionId, newTitle);
|
||||
set((state) => ({
|
||||
sessions: state.sessions.map(s =>
|
||||
s.id === sessionId ? { ...s, title: newTitle } : s
|
||||
),
|
||||
currentSession: state.currentSession?.id === sessionId
|
||||
? { ...state.currentSession, title: newTitle }
|
||||
: state.currentSession,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("重命名会话失败:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "重命名会话失败";
|
||||
set({ error: errorMessage });
|
||||
}
|
||||
},
|
||||
|
||||
// 导出会话
|
||||
exportSession: async (sessionId: number, format: string = "json") => {
|
||||
try {
|
||||
const exportData = await chatAPI.exportSession(sessionId, format);
|
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${exportData.title || `session-${sessionId}`}.${format}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error("导出会话失败:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "导出会话失败";
|
||||
set({ error: errorMessage });
|
||||
}
|
||||
},
|
||||
|
||||
// 加载消息
|
||||
loadMessages: async (sessionId: number) => {
|
||||
try {
|
||||
const messages = await chatAPI.getSessionMessages(sessionId);
|
||||
set({ messages, isLoading: false });
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "加载消息失败";
|
||||
set({ error: errorMessage, isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 发送消息(非流式)
|
||||
sendMessage: async (message: string) => {
|
||||
const { currentSession } = get();
|
||||
if (!currentSession) return;
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
// 添加用户消息
|
||||
const userMessage: ChatMessage = {
|
||||
id: Date.now(),
|
||||
role: "user",
|
||||
content: message,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
messages: [...state.messages, userMessage],
|
||||
}));
|
||||
|
||||
try {
|
||||
const response = await chatAPI.sendMessage(message, currentSession.id);
|
||||
|
||||
// 添加助手回复
|
||||
const assistantMessage: ChatMessage = {
|
||||
id: response.message_id,
|
||||
role: "assistant",
|
||||
content: response.answer,
|
||||
created_at: new Date().toISOString(),
|
||||
metadata: { sources: response.sources },
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
messages: [...state.messages, assistantMessage],
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "发送消息失败";
|
||||
set({ error: errorMessage, isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 流式发送消息
|
||||
streamMessage: async (message: string, mode: string = "normal", knowledgeBaseIds?: string[], model?: string) => {
|
||||
const { currentSession } = get();
|
||||
if (!currentSession) return;
|
||||
|
||||
// 创建新的 AbortController
|
||||
const abortController = new AbortController();
|
||||
|
||||
console.log("[DEBUG-CHAT] 开始流式发送:", {
|
||||
message: message,
|
||||
sessionId: currentSession.id,
|
||||
knowledgeBaseIds: knowledgeBaseIds
|
||||
});
|
||||
|
||||
set({ isStreaming: true, error: null, abortController });
|
||||
|
||||
// 添加用户消息
|
||||
const userMessage: ChatMessage = {
|
||||
id: Date.now(),
|
||||
role: "user",
|
||||
content: message,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
messages: [...state.messages, userMessage],
|
||||
}));
|
||||
|
||||
// 创建助手消息占位符
|
||||
const assistantMessage: ChatMessage = {
|
||||
id: Date.now() + 1,
|
||||
role: "assistant",
|
||||
content: "",
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
messages: [...state.messages, assistantMessage],
|
||||
}));
|
||||
|
||||
let chunkCount = 0;
|
||||
let totalChars = 0;
|
||||
let thinkingSteps: ThinkingStep[] = [];
|
||||
|
||||
try {
|
||||
await chatAPI.streamMessage(
|
||||
message,
|
||||
currentSession.id,
|
||||
mode, // 传递实际选择的模式
|
||||
knowledgeBaseIds,
|
||||
abortController.signal, // 新增参数
|
||||
(chunk: string) => {
|
||||
// 解析chunk
|
||||
try {
|
||||
const data = JSON.parse(chunk);
|
||||
|
||||
if (data.type === 'thinking') {
|
||||
// 处理思考过程
|
||||
thinkingSteps.push({
|
||||
stage: data.stage,
|
||||
message: data.message,
|
||||
doc_count: data.doc_count,
|
||||
time: data.time
|
||||
});
|
||||
|
||||
// 立即更新思考过程到UI
|
||||
requestAnimationFrame(() => {
|
||||
set((state) => ({
|
||||
messages: state.messages.map(msg =>
|
||||
msg.id === assistantMessage.id
|
||||
? { ...msg, thinking: [...thinkingSteps] }
|
||||
: msg
|
||||
),
|
||||
}));
|
||||
});
|
||||
} else if (data.type === 'chunk') {
|
||||
// 处理内容chunk
|
||||
chunkCount++;
|
||||
totalChars += data.content.length;
|
||||
console.log(`[DEBUG-STREAM] 接收并显示chunk ${chunkCount}:`, data.content);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
set((state) => ({
|
||||
messages: state.messages.map(msg =>
|
||||
msg.id === assistantMessage.id
|
||||
? { ...msg, content: msg.content + data.content }
|
||||
: msg
|
||||
),
|
||||
}));
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// 向后兼容:纯文本chunk
|
||||
chunkCount++;
|
||||
totalChars += chunk.length;
|
||||
console.log(`[DEBUG-STREAM] 接收并显示chunk ${chunkCount}:`, chunk);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
set((state) => ({
|
||||
messages: state.messages.map(msg =>
|
||||
msg.id === assistantMessage.id
|
||||
? { ...msg, content: msg.content + chunk }
|
||||
: msg
|
||||
),
|
||||
}));
|
||||
});
|
||||
}
|
||||
},
|
||||
(sessionId: number) => {
|
||||
// 流式完成,直接处理
|
||||
console.log(`[DEBUG-STREAM] 流式完成 - 总chunk数: ${chunkCount}, 总字符数: ${totalChars}`);
|
||||
set({ isStreaming: false, abortController: null });
|
||||
|
||||
// 刷新会话列表以确保新会话显示在顶部
|
||||
get().loadSessions();
|
||||
},
|
||||
(error: string) => {
|
||||
// 流式错误
|
||||
console.log("[DEBUG-STREAM] 流式错误:", error);
|
||||
set({ error, isStreaming: false, abortController: null });
|
||||
},
|
||||
undefined, // onStatus
|
||||
undefined, // onThinking
|
||||
undefined, // onThinkingContent
|
||||
model // 传递模型ID
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "发送消息失败";
|
||||
set({ error: errorMessage, isStreaming: false, abortController: null });
|
||||
}
|
||||
},
|
||||
|
||||
// 添加消息
|
||||
addMessage: (message: ChatMessage) => {
|
||||
set((state) => ({
|
||||
messages: [...state.messages, message],
|
||||
}));
|
||||
},
|
||||
|
||||
// 更新消息
|
||||
updateMessage: (messageId: number, content: string) => {
|
||||
set((state) => ({
|
||||
messages: state.messages.map(msg =>
|
||||
msg.id === messageId ? { ...msg, content } : msg
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
// 设置加载状态
|
||||
setLoading: (loading: boolean) => {
|
||||
set({ isLoading: loading });
|
||||
},
|
||||
|
||||
// 设置流式状态
|
||||
setStreaming: (streaming: boolean) => {
|
||||
set({ isStreaming: streaming });
|
||||
},
|
||||
|
||||
// 设置错误
|
||||
setError: (error: string | null) => {
|
||||
set({ error });
|
||||
},
|
||||
|
||||
// 清除错误
|
||||
clearError: () => {
|
||||
set({ error: null });
|
||||
},
|
||||
|
||||
// 清除消息
|
||||
clearMessages: () => {
|
||||
set({ messages: [] });
|
||||
},
|
||||
|
||||
// 编辑消息
|
||||
editMessage: async (messageId: number, newContent: string) => {
|
||||
try {
|
||||
await chatAPI.editMessage(messageId, newContent);
|
||||
set((state) => ({
|
||||
messages: state.messages.map(msg =>
|
||||
msg.id === messageId ? { ...msg, content: newContent, edited: true } : msg
|
||||
),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("编辑消息失败:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "编辑消息失败";
|
||||
set({ error: errorMessage });
|
||||
}
|
||||
},
|
||||
|
||||
// 重新生成消息
|
||||
regenerateMessage: async (messageId: number) => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
const response = await chatAPI.regenerateMessage(messageId);
|
||||
|
||||
// 删除该消息之后的所有消息
|
||||
const messageIndex = get().messages.findIndex(msg => msg.id === messageId);
|
||||
if (messageIndex !== -1) {
|
||||
set((state) => ({
|
||||
messages: state.messages.slice(0, messageIndex + 1),
|
||||
}));
|
||||
}
|
||||
|
||||
// 添加新的助手回复
|
||||
const newMessage: ChatMessage = {
|
||||
id: response.new_message_id,
|
||||
role: "assistant",
|
||||
content: response.content,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
messages: [...state.messages, newMessage],
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("重新生成消息失败:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "重新生成消息失败";
|
||||
set({ error: errorMessage, isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 反馈消息
|
||||
feedbackMessage: async (messageId: number, feedback: "like" | "dislike") => {
|
||||
try {
|
||||
await chatAPI.feedbackMessage(messageId, feedback);
|
||||
set((state) => ({
|
||||
messages: state.messages.map(msg =>
|
||||
msg.id === messageId ? { ...msg, feedback } : msg
|
||||
),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("反馈消息失败:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : "反馈消息失败";
|
||||
set({ error: errorMessage });
|
||||
}
|
||||
},
|
||||
|
||||
// 停止生成
|
||||
stopGeneration: () => {
|
||||
const { abortController } = get();
|
||||
|
||||
// 如果存在 AbortController,调用 abort 中止请求
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
console.log("[DEBUG-STOP] 已中止流式请求");
|
||||
}
|
||||
|
||||
// 清理状态
|
||||
set({ isStreaming: false, abortController: null });
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user