|
|
@@ -36,14 +36,19 @@ const { TextArea } = Input;
|
|
|
const { Text } = Typography;
|
|
|
|
|
|
// 类型定义
|
|
|
-interface Message {
|
|
|
+export interface MessageVersion {
|
|
|
+ content: string;
|
|
|
+ model: string;
|
|
|
+}
|
|
|
+
|
|
|
+export interface Message {
|
|
|
id: number;
|
|
|
- type: "user" | "ai";
|
|
|
+ type: "user" | "ai" | "error";
|
|
|
content: string;
|
|
|
timestamp: string;
|
|
|
model?: string;
|
|
|
- versions?: string[]; // 存储所有版本的内容
|
|
|
- currentVersionIndex?: number; // 当前显示的版本索引
|
|
|
+ versions?: MessageVersion[];
|
|
|
+ currentVersionIndex?: number;
|
|
|
}
|
|
|
|
|
|
interface OpenAIMessage {
|
|
|
@@ -51,11 +56,6 @@ interface OpenAIMessage {
|
|
|
content: string;
|
|
|
}
|
|
|
|
|
|
-interface AIModel {
|
|
|
- key: string;
|
|
|
- label: string;
|
|
|
-}
|
|
|
-
|
|
|
interface StreamTypeController {
|
|
|
addToken: (token: string) => void;
|
|
|
complete: () => void;
|
|
|
@@ -86,11 +86,14 @@ const AIChatComponent = ({
|
|
|
const [selectedModel, setSelectedModel] = useState<string>("");
|
|
|
const [editingMessageId, setEditingMessageId] = useState<number | null>(null);
|
|
|
const [editingContent, setEditingContent] = useState<string>("");
|
|
|
+ const [refreshingMessageId, setRefreshingMessageId] = useState<number | null>(
|
|
|
+ null
|
|
|
+ );
|
|
|
|
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
const [isTyping, setIsTyping] = useState<boolean>(false);
|
|
|
const [currentTypingMessage, setCurrentTypingMessage] = useState<string>("");
|
|
|
- const [models, setModels] = useState<IAiModel[]>(); // 可用的AI模型
|
|
|
+ const [models, setModels] = useState<IAiModel[]>();
|
|
|
|
|
|
const scrollToBottom = useCallback(() => {
|
|
|
messagesEndRef.current?.scrollIntoView({
|
|
|
@@ -120,35 +123,9 @@ const AIChatComponent = ({
|
|
|
if (initMessage) {
|
|
|
setMessages([]);
|
|
|
setInputValue(initMessage);
|
|
|
- sendMessage();
|
|
|
}
|
|
|
}, [initMessage]);
|
|
|
|
|
|
- // 打字机效果 - 支持流式输入
|
|
|
- const typeWriter = useCallback(
|
|
|
- (text: string, callback: () => void): NodeJS.Timeout => {
|
|
|
- setIsTyping(true);
|
|
|
- setCurrentTypingMessage("");
|
|
|
- let index = 0;
|
|
|
-
|
|
|
- const timer = setInterval(() => {
|
|
|
- if (index < text.length) {
|
|
|
- setCurrentTypingMessage((prev) => prev + text.charAt(index));
|
|
|
- index++;
|
|
|
- } else {
|
|
|
- clearInterval(timer);
|
|
|
- setIsTyping(false);
|
|
|
- setCurrentTypingMessage("");
|
|
|
- callback();
|
|
|
- }
|
|
|
- }, 30);
|
|
|
-
|
|
|
- return timer;
|
|
|
- },
|
|
|
- []
|
|
|
- );
|
|
|
-
|
|
|
- // 流式打字机效果
|
|
|
const streamTypeWriter = useCallback(
|
|
|
(
|
|
|
onToken?: (content: string) => void,
|
|
|
@@ -179,17 +156,16 @@ const AIChatComponent = ({
|
|
|
[]
|
|
|
);
|
|
|
|
|
|
- // 调用OpenAI API - 支持流式输出
|
|
|
const callOpenAI = useCallback(
|
|
|
async (
|
|
|
messages: OpenAIMessage[],
|
|
|
isRegenerate: boolean = false,
|
|
|
messageIndex?: number
|
|
|
- ): Promise<void> => {
|
|
|
- setIsLoading(false); // 开始流式输出时取消loading状态
|
|
|
+ ): Promise<{ success: boolean; content?: string; error?: string }> => {
|
|
|
+ setIsLoading(false);
|
|
|
if (typeof process.env.REACT_APP_OPENAI_PROXY === "undefined") {
|
|
|
console.error("no REACT_APP_OPENAI_PROXY");
|
|
|
- return;
|
|
|
+ return { success: false, error: "API配置错误" };
|
|
|
}
|
|
|
try {
|
|
|
const payload = {
|
|
|
@@ -217,7 +193,6 @@ const AIChatComponent = ({
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
}
|
|
|
|
|
|
- // 处理流式响应
|
|
|
const reader = response.body?.getReader();
|
|
|
if (!reader) {
|
|
|
throw new Error("无法获取响应流");
|
|
|
@@ -226,42 +201,47 @@ const AIChatComponent = ({
|
|
|
const decoder = new TextDecoder();
|
|
|
let buffer = "";
|
|
|
|
|
|
- // 创建流式打字机效果
|
|
|
const typeController = streamTypeWriter(
|
|
|
- (content: string) => {
|
|
|
- // 每次添加token时的回调
|
|
|
- },
|
|
|
+ (content: string) => {},
|
|
|
(finalContent: string) => {
|
|
|
- // 完成时的回调
|
|
|
if (isRegenerate && messageIndex !== undefined) {
|
|
|
- // 重新生成时,添加到版本历史中
|
|
|
setMessages((prev) => {
|
|
|
const newMessages = [...prev];
|
|
|
const targetMessage = newMessages[messageIndex];
|
|
|
if (targetMessage) {
|
|
|
if (!targetMessage.versions) {
|
|
|
- targetMessage.versions = [targetMessage.content];
|
|
|
+ targetMessage.versions = [
|
|
|
+ {
|
|
|
+ content: targetMessage.content,
|
|
|
+ model: targetMessage.model || "",
|
|
|
+ },
|
|
|
+ ];
|
|
|
targetMessage.currentVersionIndex = 0;
|
|
|
}
|
|
|
- targetMessage.versions.push(finalContent);
|
|
|
+ targetMessage.versions.push({
|
|
|
+ content: finalContent,
|
|
|
+ model: selectedModel,
|
|
|
+ });
|
|
|
targetMessage.currentVersionIndex =
|
|
|
targetMessage.versions.length - 1;
|
|
|
targetMessage.content = finalContent;
|
|
|
+ targetMessage.model = selectedModel;
|
|
|
}
|
|
|
+ setRefreshingMessageId(null);
|
|
|
return newMessages;
|
|
|
});
|
|
|
} else {
|
|
|
- // 新消息
|
|
|
const aiMessage: Message = {
|
|
|
id: Date.now(),
|
|
|
type: "ai",
|
|
|
content: finalContent,
|
|
|
timestamp: new Date().toLocaleTimeString(),
|
|
|
model: selectedModel,
|
|
|
- versions: [finalContent],
|
|
|
+ versions: [{ content: finalContent, model: selectedModel }],
|
|
|
currentVersionIndex: 0,
|
|
|
};
|
|
|
setMessages((prev) => [...prev, aiMessage]);
|
|
|
+ setRefreshingMessageId(null);
|
|
|
}
|
|
|
}
|
|
|
);
|
|
|
@@ -272,7 +252,7 @@ const AIChatComponent = ({
|
|
|
|
|
|
if (done) {
|
|
|
typeController.complete();
|
|
|
- break;
|
|
|
+ return { success: true, content: currentTypingMessage };
|
|
|
}
|
|
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
@@ -286,7 +266,7 @@ const AIChatComponent = ({
|
|
|
|
|
|
if (data === "[DONE]") {
|
|
|
typeController.complete();
|
|
|
- return;
|
|
|
+ return { success: true, content: currentTypingMessage };
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
@@ -305,74 +285,16 @@ const AIChatComponent = ({
|
|
|
} catch (error) {
|
|
|
console.error("读取流数据失败:", error);
|
|
|
typeController.complete();
|
|
|
- throw error;
|
|
|
+ return { success: false, error: "读取响应流失败" };
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error("API调用失败:", error);
|
|
|
-
|
|
|
- // 如果真实API失败,回退到模拟响应
|
|
|
- const mockResponse = await simulateAIResponse(messages);
|
|
|
- typeWriter(mockResponse, () => {
|
|
|
- if (isRegenerate && messageIndex !== undefined) {
|
|
|
- setMessages((prev) => {
|
|
|
- const newMessages = [...prev];
|
|
|
- const targetMessage = newMessages[messageIndex];
|
|
|
- if (targetMessage) {
|
|
|
- if (!targetMessage.versions) {
|
|
|
- targetMessage.versions = [targetMessage.content];
|
|
|
- targetMessage.currentVersionIndex = 0;
|
|
|
- }
|
|
|
- targetMessage.versions.push(mockResponse);
|
|
|
- targetMessage.currentVersionIndex =
|
|
|
- targetMessage.versions.length - 1;
|
|
|
- targetMessage.content = mockResponse;
|
|
|
- }
|
|
|
- return newMessages;
|
|
|
- });
|
|
|
- } else {
|
|
|
- const aiMessage: Message = {
|
|
|
- id: Date.now(),
|
|
|
- type: "ai",
|
|
|
- content: mockResponse,
|
|
|
- timestamp: new Date().toLocaleTimeString(),
|
|
|
- model: selectedModel,
|
|
|
- versions: [mockResponse],
|
|
|
- currentVersionIndex: 0,
|
|
|
- };
|
|
|
- setMessages((prev) => [...prev, aiMessage]);
|
|
|
- }
|
|
|
- });
|
|
|
+ return { success: false, error: "API调用失败,请重试" };
|
|
|
}
|
|
|
},
|
|
|
- [selectedModel, streamTypeWriter, typeWriter]
|
|
|
+ [models, selectedModel, streamTypeWriter, currentTypingMessage]
|
|
|
);
|
|
|
|
|
|
- // 模拟AI响应(作为备用方案)
|
|
|
- const simulateAIResponse = useCallback(
|
|
|
- async (conversationHistory: OpenAIMessage[]): Promise<string> => {
|
|
|
- return new Promise((resolve) => {
|
|
|
- setTimeout(() => {
|
|
|
- const lastUserMessage =
|
|
|
- conversationHistory[conversationHistory.length - 1]?.content || "";
|
|
|
- const responses = [
|
|
|
- '这是一个很好的问题。让我来为你详细解答这个关于 "' +
|
|
|
- lastUserMessage +
|
|
|
- '" 的问题。\n\n首先,我需要说明的是,这个话题涉及多个方面的考虑。从技术层面来看,我们需要考虑实现的可行性和复杂度。从用户体验的角度,我们要确保解决方案既实用又易于理解。\n\n希望这个回答对你有帮助!',
|
|
|
- '我理解你的意思。根据我的知识,关于 "' +
|
|
|
- lastUserMessage +
|
|
|
- '" 这个问题,我可以从以下几个角度来分析:\n\n1. 首先是基本概念的理解\n2. 然后是实际应用场景\n3. 最后是注意事项和建议\n\n这样的分析方法能够帮助我们更全面地理解这个问题。',
|
|
|
- '感谢你的提问。关于 "' +
|
|
|
- lastUserMessage +
|
|
|
- '" 这个话题,我的看法是这样的:\n\n这确实是一个值得深入探讨的问题。在我看来,解决这类问题的关键在于找到平衡点,既要考虑效率,也要考虑可维护性。\n\n让我知道如果你需要更详细的解释!',
|
|
|
- ];
|
|
|
- resolve(responses[Math.floor(Math.random() * responses.length)]);
|
|
|
- }, 1000);
|
|
|
- });
|
|
|
- },
|
|
|
- []
|
|
|
- );
|
|
|
-
|
|
|
- // 发送消息到AI
|
|
|
const sendMessage = useCallback(
|
|
|
async (messageText: string = inputValue): Promise<void> => {
|
|
|
if (!messageText.trim()) return;
|
|
|
@@ -382,7 +304,7 @@ const AIChatComponent = ({
|
|
|
type: "user",
|
|
|
content: messageText,
|
|
|
timestamp: new Date().toLocaleTimeString(),
|
|
|
- versions: [messageText],
|
|
|
+ versions: [{ content: messageText, model: "" }],
|
|
|
currentVersionIndex: 0,
|
|
|
};
|
|
|
|
|
|
@@ -390,33 +312,53 @@ const AIChatComponent = ({
|
|
|
setInputValue("");
|
|
|
setIsLoading(true);
|
|
|
onChat && onChat();
|
|
|
+
|
|
|
+ // Scroll to the new user message
|
|
|
+ scrollToBottom();
|
|
|
+
|
|
|
try {
|
|
|
- // 构建对话历史
|
|
|
const conversationHistory: OpenAIMessage[] = [
|
|
|
{ role: "system", content: systemPrompt },
|
|
|
...messages.map((msg) => {
|
|
|
- const newMsg: OpenAIMessage = {
|
|
|
+ const data: OpenAIMessage = {
|
|
|
role: msg.type === "user" ? "user" : "assistant",
|
|
|
content: msg.content,
|
|
|
};
|
|
|
- return newMsg;
|
|
|
+ return data;
|
|
|
}),
|
|
|
{ role: "user", content: messageText },
|
|
|
];
|
|
|
|
|
|
- // 调用OpenAI API
|
|
|
- await callOpenAI(conversationHistory);
|
|
|
+ const result = await callOpenAI(conversationHistory);
|
|
|
+ if (!result.success) {
|
|
|
+ setMessages((prev) => [
|
|
|
+ ...prev,
|
|
|
+ {
|
|
|
+ id: Date.now(),
|
|
|
+ type: "error",
|
|
|
+ content: result.error || "请求失败,请重试",
|
|
|
+ timestamp: new Date().toLocaleTimeString(),
|
|
|
+ },
|
|
|
+ ]);
|
|
|
+ setIsLoading(false);
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
console.error("发送消息失败:", error);
|
|
|
- message.error("发送消息失败,请重试");
|
|
|
+ setMessages((prev) => [
|
|
|
+ ...prev,
|
|
|
+ {
|
|
|
+ id: Date.now(),
|
|
|
+ type: "error",
|
|
|
+ content: "请求失败,请重试",
|
|
|
+ timestamp: new Date().toLocaleTimeString(),
|
|
|
+ },
|
|
|
+ ]);
|
|
|
setIsLoading(false);
|
|
|
- setIsTyping(false);
|
|
|
}
|
|
|
},
|
|
|
- [inputValue, messages, systemPrompt, callOpenAI]
|
|
|
+ [inputValue, messages, systemPrompt, callOpenAI, scrollToBottom]
|
|
|
);
|
|
|
|
|
|
- // 复制消息内容
|
|
|
const copyMessage = useCallback(async (content: string): Promise<void> => {
|
|
|
try {
|
|
|
await navigator.clipboard.writeText(content);
|
|
|
@@ -427,36 +369,89 @@ const AIChatComponent = ({
|
|
|
}
|
|
|
}, []);
|
|
|
|
|
|
- // 刷新AI回答
|
|
|
const refreshAIResponse = useCallback(
|
|
|
async (messageIndex: number): Promise<void> => {
|
|
|
const userMessage = messages[messageIndex - 1];
|
|
|
if (userMessage && userMessage.type === "user") {
|
|
|
- // 重新构建到该消息为止的对话历史
|
|
|
+ setRefreshingMessageId(messages[messageIndex].id);
|
|
|
const conversationHistory: OpenAIMessage[] = [
|
|
|
{ role: "system", content: systemPrompt },
|
|
|
...messages.slice(0, messageIndex - 1).map((msg) => {
|
|
|
- const newMsg: OpenAIMessage = {
|
|
|
+ const data: OpenAIMessage = {
|
|
|
role: msg.type === "user" ? "user" : "assistant",
|
|
|
content: msg.content,
|
|
|
};
|
|
|
- return newMsg;
|
|
|
+ return data;
|
|
|
}),
|
|
|
{ role: "user", content: userMessage.content },
|
|
|
];
|
|
|
|
|
|
try {
|
|
|
- await callOpenAI(conversationHistory, true, messageIndex);
|
|
|
+ const result = await callOpenAI(
|
|
|
+ conversationHistory,
|
|
|
+ true,
|
|
|
+ messageIndex
|
|
|
+ );
|
|
|
+ if (!result.success) {
|
|
|
+ setMessages((prev) => {
|
|
|
+ const newMessages = [...prev];
|
|
|
+ newMessages[messageIndex] = {
|
|
|
+ id: Date.now(),
|
|
|
+ type: "error",
|
|
|
+ content: result.error || "重新生成失败,请重试",
|
|
|
+ timestamp: new Date().toLocaleTimeString(),
|
|
|
+ };
|
|
|
+ setRefreshingMessageId(null);
|
|
|
+ return newMessages;
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // Ensure the message type is set to "ai" on successful refresh
|
|
|
+ setMessages((prev) => {
|
|
|
+ const newMessages = [...prev];
|
|
|
+ const targetMessage = newMessages[messageIndex];
|
|
|
+ if (targetMessage) {
|
|
|
+ targetMessage.type = "ai"; // Update type to "ai"
|
|
|
+ targetMessage.content = result.content || "";
|
|
|
+ targetMessage.model = selectedModel;
|
|
|
+ if (!targetMessage.versions) {
|
|
|
+ targetMessage.versions = [
|
|
|
+ {
|
|
|
+ content: targetMessage.content,
|
|
|
+ model: targetMessage.model || "",
|
|
|
+ },
|
|
|
+ ];
|
|
|
+ targetMessage.currentVersionIndex = 0;
|
|
|
+ }
|
|
|
+ targetMessage.versions.push({
|
|
|
+ content: result.content || "",
|
|
|
+ model: selectedModel,
|
|
|
+ });
|
|
|
+ targetMessage.currentVersionIndex =
|
|
|
+ targetMessage.versions.length - 1;
|
|
|
+ }
|
|
|
+ setRefreshingMessageId(null);
|
|
|
+ return newMessages;
|
|
|
+ });
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
console.error("刷新回答失败:", error);
|
|
|
- message.error("刷新回答失败,请重试");
|
|
|
+ setMessages((prev) => {
|
|
|
+ const newMessages = [...prev];
|
|
|
+ newMessages[messageIndex] = {
|
|
|
+ id: Date.now(),
|
|
|
+ type: "error",
|
|
|
+ content: "重新生成失败,请重试",
|
|
|
+ timestamp: new Date().toLocaleTimeString(),
|
|
|
+ };
|
|
|
+ setRefreshingMessageId(null);
|
|
|
+ return newMessages;
|
|
|
+ });
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
- [messages, systemPrompt, callOpenAI]
|
|
|
+ [messages, systemPrompt, callOpenAI, selectedModel]
|
|
|
);
|
|
|
|
|
|
- // 切换消息版本
|
|
|
const switchMessageVersion = useCallback(
|
|
|
(messageIndex: number, direction: "prev" | "next"): void => {
|
|
|
setMessages((prev) => {
|
|
|
@@ -479,7 +474,8 @@ const AIChatComponent = ({
|
|
|
|
|
|
if (newIndex !== currentIndex) {
|
|
|
message.currentVersionIndex = newIndex;
|
|
|
- message.content = message.versions[newIndex];
|
|
|
+ message.content = message.versions[newIndex].content;
|
|
|
+ message.model = message.versions[newIndex].model;
|
|
|
}
|
|
|
}
|
|
|
return newMessages;
|
|
|
@@ -488,7 +484,6 @@ const AIChatComponent = ({
|
|
|
[]
|
|
|
);
|
|
|
|
|
|
- // 开始编辑用户消息
|
|
|
const startEditingMessage = useCallback(
|
|
|
(messageIndex: number): void => {
|
|
|
const message = messages[messageIndex];
|
|
|
@@ -500,7 +495,6 @@ const AIChatComponent = ({
|
|
|
[messages]
|
|
|
);
|
|
|
|
|
|
- // 确认编辑
|
|
|
const confirmEdit = useCallback((): void => {
|
|
|
if (editingMessageId !== null) {
|
|
|
setMessages((prev) => {
|
|
|
@@ -511,10 +505,10 @@ const AIChatComponent = ({
|
|
|
if (messageIndex !== -1) {
|
|
|
const message = newMessages[messageIndex];
|
|
|
if (!message.versions) {
|
|
|
- message.versions = [message.content];
|
|
|
+ message.versions = [{ content: message.content, model: "" }];
|
|
|
message.currentVersionIndex = 0;
|
|
|
}
|
|
|
- message.versions.push(editingContent);
|
|
|
+ message.versions.push({ content: editingContent, model: "" });
|
|
|
message.currentVersionIndex = message.versions.length - 1;
|
|
|
message.content = editingContent;
|
|
|
}
|
|
|
@@ -525,13 +519,11 @@ const AIChatComponent = ({
|
|
|
}
|
|
|
}, [editingMessageId, editingContent]);
|
|
|
|
|
|
- // 取消编辑
|
|
|
const cancelEdit = useCallback((): void => {
|
|
|
setEditingMessageId(null);
|
|
|
setEditingContent("");
|
|
|
}, []);
|
|
|
|
|
|
- // 处理键盘事件
|
|
|
const handleKeyPress = useCallback(
|
|
|
(e: React.KeyboardEvent<HTMLTextAreaElement>): void => {
|
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
|
@@ -542,7 +534,6 @@ const AIChatComponent = ({
|
|
|
[sendMessage]
|
|
|
);
|
|
|
|
|
|
- // 处理编辑时的键盘事件
|
|
|
const handleEditKeyPress = useCallback(
|
|
|
(e: React.KeyboardEvent<HTMLTextAreaElement>): void => {
|
|
|
if (e.key === "Enter" && e.ctrlKey) {
|
|
|
@@ -555,7 +546,6 @@ const AIChatComponent = ({
|
|
|
[confirmEdit, cancelEdit]
|
|
|
);
|
|
|
|
|
|
- // 模型选择菜单
|
|
|
const modelMenu: MenuProps = {
|
|
|
selectedKeys: [selectedModel],
|
|
|
onClick: ({ key }) => setSelectedModel(key),
|
|
|
@@ -565,7 +555,6 @@ const AIChatComponent = ({
|
|
|
})),
|
|
|
};
|
|
|
|
|
|
- // 刷新按钮的下拉菜单
|
|
|
const refreshMenu = useCallback(
|
|
|
(messageIndex: number): MenuProps => ({
|
|
|
onClick: ({ key }) => {
|
|
|
@@ -606,67 +595,146 @@ const AIChatComponent = ({
|
|
|
backgroundColor: "#f5f5f5",
|
|
|
}}
|
|
|
>
|
|
|
- {/* 聊天显示窗口 */}
|
|
|
<div style={{ flex: 1, overflowY: "auto", padding: "16px" }}>
|
|
|
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
|
|
- {messages.map((msg, index) => (
|
|
|
- <div
|
|
|
- key={msg.id}
|
|
|
- style={{
|
|
|
- display: "flex",
|
|
|
- justifyContent: msg.type === "user" ? "flex-end" : "flex-start",
|
|
|
- }}
|
|
|
- >
|
|
|
- <div
|
|
|
- style={{
|
|
|
- maxWidth: "70%",
|
|
|
- backgroundColor: msg.type === "user" ? "#1890ff" : "#ffffff",
|
|
|
- color: msg.type === "user" ? "white" : "black",
|
|
|
- borderRadius: "8px",
|
|
|
- padding: "16px",
|
|
|
- position: "relative",
|
|
|
- border: msg.type === "ai" ? "1px solid #d9d9d9" : "none",
|
|
|
- boxShadow:
|
|
|
- msg.type === "ai"
|
|
|
- ? "0 1px 2px rgba(0, 0, 0, 0.03)"
|
|
|
- : "none",
|
|
|
- }}
|
|
|
- >
|
|
|
- <div style={{ display: "flex", alignItems: "flex-start" }}>
|
|
|
- <Space>
|
|
|
- <div>
|
|
|
+ {messages.map(
|
|
|
+ (msg, index) =>
|
|
|
+ msg.id !== refreshingMessageId && (
|
|
|
+ <div
|
|
|
+ key={msg.id}
|
|
|
+ style={{
|
|
|
+ display: "flex",
|
|
|
+ justifyContent:
|
|
|
+ msg.type === "user"
|
|
|
+ ? "flex-end"
|
|
|
+ : msg.type === "error"
|
|
|
+ ? "center"
|
|
|
+ : "flex-start",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ maxWidth: msg.type === "error" ? "100%" : "70%",
|
|
|
+ backgroundColor:
|
|
|
+ msg.type === "user"
|
|
|
+ ? "#1890ff"
|
|
|
+ : msg.type === "error"
|
|
|
+ ? "#fff1f0"
|
|
|
+ : "#ffffff",
|
|
|
+ color:
|
|
|
+ msg.type === "user"
|
|
|
+ ? "white"
|
|
|
+ : msg.type === "error"
|
|
|
+ ? "#ff4d4f"
|
|
|
+ : "black",
|
|
|
+ borderRadius: "8px",
|
|
|
+ padding: "16px",
|
|
|
+ position: "relative",
|
|
|
+ border:
|
|
|
+ msg.type === "ai"
|
|
|
+ ? "1px solid #d9d9d9"
|
|
|
+ : msg.type === "error"
|
|
|
+ ? "1px solid #ff4d4f"
|
|
|
+ : "none",
|
|
|
+ boxShadow:
|
|
|
+ msg.type === "ai" || msg.type === "error"
|
|
|
+ ? "0 1px 2px rgba(0, 0, 0, 0.03)"
|
|
|
+ : "none",
|
|
|
+ textAlign: msg.type === "error" ? "center" : "left",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div style={{ display: "flex", alignItems: "flex-start" }}>
|
|
|
<Space>
|
|
|
- <Avatar
|
|
|
- size={32}
|
|
|
- icon={
|
|
|
- msg.type === "user" ? (
|
|
|
- <UserOutlined />
|
|
|
- ) : (
|
|
|
- <RobotOutlined />
|
|
|
- )
|
|
|
- }
|
|
|
- style={{
|
|
|
- backgroundColor:
|
|
|
- msg.type === "user"
|
|
|
- ? "#rgb(0 132 253 / 50%)"
|
|
|
- : "#595959",
|
|
|
- }}
|
|
|
- />
|
|
|
- <div
|
|
|
- style={{
|
|
|
- fontSize: "14px",
|
|
|
- fontWeight: 500,
|
|
|
- marginBottom: "4px",
|
|
|
- }}
|
|
|
- >
|
|
|
- {msg.type === "user"
|
|
|
- ? "你"
|
|
|
- : msg.model
|
|
|
- ? models?.find((m) => m.uid === msg.model)?.name
|
|
|
- : "AI助手"}
|
|
|
+ {msg.type !== "error" && (
|
|
|
+ <Avatar
|
|
|
+ size={32}
|
|
|
+ icon={
|
|
|
+ msg.type === "user" ? (
|
|
|
+ <UserOutlined />
|
|
|
+ ) : (
|
|
|
+ <RobotOutlined />
|
|
|
+ )
|
|
|
+ }
|
|
|
+ style={{
|
|
|
+ backgroundColor:
|
|
|
+ msg.type === "user"
|
|
|
+ ? "rgb(0 132 253 / 50%)"
|
|
|
+ : "#595959",
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ <div>
|
|
|
+ {msg.type !== "error" && (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ fontSize: "14px",
|
|
|
+ fontWeight: 500,
|
|
|
+ marginBottom: "4px",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {msg.type === "user"
|
|
|
+ ? "你"
|
|
|
+ : msg.model
|
|
|
+ ? models?.find((m) => m.uid === msg.model)?.name
|
|
|
+ : "AI助手"}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {editingMessageId === msg.id ? (
|
|
|
+ <div>
|
|
|
+ <TextArea
|
|
|
+ value={editingContent}
|
|
|
+ onChange={(e) =>
|
|
|
+ setEditingContent(e.target.value)
|
|
|
+ }
|
|
|
+ onKeyPress={handleEditKeyPress}
|
|
|
+ autoSize={{ minRows: 2, maxRows: 8 }}
|
|
|
+ style={{ marginBottom: "8px" }}
|
|
|
+ />
|
|
|
+ <Space size="small">
|
|
|
+ <Button
|
|
|
+ size="small"
|
|
|
+ type="primary"
|
|
|
+ icon={<CheckOutlined />}
|
|
|
+ onClick={confirmEdit}
|
|
|
+ >
|
|
|
+ 确认
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ size="small"
|
|
|
+ icon={<CloseOutlined />}
|
|
|
+ onClick={cancelEdit}
|
|
|
+ >
|
|
|
+ 取消
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div>
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ fontSize: "14px",
|
|
|
+ lineHeight: "1.5",
|
|
|
+ whiteSpace: "pre-wrap",
|
|
|
+ wordBreak: "break-word",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Marked text={msg.content} />
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ fontSize: "12px",
|
|
|
+ opacity: 0.6,
|
|
|
+ marginTop: "8px",
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {msg.timestamp}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
</div>
|
|
|
</Space>
|
|
|
- {/* 显示版本导航 */}
|
|
|
+ </div>
|
|
|
+ <Space>
|
|
|
{msg.versions && msg.versions.length > 1 && (
|
|
|
<div style={{ marginBottom: "8px" }}>
|
|
|
<Space size="small">
|
|
|
@@ -706,100 +774,56 @@ const AIChatComponent = ({
|
|
|
</Space>
|
|
|
</div>
|
|
|
)}
|
|
|
-
|
|
|
- {/* 编辑模式 */}
|
|
|
- {editingMessageId === msg.id ? (
|
|
|
+ {editingMessageId !== msg.id && (
|
|
|
<div>
|
|
|
- <TextArea
|
|
|
- value={editingContent}
|
|
|
- onChange={(e) => setEditingContent(e.target.value)}
|
|
|
- onKeyPress={handleEditKeyPress}
|
|
|
- autoSize={{ minRows: 2, maxRows: 8 }}
|
|
|
- style={{ marginBottom: "8px" }}
|
|
|
- />
|
|
|
<Space size="small">
|
|
|
- <Button
|
|
|
- size="small"
|
|
|
- type="primary"
|
|
|
- icon={<CheckOutlined />}
|
|
|
- onClick={confirmEdit}
|
|
|
- >
|
|
|
- 确认
|
|
|
- </Button>
|
|
|
- <Button
|
|
|
- size="small"
|
|
|
- icon={<CloseOutlined />}
|
|
|
- onClick={cancelEdit}
|
|
|
- >
|
|
|
- 取消
|
|
|
- </Button>
|
|
|
+ <Tooltip title="复制">
|
|
|
+ <Button
|
|
|
+ size="small"
|
|
|
+ type="text"
|
|
|
+ icon={<CopyOutlined />}
|
|
|
+ onClick={() => copyMessage(msg.content)}
|
|
|
+ />
|
|
|
+ </Tooltip>
|
|
|
+ {msg.type === "user" ? (
|
|
|
+ <Tooltip title="编辑">
|
|
|
+ <Button
|
|
|
+ size="small"
|
|
|
+ type="text"
|
|
|
+ icon={<EditOutlined />}
|
|
|
+ onClick={() => startEditingMessage(index)}
|
|
|
+ />
|
|
|
+ </Tooltip>
|
|
|
+ ) : msg.type === "error" ? (
|
|
|
+ <Tooltip title="重试">
|
|
|
+ <Button
|
|
|
+ size="small"
|
|
|
+ type="text"
|
|
|
+ icon={<ReloadOutlined />}
|
|
|
+ onClick={() => refreshAIResponse(index)}
|
|
|
+ />
|
|
|
+ </Tooltip>
|
|
|
+ ) : (
|
|
|
+ <Dropdown
|
|
|
+ menu={refreshMenu(index)}
|
|
|
+ trigger={["hover"]}
|
|
|
+ >
|
|
|
+ <Button
|
|
|
+ size="small"
|
|
|
+ type="text"
|
|
|
+ icon={<ReloadOutlined />}
|
|
|
+ />
|
|
|
+ </Dropdown>
|
|
|
+ )}
|
|
|
</Space>
|
|
|
</div>
|
|
|
- ) : (
|
|
|
- <div>
|
|
|
- <div
|
|
|
- style={{
|
|
|
- fontSize: "14px",
|
|
|
- lineHeight: "1.5",
|
|
|
- whiteSpace: "pre-wrap",
|
|
|
- wordBreak: "break-word",
|
|
|
- }}
|
|
|
- >
|
|
|
- <Marked text={msg.content} />
|
|
|
- </div>
|
|
|
- <div
|
|
|
- style={{
|
|
|
- fontSize: "12px",
|
|
|
- opacity: 0.6,
|
|
|
- marginTop: "8px",
|
|
|
- }}
|
|
|
- >
|
|
|
- {msg.timestamp}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </Space>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 悬浮工具按钮 */}
|
|
|
- {editingMessageId !== msg.id && (
|
|
|
- <div>
|
|
|
- <Space size="small">
|
|
|
- <Tooltip title="复制">
|
|
|
- <Button
|
|
|
- size="small"
|
|
|
- type="text"
|
|
|
- icon={<CopyOutlined />}
|
|
|
- onClick={() => copyMessage(msg.content)}
|
|
|
- />
|
|
|
- </Tooltip>
|
|
|
- {msg.type === "user" ? (
|
|
|
- <Tooltip title="编辑">
|
|
|
- <Button
|
|
|
- size="small"
|
|
|
- type="text"
|
|
|
- icon={<EditOutlined />}
|
|
|
- onClick={() => startEditingMessage(index)}
|
|
|
- />
|
|
|
- </Tooltip>
|
|
|
- ) : (
|
|
|
- <Dropdown menu={refreshMenu(index)} trigger={["hover"]}>
|
|
|
- <Button
|
|
|
- size="small"
|
|
|
- type="text"
|
|
|
- icon={<ReloadOutlined />}
|
|
|
- />
|
|
|
- </Dropdown>
|
|
|
)}
|
|
|
</Space>
|
|
|
</div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ))}
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ )}
|
|
|
|
|
|
- {/* 显示AI正在输入的消息 */}
|
|
|
{isTyping && (
|
|
|
<div style={{ display: "flex", justifyContent: "flex-start" }}>
|
|
|
<div
|
|
|
@@ -870,11 +894,9 @@ const AIChatComponent = ({
|
|
|
<div ref={messagesEndRef} />
|
|
|
</div>
|
|
|
|
|
|
- {/* 用户输入区域 */}
|
|
|
<Affix offsetBottom={10}>
|
|
|
<Card style={{ borderRadius: "10px", borderColor: "#d9d9d9" }}>
|
|
|
<div style={{ maxWidth: "1200px", margin: "0 auto" }}>
|
|
|
- {/* 输入框 */}
|
|
|
<div style={{ display: "flex", marginBottom: "8px" }}>
|
|
|
<TextArea
|
|
|
value={inputValue}
|
|
|
@@ -886,7 +908,6 @@ const AIChatComponent = ({
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
- {/* 功能按钮和模型选择 */}
|
|
|
<div
|
|
|
style={{
|
|
|
display: "flex",
|