feat: UI component rendering in agent dialog mode (#1083)

Co-authored-by: csunny <cfqsunny@163.com>
This commit is contained in:
Hzh_97
2024-01-18 11:08:02 +08:00
committed by GitHub
parent 674104eb7c
commit 0936856c3a
46 changed files with 1597 additions and 73 deletions

View File

@@ -0,0 +1,39 @@
import ReactMarkdown from 'react-markdown';
import markdownComponents from './config';
import { renderModelIcon } from '../header/model-selector';
import { SwapRightOutlined } from '@ant-design/icons';
interface Props {
data: {
sender: string;
receiver: string;
model: string | null;
markdown: string;
}[];
}
function AgentMessages({ data }: Props) {
if (!data || !data.length) return null;
return (
<>
{data.map((item, index) => (
<div key={index} className="rounded my-4 md:my-6">
<div className="flex items-center mb-3 text-sm">
{item.model ? renderModelIcon(item.model) : <div className="rounded-full w-6 h-6 bg-gray-100" />}
<div className="ml-2 opacity-70">
{item.sender}
<SwapRightOutlined className="mx-2 text-base" />
{item.receiver}
</div>
</div>
<div className="whitespace-normal text-sm">
<ReactMarkdown components={markdownComponents}>{item.markdown}</ReactMarkdown>
</div>
</div>
))}
</>
);
}
export default AgentMessages;

View File

@@ -0,0 +1,46 @@
import { CaretRightOutlined, CheckOutlined, ClockCircleOutlined } from '@ant-design/icons';
import { Collapse } from 'antd';
import ReactMarkdown from 'react-markdown';
import markdownComponents from './config';
interface Props {
data: {
name: string;
num: number;
status: 'complete' | 'todo';
agent: string;
markdown: string;
}[];
}
function AgentPlans({ data }: Props) {
if (!data || !data.length) return null;
return (
<Collapse
bordered
className="my-3"
expandIcon={({ isActive }) => <CaretRightOutlined rotate={isActive ? 90 : 0} />}
items={data.map((item, index) => {
return {
key: index,
label: (
<div className="whitespace-normal">
<span>
{item.name} - {item.agent}
</span>
{item.status === 'complete' ? (
<CheckOutlined className="!text-green-500 ml-2" />
) : (
<ClockCircleOutlined className="!text-gray-500 ml-2" />
)}
</div>
),
children: <ReactMarkdown components={markdownComponents}>{item.markdown}</ReactMarkdown>,
};
})}
/>
);
}
export default AgentPlans;

View File

@@ -0,0 +1,38 @@
import { Datum } from '@antv/ava';
import { Table, Tabs, TabsProps } from 'antd';
import React from 'react';
import { format } from 'sql-formatter';
import { AutoChart, BackEndChartType, getChartType } from '@/components/chart/autoChart';
import { CodePreview } from './code-preview';
function ChartView({ data, type, sql }: { data: Datum[]; type: BackEndChartType; sql: string }) {
const columns = data?.[0]
? Object.keys(data?.[0])?.map((item) => {
return {
title: item,
dataIndex: item,
key: item,
};
})
: [];
const ChartItem = {
key: 'chart',
label: 'Chart',
children: <AutoChart data={data} chartType={getChartType(type)} />,
};
const SqlItem = {
key: 'sql',
label: 'SQL',
children: <CodePreview language="sql" code={format(sql, { language: 'mysql' }) as string} />,
};
const DataItem = {
key: 'data',
label: 'Data',
children: <Table dataSource={data} columns={columns} />,
};
const TabItems: TabsProps['items'] = type === 'response_table' ? [DataItem, SqlItem] : [ChartItem, SqlItem, DataItem];
return <Tabs defaultActiveKey={type === 'response_table' ? 'data' : 'chart'} items={TabItems} size="small" />;
}
export default ChartView;

View File

@@ -3,10 +3,18 @@ import { CopyOutlined } from '@ant-design/icons';
import { oneDark, coldarkDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import copy from 'copy-to-clipboard';
import { useContext } from 'react';
import { CSSProperties, useContext } from 'react';
import { ChatContext } from '@/app/chat-context';
export function CodePreview({ code, language }: { code: string; language: string }) {
interface Props {
code: string;
language: string;
customStyle?: CSSProperties;
light?: { [key: string]: CSSProperties };
dark?: { [key: string]: CSSProperties };
}
export function CodePreview({ code, light, dark, language, customStyle }: Props) {
const { mode } = useContext(ChatContext);
return (
@@ -20,7 +28,7 @@ export function CodePreview({ code, language }: { code: string; language: string
message[success ? 'success' : 'error'](success ? 'Copy success' : 'Copy failed');
}}
/>
<SyntaxHighlighter language={language} style={mode === 'dark' ? coldarkDark : oneDark}>
<SyntaxHighlighter customStyle={customStyle} language={language} style={mode === 'dark' ? dark ?? coldarkDark : light ?? oneDark}>
{code}
</SyntaxHighlighter>
</div>

View File

@@ -8,6 +8,13 @@ import { CodePreview } from './code-preview';
import { Datum } from '@antv/ava';
import rehypeRaw from 'rehype-raw';
import { IChunk } from '@/types/knowledge';
import AgentPlans from './agent-plans';
import AgentMessages from './agent-messages';
import VisConvertError from './vis-convert-error';
import VisChart from './vis-chart';
import VisDashboard from './vis-dashboard';
import VisPlugin from './vis-plugin';
import VisCode from './vis-code';
type MarkdownComponent = Parameters<typeof ReactMarkdown>['0']['components'];
@@ -27,18 +34,82 @@ function matchCustomeTagValues(context: string) {
const basicComponents: MarkdownComponent = {
code({ inline, node, className, children, style, ...props }) {
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(Array.isArray(children) ? children.join('\n') : children);
const match = /language-(\w+)/.exec(className || '');
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} />;
}
}
return (
<>
{!inline ? (
<CodePreview code={context} language={match?.[1] ?? 'javascript'} />
<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}
@@ -61,7 +132,7 @@ const basicComponents: MarkdownComponent = {
},
table({ children }) {
return (
<table className="my-2 rounded-tl-md rounded-tr-md max-w-full bg-white dark:bg-gray-900 text-sm rounded-lg overflow-hidden">{children}</table>
<table className="my-2 rounded-tl-md rounded-tr-md max-w-full bg-white dark:bg-gray-800 text-sm rounded-lg overflow-hidden">{children}</table>
);
},
thead({ children }) {

View File

@@ -0,0 +1,19 @@
import { BackEndChartType } from '@/components/chart';
import ChartView from './chart-view';
import { Datum } from '@antv/ava';
interface Props {
data: {
data: Datum[];
describe: string;
title: string;
type: BackEndChartType;
sql: string;
};
}
function VisChart({ data }: Props) {
return <ChartView data={data.data} type={data.type} sql={data.sql} />;
}
export default VisChart;

View File

@@ -0,0 +1,69 @@
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import markdownComponents from './config';
import { CodePreview } from './code-preview';
import classNames from 'classnames';
import { useState } from 'react';
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { oneLight, oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
interface Props {
data: {
code: string[];
exit_success: true;
language: string;
log: string;
};
}
function VisCode({ data }: Props) {
const { t } = useTranslation();
const [show, setShow] = useState(0);
return (
<div className="bg-[#EAEAEB] rounded overflow-hidden border border-theme-primary dark:bg-theme-dark text-sm">
<div>
<div className="flex">
{data.code.map((item, index) => (
<div
key={index}
className={classNames('px-4 py-2 text-[#121417] dark:text-white cursor-pointer', {
'bg-white dark:bg-theme-dark-container': index === show,
})}
onClick={() => {
setShow(index);
}}
>
CODE {index + 1}: {item[0]}
</div>
))}
</div>
{data.code.length && (
<CodePreview
language={data.code[show][0]}
code={data.code[show][1]}
customStyle={{ maxHeight: 300, margin: 0 }}
light={oneLight}
dark={oneDark}
/>
)}
</div>
<div>
<div className="flex">
<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" />}
</div>
</div>
<div className="p-4 max-h-72 overflow-y-auto whitespace-normal bg-white dark:dark:bg-theme-dark">
<ReactMarkdown components={markdownComponents} remarkPlugins={[remarkGfm]}>
{data.log}
</ReactMarkdown>
</div>
</div>
</div>
);
}
export default VisCode;

View File

@@ -0,0 +1,24 @@
import { format } from 'sql-formatter';
import { CodePreview } from './code-preview';
interface Props {
data: {
display_type: string;
sql: string;
thought: string;
};
}
function VisConvertError({ data }: Props) {
return (
<div className="rounded overflow-hidden">
<div className="p-3 text-white bg-red-500 whitespace-normal">{data.display_type}</div>
<div className="p-3 bg-red-50">
<div className="mb-2 whitespace-normal">{data.thought}</div>
<CodePreview code={format(data.sql)} language="sql" />
</div>
</div>
);
}
export default VisConvertError;

View File

@@ -0,0 +1,58 @@
import { AutoChart, BackEndChartType, getChartType } from '@/components/chart';
import { Datum } from '@antv/ava';
import { useMemo } from 'react';
interface Props {
data: {
data: {
data: Datum[];
describe: string;
title: string;
type: BackEndChartType;
sql: string;
}[];
title: string | null;
display_strategy: string;
chart_count: number;
};
}
const chartLayout = [[2], [1, 2], [1, 3], [2, 1, 2], [2, 1, 3], [3, 1, 3], [3, 2, 3]];
function VisDashboard({ data }: Props) {
const charts = useMemo(() => {
if (data.chart_count > 1) {
const layout = chartLayout[data.chart_count - 2];
let prevIndex = 0;
return layout.map((item) => {
const items = data.data.slice(prevIndex, prevIndex + item);
prevIndex = item;
return items;
});
}
return [data.data];
}, [data.data, data.chart_count]);
return (
<div className="flex flex-col gap-3">
{charts.map((row, index) => (
<div key={`row-${index}`} className="flex gap-3">
{row.map((chart, subIndex) => (
<div
key={`chart-${subIndex}`}
className="flex flex-1 flex-col justify-between p-4 rounded border border-gray-200 dark:border-gray-500 whitespace-normal"
>
<div>
{chart.title && <div className="mb-2 text-lg">{chart.title}</div>}
{chart.describe && <div className="mb-4 text-sm text-gray-500">{chart.describe}</div>}
</div>
<AutoChart data={chart.data} chartType={getChartType(chart.type)} />
</div>
))}
</div>
))}
</div>
);
}
export default VisDashboard;

View File

@@ -0,0 +1,64 @@
import { CheckOutlined, ClockCircleOutlined, CloseOutlined, LoadingOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import { ReactNode } from 'react';
import ReactMarkdown from 'react-markdown';
import markdownComponents from './config';
import rehypeRaw from 'rehype-raw';
interface IVisPlugin {
name: string;
args: {
query: string;
};
status: 'todo' | 'runing' | 'failed' | 'complete' | (string & {});
logo: string | null;
result: string;
err_msg: string | null;
}
interface Props {
data: IVisPlugin;
}
const pluginViewStatusMapper: Record<IVisPlugin['status'], { bgClass: string; icon: ReactNode }> = {
todo: {
bgClass: 'bg-gray-500',
icon: <ClockCircleOutlined className="ml-2" />,
},
runing: {
bgClass: 'bg-blue-500',
icon: <LoadingOutlined className="ml-2" />,
},
failed: {
bgClass: 'bg-red-500',
icon: <CloseOutlined className="ml-2" />,
},
complete: {
bgClass: 'bg-green-500',
icon: <CheckOutlined className="ml-2" />,
},
};
function VisPlugin({ data }: Props) {
const { bgClass, icon } = pluginViewStatusMapper[data.status] ?? {};
return (
<div className="bg-theme-light dark:bg-theme-dark-container rounded 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)}>
{data.name}
{icon}
</div>
{data.result ? (
<div className="px-4 md:px-6 py-4 text-sm whitespace-normal">
<ReactMarkdown components={markdownComponents} rehypePlugins={[rehypeRaw]}>
{data.result ?? ''}
</ReactMarkdown>
</div>
) : (
<div className="px-4 md:px-6 py-4 text-sm">{data.err_msg}</div>
)}
</div>
);
}
export default VisPlugin;