正在显示
4 个修改的文件
包含
301 行增加
和
88 行删除
1 | import chatStyles from "@/app/components/chat.module.scss"; | 1 | import chatStyles from "@/app/components/chat.module.scss"; |
2 | import homeStyles from "@/app/components/home.module.scss"; | 2 | import homeStyles from "@/app/components/home.module.scss"; |
3 | +import styles from "./bgRemoval.module.scss"; | ||
3 | 4 | ||
4 | import { WindowContent } from "@/app/components/home"; | 5 | import { WindowContent } from "@/app/components/home"; |
5 | import { useMobileScreen } from "@/app/utils"; | 6 | import { useMobileScreen } from "@/app/utils"; |
@@ -12,14 +13,17 @@ import { getClientConfig } from "@/app/config/client"; | @@ -12,14 +13,17 @@ import { getClientConfig } from "@/app/config/client"; | ||
12 | import React, { useEffect, useMemo, useRef, useState } from "react"; | 13 | import React, { useEffect, useMemo, useRef, useState } from "react"; |
13 | import { useAppConfig } from "@/app/store"; | 14 | import { useAppConfig } from "@/app/store"; |
14 | import { BgSiderBar } from "./bg-siderBar"; | 15 | import { BgSiderBar } from "./bg-siderBar"; |
15 | -import { message } from "antd"; | 16 | +import { Button, Flex, Upload, message } from "antd"; |
17 | +import { UploadOutlined } from "@ant-design/icons"; | ||
16 | import type { UploadFile } from "antd"; | 18 | import type { UploadFile } from "antd"; |
17 | 19 | ||
18 | import ReturnIcon from "@/app/icons/return.svg"; | 20 | import ReturnIcon from "@/app/icons/return.svg"; |
19 | import MinIcon from "@/app/icons/min.svg"; | 21 | import MinIcon from "@/app/icons/min.svg"; |
20 | import MaxIcon from "@/app/icons/max.svg"; | 22 | import MaxIcon from "@/app/icons/max.svg"; |
21 | import SDIcon from "@/app/icons/sd.svg"; | 23 | import SDIcon from "@/app/icons/sd.svg"; |
22 | -import { ChartComponent } from "../chart"; | 24 | +import LoadingIcon from "@/app/icons/three-dots.svg"; |
25 | +import BotIcon from "@/app/icons/bot.svg"; | ||
26 | +import CloseIcon from "@/app/icons/close.svg"; | ||
23 | 27 | ||
24 | export function BgRemoval() { | 28 | export function BgRemoval() { |
25 | const isMobileScreen = useMobileScreen(); | 29 | const isMobileScreen = useMobileScreen(); |
@@ -117,34 +121,48 @@ export function BgRemoval() { | @@ -117,34 +121,48 @@ export function BgRemoval() { | ||
117 | </div> | 121 | </div> |
118 | </div> | 122 | </div> |
119 | <div className={chatStyles["chat-body"]} ref={scrollRef}> | 123 | <div className={chatStyles["chat-body"]} ref={scrollRef}> |
120 | - {/* <Flex vertical justify='center' align="center" gap="middle" className={styles['panelFlex']}> | ||
121 | - {isLoading ? ( | ||
122 | - <div className={clsx("no-dark", styles["loading-content"])}> | ||
123 | - <BotIcon /> | ||
124 | - <LoadingIcon/> | ||
125 | - </div> | ||
126 | - ) : previewUrl ? ( | ||
127 | - <div className={styles['preview']}> | ||
128 | - <img src={previewUrl} alt="Preview" className={styles['previewImage']} /> | ||
129 | - <IconButton icon={<CloseIcon />} bordered onClick={closePic} className={styles['icon']} /> | ||
130 | - </div> | ||
131 | - ) : ( | ||
132 | - <Upload | ||
133 | - onChange={onChange} | ||
134 | - beforeUpload={(file) => { | ||
135 | - setFileData(file); | ||
136 | - return false; | ||
137 | - }} | ||
138 | - showUploadList={false} | ||
139 | - accept="image/*" | ||
140 | - > | ||
141 | - <Button icon={<UploadOutlined />} size='large'> | ||
142 | - 上传图片 | ||
143 | - </Button> | ||
144 | - </Upload> | ||
145 | - )} | ||
146 | - </Flex> */} | ||
147 | - <ChartComponent /> | 124 | + <Flex |
125 | + vertical | ||
126 | + justify="center" | ||
127 | + align="center" | ||
128 | + gap="middle" | ||
129 | + className={styles["panelFlex"]} | ||
130 | + > | ||
131 | + {isLoading ? ( | ||
132 | + <div className={clsx("no-dark", styles["loading-content"])}> | ||
133 | + <BotIcon /> | ||
134 | + <LoadingIcon /> | ||
135 | + </div> | ||
136 | + ) : previewUrl ? ( | ||
137 | + <div className={styles["preview"]}> | ||
138 | + <img | ||
139 | + src={previewUrl} | ||
140 | + alt="Preview" | ||
141 | + className={styles["previewImage"]} | ||
142 | + /> | ||
143 | + <IconButton | ||
144 | + icon={<CloseIcon />} | ||
145 | + bordered | ||
146 | + onClick={closePic} | ||
147 | + className={styles["icon"]} | ||
148 | + /> | ||
149 | + </div> | ||
150 | + ) : ( | ||
151 | + <Upload | ||
152 | + onChange={onChange} | ||
153 | + beforeUpload={(file) => { | ||
154 | + setFileData(file); | ||
155 | + return false; | ||
156 | + }} | ||
157 | + showUploadList={false} | ||
158 | + accept="image/*" | ||
159 | + > | ||
160 | + <Button icon={<UploadOutlined />} size="large"> | ||
161 | + 上传图片 | ||
162 | + </Button> | ||
163 | + </Upload> | ||
164 | + )} | ||
165 | + </Flex> | ||
148 | </div> | 166 | </div> |
149 | </div> | 167 | </div> |
150 | </WindowContent> | 168 | </WindowContent> |
1 | import { Card } from "antd"; | 1 | import { Card } from "antd"; |
2 | import * as echarts from "echarts/core"; | 2 | import * as echarts from "echarts/core"; |
3 | -import { GridComponent, GridComponentOption } from "echarts/components"; | ||
4 | -import { BarChart, BarSeriesOption } from "echarts/charts"; | 3 | +import { |
4 | + GridComponent, | ||
5 | + TooltipComponent, | ||
6 | + LegendComponent, | ||
7 | + TitleComponent, | ||
8 | +} from "echarts/components"; | ||
9 | +import { | ||
10 | + BarChart, | ||
11 | + PieChart, | ||
12 | + LineChart, | ||
13 | + BarSeriesOption, | ||
14 | + PieSeriesOption, | ||
15 | + LineSeriesOption, | ||
16 | +} from "echarts/charts"; | ||
5 | import { CanvasRenderer } from "echarts/renderers"; | 17 | import { CanvasRenderer } from "echarts/renderers"; |
6 | -import { useRef, useState } from "react"; | ||
7 | -const tabList = [ | ||
8 | - { | ||
9 | - key: "bar", | ||
10 | - label: "柱状图", | ||
11 | - }, | ||
12 | - { | ||
13 | - key: "pie", | ||
14 | - label: "饼图", | ||
15 | - }, | ||
16 | - { | ||
17 | - key: "line", | ||
18 | - label: "折线图", | ||
19 | - }, | ||
20 | -]; | 18 | +import { useRef, useEffect, useState } from "react"; |
19 | +import { ChartComponentProps, ChartData } from "./getChartData"; | ||
20 | + | ||
21 | +// 注册必要的组件 | ||
22 | +echarts.use([ | ||
23 | + GridComponent, | ||
24 | + TooltipComponent, | ||
25 | + LegendComponent, | ||
26 | + TitleComponent, | ||
27 | + BarChart, | ||
28 | + PieChart, | ||
29 | + LineChart, | ||
30 | + CanvasRenderer, | ||
31 | +]); | ||
21 | 32 | ||
22 | -echarts.use([GridComponent, BarChart, CanvasRenderer]); | ||
23 | type EChartsOption = echarts.ComposeOption< | 33 | type EChartsOption = echarts.ComposeOption< |
24 | - GridComponentOption | BarSeriesOption | 34 | + BarSeriesOption | PieSeriesOption | LineSeriesOption |
25 | >; | 35 | >; |
26 | 36 | ||
27 | -var option: EChartsOption; | 37 | +const tabList = [ |
38 | + { key: "bar", label: "柱状图" }, | ||
39 | + { key: "pie", label: "饼图" }, | ||
40 | + { key: "line", label: "折线图" }, | ||
41 | +]; | ||
28 | 42 | ||
29 | -option = { | ||
30 | - xAxis: { | ||
31 | - type: "category", | ||
32 | - data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], | ||
33 | - }, | ||
34 | - yAxis: { | ||
35 | - type: "value", | ||
36 | - }, | ||
37 | - series: [ | ||
38 | - { | ||
39 | - data: [120, 200, 150, 80, 70, 110, 130], | ||
40 | - type: "bar", | ||
41 | - }, | ||
42 | - ], | ||
43 | -}; | ||
44 | -export function ChartComponent() { | ||
45 | - const [activeTabKey, setActiveTabKey] = useState<string>("bar"); | ||
46 | - const line = useRef<HTMLCanvasElement>(null); | ||
47 | - const bar = useRef<HTMLCanvasElement>(null); | ||
48 | - const pie = useRef<HTMLCanvasElement>(null); | ||
49 | - const contentListNoTitle: Record<string, React.ReactNode> = { | ||
50 | - bar: <canvas ref={bar}></canvas>, | ||
51 | - pie: <canvas ref={pie}></canvas>, | ||
52 | - line: <canvas ref={line}></canvas>, | 43 | +// 图表配置生成器 |
44 | +const getOption = (type: string, data: ChartData): EChartsOption => { | ||
45 | + const commonOption = { | ||
46 | + title: { text: `${tabList.find((t) => t.key === type)?.label}` }, | ||
47 | + tooltip: { trigger: "item" }, | ||
53 | }; | 48 | }; |
54 | - // var myChart = echarts.init(bar); | 49 | + |
50 | + // 提取数值数组 | ||
51 | + const values = data.values.map((item) => item.value); | ||
52 | + | ||
53 | + switch (type) { | ||
54 | + case "bar": | ||
55 | + return { | ||
56 | + ...commonOption, | ||
57 | + xAxis: { type: "category", data: data.categories }, | ||
58 | + yAxis: { type: "value" }, | ||
59 | + series: [{ data: values, type: "bar" }], | ||
60 | + }; | ||
61 | + case "pie": | ||
62 | + return { | ||
63 | + ...commonOption, | ||
64 | + series: [ | ||
65 | + { | ||
66 | + type: "pie", | ||
67 | + data: data.categories.map((name, i) => ({ | ||
68 | + name, | ||
69 | + value: values[i], | ||
70 | + })), | ||
71 | + radius: "50%", | ||
72 | + }, | ||
73 | + ], | ||
74 | + }; | ||
75 | + case "line": | ||
76 | + return { | ||
77 | + ...commonOption, | ||
78 | + xAxis: { type: "category", data: data.categories }, | ||
79 | + yAxis: { type: "value" }, | ||
80 | + series: [ | ||
81 | + { | ||
82 | + data: values, | ||
83 | + type: "line", | ||
84 | + smooth: true, | ||
85 | + areaStyle: {}, | ||
86 | + }, | ||
87 | + ], | ||
88 | + }; | ||
89 | + default: | ||
90 | + return {}; | ||
91 | + } | ||
92 | +}; | ||
93 | + | ||
94 | +export function ChartComponent({ | ||
95 | + data = { categories: [], values: [] }, | ||
96 | +}: ChartComponentProps) { | ||
97 | + const [activeTabKey, setActiveTabKey] = useState("bar"); | ||
98 | + const chartRef = useRef<HTMLDivElement>(null); | ||
99 | + const chartInstance = useRef<echarts.ECharts | null>(null); | ||
100 | + | ||
101 | + useEffect(() => { | ||
102 | + if (!chartRef.current) return; | ||
103 | + | ||
104 | + // 数据校验 | ||
105 | + if ( | ||
106 | + !data?.categories?.length || | ||
107 | + !data?.values?.length || | ||
108 | + data.values.some((item) => typeof item.value !== "number") | ||
109 | + ) { | ||
110 | + console.warn("Invalid chart data"); | ||
111 | + return; | ||
112 | + } | ||
113 | + | ||
114 | + // 销毁旧实例 | ||
115 | + if (chartInstance.current) { | ||
116 | + chartInstance.current.dispose(); | ||
117 | + } | ||
118 | + | ||
119 | + // 初始化新图表 | ||
120 | + chartInstance.current = echarts.init(chartRef.current); | ||
121 | + chartInstance.current.setOption(getOption(activeTabKey, data)); | ||
122 | + | ||
123 | + // 窗口resize监听 | ||
124 | + const resizeHandler = () => chartInstance.current?.resize(); | ||
125 | + window.addEventListener("resize", resizeHandler); | ||
126 | + | ||
127 | + // 清理函数 | ||
128 | + return () => { | ||
129 | + window.removeEventListener("resize", resizeHandler); | ||
130 | + chartInstance.current?.dispose(); | ||
131 | + }; | ||
132 | + }, [activeTabKey, data]); | ||
133 | + | ||
55 | return ( | 134 | return ( |
56 | - <> | ||
57 | - <Card | ||
58 | - style={{ width: "100%" }} | ||
59 | - tabList={tabList} | ||
60 | - activeTabKey={activeTabKey} | ||
61 | - onTabChange={setActiveTabKey} | ||
62 | - tabProps={{ size: "middle" }} | ||
63 | - > | ||
64 | - {contentListNoTitle[activeTabKey]} | ||
65 | - </Card> | ||
66 | - </> | 135 | + <Card |
136 | + style={{ width: "100%", minHeight: 400 }} | ||
137 | + tabList={tabList} | ||
138 | + activeTabKey={activeTabKey} | ||
139 | + onTabChange={setActiveTabKey} | ||
140 | + tabProps={{ size: "middle" }} | ||
141 | + > | ||
142 | + <div | ||
143 | + ref={chartRef} | ||
144 | + style={{ | ||
145 | + width: "100%", | ||
146 | + height: 400, | ||
147 | + minHeight: 400, | ||
148 | + }} | ||
149 | + /> | ||
150 | + </Card> | ||
67 | ); | 151 | ); |
68 | } | 152 | } |
app/components/chart/getChartData.ts
0 → 100644
1 | +export interface ValueItem { | ||
2 | + value: number; | ||
3 | + itemStyle?: { | ||
4 | + color: string; | ||
5 | + }; | ||
6 | +} | ||
7 | + | ||
8 | +export interface ChartData { | ||
9 | + categories: string[]; | ||
10 | + values: ValueItem[]; | ||
11 | +} | ||
12 | +export interface ChartComponentProps { | ||
13 | + data: { | ||
14 | + categories: string[]; | ||
15 | + values: ValueItem[]; | ||
16 | + }; | ||
17 | +} | ||
18 | + | ||
19 | +export function extractDataFromText(text: string): ChartComponentProps | null { | ||
20 | + const startMarkers = ["```javascript", "```json"]; | ||
21 | + let dataStartIndex = -1; | ||
22 | + let markerLength = 0; | ||
23 | + | ||
24 | + // 查找有效的数据块起始标记 | ||
25 | + for (const marker of startMarkers) { | ||
26 | + const index = text.indexOf(marker); | ||
27 | + if (index !== -1) { | ||
28 | + dataStartIndex = index; | ||
29 | + markerLength = marker.length; | ||
30 | + break; | ||
31 | + } | ||
32 | + } | ||
33 | + | ||
34 | + let parsedData: any = null; | ||
35 | + | ||
36 | + // 如果找到有效数据块 | ||
37 | + if (dataStartIndex !== -1) { | ||
38 | + // 查找数据块结束标记 | ||
39 | + const dataEndIndex = text.indexOf("```", dataStartIndex + markerLength); | ||
40 | + if (dataEndIndex !== -1) { | ||
41 | + // 提取并清理数据块内容 | ||
42 | + const dataBlock = text | ||
43 | + .slice(dataStartIndex + markerLength, dataEndIndex) | ||
44 | + .trim() | ||
45 | + .replace(/'/g, '"') // 转换单引号为双引号 | ||
46 | + .replace(/(\w+)(?=\s*:)/g, '"$1"'); // 为键添加双引号 | ||
47 | + | ||
48 | + try { | ||
49 | + // 尝试安全解析数据块内容 | ||
50 | + parsedData = JSON.parse(dataBlock); | ||
51 | + } catch (error) { | ||
52 | + return null; | ||
53 | + } | ||
54 | + } | ||
55 | + } | ||
56 | + | ||
57 | + // 如果没有找到数据块,尝试将整个文本解析为 ChartData | ||
58 | + if (!parsedData) { | ||
59 | + try { | ||
60 | + const potentialData: any = JSON.parse(text); | ||
61 | + // 验证解析后的数据是否符合 ChartData 结构 | ||
62 | + if ( | ||
63 | + potentialData && | ||
64 | + Array.isArray(potentialData.categories) && | ||
65 | + potentialData.categories.every((c: any) => typeof c === "string") && | ||
66 | + Array.isArray(potentialData.values) && | ||
67 | + potentialData.values.every( | ||
68 | + (v: any) => | ||
69 | + typeof v?.value === "number" && | ||
70 | + v?.itemStyle && | ||
71 | + typeof v.itemStyle?.color === "string", | ||
72 | + ) | ||
73 | + ) { | ||
74 | + parsedData = potentialData; | ||
75 | + } | ||
76 | + } catch (error) { | ||
77 | + return null; // 如果解析失败,返回 null | ||
78 | + } | ||
79 | + } | ||
80 | + | ||
81 | + // 如果成功解析了数据并且符合 ChartData 类型,返回 | ||
82 | + if ( | ||
83 | + parsedData && | ||
84 | + Array.isArray(parsedData.categories) && | ||
85 | + Array.isArray(parsedData.values) && | ||
86 | + parsedData.values.every( | ||
87 | + (v: any) => | ||
88 | + typeof v?.value === "number" && | ||
89 | + v?.itemStyle && | ||
90 | + typeof v.itemStyle?.color === "string", | ||
91 | + ) | ||
92 | + ) { | ||
93 | + return { data: parsedData as ChartData }; | ||
94 | + } | ||
95 | + | ||
96 | + return null; // 如果未满足条件,则返回 null | ||
97 | +} |
@@ -25,6 +25,9 @@ import { IconButton } from "./button"; | @@ -25,6 +25,9 @@ import { IconButton } from "./button"; | ||
25 | import { useAppConfig } from "../store/config"; | 25 | import { useAppConfig } from "../store/config"; |
26 | import clsx from "clsx"; | 26 | import clsx from "clsx"; |
27 | 27 | ||
28 | +import { ChartComponentProps, extractDataFromText } from "./chart/getChartData"; | ||
29 | +import { ChartComponent } from "./chart"; | ||
30 | + | ||
28 | export function Mermaid(props: { code: string }) { | 31 | export function Mermaid(props: { code: string }) { |
29 | const ref = useRef<HTMLDivElement>(null); | 32 | const ref = useRef<HTMLDivElement>(null); |
30 | const [hasError, setHasError] = useState(false); | 33 | const [hasError, setHasError] = useState(false); |
@@ -79,6 +82,7 @@ export function PreCode(props: { children: any }) { | @@ -79,6 +82,7 @@ export function PreCode(props: { children: any }) { | ||
79 | const { height } = useWindowSize(); | 82 | const { height } = useWindowSize(); |
80 | const chatStore = useChatStore(); | 83 | const chatStore = useChatStore(); |
81 | const session = chatStore.currentSession(); | 84 | const session = chatStore.currentSession(); |
85 | + const [chartData, setChartData] = useState<ChartComponentProps | null>(null); | ||
82 | 86 | ||
83 | const renderArtifacts = useDebouncedCallback(() => { | 87 | const renderArtifacts = useDebouncedCallback(() => { |
84 | if (!ref.current) return; | 88 | if (!ref.current) return; |
@@ -102,10 +106,20 @@ export function PreCode(props: { children: any }) { | @@ -102,10 +106,20 @@ export function PreCode(props: { children: any }) { | ||
102 | const config = useAppConfig(); | 106 | const config = useAppConfig(); |
103 | const enableArtifacts = | 107 | const enableArtifacts = |
104 | session.mask?.enableArtifacts !== false && config.enableArtifacts; | 108 | session.mask?.enableArtifacts !== false && config.enableArtifacts; |
105 | - | ||
106 | //Wrap the paragraph for plain-text | 109 | //Wrap the paragraph for plain-text |
107 | useEffect(() => { | 110 | useEffect(() => { |
108 | if (ref.current) { | 111 | if (ref.current) { |
112 | + const textContent = ref.current.innerText; | ||
113 | + if (textContent) { | ||
114 | + console.log("有textContent"); | ||
115 | + console.log(textContent); | ||
116 | + const data = extractDataFromText(textContent); | ||
117 | + console.log(data); | ||
118 | + if (data) { | ||
119 | + console.log("data"); | ||
120 | + setChartData(data); | ||
121 | + } | ||
122 | + } | ||
109 | const codeElements = ref.current.querySelectorAll( | 123 | const codeElements = ref.current.querySelectorAll( |
110 | "code", | 124 | "code", |
111 | ) as NodeListOf<HTMLElement>; | 125 | ) as NodeListOf<HTMLElement>; |
@@ -156,6 +170,7 @@ export function PreCode(props: { children: any }) { | @@ -156,6 +170,7 @@ export function PreCode(props: { children: any }) { | ||
156 | /> | 170 | /> |
157 | </FullScreen> | 171 | </FullScreen> |
158 | )} | 172 | )} |
173 | + {chartData && <ChartComponent data={chartData.data} />} | ||
159 | <pre ref={ref}> | 174 | <pre ref={ref}> |
160 | <span | 175 | <span |
161 | className="copy-code-button" | 176 | className="copy-code-button" |
@@ -271,7 +286,6 @@ function _MarkDownContent(props: { content: string }) { | @@ -271,7 +286,6 @@ function _MarkDownContent(props: { content: string }) { | ||
271 | const escapedContent = useMemo(() => { | 286 | const escapedContent = useMemo(() => { |
272 | return tryWrapHtmlCode(escapeBrackets(props.content)); | 287 | return tryWrapHtmlCode(escapeBrackets(props.content)); |
273 | }, [props.content]); | 288 | }, [props.content]); |
274 | - | ||
275 | return ( | 289 | return ( |
276 | <ReactMarkdown | 290 | <ReactMarkdown |
277 | remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]} | 291 | remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]} |
-
请 注册 或 登录 后发表评论