refactor: upgrade gpt-vis (#1883)

This commit is contained in:
hustcc 2024-08-28 13:37:45 +08:00 committed by GitHub
parent 131bc7b89b
commit 1cb7e35295
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 518 additions and 260 deletions

View File

@ -2,7 +2,7 @@ import { ChatContext } from '@/app/chat-context';
import { IChatDialogueMessageSchema } from '@/types/chat'; import { IChatDialogueMessageSchema } from '@/types/chat';
import classNames from 'classnames'; import classNames from 'classnames';
import { memo, useContext } from 'react'; import { memo, useContext } from 'react';
import ReactMarkdown from 'react-markdown'; import { GPTVis } from '@antv/gpt-vis';
import markdownComponents from './chat-content/config'; import markdownComponents from './chat-content/config';
import rehypeRaw from 'rehype-raw'; import rehypeRaw from 'rehype-raw';
@ -21,15 +21,21 @@ function AgentContent({ content }: Props) {
return ( return (
<div <div
className={classNames('relative w-full p-2 md:p-4 rounded-xl break-words', { className={classNames(
'bg-white dark:bg-[#232734]': isView, "relative w-full p-2 md:p-4 rounded-xl break-words",
'lg:w-full xl:w-full pl-0': ['chat_with_db_execute', 'chat_dashboard'].includes(scene), {
})} "bg-white dark:bg-[#232734]": isView,
"lg:w-full xl:w-full pl-0": [
"chat_with_db_execute",
"chat_dashboard",
].includes(scene),
}
)}
> >
{isView ? ( {isView ? (
<ReactMarkdown components={markdownComponents} rehypePlugins={[rehypeRaw]}> <GPTVis components={markdownComponents} rehypePlugins={[rehypeRaw]}>
{formatMarkdownVal(content.context)} {formatMarkdownVal(content.context)}
</ReactMarkdown> </GPTVis>
) : ( ) : (
<div className="">{content.context}</div> <div className="">{content.context}</div>
)} )}

View File

@ -6,7 +6,7 @@ import { githubLightTheme } from '@uiw/react-json-view/githubLight';
import { Alert, Spin } from 'antd'; import { Alert, Spin } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useContext, useMemo } from 'react'; import React, { useContext, useMemo } from 'react';
import ReactMarkdown from 'react-markdown'; import { GPTVis } from '@antv/gpt-vis';
import rehypeRaw from 'rehype-raw'; import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
@ -41,30 +41,36 @@ const VisResponse: React.FC<{ data: VisResponseProps }> = ({ data }) => {
return ( return (
<div className="flex flex-1 flex-col"> <div className="flex flex-1 flex-col">
<Alert <Alert
className={classNames('mb-4', { className={classNames("mb-4", {
'bg-[#fafafa] border-[transparent]': !type, "bg-[#fafafa] border-[transparent]": !type,
})} })}
message={data.name} message={data.name}
type={type} type={type}
{...(type && { showIcon: true })} {...(type && { showIcon: true })}
{...(type === 'warning' && { icon: <Spin indicator={<LoadingOutlined spin />} /> })} {...(type === "warning" && {
icon: <Spin indicator={<LoadingOutlined spin />} />,
})}
/> />
{data.result && ( {data.result && (
<JsonView <JsonView
style={{ ...theme, width: '100%', padding: 10 }} style={{ ...theme, width: "100%", padding: 10 }}
className={classNames({ className={classNames({
'bg-[#fafafa]': mode === 'light', "bg-[#fafafa]": mode === "light",
})} })}
value={JSON.parse(data.result || '{}')} value={JSON.parse(data.result || "{}")}
enableClipboard={false} enableClipboard={false}
displayDataTypes={false} displayDataTypes={false}
objectSortKeys={false} objectSortKeys={false}
/> />
)} )}
{data.err_msg && ( {data.err_msg && (
<ReactMarkdown components={markdownComponents} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}> <GPTVis
components={markdownComponents}
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
>
{data.err_msg} {data.err_msg}
</ReactMarkdown> </GPTVis>
)} )}
</div> </div>
); );

View File

@ -1,7 +1,6 @@
import ModelIcon from '@/new-components/chat/content/ModelIcon'; import ModelIcon from '@/new-components/chat/content/ModelIcon';
import { LinkOutlined, SwapRightOutlined } from '@ant-design/icons'; import { SwapRightOutlined } from '@ant-design/icons';
import { Popover, Space } from 'antd'; import { GPTVis } from '@antv/gpt-vis';
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw'; import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import markdownComponents from './config'; import markdownComponents from './config';
@ -24,7 +23,11 @@ function AgentMessages({ data }: Props) {
{data.map((item, index) => ( {data.map((item, index) => (
<div key={index} className="rounded"> <div key={index} className="rounded">
<div className="flex items-center mb-3 text-sm"> <div className="flex items-center mb-3 text-sm">
{item.model ? <ModelIcon model={item.model} /> : <div className="rounded-full w-6 h-6 bg-gray-100" />} {item.model ? (
<ModelIcon model={item.model} />
) : (
<div className="rounded-full w-6 h-6 bg-gray-100" />
)}
<div className="ml-2 opacity-70"> <div className="ml-2 opacity-70">
{item.sender} {item.sender}
<SwapRightOutlined className="mx-2 text-base" /> <SwapRightOutlined className="mx-2 text-base" />
@ -32,11 +35,17 @@ function AgentMessages({ data }: Props) {
</div> </div>
</div> </div>
<div className="whitespace-normal text-sm mb-3"> <div className="whitespace-normal text-sm mb-3">
<ReactMarkdown components={markdownComponents} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}> <GPTVis
components={markdownComponents}
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
>
{item.markdown} {item.markdown}
</ReactMarkdown> </GPTVis>
</div> </div>
{item.resource && item.resource !== 'null' && <ReferencesContent references={item.resource} />} {item.resource && item.resource !== "null" && (
<ReferencesContent references={item.resource} />
)}
</div> </div>
))} ))}
</> </>

View File

@ -1,6 +1,6 @@
import { CaretRightOutlined, CheckOutlined, ClockCircleOutlined } from '@ant-design/icons'; import { CaretRightOutlined, CheckOutlined, ClockCircleOutlined } from '@ant-design/icons';
import { Collapse } from 'antd'; import { Collapse } from 'antd';
import ReactMarkdown from 'react-markdown'; import { GPTVis } from '@antv/gpt-vis';
import rehypeRaw from 'rehype-raw'; import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
@ -32,7 +32,7 @@ function AgentPlans({ data }: Props) {
<span> <span>
{item.name} - {item.agent} {item.name} - {item.agent}
</span> </span>
{item.status === 'complete' ? ( {item.status === "complete" ? (
<CheckOutlined className="!text-green-500 ml-2" /> <CheckOutlined className="!text-green-500 ml-2" />
) : ( ) : (
<ClockCircleOutlined className="!text-gray-500 ml-2" /> <ClockCircleOutlined className="!text-gray-500 ml-2" />
@ -40,9 +40,13 @@ function AgentPlans({ data }: Props) {
</div> </div>
), ),
children: ( children: (
<ReactMarkdown components={markdownComponents} rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}> <GPTVis
components={markdownComponents}
rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]}
>
{item.markdown} {item.markdown}
</ReactMarkdown> </GPTVis>
), ),
}; };
})} })}

View File

@ -1,146 +1,241 @@
import { AutoChart, BackEndChartType, getChartType } from '@/components/chart'; import { LinkOutlined, ReadOutlined, SyncOutlined } from "@ant-design/icons";
import { LinkOutlined, ReadOutlined, SyncOutlined } from '@ant-design/icons'; import { GPTVis, withDefaultChartCode } from "@antv/gpt-vis";
import { Datum } from '@antv/ava'; import { Table, Image, Tag, Tabs, TabsProps } from "antd";
import { Image, Table, Tabs, TabsProps, Tag } from 'antd'; import { AutoChart, BackEndChartType, getChartType } from "@/components/chart";
import ReactMarkdown from 'react-markdown'; import { Datum } from "@antv/ava";
import rehypeRaw from 'rehype-raw'; import rehypeRaw from "rehype-raw";
import remarkGfm from 'remark-gfm'; import remarkGfm from "remark-gfm";
import AgentMessages from "./agent-messages";
import AgentPlans from "./agent-plans";
import { CodePreview } from "./code-preview";
import ReferencesContent from "./ReferencesContent";
import VisChart from "./vis-chart";
import VisCode from "./vis-code";
import VisConvertError from "./vis-convert-error";
import VisDashboard from "./vis-dashboard";
import VisPlugin from "./vis-plugin";
import VisAppLink from "./VisAppLink";
import VisChatLink from "./VisChatLink";
import VisResponse from "./VisResponse";
import { formatSql } from "@/utils";
import AgentMessages from './agent-messages'; type MarkdownComponent = Parameters<typeof GPTVis>["0"]["components"];
import AgentPlans from './agent-plans';
import { CodePreview } from './code-preview';
import ReferencesContent from './ReferencesContent';
import VisChart from './vis-chart';
import VisCode from './vis-code';
import VisConvertError from './vis-convert-error';
import VisDashboard from './vis-dashboard';
import VisPlugin from './vis-plugin';
import VisAppLink from './VisAppLink';
import VisChatLink from './VisChatLink';
import VisResponse from './VisResponse';
import { formatSql } from '@/utils';
type MarkdownComponent = Parameters<typeof ReactMarkdown>['0']['components']; const customeTags: (keyof JSX.IntrinsicElements)[] = [
"custom-view",
const customeTags: (keyof JSX.IntrinsicElements)[] = ['custom-view', 'chart-view', 'references', 'summary']; "chart-view",
"references",
"summary",
];
function matchCustomeTagValues(context: string) { function matchCustomeTagValues(context: string) {
const matchValues = customeTags.reduce<string[]>((acc, tagName) => { const matchValues = customeTags.reduce<string[]>((acc, tagName) => {
const tagReg = new RegExp(`<${tagName}[^>]*\/?>`, 'gi'); const tagReg = new RegExp(`<${tagName}[^>]*\/?>`, "gi");
context = context.replace(tagReg, (matchVal) => { context = context.replace(tagReg, (matchVal) => {
acc.push(matchVal); acc.push(matchVal);
return ''; return "";
}); });
return acc; return acc;
}, []); }, []);
return { context, matchValues }; return { context, matchValues };
} }
const codeComponents = {
/**
* @description
* Custom code block rendering, which can be used to render custom components in the code block.
* Is it defined in gpt-vis, and the default rendering contains `vis-chart`.
*/
code: withDefaultChartCode({
languageRenderers: {
"agent-plans": ({
node,
className,
children,
style,
}) => {
const content = String(children);
/**
* @description
* In some cases, tags are nested within code syntax,
* so it is necessary to extract the tags present in the code block and render them separately.
*/
const lang = className?.replace("language-", "") || "javascript";
try {
const data = JSON.parse(content) as Parameters<
typeof AgentPlans
>[0]["data"];
return <AgentPlans data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
},
"agent-messages": ({
node,
className,
children,
style,
}) => {
const content = String(children);
const lang = className?.replace("language-", "") || "javascript";
try {
const data = JSON.parse(content) as Parameters<
typeof AgentMessages
>[0]["data"];
return <AgentMessages data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
},
"vis-convert-error": ({
node,
className,
children,
style,
}) => {
const content = String(children);
const lang = className?.replace("language-", "") || "javascript";
try {
const data = JSON.parse(content) as Parameters<
typeof VisConvertError
>[0]["data"];
return <VisConvertError data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
},
"vis-dashboard": ({
node,
className,
children,
style,
}) => {
const content = String(children);
const lang = className?.replace("language-", "") || "javascript";
try {
const data = JSON.parse(content) as Parameters<
typeof VisDashboard
>[0]["data"];
return <VisDashboard data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
},
"vis-chart": ({ node, className, children, style }) => {
const content = String(children);
const lang = className?.replace("language-", "") || "javascript";
try {
const data = JSON.parse(content) as Parameters<
typeof VisChart
>[0]["data"];
return <VisChart data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
},
"vis-plugin": ({
node,
className,
children,
style,
}) => {
const content = String(children);
const lang = className?.replace("language-", "") || "javascript";
try {
const data = JSON.parse(content) as Parameters<
typeof VisPlugin
>[0]["data"];
return <VisPlugin data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
},
"vis-code": ({ node, className, children, style, ...props }) => {
const content = String(children);
const lang = className?.replace("language-", "") || "javascript";
try {
const data = JSON.parse(content) as Parameters<
typeof VisCode
>[0]["data"];
return <VisCode data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
},
"vis-app-link": ({
node,
className,
children,
style,
...props
}) => {
const content = String(children);
const lang = className?.replace("language-", "") || "javascript";
try {
const data = JSON.parse(content) as Parameters<
typeof VisAppLink
>[0]["data"];
return <VisAppLink data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
},
"vis-api-response": ({
node,
className,
children,
style,
...props
}) => {
const content = String(children);
const lang = className?.replace("language-", "") || "javascript";
try {
const data = JSON.parse(content) as Parameters<
typeof VisResponse
>[0]["data"];
return <VisResponse data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
},
},
defaultRenderer({ node, className, children, style, ...props }) {
const content = String(children);
const lang = className?.replace("language-", "") || "";
const { context, matchValues } = matchCustomeTagValues(content);
console.log(111, { node, className, children, style, ...props }, lang);
return (
<>
{lang ? (
<CodePreview code={context} language={lang || "javascript"} />
) : (
<code
{...props}
style={style}
className="p-1 mx-1 rounded bg-theme-light dark:bg-theme-dark text-sm"
>
{children}
</code>
)}
<GPTVis
components={markdownComponents}
rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]}
>
{matchValues.join("\n")}
</GPTVis>
</>
);
},
}),
};
const basicComponents: MarkdownComponent = { const basicComponents: MarkdownComponent = {
code({ inline, node, className, children, style, ...props }) { ...codeComponents,
const content = String(children);
/**
* @description
* In some cases, tags are nested within code syntax,
* so it is necessary to extract the tags present in the code block and render them separately.
*/
const { context, matchValues } = matchCustomeTagValues(content);
const lang = className?.replace('language-', '') || 'javascript';
if (lang === 'agent-plans') {
try {
const data = JSON.parse(content) as Parameters<typeof AgentPlans>[0]['data'];
return <AgentPlans data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
}
if (lang === 'agent-messages') {
try {
const data = JSON.parse(content) as Parameters<typeof AgentMessages>[0]['data'];
return <AgentMessages data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
}
if (lang === 'vis-convert-error') {
try {
const data = JSON.parse(content) as Parameters<typeof VisConvertError>[0]['data'];
return <VisConvertError data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
}
if (lang === 'vis-dashboard') {
try {
const data = JSON.parse(content) as Parameters<typeof VisDashboard>[0]['data'];
return <VisDashboard data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
}
if (lang === 'vis-chart') {
try {
const data = JSON.parse(content) as Parameters<typeof VisChart>[0]['data'];
return <VisChart data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
}
if (lang === 'vis-plugin') {
try {
const data = JSON.parse(content) as Parameters<typeof VisPlugin>[0]['data'];
return <VisPlugin data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
}
if (lang === 'vis-code') {
try {
const data = JSON.parse(content) as Parameters<typeof VisCode>[0]['data'];
return <VisCode data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
}
if (lang === 'vis-app-link') {
try {
const data = JSON.parse(content) as Parameters<typeof VisAppLink>[0]['data'];
return <VisAppLink data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
}
if (lang === 'vis-api-response') {
try {
const data = JSON.parse(content) as Parameters<typeof VisResponse>[0]['data'];
return <VisResponse data={data} />;
} catch (e) {
return <CodePreview language={lang} code={content} />;
}
}
return (
<>
{!inline ? (
<CodePreview code={context} language={lang} />
) : (
<code {...props} style={style} className="p-1 mx-1 rounded bg-theme-light dark:bg-theme-dark text-sm">
{children}
</code>
)}
<ReactMarkdown components={markdownComponents} rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}>
{matchValues.join('\n')}
</ReactMarkdown>
</>
);
},
ul({ children }) { ul({ children }) {
return <ul className="py-1">{children}</ul>; return <ul className="py-1">{children}</ul>;
}, },
@ -148,22 +243,46 @@ const basicComponents: MarkdownComponent = {
return <ol className="py-1">{children}</ol>; return <ol className="py-1">{children}</ol>;
}, },
li({ children, ordered }) { li({ children, ordered }) {
return <li className={`text-sm leading-7 ml-5 pl-2 text-gray-600 dark:text-gray-300 ${ordered ? 'list-decimal' : 'list-disc'}`}>{children}</li>; return (
<li
className={`text-sm leading-7 ml-5 pl-2 text-gray-600 dark:text-gray-300 ${
ordered ? "list-decimal" : "list-disc"
}`}
>
{children}
</li>
);
}, },
table({ children }) { table({ children }) {
return <table className="my-2 rounded-tl-md rounded-tr-md bg-white dark:bg-gray-800 text-sm rounded-lg overflow-hidden">{children}</table>; return (
<table className="my-2 rounded-tl-md rounded-tr-md bg-white dark:bg-gray-800 text-sm rounded-lg overflow-hidden">
{children}
</table>
);
}, },
thead({ children }) { thead({ children }) {
return <thead className="bg-[#fafafa] dark:bg-black font-semibold">{children}</thead>; return (
<thead className="bg-[#fafafa] dark:bg-black font-semibold">
{children}
</thead>
);
}, },
th({ children }) { th({ children }) {
return <th className="!text-left p-4">{children}</th>; return <th className="!text-left p-4">{children}</th>;
}, },
td({ children }) { td({ children }) {
return <td className="p-4 border-t border-[#f0f0f0] dark:border-gray-700">{children}</td>; return (
<td className="p-4 border-t border-[#f0f0f0] dark:border-gray-700">
{children}
</td>
);
}, },
h1({ children }) { h1({ children }) {
return <h3 className="text-2xl font-bold my-4 border-b border-slate-300 pb-4">{children}</h3>; return (
<h3 className="text-2xl font-bold my-4 border-b border-slate-300 pb-4">
{children}
</h3>
);
}, },
h2({ children }) { h2({ children }) {
return <h3 className="text-xl font-bold my-3">{children}</h3>; return <h3 className="text-xl font-bold my-3">{children}</h3>;
@ -209,8 +328,8 @@ const basicComponents: MarkdownComponent = {
); );
}, },
button({ children, className, ...restProps }) { button({ children, className, ...restProps }) {
if (className === 'chat-link') { if (className === "chat-link") {
const msg = (restProps as any)?.['data-msg']; const msg = (restProps as any)?.["data-msg"];
return <VisChatLink msg={msg}>{children}</VisChatLink>; return <VisChatLink msg={msg}>{children}</VisChatLink>;
} }
return ( return (
@ -223,32 +342,32 @@ const basicComponents: MarkdownComponent = {
const returnSqlVal = (val: string) => { const returnSqlVal = (val: string) => {
const punctuationMap: any = { const punctuationMap: any = {
'': ',', "": ",",
'。': '.', "。": ".",
'': '?', "": "?",
'': '!', "": "!",
'': ':', "": ":",
'': ';', "": ";",
'“': '"', "“": '"',
'”': '"', "”": '"',
'': "'", "": "'",
'': "'", "": "'",
'': '(', "": "(",
'': ')', "": ")",
'【': '[', "【": "[",
'】': ']', "】": "]",
'《': '<', "《": "<",
'》': '>', "》": ">",
'—': '-', "—": "-",
'、': ',', "、": ",",
'…': '...', "…": "...",
}; };
const regex = new RegExp(Object.keys(punctuationMap).join('|'), 'g'); const regex = new RegExp(Object.keys(punctuationMap).join("|"), "g");
return val.replace(regex, (match) => punctuationMap[match]); return val.replace(regex, (match) => punctuationMap[match]);
}; };
const extraComponents: MarkdownComponent = { const extraComponents: MarkdownComponent = {
'chart-view': function ({ content, children }) { "chart-view": function ({ content, children }) {
let data: { let data: {
data: Datum[]; data: Datum[];
type: BackEndChartType; type: BackEndChartType;
@ -259,11 +378,12 @@ const extraComponents: MarkdownComponent = {
} catch (e) { } catch (e) {
console.log(e, content); console.log(e, content);
data = { data = {
type: 'response_table', type: "response_table",
sql: '', sql: "",
data: [], data: [],
}; };
} }
console.log(111, data);
const columns = data?.data?.[0] const columns = data?.data?.[0]
? Object.keys(data?.data?.[0])?.map((item) => { ? Object.keys(data?.data?.[0])?.map((item) => {
@ -276,25 +396,46 @@ const extraComponents: MarkdownComponent = {
: []; : [];
const ChartItem = { const ChartItem = {
key: 'chart', key: "chart",
label: 'Chart', label: "Chart",
children: <AutoChart data={data?.data} chartType={getChartType(data?.type)} />, children: (
<AutoChart data={data?.data} chartType={getChartType(data?.type)} />
),
}; };
const SqlItem = { const SqlItem = {
key: 'sql', key: "sql",
label: 'SQL', label: "SQL",
children: <CodePreview code={formatSql(returnSqlVal(data?.sql), 'mysql') as string} language={'sql'} />, children: (
<CodePreview
code={formatSql(returnSqlVal(data?.sql), "mysql") as string}
language={"sql"}
/>
),
}; };
const DataItem = { const DataItem = {
key: 'data', key: "data",
label: 'Data', label: "Data",
children: <Table dataSource={data?.data} columns={columns} scroll={{x:true}} virtual={true} />, children: (
<Table
dataSource={data?.data}
columns={columns}
scroll={{ x: true }}
virtual={true}
/>
),
}; };
const TabItems: TabsProps['items'] = data?.type === 'response_table' ? [DataItem, SqlItem] : [ChartItem, SqlItem, DataItem]; const TabItems: TabsProps["items"] =
data?.type === "response_table"
? [DataItem, SqlItem]
: [ChartItem, SqlItem, DataItem];
return ( return (
<div> <div>
<Tabs defaultActiveKey={data?.type === 'response_table' ? 'data' : 'chart'} items={TabItems} size="small" /> <Tabs
defaultActiveKey={data?.type === "response_table" ? "data" : "chart"}
items={TabItems}
size="small"
/>
{children} {children}
</div> </div>
); );

View File

@ -1,6 +1,6 @@
import { PropsWithChildren, ReactNode, memo, useContext, useMemo } from 'react'; import { PropsWithChildren, ReactNode, memo, useContext, useMemo } from 'react';
import { CheckOutlined, ClockCircleOutlined, CloseOutlined, CodeOutlined, LoadingOutlined, RobotOutlined, UserOutlined } from '@ant-design/icons'; import { CheckOutlined, ClockCircleOutlined, CloseOutlined, CodeOutlined, LoadingOutlined, RobotOutlined, UserOutlined } from '@ant-design/icons';
import ReactMarkdown from 'react-markdown'; import { GPTVis } from '@antv/gpt-vis';
import { IChatDialogueMessageSchema } from '@/types/chat'; import { IChatDialogueMessageSchema } from '@/types/chat';
import rehypeRaw from 'rehype-raw'; import rehypeRaw from 'rehype-raw';
import classNames from 'classnames'; import classNames from 'classnames';
@ -22,7 +22,7 @@ interface Props {
onLinkClick?: () => void; onLinkClick?: () => void;
} }
type MarkdownComponent = Parameters<typeof ReactMarkdown>['0']['components']; type MarkdownComponent = Parameters<typeof GPTVis>["0"]["components"];
type DBGPTView = { type DBGPTView = {
name: string; name: string;
@ -112,15 +112,23 @@ function ChatContent({ children, content, isChartChat, onLinkClick }: PropsWithC
const { bgClass, icon } = pluginViewStatusMapper[status] ?? {}; const { bgClass, icon } = pluginViewStatusMapper[status] ?? {};
return ( return (
<div className="bg-white dark:bg-[#212121] rounded-lg overflow-hidden my-2 flex flex-col lg:max-w-[80%]"> <div className="bg-white dark:bg-[#212121] rounded-lg overflow-hidden my-2 flex flex-col lg:max-w-[80%]">
<div className={classNames('flex px-4 md:px-6 py-2 items-center text-white text-sm', bgClass)}> <div
className={classNames(
"flex px-4 md:px-6 py-2 items-center text-white text-sm",
bgClass
)}
>
{name} {name}
{icon} {icon}
</div> </div>
{result ? ( {result ? (
<div className="px-4 md:px-6 py-4 text-sm"> <div className="px-4 md:px-6 py-4 text-sm">
<ReactMarkdown components={markdownComponents} rehypePlugins={[rehypeRaw]}> <GPTVis
{result ?? ''} components={markdownComponents}
</ReactMarkdown> rehypePlugins={[rehypeRaw]}
>
{result ?? ""}
</GPTVis>
</div> </div>
) : ( ) : (
<div className="px-4 md:px-6 py-4 text-sm">{err_msg}</div> <div className="px-4 md:px-6 py-4 text-sm">{err_msg}</div>
@ -136,32 +144,48 @@ function ChatContent({ children, content, isChartChat, onLinkClick }: PropsWithC
return ( return (
<div <div
className={classNames('relative flex flex-wrap w-full p-2 md:p-4 rounded-xl break-words', { className={classNames(
'bg-white dark:bg-[#232734]': isRobot, "relative flex flex-wrap w-full p-2 md:p-4 rounded-xl break-words",
'lg:w-full xl:w-full pl-0': ['chat_with_db_execute', 'chat_dashboard'].includes(scene), {
})} "bg-white dark:bg-[#232734]": isRobot,
"lg:w-full xl:w-full pl-0": [
"chat_with_db_execute",
"chat_dashboard",
].includes(scene),
}
)}
> >
<div className="mr-2 flex flex-shrink-0 items-center justify-center h-7 w-7 rounded-full text-lg sm:mr-4"> <div className="mr-2 flex flex-shrink-0 items-center justify-center h-7 w-7 rounded-full text-lg sm:mr-4">
{isRobot ? renderModelIcon(model_name) || <RobotOutlined /> : <UserOutlined />} {isRobot ? (
renderModelIcon(model_name) || <RobotOutlined />
) : (
<UserOutlined />
)}
</div> </div>
<div className="flex-1 overflow-hidden items-center text-md leading-8 pb-2"> <div className="flex-1 overflow-hidden items-center text-md leading-8 pb-2">
{/* User Input */} {/* User Input */}
{!isRobot && typeof context === 'string' && context} {!isRobot && typeof context === "string" && context}
{/* Render Report */} {/* Render Report */}
{isRobot && isChartChat && typeof context === 'object' && ( {isRobot && isChartChat && typeof context === "object" && (
<div> <div>
{`[${context.template_name}]: `} {`[${context.template_name}]: `}
<span className="text-theme-primary cursor-pointer" onClick={onLinkClick}> <span
className="text-theme-primary cursor-pointer"
onClick={onLinkClick}
>
<CodeOutlined className="mr-1" /> <CodeOutlined className="mr-1" />
{context.template_introduce || 'More Details'} {context.template_introduce || "More Details"}
</span> </span>
</div> </div>
)} )}
{/* Markdown */} {/* Markdown */}
{isRobot && typeof context === 'string' && ( {isRobot && typeof context === "string" && (
<ReactMarkdown components={{ ...markdownComponents, ...extraMarkdownComponents }} rehypePlugins={[rehypeRaw]}> <GPTVis
components={{ ...markdownComponents, ...extraMarkdownComponents }}
rehypePlugins={[rehypeRaw]}
>
{formatMarkdownVal(value)} {formatMarkdownVal(value)}
</ReactMarkdown> </GPTVis>
)} )}
{!!relations?.length && ( {!!relations?.length && (
<div className="flex flex-wrap mt-2"> <div className="flex flex-wrap mt-2">

View File

@ -1,4 +1,4 @@
import ReactMarkdown from 'react-markdown'; import { GPTVis } from '@antv/gpt-vis';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import markdownComponents from './config'; import markdownComponents from './config';
import { CodePreview } from './code-preview'; import { CodePreview } from './code-preview';
@ -29,9 +29,12 @@ function VisCode({ data }: Props) {
{data.code.map((item, index) => ( {data.code.map((item, index) => (
<div <div
key={index} key={index}
className={classNames('px-4 py-2 text-[#121417] dark:text-white cursor-pointer', { className={classNames(
'bg-white dark:bg-theme-dark-container': index === show, "px-4 py-2 text-[#121417] dark:text-white cursor-pointer",
})} {
"bg-white dark:bg-theme-dark-container": index === show,
}
)}
onClick={() => { onClick={() => {
setShow(index); setShow(index);
}} }}
@ -53,13 +56,18 @@ function VisCode({ data }: Props) {
<div> <div>
<div className="flex"> <div className="flex">
<div className="bg-white dark:bg-theme-dark-container px-4 py-2 text-[#121417] dark:text-white"> <div className="bg-white dark:bg-theme-dark-container px-4 py-2 text-[#121417] dark:text-white">
{t('Terminal')} {data.exit_success ? <CheckOutlined className="text-green-600" /> : <CloseOutlined className="text-red-600" />} {t("Terminal")}{" "}
{data.exit_success ? (
<CheckOutlined className="text-green-600" />
) : (
<CloseOutlined className="text-red-600" />
)}
</div> </div>
</div> </div>
<div className="p-4 max-h-72 overflow-y-auto whitespace-normal bg-white dark:dark:bg-theme-dark"> <div className="p-4 max-h-72 overflow-y-auto whitespace-normal bg-white dark:dark:bg-theme-dark">
<ReactMarkdown components={markdownComponents} remarkPlugins={[remarkGfm]}> <GPTVis components={markdownComponents} remarkPlugins={[remarkGfm]}>
{data.log} {data.log}
</ReactMarkdown> </GPTVis>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
import { CheckOutlined, ClockCircleOutlined, CloseOutlined, LoadingOutlined } from '@ant-design/icons'; import { CheckOutlined, ClockCircleOutlined, CloseOutlined, LoadingOutlined } from '@ant-design/icons';
import classNames from 'classnames'; import classNames from 'classnames';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import ReactMarkdown from 'react-markdown'; import { GPTVis } from '@antv/gpt-vis';
import rehypeRaw from 'rehype-raw'; import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
@ -47,15 +47,24 @@ function VisPlugin({ data }: Props) {
return ( return (
<div className="bg-theme-light dark:bg-theme-dark-container rounded overflow-hidden my-2 flex flex-col"> <div className="bg-theme-light dark:bg-theme-dark-container rounded overflow-hidden my-2 flex flex-col">
<div className={classNames('flex px-4 md:px-6 py-2 items-center text-white text-sm', bgClass)}> <div
className={classNames(
"flex px-4 md:px-6 py-2 items-center text-white text-sm",
bgClass
)}
>
{data.name} {data.name}
{icon} {icon}
</div> </div>
{data.result ? ( {data.result ? (
<div className="px-4 md:px-6 py-4 text-sm whitespace-normal"> <div className="px-4 md:px-6 py-4 text-sm whitespace-normal">
<ReactMarkdown components={markdownComponents} rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}> <GPTVis
{data.result ?? ''} components={markdownComponents}
</ReactMarkdown> rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]}
>
{data.result ?? ""}
</GPTVis>
</div> </div>
) : ( ) : (
<div className="px-4 md:px-6 py-4 text-sm">{data.err_msg}</div> <div className="px-4 md:px-6 py-4 text-sm">{data.err_msg}</div>

View File

@ -7,7 +7,7 @@ import Image from 'next/image';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import React, { memo, useMemo } from 'react'; import React, { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown'; import { GPTVis } from '@antv/gpt-vis';
import rehypeRaw from 'rehype-raw'; import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
@ -42,7 +42,7 @@ type DBGPTView = {
err_msg?: string; err_msg?: string;
}; };
type MarkdownComponent = Parameters<typeof ReactMarkdown>['0']['components']; type MarkdownComponent = Parameters<typeof GPTVis>["0"]["components"];
const pluginViewStatusMapper: Record<DBGPTView['status'], { bgClass: string; icon: React.ReactNode }> = { const pluginViewStatusMapper: Record<DBGPTView['status'], { bgClass: string; icon: React.ReactNode }> = {
todo: { todo: {
@ -143,15 +143,24 @@ const ChatContent: React.FC<{
const { bgClass, icon } = pluginViewStatusMapper[status] ?? {}; const { bgClass, icon } = pluginViewStatusMapper[status] ?? {};
return ( return (
<div className="bg-white dark:bg-[#212121] rounded-lg overflow-hidden my-2 flex flex-col lg:max-w-[80%]"> <div className="bg-white dark:bg-[#212121] rounded-lg overflow-hidden my-2 flex flex-col lg:max-w-[80%]">
<div className={classNames('flex px-4 md:px-6 py-2 items-center text-white text-sm', bgClass)}> <div
className={classNames(
"flex px-4 md:px-6 py-2 items-center text-white text-sm",
bgClass
)}
>
{name} {name}
{icon} {icon}
</div> </div>
{result ? ( {result ? (
<div className="px-4 md:px-6 py-4 text-sm"> <div className="px-4 md:px-6 py-4 text-sm">
<ReactMarkdown components={markdownComponents} rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}> <GPTVis
{result ?? ''} components={markdownComponents}
</ReactMarkdown> rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]}
>
{result ?? ""}
</GPTVis>
</div> </div>
) : ( ) : (
<div className="px-4 md:px-6 py-4 text-sm">{err_msg}</div> <div className="px-4 md:px-6 py-4 text-sm">{err_msg}</div>
@ -163,45 +172,68 @@ const ChatContent: React.FC<{
[cachePluginContext], [cachePluginContext],
); );
return ( return (
<div className="flex flex-1 gap-3 mt-6"> <div className="flex flex-1 gap-3 mt-6">
{/* icon */} {/* icon */}
<div className="flex flex-shrink-0 items-start">{isRobot ? <RobotIcon model={model_name} /> : <UserIcon />}</div> <div className="flex flex-shrink-0 items-start">
<div className={`flex ${scene === 'chat_agent' && !thinking ? 'flex-1' : ''} overflow-hidden`}> {isRobot ? <RobotIcon model={model_name} /> : <UserIcon />}
</div>
<div
className={`flex ${
scene === "chat_agent" && !thinking ? "flex-1" : ""
} overflow-hidden`}
>
{/* 用户提问 */} {/* 用户提问 */}
{!isRobot && <div className="flex flex-1 items-center text-sm text-[#1c2533] dark:text-white">{typeof context === 'string' && context}</div>} {!isRobot && (
<div className="flex flex-1 items-center text-sm text-[#1c2533] dark:text-white">
{typeof context === "string" && context}
</div>
)}
{/* ai回答 */} {/* ai回答 */}
{isRobot && ( {isRobot && (
<div className="flex flex-1 flex-col w-full"> <div className="flex flex-1 flex-col w-full">
<div className="bg-white dark:bg-[rgba(255,255,255,0.16)] p-4 rounded-2xl rounded-tl-none mb-2"> <div className="bg-white dark:bg-[rgba(255,255,255,0.16)] p-4 rounded-2xl rounded-tl-none mb-2">
{typeof context === 'object' && ( {typeof context === "object" && (
<div> <div>
{`[${context.template_name}]: `} {`[${context.template_name}]: `}
<span className="text-theme-primary cursor-pointer" onClick={onLinkClick}> <span
className="text-theme-primary cursor-pointer"
onClick={onLinkClick}
>
<CodeOutlined className="mr-1" /> <CodeOutlined className="mr-1" />
{context.template_introduce || 'More Details'} {context.template_introduce || "More Details"}
</span> </span>
</div> </div>
)} )}
{typeof context === 'string' && scene === 'chat_agent' && ( {typeof context === "string" && scene === "chat_agent" && (
<ReactMarkdown components={{ ...markdownComponents }} rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}> <GPTVis
{formatMarkdownValForAgent(value)} components={{ ...markdownComponents }}
</ReactMarkdown>
)}
{typeof context === 'string' && scene !== 'chat_agent' && (
<ReactMarkdown
components={{ ...markdownComponents, ...extraMarkdownComponents }}
rehypePlugins={[rehypeRaw]} rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
> >
{formatMarkdownVal(value)} {formatMarkdownValForAgent(value)}
</ReactMarkdown> </GPTVis>
)}
{typeof context === "string" && scene !== "chat_agent" && (
<div>
<GPTVis
components={{
...markdownComponents,
...extraMarkdownComponents,
}}
rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]}
>
{formatMarkdownVal(value)}
</GPTVis>
</div>
)} )}
{/* 正在思考 */} {/* 正在思考 */}
{thinking && !context && ( {thinking && !context && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="flex text-sm text-[#1c2533] dark:text-white">{t('thinking')}</span> <span className="flex text-sm text-[#1c2533] dark:text-white">
{t("thinking")}
</span>
<div className="flex"> <div className="flex">
<div className="w-1 h-1 rounded-full mx-1 animate-pulse1"></div> <div className="w-1 h-1 rounded-full mx-1 animate-pulse1"></div>
<div className="w-1 h-1 rounded-full mx-1 animate-pulse2"></div> <div className="w-1 h-1 rounded-full mx-1 animate-pulse2"></div>

View File

@ -1,14 +1,18 @@
import markdownComponents from '@/components/chat/chat-content/config'; import markdownComponents from '@/components/chat/chat-content/config';
import React from 'react'; import React from 'react';
import ReactMarkdown from 'react-markdown'; import { GPTVis } from '@antv/gpt-vis';
import rehypeRaw from 'rehype-raw'; import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
const MarkDownContext: React.FC<{ children: string }> = ({ children }) => { const MarkDownContext: React.FC<{ children: string }> = ({ children }) => {
return ( return (
<ReactMarkdown components={{ ...markdownComponents }} rehypePlugins={[rehypeRaw]} remarkPlugins={[remarkGfm]}> <GPTVis
components={{ ...markdownComponents }}
rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]}
>
{children} {children}
</ReactMarkdown> </GPTVis>
); );
}; };

View File

@ -46,7 +46,7 @@ const nextConfig = {
} }
}; };
const withTM = require('next-transpile-modules')(['@berryv/g2-react','@antv/g2','react-syntax-highlighter']); const withTM = require('next-transpile-modules')(['@berryv/g2-react','@antv/g2','react-syntax-highlighter', '@antv/gpt-vis']);
module.exports = withTM({ module.exports = withTM({
...nextConfig, ...nextConfig,

View File

@ -16,6 +16,7 @@
"@ant-design/icons": "^5.2.5", "@ant-design/icons": "^5.2.5",
"@antv/ava": "3.5.0-alpha.4", "@antv/ava": "3.5.0-alpha.4",
"@antv/g2": "^5.1.8", "@antv/g2": "^5.1.8",
"@antv/gpt-vis": "^0.0.5",
"@antv/s2": "^1.51.2", "@antv/s2": "^1.51.2",
"@berryv/g2-react": "^0.1.0", "@berryv/g2-react": "^0.1.0",
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
@ -55,13 +56,12 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-i18next": "^13.2.0", "react-i18next": "^13.2.0",
"react-markdown": "^8.0.7",
"react-markdown-editor-lite": "^1.3.4", "react-markdown-editor-lite": "^1.3.4",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-virtualized": "^9.22.5", "react-virtualized": "^9.22.5",
"reactflow": "^11.10.3", "reactflow": "^11.10.3",
"rehype-raw": "6.1.1", "rehype-raw": "^7.0.0",
"remark-gfm": "^3.0.1", "remark-gfm": "^4.0.0",
"sequelize": "^6.33.0", "sequelize": "^6.33.0",
"sql-formatter": "^12.2.4", "sql-formatter": "^12.2.4",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",
@ -69,8 +69,8 @@
"typescript": "5.1.3" "typescript": "5.1.3"
}, },
"devDependencies": { "devDependencies": {
"@types/cytoscape": "^3.21.0",
"@types/crypto-js": "^4.1.2", "@types/crypto-js": "^4.1.2",
"@types/cytoscape": "^3.21.0",
"@types/google-one-tap": "^1.2.4", "@types/google-one-tap": "^1.2.4",
"@types/lodash": "^4.14.195", "@types/lodash": "^4.14.195",
"@types/markdown-it": "^14.1.1", "@types/markdown-it": "^14.1.1",
@ -94,5 +94,20 @@
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"repository": "https://github.com/eosphoros-ai/DB-GPT-Web.git" "repository": "https://github.com/eosphoros-ai/DB-GPT-Web.git",
"resolutions": {
"d3-color": "2",
"d3-array": "2",
"d3-shape": "2",
"d3-path": "2",
"d3-dsv": "2",
"d3-hierarchy": "2",
"d3-scale-chromatic": "2",
"d3-format": "2",
"d3-timer": "2",
"d3-dispatch": "2",
"d3-quadtree": "2",
"d3-force": "2",
"d3-geo": "2"
}
} }