From b875f5e1ab18844ad8210080a3062f69c89dbdc6 Mon Sep 17 00:00:00 2001
From: wangyunfei <1224056307@qq.com>
Date: Fri, 12 Sep 2025 09:20:32 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=99=BA=E8=83=BD=E5=AF=B9?=
=?UTF-8?q?=E8=AF=9D=E8=8F=9C=E5=8D=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
config/routes.js | 6 +
package.json | 4 +-
src/config/api.js | 111 ++++
.../chatConversation/ChatConversation.js | 413 +++++++++++++++
.../chatConversation/ChatConversation.less | 477 ++++++++++++++++++
src/pages/chatConversation/models/api.js | 183 +++++++
src/pages/topnavbar/TopNavBar.js | 4 +
src/utils/conversationStore.js | 179 +++++++
src/utils/pageConversationStore.js | 202 ++++++++
9 files changed, 1578 insertions(+), 1 deletion(-)
create mode 100644 src/config/api.js
create mode 100644 src/pages/chatConversation/ChatConversation.js
create mode 100644 src/pages/chatConversation/ChatConversation.less
create mode 100644 src/pages/chatConversation/models/api.js
create mode 100644 src/utils/conversationStore.js
create mode 100644 src/utils/pageConversationStore.js
diff --git a/config/routes.js b/config/routes.js
index e67e5f5..0b3b878 100644
--- a/config/routes.js
+++ b/config/routes.js
@@ -223,6 +223,12 @@ export default [
name: 'systemApplicationStatistics',
component: './systemApplicationStatistics/TimeSheetList',
},
+ {
+ path: '/topnavbar00/hrefficiency/ChatConversation',
+ // icon: 'bank',
+ name: 'chatConversation',
+ component: './chatConversation/ChatConversation',
+ },
],
},
// 知识库管理
diff --git a/package.json b/package.json
index 43762f9..83a1bb9 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,9 @@
"react": "^18.2.0",
"react-contexify": "^5.0.0",
"react-dom": "^18.2.0",
- "react-split-pane": "^0.1.92"
+ "react-markdown": "^10.1.0",
+ "react-split-pane": "^0.1.92",
+ "remark-gfm": "^4.0.1"
},
"devDependencies": {
"@types/react": "^18.0.0",
diff --git a/src/config/api.js b/src/config/api.js
new file mode 100644
index 0000000..a513a9a
--- /dev/null
+++ b/src/config/api.js
@@ -0,0 +1,111 @@
+// API配置文件
+export const API_CONFIG = {
+ // DeepSeek API配置
+ DEEPSEEK: {
+ BASE_URL: 'https://api.deepseek.com/v1',
+ MODEL: 'deepseek-chat',
+ API_KEY:
+ process.env.UMI_APP_DEEPSEEK_API_KEY ||
+ process.env.REACT_APP_DEEPSEEK_API_KEY ||
+ 'your-api-key-here',
+ MAX_TOKENS: 2000,
+ TEMPERATURE: 0.7,
+ SYSTEM_PROMPT: '你是一个智能AI助手,请用简洁、专业、友好的方式回答用户的问题。'
+ },
+
+ // 其他API配置
+ OPENAI: {
+ BASE_URL: 'https://api.openai.com/v1',
+ MODEL: 'gpt-3.5-turbo',
+ API_KEY: process.env.UMI_APP_OPENAI_API_KEY || '',
+ MAX_TOKENS: 2000,
+ TEMPERATURE: 0.7
+ }
+}
+
+// 调试:开发阶段临时查看是否拿到
+if (process.env.NODE_ENV === 'development') {
+ // 只显示是否存在,不打印具体 key
+ // eslint-disable-next-line no-console
+ console.log(
+ 'DeepSeek key loaded =',
+ !!process.env.UMI_APP_DEEPSEEK_API_KEY
+ )
+}
+
+// DeepSeek 调用
+export const callDeepSeekAPI = async (messages, options = {}) => {
+ const cfg = API_CONFIG.DEEPSEEK
+ const apiKey = cfg.API_KEY
+ if (!apiKey || apiKey === 'your-api-key-here') {
+ throw new Error('NO_API_KEY')
+ }
+
+ // 组装消息(如果历史里已经含 system 就不重复)
+ const hasSystem = messages.some(m => m.role === 'system')
+ const finalMessages = hasSystem
+ ? messages
+ : [{ role: 'system', content: cfg.SYSTEM_PROMPT }, ...messages]
+
+ const payload = {
+ model: cfg.MODEL,
+ messages: finalMessages.map(m => ({ role: m.role, content: m.content })),
+ temperature: options.temperature ?? cfg.TEMPERATURE,
+ max_tokens: options.maxTokens ?? cfg.MAX_TOKENS,
+ stream: false
+ }
+
+ const resp = await fetch(`${cfg.BASE_URL}/chat/completions`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${apiKey}`
+ },
+ body: JSON.stringify(payload)
+ })
+
+ const data = await resp.json().catch(() => ({}))
+
+ if (!resp.ok) {
+ // 统一抛出,交给 handleAPIError
+ const msg = data?.error?.message || resp.statusText || 'API_ERROR'
+ const err = new Error(msg)
+ err.status = resp.status
+ err.code = data?.error?.code
+ throw err
+ }
+
+ const content = data?.choices?.[0]?.message?.content?.trim()
+ if (!content) throw new Error('EMPTY_RESPONSE')
+ return content
+}
+
+
+// DeepSeek 流式调用
+
+
+
+
+
+
+// 错误处理函数
+export const handleAPIError = (error) => {
+ const raw = error?.message || ''
+ const status = error?.status
+ const code = error?.code
+
+ if (raw === 'NO_API_KEY') return '未配置 API Key,请在 .env 里加 UMI_APP_DEEPSEEK_API_KEY 并重启。'
+ if (raw === 'EMPTY_RESPONSE') return '接口返回空结果,请重试。'
+
+ // 典型鉴权问题
+ if (status === 401 || /unauthorized|invalid api key/i.test(raw) || code === 'invalid_api_key') {
+ return 'API密钥无效,请检查是否正确。'
+ }
+
+ if (status === 429 || /rate limit/i.test(raw)) return '请求过多,被限流。'
+ if (status === 500) return '服务端错误,请稍后再试。'
+ if (/timeout/i.test(raw)) return '请求超时,请检查网络。'
+ if (/certificate|SSL/i.test(raw)) return '证书/网络问题,请更换网络或代理。'
+
+ return `调用失败:${raw || '未知错误'}`
+}
diff --git a/src/pages/chatConversation/ChatConversation.js b/src/pages/chatConversation/ChatConversation.js
new file mode 100644
index 0000000..51a5e3d
--- /dev/null
+++ b/src/pages/chatConversation/ChatConversation.js
@@ -0,0 +1,413 @@
+import React, { useState, useRef, useEffect } from 'react'
+import ReactMarkdown from 'react-markdown'
+import remarkGfm from 'remark-gfm'
+import {
+ Input,
+ Button,
+ Typography,
+ Spin,
+ message,
+ Layout,
+ Tooltip,
+ Input as AntInput,
+ Modal,
+ Upload,
+ Dropdown,
+ Space,
+ Select,
+} from 'antd'
+import {
+ ArrowUpOutlined,
+ LinuxOutlined,
+ RobotOutlined,
+ LoadingOutlined,
+ CopyOutlined,
+ ReloadOutlined,
+ PaperClipOutlined,
+} from '@ant-design/icons'
+import { callDeepSeekAPI, handleAPIError } from './models/api'
+import { conversationStore } from '@/utils/pageConversationStore'
+import './ChatConversation.less'
+
+const { Text } = Typography
+const { TextArea } = Input
+const { Sider, Content } = Layout
+
+function ChatConversation() {
+ // 会话相关状态 - 使用独立的存储
+ const [conversations, setConversations] = useState(conversationStore.getConversations())
+ const [currentConversationId, setCurrentConversationId] = useState(conversationStore.getCurrentConversationId())
+ const [inputValue, setInputValue] = useState('')
+ const [loading, setLoading] = useState(false)
+ const [sidebarExpanded, setSidebarExpanded] = useState(false)
+ const [editModalVisible, setEditModalVisible] = useState(false)
+ const [editingConversation, setEditingConversation] = useState(null)
+ const [newTitle, setNewTitle] = useState('')
+
+ const messagesEndRef = useRef(null)
+ const inputRef = useRef(null)
+
+ // 获取当前会话
+ const currentConversation = conversations.find(conv => conv.id === currentConversationId)
+ const currentMessages = currentConversation?.messages || []
+ // 自动滚动到底部
+ const chatScrollRef = useRef(null);
+ useEffect(() => {
+ if (chatScrollRef.current) {
+ chatScrollRef.current.scrollTop = chatScrollRef.current.scrollHeight;
+ }
+ }, [currentMessages, loading]); // currentMessages 是你的消息数组
+
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
+ }
+
+
+ const handleChange = value => {
+ console.log(`selected ${value}`);
+ }
+
+
+ // 订阅对话状态变化
+ useEffect(() => {
+ const unsubscribe = conversationStore.subscribe(({ conversations, currentConversationId }) => {
+ setConversations(conversations)
+ setCurrentConversationId(currentConversationId)
+ })
+
+ return unsubscribe
+ }, [])
+
+ useEffect(() => {
+ scrollToBottom()
+ }, [currentMessages])
+
+ // 创建新对话
+ const createNewConversation = () => {
+ conversationStore.createNewConversation()
+ setInputValue('')
+ }
+
+ // 切换对话
+ const switchConversation = (conversationId) => {
+ conversationStore.switchConversation(conversationId)
+ setInputValue('')
+ }
+
+ // 删除对话
+ const deleteConversation = (conversationId) => {
+ if (conversations.length <= 1) {
+ message.warning('至少需要保留一个对话')
+ return
+ }
+
+ conversationStore.deleteConversation(conversationId)
+ }
+
+ // 编辑对话标题
+ const editConversationTitle = (conversation) => {
+ setEditingConversation(conversation)
+ setNewTitle(conversation.title)
+ setEditModalVisible(true)
+ }
+
+ // 保存对话标题
+ const saveConversationTitle = () => {
+ if (!newTitle.trim()) {
+ message.warning('标题不能为空')
+ return
+ }
+
+ conversationStore.updateConversationTitle(editingConversation.id, newTitle.trim())
+
+ setEditModalVisible(false)
+ setEditingConversation(null)
+ setNewTitle('')
+ }
+
+ // 发送消息
+ const sendMessage = async () => {
+ const messageText = inputValue.trim()
+ if (!messageText || loading) return
+
+ // 添加用户消息到独立存储
+ conversationStore.addMessage('user', messageText)
+
+ setInputValue('')
+ setLoading(true)
+
+ try {
+ // 准备发送给API的消息历史
+ const apiMessages = currentMessages.map(msg => ({
+ role: msg.role,
+ content: msg.content
+ })).concat({
+ role: 'user',
+ content: messageText
+ })
+
+ // 调用DeepSeek API
+ const assistantResponse = await callDeepSeekAPI(apiMessages)
+
+ // 添加助手回复到独立存储
+ conversationStore.addMessage('assistant', assistantResponse)
+ } catch (error) {
+ const errorMessage = handleAPIError(error)
+ message.error(errorMessage)
+
+ // 添加错误消息到独立存储
+ conversationStore.addMessage('assistant', `抱歉,我遇到了一个问题:${errorMessage}`)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // 处理回车键发送
+ const handleKeyPress = (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault()
+ sendMessage()
+ }
+ }
+
+ // 清空当前对话
+ const clearCurrentChat = () => {
+ conversationStore.clearCurrentConversation()
+ }
+
+ // 兼容剪贴板
+ const copyToClipboard = async (text) => {
+ if (!text) return Promise.reject(new Error("exmpty text"))
+
+ // 现代 API,需 HTTPS 或 localhost(window.isSecureContext)
+ if (navigator.clipboard && window.isSecureContext) {
+ try {
+ await navigator.clipboard.writeText(text)
+ return
+ } catch (e) {
+ // 若失败,继续使用回退方案
+ console.warn('navigator.clipboard failed, fallback to execCommand', e)
+ }
+ }
+
+ // 回退方案
+ return new Promise((resolve, reject) => {
+ try {
+ const ta = document.createElement('textarea')
+ ta.value = text
+ ta.setAttribute('readonly', '')
+ ta.style.position = 'fixed'
+ ta.style.left = '-9999px'
+ document.body.appendChild(ta)
+ ta.select()
+ ta.setSelectionRange(0, ta.value.length)
+ const ok = document.execCommand('copy')
+ document.body.removeChild(ta)
+ if (ok) resolve()
+ else reject(new Error('execCommand failed'))
+ } catch (err) {
+ reject(err)
+ }
+ })
+ }
+
+ // 渲染单条消息 ==================================
+ const renderMessage = (msg, index) => {
+ const isUser = msg.role === 'user'
+ return (
+
+ {!isUser && (
+
+
+
+ )}
+ {isUser &&
}
+
+
+
+ {isUser ? '' : 'AI对话'}
+
+ {msg.timestamp
+ ? (typeof msg.timestamp === 'string'
+ ? msg.timestamp
+ : msg.timestamp.toLocaleString())
+ : ''}
+
+ {!isUser && (
+
+
+ }
+ onClick={async () => {
+ try {
+ await copyToClipboard(msg.content || '')
+ message.success('已复制')
+ } catch (err) {
+ console.error('copy failed', err)
+ message.error('复制失败,请在 HTTPS / localhost 或允许剪贴板权限的环境下重试')
+ }
+ }}
+ />
+
+
+ }
+ disabled={loading}
+ onClick={() => {
+ // 可复用最后一条用户消息再请求
+ const lastUser = [...currentMessages].reverse().find(m => m.role === 'user')
+ if (lastUser) {
+ setInputValue(lastUser.content)
+ sendMessage()
+ }
+ }}
+ />
+
+
+ )}
+
+
+ {/* 回答框 */}
+
+
+ {msg.content || ''}
+
+
+
+
+ )
+ }
+ // =================================================
+
+ return (
+
+
+
+ {currentMessages.length === 0 && !loading && (
+ // 已有默认初始化对话,无需此处提示
+
+
开始对话吧
+
提出一个问题,比如:请帮我总结一段文本。
+
+ )}
+
+ {/* 第一句话 */}
+ {currentMessages.map(renderMessage)}
+
+ {loading && (
+
+
+
+
+
+
+ AI
+ 正在思考…
+
+
+ }
+ size='small'
+ />
+
+
+
+
+
+
+ )}
+
+
+
+
+
+ {/* 问答输入框 */}
+
+
+
+
+
+ {/* 模型切换 */}
+
+
+ {/* 上传文件 */}
+
+ }
+ />
+
+ {/* 发送按钮 */}
+
+ }
+ onClick={sendMessage}
+ disabled={!inputValue.trim() || loading}
+ >
+
+
+
+
+
+ Enter 发送 · Shift+Enter 换行
+
|
+
清空当前对话
+
+
+
+
+ )
+}
+
+export default ChatConversation
diff --git a/src/pages/chatConversation/ChatConversation.less b/src/pages/chatConversation/ChatConversation.less
new file mode 100644
index 0000000..71ddb7f
--- /dev/null
+++ b/src/pages/chatConversation/ChatConversation.less
@@ -0,0 +1,477 @@
+
+// // 滚动条样式
+// .chat-list::-webkit-scrollbar {
+// width: 6px;
+// }
+
+// .chat-list::-webkit-scrollbar-track {
+// background: #f1f1f1;
+// border-radius: 3px;
+// }
+
+// .chat-list::-webkit-scrollbar-thumb {
+// background: #c1c1c1;
+// border-radius: 3px;
+// }
+
+// .chat-list::-webkit-scrollbar-thumb:hover {
+// background: #a8a8a8;
+// }
+
+// // 响应式设计
+// @media (max-width: 768px) {
+// .sidebar-column {
+// position: absolute !important;
+// z-index: 1000;
+// height: 100vh !important;
+// box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
+// }
+
+// .content-column {
+// padding: 8px;
+// }
+
+// .chat-conversation-card {
+// .chat-messages {
+// margin: 8px;
+// }
+
+// .chat-list {
+// padding: 12px;
+// }
+
+// .message-bubble {
+// max-width: 85% !important;
+// }
+
+// .chat-input {
+// padding: 12px;
+// }
+// }
+
+// .chat-sidebar {
+// padding: 12px;
+
+// .conversation-item {
+// padding: 8px;
+
+// .conversation-content .conversation-title .title-text {
+// max-width: 100px;
+// }
+// }
+// }
+// }
+
+
+
+// @keyframes fadeIn {
+// from {
+// opacity: 0;
+// transform: translateY(10px);
+// }
+
+// to {
+// opacity: 1;
+// transform: translateY(0);
+// }
+// }
+
+
+
+// @keyframes slideIn {
+// from {
+// opacity: 0;
+// transform: translateX(-10px);
+// }
+
+// to {
+// opacity: 1;
+// transform: translateX(0);
+// }
+// }
+
+
+
+/* DeepSeek 风格仿制样式 */
+
+.ds-chat-page {
+ position: relative;
+ height: 100%;
+ min-height: 100vh;
+ // background: #f5f7fb;
+ display: flex;
+ overflow-y: hidden;
+ flex-direction: column;
+ padding-bottom: 140px;
+ /* 给输入区预留空间 */
+ color: #1f2329;
+ // background: #f5f6fa;
+ border-radius: 8px;
+ // padding: 16px 20px 5px 20px;
+ padding: 0px 20px 0px 20px;
+ min-height: 100%;
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
+
+}
+
+.ds-chat-scroll {
+ flex: 1;
+ overflow-y: auto;
+ scrollbar-width: thin;
+}
+
+.ds-chat-scroll::-webkit-scrollbar {
+ width: 8px;
+}
+
+.ds-chat-scroll::-webkit-scrollbar-thumb {
+ background: rgba(0, 0, 0, .15);
+ border-radius: 4px;
+}
+
+.ds-chat-inner {
+ max-width: 860px;
+ margin: 0 auto;
+ padding: 40px 24px 24px;
+ box-sizing: border-box;
+}
+
+.ds-empty {
+ text-align: center;
+ margin-top: 80px;
+
+ h3 {
+ font-weight: 600;
+ font-size: 20px;
+ margin-bottom: 12px;
+ }
+
+ p {
+ color: #667085;
+ font-size: 14px;
+ }
+}
+
+.ds-message-row {
+ display: flex;
+ align-items: flex-start;
+ gap: 14px;
+ margin-bottom: 28px;
+ line-height: 1.6;
+
+ &.from-user {
+ flex-direction: row-reverse;
+
+ .ds-message-body {
+ align-items: flex-end;
+ }
+
+ .ds-message-content {
+ background: #f4f4f4;
+ color: #111;
+ border-radius: 20px;
+ font-size: 17px;
+ // border: 1px solid #3478f6;
+ // box-shadow: 0 2px 6px rgba(52, 120, 246, .3);
+
+ p {
+ color: #111;
+ }
+ }
+
+ .ds-message-meta {
+ justify-content: flex-end;
+
+ .role {
+ color: #3478f6;
+ }
+ }
+ }
+
+ &.thinking .ds-message-content {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+}
+
+.ds-avatar {
+ width: 34px;
+ height: 34px;
+ flex-shrink: 0;
+ border-radius: 8px;
+ background: #e9eef5;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ color: #355070;
+
+ &.user {
+ background: #e9eef5; // 用户头像也用这个颜色
+ color: #4f5fa3;
+ }
+}
+
+.ds-message-body {
+ display: flex;
+ flex-direction: column;
+ max-width: 100%;
+ // flex: 1;
+ position: relative;
+}
+
+.ds-message-meta {
+ font-size: 12px;
+ color: #86909c;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 4px;
+
+ .role {
+ font-weight: 600;
+ color: #1f2329;
+ }
+
+ .actions {
+ margin-left: auto;
+ opacity: 0;
+ transition: opacity .2s;
+ display: flex;
+ gap: 4px;
+
+ .ant-btn {
+ padding: 0 4px;
+ height: 24px;
+ }
+ }
+}
+
+.ds-message-row:hover .ds-message-meta .actions {
+ opacity: 1;
+}
+
+.ds-message-content {
+ display: inline-block;
+ max-width: 100%;
+ min-width: 48px;
+ background: #fff;
+ border: 1px solid #e3e8ef;
+ border-radius: 14px;
+ padding: 6px 16px;
+ font-size: 17px;
+ color: #1f2329;
+ position: relative;
+ overflow: hidden;
+ word-break: break-word;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, .04);
+
+ p {
+ margin: 0 0 8px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ code {
+ background: #f2f4f8;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-family: ui-monospace, Menlo, Consolas, monospace;
+ font-size: 13px;
+ }
+
+ pre {
+ background: #0f1115;
+ color: #e9edf2;
+ padding: 14px 16px;
+ border-radius: 12px;
+ overflow: auto;
+ font-size: 13px;
+ line-height: 1.5;
+
+ code {
+ background: transparent;
+ padding: 0;
+ color: inherit;
+ }
+ }
+}
+
+.typing-dots {
+ display: inline-flex;
+ gap: 4px;
+ margin-left: 6px;
+
+ i {
+ width: 6px;
+ height: 6px;
+ background: #3478f6;
+ border-radius: 50%;
+ display: block;
+ animation: blink 1s infinite ease-in-out;
+ }
+
+ i:nth-child(2) {
+ animation-delay: .2s;
+ }
+
+ i:nth-child(3) {
+ animation-delay: .4s;
+ }
+}
+
+@keyframes blink {
+
+ 0%,
+ 80%,
+ 100% {
+ opacity: .2;
+ transform: translateY(0);
+ }
+
+ 40% {
+ opacity: 1;
+ transform: translateY(-3px);
+ }
+}
+
+.ds-input-bar {
+ // display: flex;
+ // align-items: flex-end;
+ // position: fixed;
+ left: 0;
+ right: 0;
+ // bottom: -10vh;
+ // background: linear-gradient(to top, #f5f7fb 40%, rgba(245, 247, 251, 0));
+ // padding: 32px 0 28px;
+ z-index: 20;
+}
+
+.ds-input-inner {
+ display: flex;
+ align-items: flex-end;
+ width: 900px;
+ margin: 0 auto;
+ background: #fff;
+ border: 1px solid #d9dde3;
+ border-radius: 20px;
+ padding: 12px 14px 12px 16px;
+
+ gap: 12px;
+ box-shadow: 0 6px 28px -8px rgba(33, 53, 79, .18), 0 4px 12px -4px rgba(33, 53, 79, .08);
+}
+
+.ds-textarea-wrap {
+ display: flex;
+ flex: 1;
+
+ .ant-input {
+ font-size: 19px;
+ line-height: 1.5;
+ padding: 0;
+ background: transparent;
+ }
+
+ .ant-input:focus {
+ box-shadow: none;
+ }
+}
+
+.ds-uploading {
+
+ .ant-btn.ant-btn-icon-only .anticon {
+ font-size: 18px;
+ }
+
+ .ant-btn {
+ border: none;
+ background: transparent;
+ box-shadow: none;
+ color: #111;
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ background: #f2f3f4;
+ color: #111;
+ box-shadow: none;
+ }
+ }
+
+}
+
+.ds-input-actions {
+ display: flex;
+ align-items: flex-end;
+
+ .ant-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 500;
+ padding: 0 0px;
+ margin: 0;
+ height: 36px;
+ width: 36px;
+ background-color: #b0c1ff;
+ border: none;
+ color: #fff;
+
+ }
+}
+
+
+
+.ds-input-hint {
+ max-width: 860px;
+ margin: 6px auto 0;
+ font-size: 12px;
+ color: #808791;
+ display: flex;
+ gap: 8px;
+
+ a {
+ color: #3478f6;
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .sep {
+ opacity: .3;
+ }
+}
+.ds-select,
+.ds-select .ant-select-selector,
+.ds-select .ant-select-selection-item,
+.ds-select .ant-select-dropdown {
+ font-size: 12px !important;
+ color: #808791 !important;
+}
+.ds-select-dropdown .ant-select-item {
+ font-size: 12px !important;
+ color: #808791 !important;
+}
+
+@media (max-width: 1000px) {
+
+ .ds-chat-inner,
+ .ds-input-inner,
+ .ds-input-hint {
+ max-width: 100%;
+ padding-left: 16px;
+ padding-right: 16px;
+ }
+
+ .ds-input-inner {
+ border-radius: 16px;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/chatConversation/models/api.js b/src/pages/chatConversation/models/api.js
new file mode 100644
index 0000000..ec4c2d1
--- /dev/null
+++ b/src/pages/chatConversation/models/api.js
@@ -0,0 +1,183 @@
+// API配置文件
+export const API_CONFIG = {
+ // DeepSeek API配置
+ DEEPSEEK: {
+ BASE_URL: 'https://api.deepseek.com/v1',
+ MODEL: 'deepseek-chat',
+ API_KEY:
+ process.env.UMI_APP_DEEPSEEK_API_KEY ||
+ process.env.REACT_APP_DEEPSEEK_API_KEY ||
+ 'your-api-key-here',
+ MAX_TOKENS: 2000,
+ TEMPERATURE: 0.7,
+ SYSTEM_PROMPT: '你是一个智能AI助手,请用简洁、专业、友好的方式回答用户的问题。'
+ },
+
+ // 其他API配置
+ OPENAI: {
+ BASE_URL: 'https://api.openai.com/v1',
+ MODEL: 'gpt-3.5-turbo',
+ API_KEY: process.env.UMI_APP_OPENAI_API_KEY || '',
+ MAX_TOKENS: 2000,
+ TEMPERATURE: 0.7
+ }
+}
+
+// 调试:开发阶段临时查看是否拿到
+if (process.env.NODE_ENV === 'development') {
+ // 只显示是否存在,不打印具体 key
+ // eslint-disable-next-line no-console
+ console.log(
+ 'DeepSeek key loaded =',
+ !!process.env.UMI_APP_DEEPSEEK_API_KEY
+ )
+}
+
+// DeepSeek 调用
+export const callDeepSeekAPI = async (messages, options = {}) => {
+ const cfg = API_CONFIG.DEEPSEEK
+ const apiKey = cfg.API_KEY
+ if (!apiKey || apiKey === 'your-api-key-here') {
+ throw new Error('NO_API_KEY')
+ }
+
+ // 组装消息(如果历史里已经含 system 就不重复)
+ const hasSystem = messages.some(m => m.role === 'system')
+ const finalMessages = hasSystem
+ ? messages
+ : [{ role: 'system', content: cfg.SYSTEM_PROMPT }, ...messages]
+
+ const payload = {
+ model: cfg.MODEL,
+ messages: finalMessages.map(m => ({ role: m.role, content: m.content })),
+ temperature: options.temperature ?? cfg.TEMPERATURE,
+ max_tokens: options.maxTokens ?? cfg.MAX_TOKENS,
+ stream: false
+ }
+
+ const resp = await fetch(`${cfg.BASE_URL}/chat/completions`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${apiKey}`
+ },
+ body: JSON.stringify(payload)
+ })
+
+ const data = await resp.json().catch(() => ({}))
+
+ if (!resp.ok) {
+ // 统一抛出,交给 handleAPIError
+ const msg = data?.error?.message || resp.statusText || 'API_ERROR'
+ const err = new Error(msg)
+ err.status = resp.status
+ err.code = data?.error?.code
+ throw err
+ }
+
+ const content = data?.choices?.[0]?.message?.content?.trim()
+ if (!content) throw new Error('EMPTY_RESPONSE')
+ return content
+}
+
+// 本地服务调用(调试版:打印详细日志,方便逐步排查)
+export const callLocalChatAPI = async (prompt) => {
+ const base = '/api/chat/stream'
+ const url = `${base}?prompt=${encodeURIComponent(prompt)}`
+
+ // 逐步调试日志
+ // eslint-disable-next-line no-console
+ console.debug('[callLocalChatAPI] url:', url)
+ try {
+ // eslint-disable-next-line no-console
+ console.debug('[callLocalChatAPI] fetch start')
+ const resp = await fetch(url, { method: 'GET' })
+ // eslint-disable-next-line no-console
+ console.debug('[callLocalChatAPI] fetch done', {
+ ok: resp.ok,
+ status: resp.status,
+ statusText: resp.statusText,
+ headers: Object.fromEntries(resp.headers.entries())
+ })
+
+ if (!resp.ok) {
+ const txt = await resp.text().catch(() => null)
+ // eslint-disable-next-line no-console
+ console.error('[callLocalChatAPI] non-OK response body:', txt)
+ const msg = txt || resp.statusText || `HTTP_${resp.status}`
+ const err = new Error(msg)
+ err.status = resp.status
+ throw err
+ }
+
+ // 最简单:一次性读取完整响应文本(非流式),然后从 SSE 格式中提取第一个 event:message 的 data
+ const raw = await resp.text().catch(e => {
+ // eslint-disable-next-line no-console
+ console.error('[callLocalChatAPI] text read error:', e)
+ return ''
+ })
+ // eslint-disable-next-line no-console
+ console.debug('[callLocalChatAPI] raw length:', raw?.length || 0, 'preview:', (raw || '').slice(0, 500))
+ if (!raw) throw new Error('EMPTY_RESPONSE')
+
+ // 按空行分块,优先找到 event:message 的 data
+ const blocks = raw.split(/\r?\n\r?\n/).map(b => b.trim()).filter(Boolean)
+ for (const block of blocks) {
+ const lines = block.split(/\r?\n/).map(l => l.trim())
+ let eventType = ''
+ const dataLines = []
+ for (const line of lines) {
+ if (/^event\s*:/i.test(line)) {
+ eventType = line.split(':').slice(1).join(':').trim()
+ } else if (/^data\s*:/i.test(line)) {
+ dataLines.push(line.split(':').slice(1).join(':'))
+ }
+ }
+ if (eventType === 'message' && dataLines.length > 0) {
+ const message = dataLines.join('\n').trim()
+ // eslint-disable-next-line no-console
+ console.debug('[callLocalChatAPI] parsed message data preview:', message.slice(0, 300))
+ if (!message) throw new Error('EMPTY_RESPONSE')
+ return message
+ }
+ }
+
+ // 回退:取第一个 data: 行的内容
+ const firstDataMatch = raw.match(/^[ \t]*data\s*:(.*)$/im)
+ if (firstDataMatch && firstDataMatch[1]) {
+ const fallback = firstDataMatch[1].trim()
+ // eslint-disable-next-line no-console
+ console.debug('[callLocalChatAPI] fallback data:', fallback.slice(0, 300))
+ return fallback
+ }
+
+ // 最后回退:返回原始文本
+ return raw
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error('[callLocalChatAPI] ERROR:', err)
+ throw err
+ }
+}
+
+// 错误处理函数
+export const handleAPIError = (error) => {
+ const raw = error?.message || ''
+ const status = error?.status
+ const code = error?.code
+
+ if (raw === 'NO_API_KEY') return '未配置 API Key,请在 .env 里加 UMI_APP_DEEPSEEK_API_KEY 并重启。'
+ if (raw === 'EMPTY_RESPONSE') return '接口返回空结果,请重试。'
+
+ // 典型鉴权问题
+ if (status === 401 || /unauthorized|invalid api key/i.test(raw) || code === 'invalid_api_key') {
+ return 'API密钥无效,请检查是否正确。'
+ }
+
+ if (status === 429 || /rate limit/i.test(raw)) return '请求过多,被限流。'
+ if (status === 500) return '服务端错误,请稍后再试。'
+ if (/timeout/i.test(raw)) return '请求超时,请检查网络。'
+ if (/certificate|SSL/i.test(raw)) return '证书/网络问题,请更换网络或代理。'
+
+ return `调用失败:${raw || '未知错误'}`
+}
diff --git a/src/pages/topnavbar/TopNavBar.js b/src/pages/topnavbar/TopNavBar.js
index 4064782..583aca4 100644
--- a/src/pages/topnavbar/TopNavBar.js
+++ b/src/pages/topnavbar/TopNavBar.js
@@ -48,6 +48,10 @@ const menuItem = [
key: '/topnavbar00/hrefficiency/backgroundManagement',
// icon: ,
},
+ {
+ label: '智能对话',
+ path: '/topnavbar00/hrefficiency/ChatConversation',
+ },
// {
// label: '智能体配置1',
// key: '/topnavbar00/hrefficiency/staffuph',
diff --git a/src/utils/conversationStore.js b/src/utils/conversationStore.js
new file mode 100644
index 0000000..c4e08c0
--- /dev/null
+++ b/src/utils/conversationStore.js
@@ -0,0 +1,179 @@
+// 全局对话状态管理
+class ConversationStore {
+ constructor() {
+ this.conversations = [
+ {
+ id: '1',
+ title: '新对话',
+ messages: [
+ {
+ role: 'assistant',
+ content: '你好!我是AI助手,有什么可以帮助你的吗?',
+ timestamp: new Date().toLocaleTimeString()
+ }
+ ],
+ lastUpdate: new Date(),
+ isActive: true
+ }
+ ]
+
+ this.currentConversationId = '1'
+ this.listeners = []
+ }
+
+ // 获取所有对话
+ getConversations() {
+ return this.conversations
+ }
+
+ // 获取当前对话
+ getCurrentConversation() {
+ return this.conversations.find(conv => conv.id === this.currentConversationId)
+ }
+
+ // 获取当前对话ID
+ getCurrentConversationId() {
+ return this.currentConversationId
+ }
+
+ // 创建新对话
+ createNewConversation() {
+ const newId = Date.now().toString()
+ const newConversation = {
+ id: newId,
+ title: '新对话',
+ messages: [
+ {
+ role: 'assistant',
+ content: '你好!我是AI助手,有什么可以帮助你的吗?',
+ timestamp: new Date().toLocaleTimeString()
+ }
+ ],
+ lastUpdate: new Date(),
+ isActive: true
+ }
+
+ // 将之前的对话设为非活跃
+ this.conversations = this.conversations.map(conv => ({ ...conv, isActive: false }))
+
+ // 添加新对话
+ this.conversations.push(newConversation)
+ this.currentConversationId = newId
+
+ this.notifyListeners()
+ return newId
+ }
+
+ // 切换对话
+ switchConversation(conversationId) {
+ this.conversations = this.conversations.map(conv => ({
+ ...conv,
+ isActive: conv.id === conversationId
+ }))
+ this.currentConversationId = conversationId
+
+ this.notifyListeners()
+ }
+
+ // 删除对话
+ deleteConversation(conversationId) {
+ if (this.conversations.length <= 1) {
+ return false
+ }
+
+ this.conversations = this.conversations.filter(conv => conv.id !== conversationId)
+
+ // 如果删除的是当前对话,切换到第一个对话
+ if (conversationId === this.currentConversationId) {
+ if (this.conversations.length > 0) {
+ this.currentConversationId = this.conversations[0].id
+ this.conversations = this.conversations.map((conv, index) =>
+ index === 0 ? { ...conv, isActive: true } : { ...conv, isActive: false }
+ )
+ }
+ }
+
+ this.notifyListeners()
+ return true
+ }
+
+ // 添加消息到当前对话
+ addMessage(role, content) {
+ const currentConversation = this.getCurrentConversation()
+ if (!currentConversation) return
+
+ const message = {
+ role,
+ content,
+ timestamp: new Date().toLocaleTimeString()
+ }
+
+ currentConversation.messages.push(message)
+ currentConversation.lastUpdate = new Date()
+
+ // 如果是用户的第一条消息,更新对话标题
+ if (role === 'user' && currentConversation.title === '新对话') {
+ currentConversation.title = content.slice(0, 20) + '...'
+ }
+
+ this.notifyListeners()
+ }
+
+ // 更新对话标题
+ updateConversationTitle(conversationId, newTitle) {
+ const conversation = this.conversations.find(conv => conv.id === conversationId)
+ if (conversation) {
+ conversation.title = newTitle
+ this.notifyListeners()
+ }
+ }
+
+ // 清空当前对话
+ clearCurrentConversation() {
+ const currentConversation = this.getCurrentConversation()
+ if (currentConversation) {
+ currentConversation.messages = [
+ {
+ role: 'assistant',
+ content: '你好!我是AI助手,有什么可以帮助你的吗?',
+ timestamp: new Date().toLocaleTimeString()
+ }
+ ]
+ currentConversation.title = '新对话'
+ currentConversation.lastUpdate = new Date()
+
+ this.notifyListeners()
+ }
+ }
+
+ // 订阅状态变化
+ subscribe(listener) {
+ this.listeners.push(listener)
+
+ // 返回取消订阅函数
+ return () => {
+ const index = this.listeners.indexOf(listener)
+ if (index > -1) {
+ this.listeners.splice(index, 1)
+ }
+ }
+ }
+
+ // 通知所有监听器
+ notifyListeners() {
+ this.listeners.forEach(listener => {
+ listener({
+ conversations: this.conversations,
+ currentConversationId: this.currentConversationId,
+ currentConversation: this.getCurrentConversation()
+ })
+ })
+ }
+}
+
+// 创建全局单例实例
+const conversationStore = new ConversationStore()
+
+// 导出实例和类
+export default conversationStore
+export { ConversationStore }
diff --git a/src/utils/pageConversationStore.js b/src/utils/pageConversationStore.js
new file mode 100644
index 0000000..f7a58f2
--- /dev/null
+++ b/src/utils/pageConversationStore.js
@@ -0,0 +1,202 @@
+// 每个页面独立的对话存储工具
+class PageConversationStore {
+ constructor(pageName) {
+ this.pageName = pageName
+ this.storageKey = `page_conversations_${pageName}`
+ console.log('Initializing store for page:', this.storageKey)
+ this.conversations = this.loadFromStorage()
+ this.currentConversationId = this.conversations.length > 0 ? this.conversations[0].id : null
+ this.subscribers = []
+
+ // 如果没有对话,创建默认对话
+ if (this.conversations.length === 0) {
+ this.createNewConversation()
+ }
+ }
+
+ // 从本地存储加载对话
+ loadFromStorage() {
+ try {
+ const stored = localStorage.getItem(this.storageKey)
+ if (stored) {
+ const parsed = JSON.parse(stored)
+ // 恢复日期对象
+ return parsed.map(conv => ({
+ ...conv,
+ lastUpdate: new Date(conv.lastUpdate),
+ messages: conv.messages.map(msg => ({
+ ...msg,
+ timestamp: new Date(msg.timestamp)
+ }))
+ }))
+ }
+ } catch (error) {
+ console.error(`Error loading conversations for ${this.pageName}:`, error)
+ }
+ return []
+ }
+
+ // 保存到本地存储
+ saveToStorage() {
+ try {
+ localStorage.setItem(this.storageKey, JSON.stringify(this.conversations))
+ } catch (error) {
+ console.error(`Error saving conversations for ${this.pageName}:`, error)
+ }
+ }
+
+ // 获取所有对话
+ getConversations() {
+ return this.conversations
+ }
+
+ // 获取当前对话ID
+ getCurrentConversationId() {
+ return this.currentConversationId
+ }
+
+ // 获取当前对话
+ getCurrentConversation() {
+ return this.conversations.find(conv => conv.id === this.currentConversationId)
+ }
+
+ // 创建新对话
+ createNewConversation() {
+ const newConversation = {
+ id: `${this.pageName}_${Date.now()}`,
+ title: `新对话 ${this.conversations.length + 1}`,
+ messages: [
+ {
+ role: 'assistant',
+ content: `欢迎使用${this.pageName}!我是您的AI助手,有什么可以帮助您的吗?`,
+ timestamp: new Date()
+ }
+ ],
+ lastUpdate: new Date(),
+ isActive: true
+ }
+
+ // 将之前的对话设为非活跃
+ this.conversations.forEach(conv => conv.isActive = false)
+
+ this.conversations.unshift(newConversation)
+ this.currentConversationId = newConversation.id
+ this.saveToStorage()
+ this.notifySubscribers()
+ }
+
+ // 切换对话
+ switchConversation(conversationId) {
+ this.conversations.forEach(conv => conv.isActive = false)
+ const targetConversation = this.conversations.find(conv => conv.id === conversationId)
+ if (targetConversation) {
+ targetConversation.isActive = true
+ this.currentConversationId = conversationId
+ this.saveToStorage()
+ this.notifySubscribers()
+ }
+ }
+
+ // 添加消息
+ addMessage(role, content) {
+ const currentConversation = this.getCurrentConversation()
+ if (currentConversation) {
+ const newMessage = {
+ role,
+ content,
+ timestamp: new Date()
+ }
+
+ currentConversation.messages.push(newMessage)
+ currentConversation.lastUpdate = new Date()
+
+ // 如果是用户消息,更新标题
+ if (role === 'user') {
+ const words = content.split(' ').slice(0, 5)
+ currentConversation.title = words.join(' ') + (content.length > 30 ? '...' : '')
+ }
+
+ this.saveToStorage()
+ this.notifySubscribers()
+ }
+ }
+
+ // 更新对话标题
+ updateConversationTitle(conversationId, newTitle) {
+ const conversation = this.conversations.find(conv => conv.id === conversationId)
+ if (conversation) {
+ conversation.title = newTitle
+ this.saveToStorage()
+ this.notifySubscribers()
+ }
+
+ }
+
+ // 删除对话
+ deleteConversation(conversationId) {
+ const index = this.conversations.findIndex(conv => conv.id === conversationId)
+ if (index !== -1) {
+ this.conversations.splice(index, 1)
+
+ // 如果删除的是当前对话,切换到第一个对话
+ if (conversationId === this.currentConversationId) {
+ if (this.conversations.length > 0) {
+ this.currentConversationId = this.conversations[0].id
+ this.conversations[0].isActive = true
+ } else {
+ this.currentConversationId = null
+ }
+ }
+
+ this.saveToStorage()
+ this.notifySubscribers()
+ }
+ }
+
+ // 清空当前对话
+ clearCurrentConversation() {
+ const currentConversation = this.getCurrentConversation()
+ if (currentConversation) {
+ currentConversation.messages = [
+ {
+ role: 'assistant',
+ content: `对话已清空,有什么可以帮助您的吗?`,
+ timestamp: new Date()
+ }
+ ]
+ currentConversation.lastUpdate = new Date()
+ this.saveToStorage()
+ this.notifySubscribers()
+ }
+ }
+
+ // 订阅状态变化
+ subscribe(callback) {
+ this.subscribers.push(callback)
+ return () => {
+ const index = this.subscribers.indexOf(callback)
+ if (index !== -1) {
+ this.subscribers.splice(index, 1)
+ }
+ }
+ }
+
+ // 通知订阅者
+ notifySubscribers() {
+ this.subscribers.forEach(callback => {
+ callback({
+ conversations: this.conversations,
+ currentConversationId: this.currentConversationId
+ })
+ })
+ }
+}
+
+// 为每个页面创建独立的存储实例
+export const dataAnalysisStore = new PageConversationStore('数据分析')
+export const conversationStore = new PageConversationStore('智能对话')
+export const writingStore = new PageConversationStore('智能写作')
+export const mobileStore = new PageConversationStore('问答移动端')
+export const assistantStore = new PageConversationStore('智能助理')
+
+export default PageConversationStore