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

chore: update

上级 f41baf12
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -12,7 +12,6 @@
"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",
"@dnd-kit/sortable": "^10.0.0",
......@@ -28,6 +27,7 @@
"echarts-wordcloud": "^2.1.0",
"lodash-es": "^4.17.21",
"lucide-react": "^0.484.0",
"normalize.css": "^8.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^5.0.0",
......
body,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
p,
blockquote,
dl,
dt,
dd,
ul,
ol,
li,
pre,
form,
fieldset,
legend,
button,
input,
textarea,
th,
td {
margin: 0;
padding: 0;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: 100%;
}
ul,
ol,
li {
list-style: none;
}
em,
i {
font-style: normal;
}
strong,
b {
font-weight: normal;
}
img {
border: none;
}
input,
img {
vertical-align: middle;
}
a {
color: inherit;
text-decoration: none;
}
input,
button,
select,
textarea {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
appearance: none;
border: 0;
border-radius: 0;
font: inherit;
}
textarea:focus {
outline: 0;
}
@import 'normalize.css';
html,
body,
#root {
......@@ -139,3 +64,9 @@ body,
color: #fff;
}
}
.ai-thinking {
margin-bottom: 20px;
padding-left: 16px;
border-left: 1px solid rgba(0, 0, 0, 0.45);
}
......@@ -4,34 +4,35 @@ import { AIData, AIMessage } from './types'
import { extractJSON } from '@/utils/helper'
import { sseRequest, SSEOptions } from '@/utils/sseRequest'
function updateTransform(res: any): AIMessage | null {
try {
const message = JSON.parse(res)
if (message.choices && message.choices.length > 0) {
const delta = message.choices[0].delta
function transform(messages: any[]): AIMessage {
return messages.reduce(
(result, message) => {
let delta = null
if (message.choices && message.choices.length > 0) {
delta = message.choices[0].delta
}
const content = result.content + (delta.content || '')
const reasoning_content = result.reasoning_content + (delta.reasoning_content || '')
let full_content = ''
if (reasoning_content) {
full_content = `<div class="ai-thinking">${reasoning_content}`
if (content) {
full_content += `</div>${content}`
}
} else {
full_content = content
}
return {
id: message.id,
role: 'assistant',
content: delta.content || '',
reasoning_content: delta.reasoning_content || '',
content,
reasoning_content,
full_content,
json: extractJSON(content),
}
}
} catch (error) {
console.error(error)
}
return null
}
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) }
}
// 文心一言
......@@ -47,8 +48,7 @@ export async function yiyan(data: AIData, options: SSEOptions) {
...options,
body: JSON.stringify({ stream: true, ...data }),
},
updateTransform,
successTransform
transform
)
}
......@@ -61,8 +61,7 @@ export async function deepseek(data: AIData, options: SSEOptions) {
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer sk-f1a6f0a7013241de8393cb2cb108e777' },
body: JSON.stringify({ model: 'deepseek-reasoner', stream: true, ...data }),
},
updateTransform,
successTransform
transform
)
}
......@@ -78,8 +77,7 @@ export async function siliconflow(data: AIData, options: SSEOptions) {
},
body: JSON.stringify({ model: 'deepseek-ai/DeepSeek-R1', stream: true, ...data }),
},
updateTransform,
successTransform
transform
)
}
......@@ -92,8 +90,7 @@ export async function qwen(data: AIData, options: SSEOptions) {
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer sk-afd0fcdb53bf4058b2068b8548820150' },
body: JSON.stringify({ model: 'qwen-max-latest', stream: true, ...data }),
},
updateTransform,
successTransform
transform
)
}
......@@ -126,8 +123,7 @@ export async function openAI(data: AIData, options: SSEOptions) {
headers: { 'Content-Type': 'application/json', Authorization: 'ezijing@20250331' },
body: JSON.stringify({ stream: true, ...data }),
},
updateTransform,
successTransform
transform
)
}
......@@ -150,7 +146,7 @@ const aiService = {
openAI,
}
const provider = providers[data.model as keyof typeof providers] || qwen
const provider = providers[data.model as keyof typeof providers] || openAI
await provider(data, options)
},
}
......
......@@ -8,8 +8,9 @@ export interface AIMessage {
role: 'user' | 'assistant' | 'system'
content: string
reasoning_content?: string
full_content?: string
json?: any
loading?: boolean
status?: 'loading' | 'success' | 'error'
}
export interface AIData {
......
......@@ -12,11 +12,6 @@ export function useAI(globalOptions?: SSEOptions) {
const [error, setError] = useState<string | null>(null)
const controllerRef = useRef<AbortController | null>(null)
// 使用 useRef 存储最新消息,避免闭包问题
const latestMessageRef = useRef<AIMessage | null>(null)
// 使用 useRef 存储消息列表,避免异步更新问题
const messagesRef = useRef<AIMessage[]>([])
useEffect(() => {
localStorage.setItem('ai', ai)
}, [ai])
......@@ -47,7 +42,6 @@ export function useAI(globalOptions?: SSEOptions) {
// 添加用户消息
const userMessages = data.messages.filter((item) => item.role !== 'system')
messagesRef.current = [...messagesRef.current, ...userMessages]
setMessages((prev) => [...prev, ...userMessages])
aiService.post(
......@@ -55,31 +49,13 @@ export function useAI(globalOptions?: SSEOptions) {
{
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])
}
setMessage(message)
setMessages((prev) => {
const messageIndex = prev.findIndex((item) => item.id === message.id)
return messageIndex === -1
? [...prev, message]
: prev.map((item) => (item.id === message.id ? message : item))
})
},
onSuccess: (message) => {
controllerRef.current = null
......
......@@ -54,25 +54,13 @@ export const useAIStore = create<AIState>((set, get) => ({
{
signal: controller.signal,
onUpdate: (message) => {
console.log(message)
set((state) => {
const messageIndex = state.messages.findIndex((msg) => msg.id === message.id)
if (messageIndex === -1) {
return { message, messages: [...state.messages, message] }
} else {
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 }
}
const messageIndex = state.messages.findIndex((item) => item.id === message.id)
const messages =
messageIndex === -1
? [...state.messages, message]
: state.messages.map((msg) => (msg.id === message.id ? message : msg))
return { message, messages }
})
},
onSuccess: (message) => {
......
......@@ -41,12 +41,10 @@ $table-bg-color: #f6f8fa;
font-size: 1.25em;
}
// 段落样式
p {
margin: 1em 0;
margin: 0;
}
// 列表样式
ul,
ol {
padding-left: 2em;
......
......@@ -65,7 +65,7 @@ const transformData = (data: any[], yField: string[], yRule: string, ySort: stri
: { all: transformedData }
// Calculate results for each group
transformedData = Object.entries(groupedData).map(([_, group]) => {
transformedData = Object.entries(groupedData).map(([, group]) => {
const newItem = { ...group[0] }
yField.forEach((field) => {
const values = group.map((d) => parseFloat(d[field]) || 0)
......@@ -134,6 +134,7 @@ export default function Chart({
style = { height: '400px', width: '100%' },
yRule = '',
ySort = '',
fieldColors = [],
...props
}) {
const { data } = useDataQuery()
......@@ -147,6 +148,13 @@ export default function Chart({
const datalist = transformData(data.list, yField, yRule, ySort, xField)
const dataset = { dimensions: [xField, ...yField].filter(Boolean), source: datalist }
const fieldColorMap = fieldColors.reduce((acc: Record<string, string>, item: any) => {
if (item.field && item.color) {
acc[item.field] = item.color
}
return acc
}, {})
let defaultOptions: any = {
legend: {},
tooltip: {},
......@@ -172,7 +180,7 @@ export default function Chart({
return datalist[params.dataIndex][labelField]
},
},
itemStyle: { borderRadius, color: itemStyleColor },
itemStyle: { borderRadius, color: fieldColorMap[field] || itemStyleColor },
})),
})
break
......@@ -198,7 +206,8 @@ export default function Chart({
dataset,
xAxis: { show: false },
yAxis: { show: false },
series: yField.map(() => ({
series: yField.map((field) => ({
name: getFieldName(field),
type: 'pie',
label: {
show: !!labelField,
......@@ -384,7 +393,6 @@ export default function Chart({
show: !!labelField,
position: 'top',
formatter: (params: any) => {
console.log(params.dataIndex)
return datalist[params.dataIndex][labelField]
},
},
......
......@@ -4,6 +4,7 @@ import { useDataFieldQuery } from '@/hooks/useQuery'
import { useCreateChart, useUpdateChart, useViewChartQuery } from '@/hooks/useChartQuery'
import { useAI } from '@/ai/useAI'
import Chart from './Chart'
import ColorField from './ColorField'
interface Props {
id?: string
......@@ -59,15 +60,13 @@ const Step1 = ({ fieldOptions, numberFields, type }: any) => {
</Form.Item>
</Col>
</Row>
{!['11', '10'].includes(type) && (
<Row gutter={20}>
<Col span={8}>
<Form.Item label='请选择"维度"字段' name="x">
<Select options={fieldOptions} placeholder="请选择" allowClear></Select>
</Form.Item>
</Col>
</Row>
)}
<Row gutter={20}>
<Col span={8}>
<Form.Item label='请选择"维度"字段' name="x">
<Select options={fieldOptions} placeholder="请选择" allowClear></Select>
</Form.Item>
</Col>
</Row>
<Row gutter={20}>
<Col span={8}>
<Form.Item label="是否显示行轴" name="showX" hidden={!['1', '2'].includes(type)}>
......@@ -85,7 +84,7 @@ const Step1 = ({ fieldOptions, numberFields, type }: any) => {
}
// 辅助可视化设置
const Step2 = ({ fieldOptions, type, showTitle }: any) => {
const Step2 = ({ labelOptions, type, showTitle }: any) => {
return (
<>
<Divider orientation="left" orientationMargin="0">
......@@ -95,16 +94,22 @@ const Step2 = ({ fieldOptions, type, showTitle }: any) => {
{!['11'].includes(type) && (
<Col span={12}>
<Form.Item label='请选择"标签"字段' name="labelField">
<Select options={fieldOptions} placeholder="请选择" allowClear></Select>
<Select options={labelOptions} placeholder="请选择" allowClear></Select>
</Form.Item>
</Col>
)}
<Col span={12}>
<Form.Item label="请选择颜色规则" name="color">
<Radio.Group options={[{ label: '自动颜色', value: 'auto' }]}></Radio.Group>
<Form.Item label="请选择颜色规则">
<Form.Item name="color" noStyle>
<Radio.Group options={[{ label: '自动颜色', value: 'auto' }]}></Radio.Group>
</Form.Item>
<Form.Item name="fieldColors" noStyle>
<ColorField fieldOptions={labelOptions} />
</Form.Item>
</Form.Item>
</Col>
</Row>
<Row gutter={20}>
<Col span={12}>
<Flex>
......@@ -256,7 +261,15 @@ const WordCloudStep = ({ fieldOptions, numberFields, showTitle }: any) => {
)
}
const FormStep = ({ type, fieldOptions, numberFields, showTitle }: any) => {
const FormStep = ({
type,
fieldOptions,
labelOptions,
numberFields,
showTitle,
onFieldColorsChange,
currentFieldColors,
}: any) => {
if (type === '12') {
return <TableStep fieldOptions={fieldOptions} />
} else if (type === '9') {
......@@ -267,7 +280,14 @@ const FormStep = ({ type, fieldOptions, numberFields, showTitle }: any) => {
return (
<>
<Step1 fieldOptions={fieldOptions} numberFields={numberFields} type={type} />
<Step2 fieldOptions={fieldOptions} type={type} showTitle={showTitle} />
<Step2
fieldOptions={fieldOptions}
labelOptions={labelOptions}
type={type}
showTitle={showTitle}
onFieldColorsChange={onFieldColorsChange}
currentFieldColors={currentFieldColors}
/>
</>
)
}
......@@ -304,6 +324,8 @@ const ModalContent = ({ setOpen, type, id = '' }: Props) => {
const fillPattern = Form.useWatch('fillPattern', form)
const yRule = Form.useWatch('yRule', form)
const ySort = Form.useWatch('ySort', form)
const fieldColors = Form.useWatch('fieldColors', form) || []
const config = {
title: { show: showTitle, text: title },
legend: { show: showLegend },
......@@ -317,9 +339,15 @@ const ModalContent = ({ setOpen, type, id = '' }: Props) => {
fillPattern,
yRule,
ySort,
fieldColors,
...results,
}
console.log(config)
const labelOptions = fieldOptions.filter((item: any) => {
return item.value === xField || yField?.includes(item.value)
})
const { post } = useAI()
const handlePreview = async () => {
......@@ -406,13 +434,20 @@ const ModalContent = ({ setOpen, type, id = '' }: Props) => {
showLegend: true,
showTitle: false,
fillPattern: 'solid',
yRule: '',
yRule: ['6', '7'].includes(type) ? '' : 'sum',
ySort: '',
}}>
<Form.Item label="组件名称" name="name" rules={[{ required: true, message: '请输入组件名称' }]}>
<Input placeholder="请输入" />
</Form.Item>
<FormStep type={type} fieldOptions={fieldOptions} numberFields={numberFields} showTitle={showTitle} />
<FormStep
type={type}
fieldOptions={fieldOptions}
labelOptions={labelOptions}
numberFields={numberFields}
showTitle={showTitle}
currentFieldColors={fieldColors}
/>
<Divider orientation="left" orientationMargin="0">
预览组件效果
</Divider>
......
import { useState } from 'react'
import { Button, Col, ColorPicker, Form, Modal, Row, Select } from 'antd'
function ColorField({ onChange, fieldOptions }: any) {
const [modalVisible, setModalVisible] = useState(false)
const [modalForm] = Form.useForm()
const handleModalOk = () => {
modalForm.validateFields().then((values) => {
onChange?.(values.fieldColors)
setModalVisible(false)
})
}
return (
<>
<Button type="dashed" onClick={() => setModalVisible(true)}>
自定义颜色字段
</Button>
<Modal
title="自定义颜色字段"
open={modalVisible}
onOk={handleModalOk}
onCancel={() => setModalVisible(false)}
width={400}>
<Form form={modalForm} layout="vertical">
<Form.List name="fieldColors">
{(fields, { add, remove }) => (
<>
{fields.map((field) => (
<Row gutter={20} key={field.key} align="top">
<Col span={14}>
<Form.Item
{...field}
name={[field.name, 'field']}
rules={[{ required: true, message: '请选择字段' }]}>
<Select options={fieldOptions} placeholder="请选择字段" />
</Form.Item>
</Col>
<Col span={4}>
<Form.Item
{...field}
name={[field.name, 'color']}
getValueFromEvent={(event) => event.toHexString()}
rules={[{ required: true, message: '请选择颜色' }]}>
<ColorPicker />
</Form.Item>
</Col>
<Col span={6}>
<Button type="dashed" onClick={() => remove(field.name)}>
删除
</Button>
</Col>
</Row>
))}
<Form.Item>
<Button type="dashed" onClick={() => add()} block>
添加字段颜色
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form>
</Modal>
</>
)
}
export default ColorField
......@@ -190,7 +190,7 @@ export default function DataLayout() {
<div className={`data-layout ${collapsed ? 'collapsed' : ''}`}>
<div className="data-layout-sidebar">
<div className="data-layout-sidebar-header">
{collapsed ? '' : <h2>AI数据分析功能区</h2>}
{collapsed ? '' : <h4>AI数据分析功能区</h4>}
<span onClick={toggleCollapsed}>{collapsed ? <CircleArrowRight /> : <CircleArrowLeft />}</span>
</div>
<div className="data-layout-sidebar-nav">
......
......@@ -37,6 +37,7 @@ json
},
],
})
console.log(message)
if (message.json && message.json.results) {
form.setFieldsValue(message.json.results)
}
......
......@@ -26,7 +26,7 @@ export default function DataReport() {
数据分析报告
</Button>
<Modal title="数据分析报告" open={open} footer={null} width={1000} onCancel={() => setOpen(false)} destroyOnClose>
<AIBubble loading={!message?.content} typing={isLoading} content={message?.content}></AIBubble>
<AIBubble loading={!message?.full_content} typing={isLoading} content={message?.full_content}></AIBubble>
</Modal>
</>
)
......
import { fetchEventSource, FetchEventSourceInit, EventSourceMessage } from '@fortaine/fetch-event-source'
import { message } from 'antd'
export interface SSEOptions extends Omit<FetchEventSourceInit, 'onopen' | 'onmessage' | 'onerror' | 'onclose'> {
/**
......@@ -17,12 +18,7 @@ export interface SSEOptions extends Omit<FetchEventSourceInit, 'onopen' | 'onmes
onError?: (error: any) => void
}
export async function sseRequest(
url: string,
options: SSEOptions = {},
updateTransform?: (data: string) => any,
successTransform?: (data: any[]) => any
) {
export async function sseRequest(url: string, options: SSEOptions = {}, transform?: (data: any[]) => any) {
const { onUpdate, onError, onSuccess, ...rest } = options
const accumulatedData: string[] = []
......@@ -39,18 +35,23 @@ export async function sseRequest(
onmessage: (event) => {
if (event.data && event.data !== '[DONE]') {
const data = updateTransform ? updateTransform(event.data) : event.data
accumulatedData.push(data)
onUpdate?.(data, event)
try {
const data = JSON.parse(event.data)
accumulatedData.push(data)
onUpdate?.(transform ? transform(accumulatedData) : accumulatedData, event)
} catch (error) {
console.error(error)
}
}
},
onclose: () => {
onSuccess?.(successTransform ? successTransform(accumulatedData) : accumulatedData)
onSuccess?.(transform ? transform(accumulatedData) : accumulatedData)
},
onerror: (err) => {
onError?.(err)
message.error(err.message || '请求失败')
},
})
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论