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

feat: add new dependencies and enhance AI functionality with SSE support

上级 2bb9afe4
......@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@ant-design/x": "^1.1.1",
"@antv/g2": "^5.2.12",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
......@@ -27,6 +28,7 @@
"lucide-react": "^0.484.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^5.0.0",
"react-markdown": "^10.1.0",
"react-router": "^7.4.0",
"rehype-highlight": "^7.0.2",
......@@ -167,6 +169,52 @@
"react": ">=16.9.0"
}
},
"node_modules/@ant-design/x": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@ant-design/x/-/x-1.1.1.tgz",
"integrity": "sha512-DFzNQfCTdYBhTJxorGI1ylhH/ZODcU5yn64xt/8/11slT0ALMuHC5Afx717KNQiao4RleXjypiEWuP0K1f+Szg==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^7.1.0",
"@ant-design/cssinjs": "^1.21.1",
"@ant-design/cssinjs-utils": "^1.1.0",
"@ant-design/fast-color": "^2.0.6",
"@ant-design/icons": "^5.4.0",
"@babel/runtime": "^7.25.6",
"classnames": "^2.5.1",
"rc-motion": "^2.9.2",
"rc-util": "^5.43.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ant-design"
},
"peerDependencies": {
"antd": "^5.20.3",
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/@ant-design/x/node_modules/@ant-design/icons": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz",
"integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^7.0.0",
"@ant-design/icons-svg": "^4.4.0",
"@babel/runtime": "^7.24.8",
"classnames": "^2.2.6",
"rc-util": "^5.31.1"
},
"engines": {
"node": ">=8"
},
"peerDependencies": {
"react": ">=16.0.0",
"react-dom": ">=16.0.0"
}
},
"node_modules/@antv/component": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@antv/component/-/component-2.1.2.tgz",
......@@ -7674,6 +7722,18 @@
"react": "^18.3.1"
}
},
"node_modules/react-error-boundary": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-5.0.0.tgz",
"integrity": "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"peerDependencies": {
"react": ">=16.13.1"
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
......
......@@ -11,6 +11,7 @@
},
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@ant-design/x": "^1.1.1",
"@antv/g2": "^5.2.12",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
......@@ -29,6 +30,7 @@
"lucide-react": "^0.484.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^5.0.0",
"react-markdown": "^10.1.0",
"react-router": "^7.4.0",
"rehype-highlight": "^7.0.2",
......
......@@ -3,6 +3,8 @@ import { Spin } from 'antd'
import { useRoutes, useLocation, useNavigate } from 'react-router'
import routes from './router/routes'
import './App.scss'
import { Alert } from 'antd'
import { ErrorBoundary } from 'react-error-boundary'
const params = new URLSearchParams(window.location.search)
const experimentId = params.get('experiment_id') || '7028276368903241728'
......@@ -21,15 +23,18 @@ const App = () => {
}, [location, navigate])
return (
<Suspense
fallback={
<div
style={{ width: '100%', height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Spin size="large" />
</div>
}>
{element}
</Suspense>
<ErrorBoundary
fallbackRender={({ error }) => <Alert message="系统错误" description={error.message} type="error" />}>
<Suspense
fallback={
<div
style={{ width: '100%', height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Spin size="large" />
</div>
}>
{element}
</Suspense>
</ErrorBoundary>
)
}
......
import axios from 'axios'
import md5 from 'blueimp-md5'
import { fetchEventSource, FetchEventSourceInit } from '@fortaine/fetch-event-source'
import { AIData, AIMessage, InitOptions } from './types'
import { AIData, AIMessage } from './types'
import { extractJSON } from '@/utils/helper'
import { message } from 'antd'
import { sseRequest, SSEOptions } from '@/utils/sseRequest'
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,
})
}
function getCommonResponseMessage(res: any) {
if (res.data === '[DONE]') return
function updateTransform(res: any): AIMessage | null {
try {
const message = JSON.parse(res.data)
const message = JSON.parse(res)
if (message.choices && message.choices.length > 0) {
const content = message.choices[0].delta?.content || ''
return { id: message.id, role: 'assistant', content, json: extractJSON(content) } as AIMessage
const delta = message.choices[0].delta
return {
id: message.id,
role: 'assistant',
content: delta.content || '',
reasoning_content: delta.reasoning_content || '',
}
}
} catch (error) {
console.error(error)
}
return null
}
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu
export async function yiyan(data: AIData, options: InitOptions): Promise<void> {
const getYiyanAccessToken = async () => {
const AK = 'wY7bvMpkWeZbDVq9w3EDvpjU'
const SK = 'XJwpiJWxs5HXkOtbo6tQrvYPZFJAWdAy'
const resp = await axios.post(
`/api/qianfan/oauth/2.0/token?grant_type=client_credentials&client_id=${AK}&client_secret=${SK}`
)
return resp.data.access_token
}
const accessToken = await getYiyanAccessToken()
const params = { stream: true, ...data }
await fetchAIEventSource(
`/api/qianfan/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant?access_token=${accessToken}`,
function successTransform(messages: AIMessage[]) {
const result = messages.reduce(
(acc, curr) => ({
...acc,
content: acc.content + curr.content,
reasoning_content: acc.reasoning_content + curr.reasoning_content,
}),
{ content: '', reasoning_content: '' }
)
return { ...result, json: extractJSON(result.content) }
}
// 文心一言
export async function yiyan(data: AIData, options: SSEOptions) {
const AK = 'wY7bvMpkWeZbDVq9w3EDvpjU'
const SK = 'XJwpiJWxs5HXkOtbo6tQrvYPZFJAWdAy'
const resp = await axios.post(
`/api/qianfan/oauth/2.0/token?grant_type=client_credentials&client_id=${AK}&client_secret=${SK}`
)
await sseRequest(
`/api/qianfan/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant?access_token=${resp.data.access_token}`,
{
...options,
body: JSON.stringify(params),
onmessage(res) {
try {
const message = JSON.parse(res.data)
if (options.onMessage) options.onMessage({ id: message.id, content: message.result || '' } as any)
} catch (error) {
console.error(error)
}
},
}
body: JSON.stringify({ stream: true, ...data }),
},
updateTransform,
successTransform
)
}
// https://api-docs.deepseek.com/zh-cn/api/create-chat-completion
export async function deepseek(data: AIData, options: InitOptions): Promise<void> {
const apiKey = 'sk-f1a6f0a7013241de8393cb2cb108e777'
const params = { model: 'deepseek-reasoner', stream: true, ...data }
await fetchAIEventSource('/api/deepseek/chat/completions', {
...options,
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
body: JSON.stringify(params),
onmessage(res) {
const message = getCommonResponseMessage(res)
if (message) options.onMessage?.(message)
// DeepSeek
export async function deepseek(data: AIData, options: SSEOptions) {
await sseRequest(
'/api/deepseek/chat/completions',
{
...options,
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer sk-f1a6f0a7013241de8393cb2cb108e777' },
body: JSON.stringify({ model: 'deepseek-reasoner', stream: true, ...data }),
},
})
updateTransform,
successTransform
)
}
// https://docs.siliconflow.cn/cn/api-reference/chat-completions/chat-completions
export async function siliconflow(data: AIData, options: InitOptions): Promise<void> {
const apiKey = 'sk-bivnwauskdbvpspvmdorrgkrpwlyfxbfcezqsfsevowzubdj'
const params = { model: 'deepseek-ai/DeepSeek-R1', stream: true, ...data }
await fetchAIEventSource('/api/siliconflow/v1/chat/completions', {
...options,
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
body: JSON.stringify(params),
onmessage(res) {
const message = getCommonResponseMessage(res)
if (message) options.onMessage?.(message)
// SiliconFlow
export async function siliconflow(data: AIData, options: SSEOptions) {
await sseRequest(
'/api/siliconflow/v1/chat/completions',
{
...options,
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer sk-bivnwauskdbvpspvmdorrgkrpwlyfxbfcezqsfsevowzubdj',
},
body: JSON.stringify({ model: 'deepseek-ai/DeepSeek-R1', stream: true, ...data }),
},
})
updateTransform,
successTransform
)
}
// https://help.aliyun.com/zh/model-studio/developer-reference/use-qwen-by-calling-api
export async function qwen(data: AIData, options: InitOptions): Promise<void> {
const apiKey = 'sk-afd0fcdb53bf4058b2068b8548820150'
const params = { model: 'qwen-max-latest', stream: true, ...data }
await fetchAIEventSource('/api/qwen/compatible-mode/v1/chat/completions', {
...options,
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
body: JSON.stringify(params),
onmessage(res) {
const message = getCommonResponseMessage(res)
if (message) options.onMessage?.(message)
// 通义千问
export async function qwen(data: AIData, options: SSEOptions) {
await sseRequest(
'/api/qwen/compatible-mode/v1/chat/completions',
{
...options,
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer sk-afd0fcdb53bf4058b2068b8548820150' },
body: JSON.stringify({ model: 'qwen-max-latest', stream: true, ...data }),
},
})
updateTransform,
successTransform
)
}
export async function tiangong(data: AIData, options: InitOptions): Promise<void> {
// 天工
export async function tiangong(data: AIData, options: SSEOptions) {
const appKey = 'a8701b73637562d33a53c668a90ee3be'
const appSecret = 'e191593f486bb88a39c634f46926762dddc97b9082e192af'
const timestamp = Math.floor(Date.now() / 1000).toString()
const sign = md5(`${appKey}${appSecret}${timestamp}`)
await fetchAIEventSource('/api/tiangong/sky-saas-writing/api/v1/chat', {
await sseRequest('/api/tiangong/sky-saas-writing/api/v1/chat', {
...options,
headers: {
'Content-Type': 'application/json',
......@@ -120,73 +114,44 @@ export async function tiangong(data: AIData, options: InitOptions): Promise<void
stream: 'true',
},
body: JSON.stringify({ chat_history: data.messages, stream_resp_type: 'delta' }),
onmessage(res) {
if (res.data === '[DONE]') return
try {
const message = JSON.parse(res.data)
if (message.type !== 1) return
const messageId = message.conversation_id
const messageContent = message?.arguments?.[0]?.messages?.[0]?.text || ''
if (options.onMessage) options.onMessage({ id: messageId, content: messageContent } as any)
} catch (error) {
console.error(error)
}
},
})
}
export async function openAIStream(data: AIData, options: InitOptions): Promise<void> {
const params = { model: 'qwen-max-latest', stream: true, ...data }
await fetchEventSource('/api/openai/chat/create', {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: 'ezijing@20250331' },
body: JSON.stringify(params),
async onopen(response) {
if (response.ok) return
else throw response
},
onmessage(res) {
const message = getCommonResponseMessage(res)
if (message) options.onMessage?.(message)
},
})
}
export async function openAI(data: AIData, options: InitOptions): Promise<void> {
try {
const response = await axios.post('/api/openai/chat/create', data, {
// OpenAI
export async function openAI(data: AIData, options: SSEOptions) {
await sseRequest(
'/api/openai/chat/create',
{
...options,
headers: { 'Content-Type': 'application/json', Authorization: 'ezijing@20250331' },
})
const content = response.data.choices[0]?.message?.content || ''
const message: AIMessage = {
id: response.data.id,
role: 'assistant',
content,
json: extractJSON(content),
}
options.onMessage?.(message)
options.onclose?.()
} catch (error: any) {
console.error(error)
options.onerror?.(error)
message.error(error.response.data.error)
}
body: JSON.stringify({ stream: true, ...data }),
},
updateTransform,
successTransform
)
}
const aiService = {
async post(data: AIData, options: InitOptions): Promise<void> {
async post(data: AIData, options: SSEOptions) {
const messages: AIMessage[] = []
const dataset = localStorage.getItem('dataset')
if (dataset) {
const datasetInfo = JSON.parse(dataset)
messages.push({
role: 'system',
content: `这是一个数据集:${JSON.stringify(datasetInfo)}`,
})
}
// const dataset = localStorage.getItem('dataset')
// if (dataset) {
// const datasetInfo = JSON.parse(dataset)
// messages.push({ role: 'system', content: `这是一个数据集:${JSON.stringify(datasetInfo)}` })
// }
data.messages = [...messages, ...data.messages]
openAI(data, options)
const providers = {
yiyan,
deepseek,
siliconflow,
qwen,
tiangong,
openAI,
}
const provider = providers[data.model as keyof typeof providers] || openAI
await provider(data, options)
},
}
......
import { FetchEventSourceInit } from '@fortaine/fetch-event-source'
export interface AIOption {
label: string
value: string
......@@ -9,6 +7,7 @@ export interface AIMessage {
id?: string
role: 'user' | 'assistant' | 'system'
content: string
reasoning_content?: string
json?: any
}
......@@ -17,13 +16,3 @@ export interface AIData {
model?: string
messages: AIMessage[]
}
export interface AIResponse {
id: string
content: string
json?: any
}
export interface InitOptions extends FetchEventSourceInit {
onMessage?: (message: AIMessage) => void
}
import { useState, useEffect, useCallback, useRef } from 'react'
import { AI_OPTIONS } from './config'
import aiService from './api'
import type { AIMessage, AIData, InitOptions } from './types'
import type { AIMessage, AIData } from './types'
import { SSEOptions } from '@/utils/sseRequest'
export function useAI(globalOptions?: InitOptions) {
const [ai, setAI] = useState<string>(localStorage.getItem('ai') || 'qwen')
export function useAI(globalOptions?: SSEOptions) {
const [ai, setAI] = useState<string>(localStorage.getItem('ai') || 'Pro/deepseek-ai/DeepSeek-R1')
const [messages, setMessages] = useState<AIMessage[]>([])
const [message, setMessage] = useState<AIMessage | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)
const controllerRef = useRef<AbortController | null>(null)
// 使用 useRef 存储最新消息,避免闭包问题
const latestMessageRef = useRef<AIMessage | null>(null)
......@@ -18,88 +21,92 @@ export function useAI(globalOptions?: InitOptions) {
localStorage.setItem('ai', ai)
}, [ai])
// 更新消息的辅助函数
const updateMessage = useCallback((response: any, isNew: boolean) => {
if (isNew) {
// 新消息
const newMessage: AIMessage = {
id: response.id,
role: 'assistant',
content: response.content,
json: response.json,
}
latestMessageRef.current = newMessage
messagesRef.current = [...messagesRef.current, newMessage]
// 更新状态
setMessage(newMessage)
setMessages(messagesRef.current)
} else {
// 更新现有消息
messagesRef.current = messagesRef.current.map((msg) => {
if (msg.id === response.id) {
const updatedMessage = {
...msg,
content: msg.content + response.content,
json: msg.json || response.json,
}
latestMessageRef.current = updatedMessage
setMessage(updatedMessage)
return updatedMessage
}
return msg
})
const clearError = useCallback(() => {
setError(null)
}, [])
setMessages([...messagesRef.current])
const abort = useCallback(() => {
if (controllerRef.current) {
controllerRef.current.abort()
controllerRef.current = null
setIsLoading(false)
}
}, [])
const post = useCallback(
async (data: AIData, options?: InitOptions) => {
async (data: AIData) => {
// 如果已经有正在进行的请求,先取消它
abort()
return new Promise<AIMessage>((resolve, reject) => {
try {
setIsLoading(true)
setIsLoading(true)
setError(null)
// 添加用户消息
const userMessages = data.messages.filter((item) => item.role !== 'system')
messagesRef.current = [...messagesRef.current, ...userMessages]
setMessages((prev) => [...prev, ...userMessages])
// 创建新的 controller
controllerRef.current = new AbortController()
aiService.post(
{ ...data, model: ai },
{
onMessage: (response) => {
const messageIndex = messagesRef.current.findIndex((msg) => msg.id === response.id)
updateMessage(response, messageIndex === -1)
},
onerror: (error) => {
console.error('AI service error:', error)
setIsLoading(false)
reject(error)
},
onclose: () => {
setIsLoading(false)
if (latestMessageRef.current) {
resolve(latestMessageRef.current)
} else {
reject(new Error('No message received'))
}
},
...globalOptions,
...options,
}
)
} catch (error) {
console.error('Post error:', error)
setIsLoading(false)
reject(error)
}
// 添加用户消息
const userMessages = data.messages.filter((item) => item.role !== 'system')
messagesRef.current = [...messagesRef.current, ...userMessages]
setMessages((prev) => [...prev, ...userMessages])
aiService.post(
{ model: ai, ...data },
{
signal: controllerRef.current.signal,
onUpdate: (message) => {
const messageIndex = messagesRef.current.findIndex((msg) => msg.id === message.id)
if (messageIndex === -1) {
// 新消息
latestMessageRef.current = message
messagesRef.current = [...messagesRef.current, message]
setMessage(message)
setMessages(messagesRef.current)
} else {
// 更新现有消息
messagesRef.current = messagesRef.current.map((msg) => {
if (msg.id === message.id) {
const updatedMessage = {
...msg,
content: msg.content + message.content,
reasoning_content: msg.reasoning_content + message.reasoning_content,
}
latestMessageRef.current = updatedMessage
setMessage(updatedMessage)
return updatedMessage
}
return msg
})
setMessages([...messagesRef.current])
}
},
onSuccess: (message) => {
controllerRef.current = null
setIsLoading(false)
resolve(message)
},
onError: (err) => {
controllerRef.current = null
setIsLoading(false)
setError(err.message)
reject(err)
},
...globalOptions,
}
)
})
},
[ai, updateMessage, globalOptions]
[ai, globalOptions, abort]
)
// 组件卸载时取消请求
useEffect(() => {
return () => {
abort()
}
}, [abort])
return {
ai,
setAI,
......@@ -108,5 +115,8 @@ export function useAI(globalOptions?: InitOptions) {
messages,
message,
isLoading,
error,
clearError,
abort,
}
}
import { create } from 'zustand'
import { AI_OPTIONS } from './config'
import aiService from './api'
import type { AIOption, AIMessage, AIData, InitOptions } from './types'
import type { AIOption, AIMessage, AIData } from './types'
interface AIState {
ai: string
......@@ -10,18 +10,23 @@ interface AIState {
messages: AIMessage[]
isLoading: boolean
collapsed: boolean
error: string | null
controller: AbortController | null
setAI: (ai: string) => void
toggleCollapsed: () => void
post: (data: AIData, options?: InitOptions) => Promise<AIMessage>
post: (data: AIData) => Promise<AIMessage>
clearError: () => void
}
export const useAIStore = create<AIState>((set, get) => ({
ai: localStorage.getItem('ai') || 'qwen',
ai: localStorage.getItem('ai') || 'Pro/deepseek-ai/DeepSeek-R1',
options: AI_OPTIONS,
message: null,
messages: [],
isLoading: false,
collapsed: false,
error: null,
controller: null,
setAI: (ai) => {
localStorage.setItem('ai', ai)
set({ ai })
......@@ -29,76 +34,57 @@ export const useAIStore = create<AIState>((set, get) => ({
toggleCollapsed: () => {
set((state) => ({ collapsed: !state.collapsed }))
},
post: async (data, options) => {
clearError: () => {
set({ error: null })
},
post: async (data) => {
const { ai, messages } = get()
// 处理用户消息(去掉 system 角色的消息)
const userMessages = data.messages.filter((item) => item.role !== 'system')
const controller = new AbortController()
set({
controller,
collapsed: true,
isLoading: true,
messages: [...messages, ...userMessages],
error: null,
messages: [...messages, ...data.messages.filter((item) => item.role !== 'system')],
})
return new Promise<AIMessage>((resolve, reject) => {
try {
aiService.post(
{ ...data, model: ai },
{
onMessage: (response) => {
set((state) => {
const messageIndex = state.messages.findIndex((msg) => msg.id === response.id)
aiService.post(
{ model: ai, ...data },
{
signal: controller.signal,
onUpdate: (message) => {
set((state) => {
const messageIndex = state.messages.findIndex((msg) => msg.id === message.id)
if (messageIndex === -1) {
// 新的 AI 回复
const newMessage: AIMessage = {
id: response.id,
role: 'assistant',
content: response.content,
json: response.json,
}
return {
message: newMessage, // 存储最新的 AI 消息
messages: [...state.messages, newMessage], // 追加到历史消息
}
} else {
// 追加内容到已有的消息
const updatedMessages = state.messages.map((msg) =>
msg.id === response.id
? { ...msg, content: msg.content + response.content, json: msg.json || response.json }
: msg
)
return {
message: updatedMessages[messageIndex], // 更新最新的 AI 消息
messages: updatedMessages,
}
}
})
},
onerror: (err) => {
console.error('AI 请求失败:', err)
set({ isLoading: false })
reject(err)
},
onclose: () => {
set({ isLoading: false })
const { message } = get()
if (message) {
resolve(message)
if (messageIndex === -1) {
return { message, messages: [...state.messages, message] }
} else {
reject(new Error('No message received'))
const updatedMessages = state.messages.map((msg) =>
msg.id === message.id
? {
...msg,
content: msg.content + message.content,
reasoning_content: msg.reasoning_content + message.reasoning_content,
}
: msg
)
return { message: updatedMessages[messageIndex], messages: updatedMessages }
}
},
...options,
}
)
} catch (err) {
console.error('AI 请求失败:', err)
set({ isLoading: false })
reject(err)
}
})
},
onSuccess: (message) => {
resolve(message)
set({ isLoading: false })
},
onError: (err) => {
reject(err)
set({ isLoading: false })
set({ error: err.message })
},
}
)
})
},
}))
......
......@@ -36,7 +36,7 @@ const showOptions = [
]
// 数字字段设置
const Step1 = ({ fieldOptions, getFieldOptions, type }: any) => {
const Step1 = ({ fieldOptions, numberFields, type }: any) => {
return (
<>
<Divider orientation="left" orientationMargin="0">
......@@ -45,7 +45,7 @@ const Step1 = ({ fieldOptions, getFieldOptions, type }: any) => {
<Row gutter={20}>
<Col span={8}>
<Form.Item label='请选择"度量"字段' name="y">
<Select options={getFieldOptions('number')} placeholder="请选择" mode="multiple" allowClear></Select>
<Select options={numberFields} placeholder="请选择" mode="multiple" allowClear></Select>
</Form.Item>
</Col>
<Col span={8}>
......@@ -168,7 +168,7 @@ const TableStep = ({ fieldOptions }: any) => {
}
// 指标卡设置
const GaugeStep = ({ getFieldOptions, showTitle }: any) => {
const GaugeStep = ({ numberFields, showTitle }: any) => {
return (
<>
<Divider orientation="left" orientationMargin="0">
......@@ -177,12 +177,7 @@ const GaugeStep = ({ getFieldOptions, showTitle }: any) => {
<Row gutter={20}>
<Col span={8}>
<Form.Item label='请选择"度量"字段' name="y">
<Select
options={getFieldOptions('number')}
placeholder="请选择"
mode="multiple"
allowClear
maxCount={1}></Select>
<Select options={numberFields} placeholder="请选择" mode="multiple" allowClear maxCount={1}></Select>
</Form.Item>
</Col>
<Col span={8}>
......@@ -214,7 +209,7 @@ const GaugeStep = ({ getFieldOptions, showTitle }: any) => {
}
// 词云设置
const WordCloudStep = ({ fieldOptions, getFieldOptions, showTitle }: any) => {
const WordCloudStep = ({ fieldOptions, numberFields, showTitle }: any) => {
return (
<>
<Divider orientation="left" orientationMargin="0">
......@@ -223,12 +218,7 @@ const WordCloudStep = ({ fieldOptions, getFieldOptions, showTitle }: any) => {
<Row gutter={20}>
<Col span={12}>
<Form.Item label="大小" name="y" rules={[{ required: true, message: '请选择' }]}>
<Select
options={getFieldOptions('number')}
placeholder="请选择"
mode="multiple"
allowClear
maxCount={1}></Select>
<Select options={numberFields} placeholder="请选择" mode="multiple" allowClear maxCount={1}></Select>
</Form.Item>
</Col>
<Col span={12}>
......@@ -266,19 +256,17 @@ const WordCloudStep = ({ fieldOptions, getFieldOptions, showTitle }: any) => {
)
}
const FormStep = ({ type, fieldOptions, getFieldOptions, showTitle }: any) => {
const FormStep = ({ type, fieldOptions, numberFields, showTitle }: any) => {
if (type === '12') {
return <TableStep fieldOptions={fieldOptions} />
} else if (type === '9') {
return <GaugeStep fieldOptions={fieldOptions} getFieldOptions={getFieldOptions} type={type} showTitle={showTitle} />
return <GaugeStep fieldOptions={fieldOptions} numberFields={numberFields} type={type} showTitle={showTitle} />
} else if (type === '6' || type === '7') {
return (
<WordCloudStep fieldOptions={fieldOptions} getFieldOptions={getFieldOptions} type={type} showTitle={showTitle} />
)
return <WordCloudStep fieldOptions={fieldOptions} numberFields={numberFields} type={type} showTitle={showTitle} />
} else {
return (
<>
<Step1 fieldOptions={fieldOptions} getFieldOptions={getFieldOptions} type={type} />
<Step1 fieldOptions={fieldOptions} numberFields={numberFields} type={type} />
<Step2 fieldOptions={fieldOptions} type={type} showTitle={showTitle} />
</>
)
......@@ -286,7 +274,7 @@ const FormStep = ({ type, fieldOptions, getFieldOptions, showTitle }: any) => {
}
const ModalContent = ({ setOpen, type, id = '' }: Props) => {
const { fieldOptions, getFieldOptions } = useDataFieldQuery()
const { fieldOptions, numberFields } = useDataFieldQuery()
const { data: chartData } = useViewChartQuery(id)
const [form] = Form.useForm()
......@@ -424,7 +412,7 @@ const ModalContent = ({ setOpen, type, id = '' }: Props) => {
<Form.Item label="组件名称" name="name" rules={[{ required: true, message: '请输入组件名称' }]}>
<Input placeholder="请输入" />
</Form.Item>
<FormStep type={type} fieldOptions={fieldOptions} getFieldOptions={getFieldOptions} showTitle={showTitle} />
<FormStep type={type} fieldOptions={fieldOptions} numberFields={numberFields} showTitle={showTitle} />
<Divider orientation="left" orientationMargin="0">
预览组件效果
</Divider>
......
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { getUser, getMapList, getMyList, getMyField, getProcessProgress } from '@/api/base'
import { useUserStore } from '@/stores/user'
......@@ -93,28 +93,45 @@ export function useDataFieldQuery() {
return { data: [] }
},
})
const fields =
query.data?.map((item) => {
return { ...item, label: item.name, value: item.english_name }
}) || []
const getFieldName = (value: string) => {
return fields.find((item) => item.value === value)?.label || value
}
const getFieldNames = (values: string[]) => {
return values.map((value) => getFieldName(value))
}
const getFieldOptions = (type: string) => {
return fields.filter((option) => {
if (type === 'string') return option.type.includes('VARCHAR')
if (type === 'number') return option.type.includes('DECIMAL') || option.type.includes('SMALLINT')
return true
})
const fields = useMemo(
() => query.data?.map((item) => ({ ...item, label: item.name, value: item.english_name })) || [],
[query.data]
)
const getFieldName = useCallback(
(value: string) => {
return fields.find((item) => item.value === value)?.label || value
},
[fields]
)
const getFieldNames = useCallback((values: string[]) => values.map((value) => getFieldName(value)), [getFieldName])
const getFieldOptions = useCallback(
(type: string) => {
return fields.filter((option) => {
if (type === 'string') return option.type.includes('VARCHAR')
if (type === 'number') return option.type.includes('DECIMAL') || option.type.includes('SMALLINT')
return true
})
},
[fields]
)
const numberFields = useMemo(() => getFieldOptions('number'), [getFieldOptions])
const stringFields = useMemo(() => getFieldOptions('string'), [getFieldOptions])
return {
...query,
fields,
fieldOptions: fields,
numberFields,
stringFields,
getFieldName,
getFieldNames,
getFieldOptions,
}
return { ...query, fields, fieldOptions: fields, getFieldName, getFieldNames, getFieldOptions }
}
// 进度查询
......
......@@ -4,55 +4,53 @@ import { DownloadOutlined } from '@ant-design/icons'
import { useDataFieldQuery, useDataQuery } from '@/hooks/useQuery'
import { utils, writeFile } from 'xlsx'
interface ResultItem {
name: string
[key: string]: any
}
const buttons = ['最大值', '最小值', '平均值', '中位数', '众数', '1/4位数', '3/4位数', '方差', '标准差', '极差']
interface DataField {
name: string
english_name: string
type: string
}
export default function ButtonModal() {
const { data } = useDataQuery()
const { numberFields } = useDataFieldQuery()
interface DataListItem {
[key: string]: string | number
}
const [open, setOpen] = useState(false)
const [title, setTitle] = useState('')
interface Stats {
最大值: number
最小值: number
平均值: number
中位数: number
众数: number
'1/4位数': number
'3/4位数': number
方差: number
标准差: number
极差: number
}
const handleOpen = (buttonName: string) => {
setSelectedButtons([buttonName])
setTitle(`探索` + buttonName)
setOpen(true)
}
const buttons = [
'最大值',
'最小值',
'平均值',
'中位数',
'众数',
'1/4位数',
'3/4位数',
'方差',
'标准差',
'极差',
] as const
const [selectedButtons, setSelectedButtons] = useState<string[]>([])
export default function ButtonModal() {
const { data } = useDataQuery()
const { getFieldOptions } = useDataFieldQuery()
const numberFields = getFieldOptions('number') as DataField[]
const columns: any = useMemo(() => {
const baseColumns = [
{
title: '序号',
key: 'index',
render(_value: any, _record: any, index: any) {
return index + 1
},
width: 62,
align: 'center',
},
{
title: '字段名称',
dataIndex: 'name',
align: 'center' as const,
},
]
const [selectedButtons, setSelectedButtons] = useState<string[]>([])
const selectedColumns = buttons
.filter((button) => selectedButtons.includes(button))
.map((button) => ({
title: button,
dataIndex: button,
align: 'center' as const,
}))
const calculateStats = (values: number[]): Stats => {
return [...baseColumns, ...selectedColumns]
}, [selectedButtons])
const calculateStats = (values: number[]) => {
const sortedValues = [...values].sort((a, b) => a - b)
const n = values.length
const sum = values.reduce((a, b) => a + b, 0)
......@@ -82,72 +80,37 @@ export default function ButtonModal() {
极差: roundToTwo(Math.max(...values) - Math.min(...values)),
}
}
const [dataSource, setDataSource] = useState<any[]>([])
useEffect(() => {
setDataSource(numberFields.map((field) => ({ name: field.name })))
}, [numberFields])
const results = numberFields.map((field) => {
const fieldData = (data.list as DataListItem[])
.map((item) => Number(item[field.english_name]))
.filter((v: number) => !isNaN(v))
if (fieldData.length === 0) return field
const handleAI = () => {
const newDataSource = numberFields.map((field) => {
const fieldData = data.list.map((item: any) => Number(item[field.english_name])).filter((v: number) => !isNaN(v))
const stats = calculateStats(fieldData)
const result: ResultItem = { name: field.name }
if (fieldData.length === 0) return { name: field.name }
selectedButtons.forEach((button) => {
result[button] = stats[button as keyof Stats]
})
const stats = calculateStats(fieldData)
const result: { [key: string]: number | string } = { name: field.name }
return result
})
selectedButtons.forEach((button) => {
result[button] = stats[button as keyof typeof stats]
})
const [open, setOpen] = useState(false)
const [title, setTitle] = useState('')
return result
})
const handleOpen = (buttonName: string) => {
setSelectedButtons([buttonName])
setTitle(`探索` + buttonName)
setOpen(true)
setDataSource(newDataSource)
}
const dataSource = results.map((item) => {
return { ...item, key: item.name }
})
const columns: any = useMemo(() => {
const baseColumns = [
{
title: '序号',
key: 'index',
render(_value: any, _record: any, index: any) {
return index + 1
},
width: 62,
align: 'center',
},
{
title: '字段名称',
dataIndex: 'name',
align: 'center' as const,
},
]
const selectedColumns = buttons
.filter((button) => selectedButtons.includes(button))
.map((button) => ({
title: button,
dataIndex: button,
align: 'center' as const,
}))
return [...baseColumns, ...selectedColumns]
}, [selectedButtons])
const handleButtonChange = (checkedValues: string[]) => {
setSelectedButtons(checkedValues)
}
// 导出
const handleExportExcel = () => {
const worksheet = utils.json_to_sheet(results)
const worksheet = utils.json_to_sheet(dataSource)
const workbook = utils.book_new()
utils.book_append_sheet(workbook, worksheet, 'Sheet1')
writeFile(workbook, 'data.xlsx')
......@@ -160,7 +123,19 @@ export default function ButtonModal() {
{button}
</Button>
))}
<Modal title={title} open={open} footer={null} destroyOnClose width={1000} onCancel={() => setOpen(false)}>
<Modal
title={title}
open={open}
footer={
<Flex justify="center" gap={20}>
<Button type="primary" onClick={handleAI}>
一键计算
</Button>
</Flex>
}
destroyOnClose
width={1000}
onCancel={() => setOpen(false)}>
<div style={{ minHeight: 300, padding: '20px 0' }}>
<Flex justify="space-between" align="center" style={{ marginBottom: '20px' }}>
<div>
......@@ -177,7 +152,14 @@ export default function ButtonModal() {
))}
</Checkbox.Group>
</Flex>
<Table bordered dataSource={dataSource} columns={columns} pagination={false} scroll={{ x: 'max-content' }} />
<Table
rowKey="name"
bordered
dataSource={dataSource}
columns={columns}
pagination={false}
scroll={{ x: 'max-content' }}
/>
</div>
</Modal>
</>
......
import { XStream } from '@ant-design/x'
import { sseRequest } from '@/utils/sseRequest'
async function request() {
const response = await fetch('/api/openai/chat/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'ezijing@20250331',
},
body: JSON.stringify({
stream: true,
model: 'qwen-max-latest',
messages: [{ role: 'user', content: '你好' }],
}),
})
console.log(response)
for await (const chunk of XStream({
readableStream: response.body,
})) {
console.log(chunk)
}
}
const controller = new AbortController()
async function request2() {
const res = await sseRequest('/api/openai/chat/create', {
method: 'POST',
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
Authorization: 'ezijing@20250331',
},
body: JSON.stringify({
stream: true,
model: 'qwen-max-latest',
messages: [{ role: 'user', content: '你好' }],
}),
onUpdate: (data) => {
console.log('update', data)
},
onSuccess: (data) => {
console.log('success', data)
},
onError: (error) => {
console.log('error', error)
},
})
console.log(res)
}
export default function App() {
return (
<div>
<button onClick={request2}>请求</button>
<br />
<br />
<button onClick={() => controller.abort()}>取消</button>
</div>
)
}
......@@ -14,4 +14,8 @@ export const routes: RouteObject[] = [
path: '/demo/chart',
Component: lazy(() => import('./chart')),
},
{
path: '/demo/request',
Component: lazy(() => import('./request')),
},
]
import { fetchEventSource, FetchEventSourceInit, EventSourceMessage } from '@fortaine/fetch-event-source'
export interface SSEOptions extends Omit<FetchEventSourceInit, 'onopen' | 'onmessage' | 'onerror' | 'onclose'> {
/**
* 每次接收服务端消息时触发
*/
onUpdate?: (data: any, event: EventSourceMessage) => void
/**
* SSE 正常结束时触发,包含所有已接收数据
*/
onSuccess?: (data: any) => void
/**
* SSE 发生错误时触发
*/
onError?: (error: any) => void
}
export async function sseRequest(
url: string,
options: SSEOptions = {},
updateTransform?: (data: string) => any,
successTransform?: (data: any[]) => any
) {
const { onUpdate, onError, onSuccess, ...rest } = options
const accumulatedData: string[] = []
await fetchEventSource(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
async onopen(response) {
if (response.ok) return
else throw response
},
...rest,
onmessage: (event) => {
if (event.data && event.data !== '[DONE]') {
const data = updateTransform ? updateTransform(event.data) : event.data
accumulatedData.push(data)
onUpdate?.(data, event)
}
},
onclose: () => {
onSuccess?.(successTransform ? successTransform(accumulatedData) : accumulatedData)
},
onerror: (err) => {
onError?.(err)
},
})
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论