提交 350da781 authored 作者: 王鹏飞's avatar 王鹏飞

chore: update

上级 068ee8d5
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
export default function MarkdownRender(props: { children?: string }) {
return <Markdown remarkPlugins={[remarkGfm]}>{props.children}</Markdown>
}
...@@ -2,9 +2,8 @@ import { useState, KeyboardEvent, useEffect, useRef } from 'react' ...@@ -2,9 +2,8 @@ import { useState, KeyboardEvent, useEffect, useRef } from 'react'
import { Button, Card, FloatButton, Input, Select } from 'antd' import { Button, Card, FloatButton, Input, Select } from 'antd'
import { CircleArrowLeft, CircleArrowRight } from 'lucide-react' import { CircleArrowLeft, CircleArrowRight } from 'lucide-react'
import { OpenAIOutlined, ArrowUpOutlined } from '@ant-design/icons' import { OpenAIOutlined, ArrowUpOutlined } from '@ant-design/icons'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { useAIStore, AIMessage } from '@/stores/ai' import { useAIStore, AIMessage } from '@/stores/ai'
import MarkdownRender from '../MarkdownRender'
import './AIChat.scss' import './AIChat.scss'
export const MessageItem = ({ message }: { message: AIMessage }) => { export const MessageItem = ({ message }: { message: AIMessage }) => {
...@@ -12,7 +11,7 @@ export const MessageItem = ({ message }: { message: AIMessage }) => { ...@@ -12,7 +11,7 @@ export const MessageItem = ({ message }: { message: AIMessage }) => {
<div className={`message-item ${message.role}`}> <div className={`message-item ${message.role}`}>
<div className="message-box"> <div className="message-box">
<div className="message-content"> <div className="message-content">
<Markdown remarkPlugins={[remarkGfm]}>{message.content}</Markdown> <MarkdownRender>{message.content}</MarkdownRender>
</div> </div>
</div> </div>
</div> </div>
......
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import aiService, { AIMessage, AIData, AI_OPTIONS } from '@/utils/ai' import aiService, { AIMessage, AIData, AI_OPTIONS, InitOptions } from '@/utils/ai'
export function useAI() { export function useAI(globalOptions?: InitOptions) {
const [ai, setAI] = useState<string>(localStorage.getItem('ai') || 'qwen') const [ai, setAI] = useState<string>(localStorage.getItem('ai') || 'qwen')
const [messages, setMessages] = useState<AIMessage[]>([]) const [messages, setMessages] = useState<AIMessage[]>([])
const [message, setMessage] = useState<AIMessage | null>(null) // 存储最新的消息
const [isLoading, setIsLoading] = useState<boolean>(false) const [isLoading, setIsLoading] = useState<boolean>(false)
useEffect(() => { useEffect(() => {
localStorage.setItem('ai', ai) localStorage.setItem('ai', ai)
}, [ai]) }, [ai])
const post = useCallback( const post = useCallback(
async (data: AIData) => { async (data: AIData, options?: InitOptions) => {
setIsLoading(true) setIsLoading(true)
setMessages((prevMessages) => [...prevMessages, ...data.messages.filter((item) => item.role !== 'system')])
try { // 先添加用户消息(去掉 system 角色的消息)
await aiService.post(ai, data, { const userMessages = data.messages.filter((item) => item.role !== 'system')
onUpdate: (response) => { setMessages((prevMessages) => [...prevMessages, ...userMessages])
setMessages((prevMessages) => {
const messageIndex = prevMessages.findIndex((msg) => msg.id === response.id) await aiService.post(ai, data, {
if (messageIndex === -1) { onMessage: (response) => {
return [...prevMessages, { id: response.id, role: 'assistant', content: response.content }] setMessages((prevMessages) => {
} else { const messageIndex = prevMessages.findIndex((msg) => msg.id === response.id)
return prevMessages.map((msg) => if (messageIndex === -1) {
msg.id === response.id ? { ...msg, content: msg.content + response.content } : msg const newMessage: AIMessage = { id: response.id, role: 'assistant', content: response.content }
) setMessage(newMessage) // 更新最新消息
} return [...prevMessages, newMessage]
}) } else {
}, return prevMessages.map((msg) => {
onError: (err) => { if (msg.id === response.id) {
console.error('AI 请求失败:', err) const updatedMessage = { ...msg, content: msg.content + response.content }
setIsLoading(false) setMessage(updatedMessage) // 更新最新消息
}, return updatedMessage
onComplete: () => { }
setIsLoading(false) return msg
}, })
}) }
} catch (err) { })
console.error('AI 请求失败:', err) },
setIsLoading(false) onerror: () => {
} setIsLoading(false)
},
onclose: () => {
setIsLoading(false)
},
...globalOptions,
...options,
})
}, },
[ai] [ai]
) )
return { ai, setAI, options: AI_OPTIONS, post, messages, isLoading } return { ai, setAI, options: AI_OPTIONS, post, messages, message, isLoading }
} }
import { Button, Flex, Modal, Spin } from 'antd'
import { useEffect, useState } from 'react'
import { useAI } from '@/hooks/useAI'
import MarkdownRender from '@/components/MarkdownRender'
export default function DataReport() {
const [open, setOpen] = useState(false)
const { post, isLoading, message } = useAI()
useEffect(() => {
if (open) {
post({
messages: [
{
role: 'user',
content:
'作为数据分析师,请基于提供的数据集,生成一份结构化的数据质量分析报告。需包含字段解释、关系梳理、质量评估及改进建议。',
},
],
})
}
}, [open])
return (
<>
<Button type="primary" onClick={() => setOpen(true)}>
数据分析报告
</Button>
<Modal title="数据分析报告" open={open} footer={null} width={1000} onCancel={() => setOpen(false)} destroyOnClose>
<MarkdownRender>{message?.content}</MarkdownRender>
<Flex justify="center">
<Spin size="large" spinning={isLoading}></Spin>
</Flex>
</Modal>
</>
)
}
import { Link } from 'react-router' import { Link } from 'react-router'
import { Button, Empty, Flex, Space } from 'antd' import { Button, Empty, Flex, Space } from 'antd'
import DataWrap from '@/components/data/DataWrap' import DataWrap from '@/components/data/DataWrap'
import DataReport from '../components/DataReport'
// 无数据渲染 // 无数据渲染
const EmptyRender = () => { const EmptyRender = () => {
...@@ -37,7 +38,7 @@ export default function DataWriteMy() { ...@@ -37,7 +38,7 @@ export default function DataWriteMy() {
headerRender={(data) => ( headerRender={(data) => (
<Flex justify="space-between" align="middle" style={{ marginBottom: '20px' }}> <Flex justify="space-between" align="middle" style={{ marginBottom: '20px' }}>
<h4>数据集名称:{data.info.name}</h4> <h4>数据集名称:{data.info.name}</h4>
<Button type="primary">数据质量分析报告</Button> <DataReport />
</Flex> </Flex>
)} )}
empty={<EmptyRender />}></DataWrap> empty={<EmptyRender />}></DataWrap>
......
import { create } from 'zustand' import { create } from 'zustand'
import aiService, { AIOption, AIMessage, AIData, AI_OPTIONS } from '@/utils/ai' import aiService, { AIOption, AIMessage, AIData, AI_OPTIONS, InitOptions } from '@/utils/ai'
interface AIState { interface AIState {
ai: string ai: string
options: AIOption[] options: AIOption[]
message: AIMessage | null
messages: AIMessage[] messages: AIMessage[]
isLoading: boolean isLoading: boolean
collapsed: boolean collapsed: boolean
setAI: (ai: string) => void setAI: (ai: string) => void
toggleCollapsed: () => void toggleCollapsed: () => void
post: (data: AIData) => Promise<void> post: (data: AIData, options?: InitOptions) => Promise<void>
} }
export const useAIStore = create<AIState>((set, get) => ({ export const useAIStore = create<AIState>((set, get) => ({
ai: localStorage.getItem('ai') || 'qwen', ai: localStorage.getItem('ai') || 'qwen',
options: AI_OPTIONS, options: AI_OPTIONS,
message: null,
messages: [], messages: [],
isLoading: false, isLoading: false,
collapsed: false, collapsed: false,
...@@ -25,40 +27,52 @@ export const useAIStore = create<AIState>((set, get) => ({ ...@@ -25,40 +27,52 @@ export const useAIStore = create<AIState>((set, get) => ({
toggleCollapsed: () => { toggleCollapsed: () => {
set((state) => ({ collapsed: !state.collapsed })) set((state) => ({ collapsed: !state.collapsed }))
}, },
post: async (data) => { post: async (data, options) => {
const { ai, messages } = get() const { ai, messages } = get()
// 处理用户消息(去掉 system 角色的消息)
const userMessages = data.messages.filter((item) => item.role !== 'system')
set({ set({
collapsed: true, collapsed: true,
isLoading: true, isLoading: true,
messages: [...messages, ...data.messages.filter((item) => item.role !== 'system')], messages: [...messages, ...userMessages],
}) })
try { try {
await aiService.post(ai, data, { await aiService.post(ai, data, {
onUpdate: (response) => { onMessage: (response) => {
set((state) => { set((state) => {
const messageIndex = state.messages.findIndex((msg) => msg.id === response.id) const messageIndex = state.messages.findIndex((msg) => msg.id === response.id)
if (messageIndex === -1) { if (messageIndex === -1) {
// 新的 AI 回复
const newMessage: AIMessage = { id: response.id, role: 'assistant', content: response.content }
return { return {
messages: [...state.messages, { id: response.id, role: 'assistant', content: response.content }], message: newMessage, // 存储最新的 AI 消息
messages: [...state.messages, newMessage], // 追加到历史消息
} }
} else { } else {
// 追加内容到已有的消息
const updatedMessages = state.messages.map((msg) =>
msg.id === response.id ? { ...msg, content: msg.content + response.content } : msg
)
return { return {
messages: state.messages.map((msg) => message: updatedMessages[messageIndex], // 更新最新的 AI 消息
msg.id === response.id ? { ...msg, content: msg.content + response.content } : msg messages: updatedMessages,
),
} }
} }
}) })
}, },
onError: (err) => { onerror: (err) => {
console.error('AI 请求失败:', err) console.error('AI 请求失败:', err)
set({ isLoading: false }) set({ isLoading: false })
}, },
onComplete: () => { onclose: () => {
set({ isLoading: false }) set({ isLoading: false })
}, },
...options,
}) })
} catch (err) { } catch (err) {
console.error('AI 请求失败:', err) console.error('AI 请求失败:', err)
......
import md5 from 'blueimp-md5' import md5 from 'blueimp-md5'
import axios from 'axios' import axios from 'axios'
import { fetchEventSource } from '@fortaine/fetch-event-source' import { fetchEventSource, FetchEventSourceInit } from '@fortaine/fetch-event-source'
export interface AIOption { export interface AIOption {
label: string label: string
...@@ -20,15 +20,12 @@ export interface AIData { ...@@ -20,15 +20,12 @@ export interface AIData {
} }
export interface AIResponse { export interface AIResponse {
content: string
id?: string id?: string
isStream?: boolean content: string
} }
export interface AIStreamHandlers { export interface InitOptions extends FetchEventSourceInit {
onUpdate: (response: AIResponse) => void onMessage?: (message: AIResponse) => void
onError: (error: any) => void
onComplete?: () => void
} }
// Available AI options for different implementations // Available AI options for different implementations
...@@ -39,7 +36,6 @@ export const AI_OPTIONS: AIOption[] = [ ...@@ -39,7 +36,6 @@ export const AI_OPTIONS: AIOption[] = [
// { label: '天工', value: 'tiangong' }, // { label: '天工', value: 'tiangong' },
] ]
// Individual AI service functions
export async function getYiyanAccessToken() { export async function getYiyanAccessToken() {
const AK = 'wY7bvMpkWeZbDVq9w3EDvpjU' const AK = 'wY7bvMpkWeZbDVq9w3EDvpjU'
const SK = 'XJwpiJWxs5HXkOtbo6tQrvYPZFJAWdAy' const SK = 'XJwpiJWxs5HXkOtbo6tQrvYPZFJAWdAy'
...@@ -49,184 +45,113 @@ export async function getYiyanAccessToken() { ...@@ -49,184 +45,113 @@ export async function getYiyanAccessToken() {
return resp.data.access_token return resp.data.access_token
} }
export async function fetchAIEventSource(url: string, options: FetchEventSourceInit) {
await fetchEventSource(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
async onopen(response) {
if (response.ok) return
else throw response
},
...options,
})
}
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu
export async function yiyan(data: AIData, handlers: AIStreamHandlers): Promise<void> { export async function yiyan(data: AIData, options: InitOptions): Promise<void> {
const accessToken = await getYiyanAccessToken() const accessToken = await getYiyanAccessToken()
const params = { stream: true, ...data } const params = { stream: true, ...data }
await fetchEventSource( await fetchAIEventSource(
`/api/qianfan/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant?access_token=${accessToken}`, `/api/qianfan/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant?access_token=${accessToken}`,
{ {
method: 'POST', ...options,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params), body: JSON.stringify(params),
async onopen(response) {
if (response.ok) {
return
} else {
throw response
}
},
onmessage(res) { onmessage(res) {
if (res.data === '[DONE]') {
handlers.onComplete?.()
return
}
try { try {
const message = JSON.parse(res.data) const message = JSON.parse(res.data)
handlers.onUpdate({ id: message.id, content: message.result || '' }) if (options.onMessage) options.onMessage({ id: message.id, content: message.result || '' } as any)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
}, },
onerror(err) {
handlers.onError(err)
},
onclose() {
if (handlers.onComplete) {
handlers.onComplete()
}
},
} }
) )
} }
// https://api-docs.deepseek.com/zh-cn/api/create-chat-completion // https://api-docs.deepseek.com/zh-cn/api/create-chat-completion
export async function deepseek(data: AIData, handlers: AIStreamHandlers): Promise<void> { export async function deepseek(data: AIData, options: InitOptions): Promise<void> {
const apiKey = 'sk-f1a6f0a7013241de8393cb2cb108e777' const apiKey = 'sk-f1a6f0a7013241de8393cb2cb108e777'
const params = { model: 'deepseek-reasoner', stream: true, ...data } const params = { model: 'deepseek-reasoner', stream: true, ...data }
await fetchEventSource('/api/deepseek/chat/completions', { await fetchAIEventSource('/api/deepseek/chat/completions', {
method: 'POST', ...options,
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
body: JSON.stringify(params), body: JSON.stringify(params),
async onopen(response) {
if (response.ok) {
return
} else {
throw response
}
},
onmessage(res) { onmessage(res) {
if (res.data === '[DONE]') { if (res.data === '[DONE]') return
handlers.onComplete?.()
return
}
try { try {
const message = JSON.parse(res.data) const message = JSON.parse(res.data)
if (message.choices && message.choices.length > 0) { if (message.choices && message.choices.length > 0 && options.onMessage) {
handlers.onUpdate({ options.onMessage({ id: message.id, content: message.choices[0].delta?.content || '' } as any)
id: message.id,
content: message.choices[0].delta?.content || '',
})
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
}, },
onerror(err) {
handlers.onError(err)
},
onclose() {
if (handlers.onComplete) {
handlers.onComplete()
}
},
}) })
} }
// https://docs.siliconflow.cn/cn/api-reference/chat-completions/chat-completions // https://docs.siliconflow.cn/cn/api-reference/chat-completions/chat-completions
export async function siliconflow(data: AIData, handlers: AIStreamHandlers): Promise<void> { export async function siliconflow(data: AIData, options: InitOptions): Promise<void> {
const apiKey = 'sk-bivnwauskdbvpspvmdorrgkrpwlyfxbfcezqsfsevowzubdj' const apiKey = 'sk-bivnwauskdbvpspvmdorrgkrpwlyfxbfcezqsfsevowzubdj'
const params = { model: 'deepseek-ai/DeepSeek-R1', stream: true, ...data } const params = { model: 'deepseek-ai/DeepSeek-R1', stream: true, ...data }
await fetchEventSource('/api/siliconflow/v1/chat/completions', { await fetchAIEventSource('/api/siliconflow/v1/chat/completions', {
method: 'POST', ...options,
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
body: JSON.stringify(params), body: JSON.stringify(params),
async onopen(response) {
if (response.ok) {
return
} else {
throw response
}
},
onmessage(res) { onmessage(res) {
if (res.data === '[DONE]') { if (res.data === '[DONE]') return
handlers.onComplete?.()
return
}
try { try {
const message = JSON.parse(res.data) const message = JSON.parse(res.data)
if (message.choices && message.choices.length > 0) { if (message.choices && message.choices.length > 0 && options.onMessage) {
handlers.onUpdate({ options.onMessage({ id: message.id, content: message.choices[0].delta?.content || '' } as any)
id: message.id,
content: message.choices[0].delta?.content || '',
})
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
}, },
onerror(err) {
handlers.onError(err)
},
onclose() {
if (handlers.onComplete) {
handlers.onComplete()
}
},
}) })
} }
// https://help.aliyun.com/zh/model-studio/developer-reference/use-qwen-by-calling-api // https://help.aliyun.com/zh/model-studio/developer-reference/use-qwen-by-calling-api
export async function qwen(data: AIData, handlers: AIStreamHandlers): Promise<void> { export async function qwen(data: AIData, options: InitOptions): Promise<void> {
const apiKey = 'sk-afd0fcdb53bf4058b2068b8548820150' const apiKey = 'sk-afd0fcdb53bf4058b2068b8548820150'
const params = { model: 'qwen-max-latest', stream: true, ...data } const params = { model: 'qwen-max-latest', stream: true, ...data }
await fetchEventSource('/api/qwen/compatible-mode/v1/chat/completions', { await fetchAIEventSource('/api/qwen/compatible-mode/v1/chat/completions', {
method: 'POST', ...options,
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
body: JSON.stringify(params), body: JSON.stringify(params),
async onopen(response) {
if (response.ok) {
return
} else {
throw response
}
},
onmessage(res) { onmessage(res) {
if (res.data === '[DONE]') { if (res.data === '[DONE]') return
handlers.onComplete?.()
return
}
try { try {
const message = JSON.parse(res.data) const message = JSON.parse(res.data)
if (message.choices && message.choices.length > 0) { if (message.choices && message.choices.length > 0 && options.onMessage) {
handlers.onUpdate({ options.onMessage({ id: message.id, content: message.choices[0].delta?.content || '' } as any)
id: message.id,
content: message.choices[0].delta?.content || '',
})
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
}, },
onerror(err) {
handlers.onError(err)
},
onclose() {
if (handlers.onComplete) {
handlers.onComplete()
}
},
}) })
} }
export async function tiangong(data: AIData, handlers: AIStreamHandlers): Promise<void> { export async function tiangong(data: AIData, options: InitOptions): Promise<void> {
const appKey = 'a8701b73637562d33a53c668a90ee3be' const appKey = 'a8701b73637562d33a53c668a90ee3be'
const appSecret = 'e191593f486bb88a39c634f46926762dddc97b9082e192af' const appSecret = 'e191593f486bb88a39c634f46926762dddc97b9082e192af'
const timestamp = Math.floor(Date.now() / 1000).toString() const timestamp = Math.floor(Date.now() / 1000).toString()
const sign = md5(`${appKey}${appSecret}${timestamp}`) const sign = md5(`${appKey}${appSecret}${timestamp}`)
await fetchEventSource('/api/tiangong/sky-saas-writing/api/v1/chat', { await fetchAIEventSource('/api/tiangong/sky-saas-writing/api/v1/chat', {
method: 'POST', ...options,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
app_key: appKey, app_key: appKey,
...@@ -234,41 +159,19 @@ export async function tiangong(data: AIData, handlers: AIStreamHandlers): Promis ...@@ -234,41 +159,19 @@ export async function tiangong(data: AIData, handlers: AIStreamHandlers): Promis
timestamp, timestamp,
stream: 'true', stream: 'true',
}, },
body: JSON.stringify({ body: JSON.stringify({ chat_history: data.messages, stream_resp_type: 'delta' }),
chat_history: data.messages,
stream_resp_type: 'delta',
}),
async onopen(response) {
if (response.ok) {
return
} else {
throw response
}
},
onmessage(res) { onmessage(res) {
if (res.data === '[DONE]') return
try { try {
const message = JSON.parse(res.data) const message = JSON.parse(res.data)
if (message.type !== 1) return if (message.type !== 1) return
const messageId = message.conversation_id const messageId = message.conversation_id
const messageContent = message?.arguments?.[0]?.messages?.[0]?.text || '' const messageContent = message?.arguments?.[0]?.messages?.[0]?.text || ''
if (options.onMessage) options.onMessage({ id: messageId, content: messageContent } as any)
handlers.onUpdate({
id: messageId,
content: messageContent,
})
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
}, },
onerror(err) {
handlers.onError(err)
},
onclose() {
if (handlers.onComplete) {
handlers.onComplete()
}
},
}) })
} }
...@@ -280,7 +183,7 @@ const aiService = { ...@@ -280,7 +183,7 @@ const aiService = {
qwen, qwen,
tiangong, tiangong,
async post(type: string, data: AIData, handlers: AIStreamHandlers): Promise<void> { async post(type: string, data: AIData, options: InitOptions): Promise<void> {
const messages: AIMessage[] = [] const messages: AIMessage[] = []
const dataset = localStorage.getItem('dataset') const dataset = localStorage.getItem('dataset')
if (dataset) { if (dataset) {
...@@ -290,19 +193,19 @@ const aiService = { ...@@ -290,19 +193,19 @@ const aiService = {
data.messages = [...messages, ...data.messages] data.messages = [...messages, ...data.messages]
switch (type) { switch (type) {
case 'yiyan': case 'yiyan':
return yiyan(data, handlers) return yiyan(data, options)
case 'deepseek': case 'deepseek':
return deepseek(data, handlers) return deepseek(data, options)
case 'siliconflow': case 'siliconflow':
return siliconflow(data, handlers) return siliconflow(data, options)
case 'qwen': case 'qwen':
return qwen(data, handlers) return qwen(data, options)
case 'tiangong': case 'tiangong':
return tiangong(data, handlers) return tiangong(data, options)
default: default:
throw new Error(`未找到对应的 AI 配置: ${type}`) throw new Error(`未找到对应的 AI 配置: ${type}`)
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论