dify ai
This commit is contained in:
2
.env
2
.env
@@ -1,6 +1,6 @@
|
|||||||
VITE_SUPABASE_URL=https://base.uppmkt.com
|
VITE_SUPABASE_URL=https://base.uppmkt.com
|
||||||
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
|
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
|
||||||
|
VITE_DIFY_API_KEY=app-OK28lKfA6Ib82K0B4vwZcsXK
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
218
src/components/difyChatAi/index.jsx
Normal file
218
src/components/difyChatAi/index.jsx
Normal file
@@ -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 (
|
||||||
|
<Drawer
|
||||||
|
title={
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-lg font-medium text-gray-800">AI 助手</span>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
className="hover:bg-gray-100"
|
||||||
|
onClick={clearHistory}
|
||||||
|
>
|
||||||
|
清空历史
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement="right"
|
||||||
|
width={800}
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
className="rounded-l-xl"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col h-[calc(100vh-108px)]">
|
||||||
|
<div className="flex-1 overflow-y-auto px-4 space-y-6">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className={`rounded-lg p-4 transition-all ${
|
||||||
|
message.role === 'assistant'
|
||||||
|
? 'bg-blue-50 hover:bg-blue-100'
|
||||||
|
: 'bg-gray-50 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center mb-3">
|
||||||
|
<span className={`font-medium ${
|
||||||
|
message.role === 'assistant' ? 'text-blue-600' : 'text-gray-600'
|
||||||
|
}`}>
|
||||||
|
{message.role === 'assistant' ? 'AI 助手' : '用户'}
|
||||||
|
</span>
|
||||||
|
{message.role === 'assistant' && (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={() => handleEdit(message)}
|
||||||
|
className="text-gray-500 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
onClick={() => handleExport(message.content)}
|
||||||
|
className="text-gray-500 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{message.role === "assistant" ? (
|
||||||
|
<div className="relative">
|
||||||
|
{editingMessageId === message.id ? (
|
||||||
|
<div className="rounded-lg border border-blue-200">
|
||||||
|
<Editor
|
||||||
|
height="300px"
|
||||||
|
defaultLanguage="json"
|
||||||
|
value={editingContent}
|
||||||
|
theme="vs-light"
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: false },
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
fontSize: 14,
|
||||||
|
lineNumbers: 'on',
|
||||||
|
renderLineHighlight: 'none',
|
||||||
|
roundedSelection: true,
|
||||||
|
}}
|
||||||
|
onChange={setEditingContent}
|
||||||
|
onMount={(editor) => {
|
||||||
|
editor.getModel()?.updateOptions({ tabSize: 2 });
|
||||||
|
editor.focus();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end gap-2 p-2 bg-gray-50 border-t">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={handleCancelEdit}
|
||||||
|
className="hover:bg-gray-200"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon={<CheckOutlined />}
|
||||||
|
onClick={handleSaveEdit}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CodeHighlight
|
||||||
|
code={message.content}
|
||||||
|
language="json"
|
||||||
|
copyLabel="复制代码"
|
||||||
|
copiedLabel="已复制!"
|
||||||
|
withLineNumbers
|
||||||
|
className="rounded-lg"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-gray-700 whitespace-pre-wrap break-words">
|
||||||
|
{message.content}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t bg-white p-4">
|
||||||
|
<form onSubmit={handleSendMessage} className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={input}
|
||||||
|
placeholder="请输入您的问题..."
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={isLoading}
|
||||||
|
className="rounded-lg bg-blue-600 hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
143
src/hooks/aichat.jsx
Normal file
143
src/hooks/aichat.jsx
Normal file
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -21,8 +21,8 @@ import { supabase } from "@/config/supabase";
|
|||||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import SectionList from '@/components/SectionList'
|
import SectionList from '@/components/SectionList'
|
||||||
import ChatAIDrawer from '@/components/ChatAi';
|
import DifyChatDrawer from '@/components/difyChatAi';
|
||||||
|
import ChatAIDrawer from '@/components/ChatAi';
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
// 添加货币符号映射
|
// 添加货币符号映射
|
||||||
@@ -311,7 +311,8 @@ const QuotationForm = () => {
|
|||||||
}
|
}
|
||||||
}, [id, templateId]);
|
}, [id, templateId]);
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [difyOpen, setDifyOpen] = useState(false);
|
||||||
|
const [vercelOpen, setVercelOpen] = useState(false);
|
||||||
|
|
||||||
const handleExport = (data) => {
|
const handleExport = (data) => {
|
||||||
if(data?.activityName&&data?.currency){
|
if(data?.activityName&&data?.currency){
|
||||||
@@ -353,7 +354,8 @@ const QuotationForm = () => {
|
|||||||
message.success('已添加新的服务项目');
|
message.success('已添加新的服务项目');
|
||||||
}
|
}
|
||||||
|
|
||||||
setOpen(false);
|
setDifyOpen(false);
|
||||||
|
setVercelOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -393,8 +395,11 @@ const QuotationForm = () => {
|
|||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button onClick={() => setOpen(true)}>
|
<Button onClick={() => setDifyOpen(true)}>
|
||||||
AI 助手
|
AI for Dify
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setVercelOpen(true)}>
|
||||||
|
AI for Vercel
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -497,9 +502,15 @@ const QuotationForm = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<DifyChatDrawer
|
||||||
|
open={difyOpen}
|
||||||
|
onClose={() => setDifyOpen(false)}
|
||||||
|
onExport={handleExport}
|
||||||
|
/>
|
||||||
<ChatAIDrawer
|
<ChatAIDrawer
|
||||||
open={open}
|
open={vercelOpen}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setVercelOpen(false)}
|
||||||
onExport={handleExport}
|
onExport={handleExport}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user