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

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

上级 2bb9afe4
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@ant-design/icons": "^6.0.0", "@ant-design/icons": "^6.0.0",
"@ant-design/x": "^1.1.1",
"@antv/g2": "^5.2.12", "@antv/g2": "^5.2.12",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/modifiers": "^9.0.0",
...@@ -27,6 +28,7 @@ ...@@ -27,6 +28,7 @@
"lucide-react": "^0.484.0", "lucide-react": "^0.484.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-error-boundary": "^5.0.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router": "^7.4.0", "react-router": "^7.4.0",
"rehype-highlight": "^7.0.2", "rehype-highlight": "^7.0.2",
...@@ -167,6 +169,52 @@ ...@@ -167,6 +169,52 @@
"react": ">=16.9.0" "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": { "node_modules/@antv/component": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/@antv/component/-/component-2.1.2.tgz", "resolved": "https://registry.npmjs.org/@antv/component/-/component-2.1.2.tgz",
...@@ -7674,6 +7722,18 @@ ...@@ -7674,6 +7722,18 @@
"react": "^18.3.1" "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": { "node_modules/react-is": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^6.0.0", "@ant-design/icons": "^6.0.0",
"@ant-design/x": "^1.1.1",
"@antv/g2": "^5.2.12", "@antv/g2": "^5.2.12",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/modifiers": "^9.0.0",
...@@ -29,6 +30,7 @@ ...@@ -29,6 +30,7 @@
"lucide-react": "^0.484.0", "lucide-react": "^0.484.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-error-boundary": "^5.0.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router": "^7.4.0", "react-router": "^7.4.0",
"rehype-highlight": "^7.0.2", "rehype-highlight": "^7.0.2",
......
...@@ -3,6 +3,8 @@ import { Spin } from 'antd' ...@@ -3,6 +3,8 @@ import { Spin } from 'antd'
import { useRoutes, useLocation, useNavigate } from 'react-router' import { useRoutes, useLocation, useNavigate } from 'react-router'
import routes from './router/routes' import routes from './router/routes'
import './App.scss' import './App.scss'
import { Alert } from 'antd'
import { ErrorBoundary } from 'react-error-boundary'
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search)
const experimentId = params.get('experiment_id') || '7028276368903241728' const experimentId = params.get('experiment_id') || '7028276368903241728'
...@@ -21,6 +23,8 @@ const App = () => { ...@@ -21,6 +23,8 @@ const App = () => {
}, [location, navigate]) }, [location, navigate])
return ( return (
<ErrorBoundary
fallbackRender={({ error }) => <Alert message="系统错误" description={error.message} type="error" />}>
<Suspense <Suspense
fallback={ fallback={
<div <div
...@@ -30,6 +34,7 @@ const App = () => { ...@@ -30,6 +34,7 @@ const App = () => {
}> }>
{element} {element}
</Suspense> </Suspense>
</ErrorBoundary>
) )
} }
......
差异被折叠。
import { FetchEventSourceInit } from '@fortaine/fetch-event-source'
export interface AIOption { export interface AIOption {
label: string label: string
value: string value: string
...@@ -9,6 +7,7 @@ export interface AIMessage { ...@@ -9,6 +7,7 @@ export interface AIMessage {
id?: string id?: string
role: 'user' | 'assistant' | 'system' role: 'user' | 'assistant' | 'system'
content: string content: string
reasoning_content?: string
json?: any json?: any
} }
...@@ -17,13 +16,3 @@ export interface AIData { ...@@ -17,13 +16,3 @@ export interface AIData {
model?: string model?: string
messages: AIMessage[] 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 { useState, useEffect, useCallback, useRef } from 'react'
import { AI_OPTIONS } from './config' import { AI_OPTIONS } from './config'
import aiService from './api' 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) { export function useAI(globalOptions?: SSEOptions) {
const [ai, setAI] = useState<string>(localStorage.getItem('ai') || 'qwen') const [ai, setAI] = useState<string>(localStorage.getItem('ai') || 'Pro/deepseek-ai/DeepSeek-R1')
const [messages, setMessages] = useState<AIMessage[]>([]) const [messages, setMessages] = useState<AIMessage[]>([])
const [message, setMessage] = useState<AIMessage | null>(null) const [message, setMessage] = useState<AIMessage | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(false) const [isLoading, setIsLoading] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)
const controllerRef = useRef<AbortController | null>(null)
// 使用 useRef 存储最新消息,避免闭包问题 // 使用 useRef 存储最新消息,避免闭包问题
const latestMessageRef = useRef<AIMessage | null>(null) const latestMessageRef = useRef<AIMessage | null>(null)
...@@ -18,31 +21,56 @@ export function useAI(globalOptions?: InitOptions) { ...@@ -18,31 +21,56 @@ export function useAI(globalOptions?: InitOptions) {
localStorage.setItem('ai', ai) localStorage.setItem('ai', ai)
}, [ai]) }, [ai])
// 更新消息的辅助函数 const clearError = useCallback(() => {
const updateMessage = useCallback((response: any, isNew: boolean) => { setError(null)
if (isNew) { }, [])
// 新消息
const newMessage: AIMessage = { const abort = useCallback(() => {
id: response.id, if (controllerRef.current) {
role: 'assistant', controllerRef.current.abort()
content: response.content, controllerRef.current = null
json: response.json, setIsLoading(false)
} }
}, [])
const post = useCallback(
async (data: AIData) => {
// 如果已经有正在进行的请求,先取消它
abort()
return new Promise<AIMessage>((resolve, reject) => {
setIsLoading(true)
setError(null)
latestMessageRef.current = newMessage // 创建新的 controller
messagesRef.current = [...messagesRef.current, newMessage] controllerRef.current = new AbortController()
// 更新状态 // 添加用户消息
setMessage(newMessage) 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) setMessages(messagesRef.current)
} else { } else {
// 更新现有消息 // 更新现有消息
messagesRef.current = messagesRef.current.map((msg) => { messagesRef.current = messagesRef.current.map((msg) => {
if (msg.id === response.id) { if (msg.id === message.id) {
const updatedMessage = { const updatedMessage = {
...msg, ...msg,
content: msg.content + response.content, content: msg.content + message.content,
json: msg.json || response.json, reasoning_content: msg.reasoning_content + message.reasoning_content,
} }
latestMessageRef.current = updatedMessage latestMessageRef.current = updatedMessage
setMessage(updatedMessage) setMessage(updatedMessage)
...@@ -50,56 +78,35 @@ export function useAI(globalOptions?: InitOptions) { ...@@ -50,56 +78,35 @@ export function useAI(globalOptions?: InitOptions) {
} }
return msg return msg
}) })
setMessages([...messagesRef.current]) setMessages([...messagesRef.current])
} }
}, [])
const post = useCallback(
async (data: AIData, options?: InitOptions) => {
return new Promise<AIMessage>((resolve, reject) => {
try {
setIsLoading(true)
// 添加用户消息
const userMessages = data.messages.filter((item) => item.role !== 'system')
messagesRef.current = [...messagesRef.current, ...userMessages]
setMessages((prev) => [...prev, ...userMessages])
aiService.post(
{ ...data, model: ai },
{
onMessage: (response) => {
const messageIndex = messagesRef.current.findIndex((msg) => msg.id === response.id)
updateMessage(response, messageIndex === -1)
}, },
onerror: (error) => { onSuccess: (message) => {
console.error('AI service error:', error) controllerRef.current = null
setIsLoading(false) setIsLoading(false)
reject(error) resolve(message)
}, },
onclose: () => { onError: (err) => {
controllerRef.current = null
setIsLoading(false) setIsLoading(false)
if (latestMessageRef.current) { setError(err.message)
resolve(latestMessageRef.current) reject(err)
} else {
reject(new Error('No message received'))
}
}, },
...globalOptions, ...globalOptions,
...options,
} }
) )
} catch (error) {
console.error('Post error:', error)
setIsLoading(false)
reject(error)
}
}) })
}, },
[ai, updateMessage, globalOptions] [ai, globalOptions, abort]
) )
// 组件卸载时取消请求
useEffect(() => {
return () => {
abort()
}
}, [abort])
return { return {
ai, ai,
setAI, setAI,
...@@ -108,5 +115,8 @@ export function useAI(globalOptions?: InitOptions) { ...@@ -108,5 +115,8 @@ export function useAI(globalOptions?: InitOptions) {
messages, messages,
message, message,
isLoading, isLoading,
error,
clearError,
abort,
} }
} }
import { create } from 'zustand' import { create } from 'zustand'
import { AI_OPTIONS } from './config' import { AI_OPTIONS } from './config'
import aiService from './api' import aiService from './api'
import type { AIOption, AIMessage, AIData, InitOptions } from './types' import type { AIOption, AIMessage, AIData } from './types'
interface AIState { interface AIState {
ai: string ai: string
...@@ -10,18 +10,23 @@ interface AIState { ...@@ -10,18 +10,23 @@ interface AIState {
messages: AIMessage[] messages: AIMessage[]
isLoading: boolean isLoading: boolean
collapsed: boolean collapsed: boolean
error: string | null
controller: AbortController | null
setAI: (ai: string) => void setAI: (ai: string) => void
toggleCollapsed: () => void toggleCollapsed: () => void
post: (data: AIData, options?: InitOptions) => Promise<AIMessage> post: (data: AIData) => Promise<AIMessage>
clearError: () => void
} }
export const useAIStore = create<AIState>((set, get) => ({ export const useAIStore = create<AIState>((set, get) => ({
ai: localStorage.getItem('ai') || 'qwen', ai: localStorage.getItem('ai') || 'Pro/deepseek-ai/DeepSeek-R1',
options: AI_OPTIONS, options: AI_OPTIONS,
message: null, message: null,
messages: [], messages: [],
isLoading: false, isLoading: false,
collapsed: false, collapsed: false,
error: null,
controller: null,
setAI: (ai) => { setAI: (ai) => {
localStorage.setItem('ai', ai) localStorage.setItem('ai', ai)
set({ ai }) set({ ai })
...@@ -29,76 +34,57 @@ export const useAIStore = create<AIState>((set, get) => ({ ...@@ -29,76 +34,57 @@ export const useAIStore = create<AIState>((set, get) => ({
toggleCollapsed: () => { toggleCollapsed: () => {
set((state) => ({ collapsed: !state.collapsed })) set((state) => ({ collapsed: !state.collapsed }))
}, },
post: async (data, options) => { clearError: () => {
set({ error: null })
},
post: async (data) => {
const { ai, messages } = get() const { ai, messages } = get()
const controller = new AbortController()
// 处理用户消息(去掉 system 角色的消息)
const userMessages = data.messages.filter((item) => item.role !== 'system')
set({ set({
controller,
collapsed: true, collapsed: true,
isLoading: true, isLoading: true,
messages: [...messages, ...userMessages], error: null,
messages: [...messages, ...data.messages.filter((item) => item.role !== 'system')],
}) })
return new Promise<AIMessage>((resolve, reject) => { return new Promise<AIMessage>((resolve, reject) => {
try {
aiService.post( aiService.post(
{ ...data, model: ai }, { model: ai, ...data },
{ {
onMessage: (response) => { signal: controller.signal,
onUpdate: (message) => {
set((state) => { set((state) => {
const messageIndex = state.messages.findIndex((msg) => msg.id === response.id) const messageIndex = state.messages.findIndex((msg) => msg.id === message.id)
if (messageIndex === -1) { if (messageIndex === -1) {
// 新的 AI 回复 return { message, messages: [...state.messages, message] }
const newMessage: AIMessage = {
id: response.id,
role: 'assistant',
content: response.content,
json: response.json,
}
return {
message: newMessage, // 存储最新的 AI 消息
messages: [...state.messages, newMessage], // 追加到历史消息
}
} else { } else {
// 追加内容到已有的消息
const updatedMessages = state.messages.map((msg) => const updatedMessages = state.messages.map((msg) =>
msg.id === response.id msg.id === message.id
? { ...msg, content: msg.content + response.content, json: msg.json || response.json } ? {
...msg,
content: msg.content + message.content,
reasoning_content: msg.reasoning_content + message.reasoning_content,
}
: msg : msg
) )
return { return { message: updatedMessages[messageIndex], messages: updatedMessages }
message: updatedMessages[messageIndex], // 更新最新的 AI 消息
messages: updatedMessages,
}
} }
}) })
}, },
onerror: (err) => { onSuccess: (message) => {
console.error('AI 请求失败:', err) resolve(message)
set({ isLoading: false }) set({ isLoading: false })
reject(err)
}, },
onclose: () => { onError: (err) => {
reject(err)
set({ isLoading: false }) set({ isLoading: false })
const { message } = get() set({ error: err.message })
if (message) {
resolve(message)
} else {
reject(new Error('No message received'))
}
}, },
...options,
} }
) )
} catch (err) {
console.error('AI 请求失败:', err)
set({ isLoading: false })
reject(err)
}
}) })
}, },
})) }))
......
...@@ -36,7 +36,7 @@ const showOptions = [ ...@@ -36,7 +36,7 @@ const showOptions = [
] ]
// 数字字段设置 // 数字字段设置
const Step1 = ({ fieldOptions, getFieldOptions, type }: any) => { const Step1 = ({ fieldOptions, numberFields, type }: any) => {
return ( return (
<> <>
<Divider orientation="left" orientationMargin="0"> <Divider orientation="left" orientationMargin="0">
...@@ -45,7 +45,7 @@ const Step1 = ({ fieldOptions, getFieldOptions, type }: any) => { ...@@ -45,7 +45,7 @@ const Step1 = ({ fieldOptions, getFieldOptions, type }: any) => {
<Row gutter={20}> <Row gutter={20}>
<Col span={8}> <Col span={8}>
<Form.Item label='请选择"度量"字段' name="y"> <Form.Item label='请选择"度量"字段' name="y">
<Select options={getFieldOptions('number')} placeholder="请选择" mode="multiple" allowClear></Select> <Select options={numberFields} placeholder="请选择" mode="multiple" allowClear></Select>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={8}> <Col span={8}>
...@@ -168,7 +168,7 @@ const TableStep = ({ fieldOptions }: any) => { ...@@ -168,7 +168,7 @@ const TableStep = ({ fieldOptions }: any) => {
} }
// 指标卡设置 // 指标卡设置
const GaugeStep = ({ getFieldOptions, showTitle }: any) => { const GaugeStep = ({ numberFields, showTitle }: any) => {
return ( return (
<> <>
<Divider orientation="left" orientationMargin="0"> <Divider orientation="left" orientationMargin="0">
...@@ -177,12 +177,7 @@ const GaugeStep = ({ getFieldOptions, showTitle }: any) => { ...@@ -177,12 +177,7 @@ const GaugeStep = ({ getFieldOptions, showTitle }: any) => {
<Row gutter={20}> <Row gutter={20}>
<Col span={8}> <Col span={8}>
<Form.Item label='请选择"度量"字段' name="y"> <Form.Item label='请选择"度量"字段' name="y">
<Select <Select options={numberFields} placeholder="请选择" mode="multiple" allowClear maxCount={1}></Select>
options={getFieldOptions('number')}
placeholder="请选择"
mode="multiple"
allowClear
maxCount={1}></Select>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={8}> <Col span={8}>
...@@ -214,7 +209,7 @@ const GaugeStep = ({ getFieldOptions, showTitle }: any) => { ...@@ -214,7 +209,7 @@ const GaugeStep = ({ getFieldOptions, showTitle }: any) => {
} }
// 词云设置 // 词云设置
const WordCloudStep = ({ fieldOptions, getFieldOptions, showTitle }: any) => { const WordCloudStep = ({ fieldOptions, numberFields, showTitle }: any) => {
return ( return (
<> <>
<Divider orientation="left" orientationMargin="0"> <Divider orientation="left" orientationMargin="0">
...@@ -223,12 +218,7 @@ const WordCloudStep = ({ fieldOptions, getFieldOptions, showTitle }: any) => { ...@@ -223,12 +218,7 @@ const WordCloudStep = ({ fieldOptions, getFieldOptions, showTitle }: any) => {
<Row gutter={20}> <Row gutter={20}>
<Col span={12}> <Col span={12}>
<Form.Item label="大小" name="y" rules={[{ required: true, message: '请选择' }]}> <Form.Item label="大小" name="y" rules={[{ required: true, message: '请选择' }]}>
<Select <Select options={numberFields} placeholder="请选择" mode="multiple" allowClear maxCount={1}></Select>
options={getFieldOptions('number')}
placeholder="请选择"
mode="multiple"
allowClear
maxCount={1}></Select>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
...@@ -266,19 +256,17 @@ const WordCloudStep = ({ fieldOptions, getFieldOptions, showTitle }: any) => { ...@@ -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') { if (type === '12') {
return <TableStep fieldOptions={fieldOptions} /> return <TableStep fieldOptions={fieldOptions} />
} else if (type === '9') { } 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') { } else if (type === '6' || type === '7') {
return ( return <WordCloudStep fieldOptions={fieldOptions} numberFields={numberFields} type={type} showTitle={showTitle} />
<WordCloudStep fieldOptions={fieldOptions} getFieldOptions={getFieldOptions} type={type} showTitle={showTitle} />
)
} else { } else {
return ( return (
<> <>
<Step1 fieldOptions={fieldOptions} getFieldOptions={getFieldOptions} type={type} /> <Step1 fieldOptions={fieldOptions} numberFields={numberFields} type={type} />
<Step2 fieldOptions={fieldOptions} type={type} showTitle={showTitle} /> <Step2 fieldOptions={fieldOptions} type={type} showTitle={showTitle} />
</> </>
) )
...@@ -286,7 +274,7 @@ const FormStep = ({ type, fieldOptions, getFieldOptions, showTitle }: any) => { ...@@ -286,7 +274,7 @@ const FormStep = ({ type, fieldOptions, getFieldOptions, showTitle }: any) => {
} }
const ModalContent = ({ setOpen, type, id = '' }: Props) => { const ModalContent = ({ setOpen, type, id = '' }: Props) => {
const { fieldOptions, getFieldOptions } = useDataFieldQuery() const { fieldOptions, numberFields } = useDataFieldQuery()
const { data: chartData } = useViewChartQuery(id) const { data: chartData } = useViewChartQuery(id)
const [form] = Form.useForm() const [form] = Form.useForm()
...@@ -424,7 +412,7 @@ const ModalContent = ({ setOpen, type, id = '' }: Props) => { ...@@ -424,7 +412,7 @@ const ModalContent = ({ setOpen, type, id = '' }: Props) => {
<Form.Item label="组件名称" name="name" rules={[{ required: true, message: '请输入组件名称' }]}> <Form.Item label="组件名称" name="name" rules={[{ required: true, message: '请输入组件名称' }]}>
<Input placeholder="请输入" /> <Input placeholder="请输入" />
</Form.Item> </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 orientation="left" orientationMargin="0">
预览组件效果 预览组件效果
</Divider> </Divider>
......
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query' import { useQuery, useQueryClient } from '@tanstack/react-query'
import { getUser, getMapList, getMyList, getMyField, getProcessProgress } from '@/api/base' import { getUser, getMapList, getMyList, getMyField, getProcessProgress } from '@/api/base'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
...@@ -93,28 +93,45 @@ export function useDataFieldQuery() { ...@@ -93,28 +93,45 @@ export function useDataFieldQuery() {
return { data: [] } return { data: [] }
}, },
}) })
const fields = const fields = useMemo(
query.data?.map((item) => { () => query.data?.map((item) => ({ ...item, label: item.name, value: item.english_name })) || [],
return { ...item, label: item.name, value: item.english_name } [query.data]
}) || [] )
const getFieldName = (value: string) => { const getFieldName = useCallback(
(value: string) => {
return fields.find((item) => item.value === value)?.label || value return fields.find((item) => item.value === value)?.label || value
} },
[fields]
)
const getFieldNames = (values: string[]) => { const getFieldNames = useCallback((values: string[]) => values.map((value) => getFieldName(value)), [getFieldName])
return values.map((value) => getFieldName(value))
}
const getFieldOptions = (type: string) => { const getFieldOptions = useCallback(
(type: string) => {
return fields.filter((option) => { return fields.filter((option) => {
if (type === 'string') return option.type.includes('VARCHAR') if (type === 'string') return option.type.includes('VARCHAR')
if (type === 'number') return option.type.includes('DECIMAL') || option.type.includes('SMALLINT') if (type === 'number') return option.type.includes('DECIMAL') || option.type.includes('SMALLINT')
return true 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' ...@@ -4,55 +4,53 @@ import { DownloadOutlined } from '@ant-design/icons'
import { useDataFieldQuery, useDataQuery } from '@/hooks/useQuery' import { useDataFieldQuery, useDataQuery } from '@/hooks/useQuery'
import { utils, writeFile } from 'xlsx' import { utils, writeFile } from 'xlsx'
interface ResultItem { const buttons = ['最大值', '最小值', '平均值', '中位数', '众数', '1/4位数', '3/4位数', '方差', '标准差', '极差']
name: string
[key: string]: any
}
interface DataField { export default function ButtonModal() {
name: string const { data } = useDataQuery()
english_name: string const { numberFields } = useDataFieldQuery()
type: string
}
interface DataListItem { const [open, setOpen] = useState(false)
[key: string]: string | number const [title, setTitle] = useState('')
}
interface Stats { const handleOpen = (buttonName: string) => {
最大值: number setSelectedButtons([buttonName])
最小值: number setTitle(`探索` + buttonName)
平均值: number setOpen(true)
中位数: number }
众数: number
'1/4位数': number
'3/4位数': number
方差: number
标准差: number
极差: number
}
const buttons = [ const [selectedButtons, setSelectedButtons] = useState<string[]>([])
'最大值',
'最小值',
'平均值',
'中位数',
'众数',
'1/4位数',
'3/4位数',
'方差',
'标准差',
'极差',
] as const
export default function ButtonModal() { const columns: any = useMemo(() => {
const { data } = useDataQuery() const baseColumns = [
const { getFieldOptions } = useDataFieldQuery() {
const numberFields = getFieldOptions('number') as DataField[] 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,
}))
return [...baseColumns, ...selectedColumns]
}, [selectedButtons])
const calculateStats = (values: number[]): Stats => { const calculateStats = (values: number[]) => {
const sortedValues = [...values].sort((a, b) => a - b) const sortedValues = [...values].sort((a, b) => a - b)
const n = values.length const n = values.length
const sum = values.reduce((a, b) => a + b, 0) const sum = values.reduce((a, b) => a + b, 0)
...@@ -82,72 +80,37 @@ export default function ButtonModal() { ...@@ -82,72 +80,37 @@ export default function ButtonModal() {
极差: roundToTwo(Math.max(...values) - Math.min(...values)), 极差: roundToTwo(Math.max(...values) - Math.min(...values)),
} }
} }
const [dataSource, setDataSource] = useState<any[]>([])
useEffect(() => {
setDataSource(numberFields.map((field) => ({ name: field.name })))
}, [numberFields])
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 results = numberFields.map((field) => { if (fieldData.length === 0) return { name: field.name }
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 stats = calculateStats(fieldData) const stats = calculateStats(fieldData)
const result: ResultItem = { name: field.name } const result: { [key: string]: number | string } = { name: field.name }
selectedButtons.forEach((button) => { selectedButtons.forEach((button) => {
result[button] = stats[button as keyof Stats] result[button] = stats[button as keyof typeof stats]
}) })
return result return result
}) })
const [open, setOpen] = useState(false) setDataSource(newDataSource)
const [title, setTitle] = useState('')
const handleOpen = (buttonName: string) => {
setSelectedButtons([buttonName])
setTitle(`探索` + buttonName)
setOpen(true)
} }
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[]) => { const handleButtonChange = (checkedValues: string[]) => {
setSelectedButtons(checkedValues) setSelectedButtons(checkedValues)
} }
// 导出 // 导出
const handleExportExcel = () => { const handleExportExcel = () => {
const worksheet = utils.json_to_sheet(results) const worksheet = utils.json_to_sheet(dataSource)
const workbook = utils.book_new() const workbook = utils.book_new()
utils.book_append_sheet(workbook, worksheet, 'Sheet1') utils.book_append_sheet(workbook, worksheet, 'Sheet1')
writeFile(workbook, 'data.xlsx') writeFile(workbook, 'data.xlsx')
...@@ -160,7 +123,19 @@ export default function ButtonModal() { ...@@ -160,7 +123,19 @@ export default function ButtonModal() {
{button} {button}
</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' }}> <div style={{ minHeight: 300, padding: '20px 0' }}>
<Flex justify="space-between" align="center" style={{ marginBottom: '20px' }}> <Flex justify="space-between" align="center" style={{ marginBottom: '20px' }}>
<div> <div>
...@@ -177,7 +152,14 @@ export default function ButtonModal() { ...@@ -177,7 +152,14 @@ export default function ButtonModal() {
))} ))}
</Checkbox.Group> </Checkbox.Group>
</Flex> </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> </div>
</Modal> </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[] = [ ...@@ -14,4 +14,8 @@ export const routes: RouteObject[] = [
path: '/demo/chart', path: '/demo/chart',
Component: lazy(() => import('./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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论