diff --git a/.env b/.env index 2d2c2e1..a573941 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ VITE_SUPABASE_URL=https://base.uppmkt.com VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q - +VITE_DIFY_API_KEY=app-OK28lKfA6Ib82K0B4vwZcsXK diff --git a/src/components/difyChatAi/index.jsx b/src/components/difyChatAi/index.jsx new file mode 100644 index 0000000..4e93cfd --- /dev/null +++ b/src/components/difyChatAi/index.jsx @@ -0,0 +1,218 @@ +import { Button, Drawer, Input, Space, message } from 'antd'; +import { CodeHighlight } from "@mantine/code-highlight"; +import { DownloadOutlined, EditOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons'; +import { useRef, useEffect, useState } from 'react'; +import { useDifyChat } from '@/hooks/aichat'; + +export default function DifyChatDrawer({ open, onClose, onExport }) { + const { + messages, + setMessages, + isLoading, + sendMessage, + clearHistory, + storedMessages, + setStoredMessages + } = useDifyChat(); + + const [input, setInput] = useState(''); + const messagesEndRef = useRef(null); + + const [editingMessageId, setEditingMessageId] = useState(null); + const [editingContent, setEditingContent] = useState(''); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + const handleSendMessage = async (e) => { + e.preventDefault(); + await sendMessage(input); + setInput(''); + }; + + const handleEdit = (message) => { + if(isLoading) return; + setEditingContent(message.content); + setEditingMessageId(message.id); + }; + + const handleSaveEdit = () => { + try { + JSON.parse(editingContent); + setMessages(messages.map(msg => + msg.id === editingMessageId + ? { ...msg, content: editingContent } + : msg + )); + setEditingMessageId(null); + message.success('编辑成功'); + } catch (error) { + message.error('请输入有效的 JSON 格式'); + } + }; + + const handleCancelEdit = () => { + setEditingMessageId(null); + setEditingContent(''); + }; + + const handleExport = (content) => { + try { + const jsonContent = JSON.parse(content); + onExport?.(jsonContent); + } catch (error) { + console.error(error); + message.error('导出失败,请重试'); + } + }; + + return ( + + AI 助手 + + + } + placement="right" + width={800} + open={open} + onClose={onClose} + className="rounded-l-xl" + > +
+
+ {messages.map((message) => ( +
+
+ + {message.role === 'assistant' ? 'AI 助手' : '用户'} + + {message.role === 'assistant' && ( + + + + + )} +
+ + {message.role === "assistant" ? ( +
+ {editingMessageId === message.id ? ( +
+ { + editor.getModel()?.updateOptions({ tabSize: 2 }); + editor.focus(); + }} + /> +
+ + +
+
+ ) : ( + + )} +
+ ) : ( +
+ {message.content} +
+ )} +
+ ))} +
+
+ +
+
+ setInput(e.target.value)} + disabled={isLoading} + className="flex-1 rounded-lg border-gray-300 hover:border-blue-400 focus:border-blue-600 focus:shadow-blue-100" + /> + +
+
+
+ + ); +} diff --git a/src/hooks/aichat.jsx b/src/hooks/aichat.jsx new file mode 100644 index 0000000..0dc62ec --- /dev/null +++ b/src/hooks/aichat.jsx @@ -0,0 +1,143 @@ +import { useState, useRef } from 'react'; +import { useSessionStorage } from 'react-use'; +import { message } from 'antd'; + +const DIFY_API_KEY = import.meta.env.VITE_DIFY_API_KEY; +const DIFY_API_ENDPOINT = 'https://api.dify.ai/v1'; + +const STORAGE_KEY = 'dify_chat_history'; +const CONVERSATION_ID_KEY = 'dify_conversation_id'; + +export function useDifyChat() { + const [storedMessages, setStoredMessages] = useSessionStorage(STORAGE_KEY, '[]'); + const [conversationId, setConversationId] = useSessionStorage(CONVERSATION_ID_KEY, ''); + const [messages, setMessages] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const abortController = useRef(null); + + const clearHistory = () => { + setMessages([]); + setStoredMessages('[]'); + setConversationId(''); + message.success('历史记录已清空'); + }; + + const sendMessage = async (input) => { + if (!input.trim() || isLoading) return; + + try { + setIsLoading(true); + + const userMessage = { role: 'user', content: input, id: Date.now().toString() }; + const newMessages = [...messages, userMessage]; + setMessages(newMessages); + + const conversationMessages = newMessages.map(msg => ({ + role: msg.role, + content: msg.content + })); + + abortController.current = new AbortController(); + + const requestBody = { + query: input, + user: 'default-user', + response_mode: "streaming", + inputs: {}, + conversation_history: conversationMessages + }; + + if (conversationId) { + requestBody.conversation_id = conversationId; + } + + const response = await fetch(`${DIFY_API_ENDPOINT}/chat-messages`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${DIFY_API_KEY}` + }, + body: JSON.stringify(requestBody), + signal: abortController.current.signal + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'API 请求失败'); + } + + const aiMessage = { role: 'assistant', content: '', id: Date.now().toString() }; + setMessages(msgs => [...msgs, aiMessage]); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + const lines = chunk.split('\n').filter(line => line.trim()); + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.slice(6)); + + switch (data.event) { + case 'workflow_started': + if (data.conversation_id) { + setConversationId(data.conversation_id); + } + break; + + case 'message': + if (data.answer) { + aiMessage.content += data.answer; + setMessages(msgs => msgs.map(msg => + msg.id === aiMessage.id ? aiMessage : msg + )); + } + break; + + case 'error': + throw new Error(data.data?.message || '未知错误'); + + case 'done': + console.log('Stream completed'); + break; + + default: + break; + } + } catch (e) { + console.error('Error parsing stream:', e); + } + } + } + } + + } catch (error) { + if (error.name === 'AbortError') { + message.info('消息发送已取消'); + } else { + console.error('Error:', error); + message.error('发送消息失败'); + } + } finally { + setIsLoading(false); + abortController.current = null; + } + }; + + return { + messages, + setMessages, + isLoading, + sendMessage, + clearHistory, + storedMessages, + setStoredMessages + }; +} \ No newline at end of file diff --git a/src/pages/company/quotation/detail/index.jsx b/src/pages/company/quotation/detail/index.jsx index 34dbb1a..63809ab 100644 --- a/src/pages/company/quotation/detail/index.jsx +++ b/src/pages/company/quotation/detail/index.jsx @@ -21,8 +21,8 @@ import { supabase } from "@/config/supabase"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { v4 as uuidv4 } from "uuid"; import SectionList from '@/components/SectionList' -import ChatAIDrawer from '@/components/ChatAi'; - +import DifyChatDrawer from '@/components/difyChatAi'; +import ChatAIDrawer from '@/components/ChatAi'; const { Title } = Typography; // 添加货币符号映射 @@ -311,7 +311,8 @@ const QuotationForm = () => { } }, [id, templateId]); - const [open, setOpen] = useState(false); + const [difyOpen, setDifyOpen] = useState(false); + const [vercelOpen, setVercelOpen] = useState(false); const handleExport = (data) => { if(data?.activityName&&data?.currency){ @@ -353,7 +354,8 @@ const QuotationForm = () => { message.success('已添加新的服务项目'); } - setOpen(false); + setDifyOpen(false); + setVercelOpen(false); }; return ( @@ -393,8 +395,11 @@ const QuotationForm = () => { 保存 - + )} @@ -497,9 +502,15 @@ const QuotationForm = () => { + + setDifyOpen(false)} + onExport={handleExport} + /> setOpen(false)} + open={vercelOpen} + onClose={() => setVercelOpen(false)} onExport={handleExport} />