Skip to content

Commit 9cb4d30

Browse files
author
zhaoge
committed
feat: support AI quick command feat
1 parent 3b47ed7 commit 9cb4d30

File tree

8 files changed

+9603
-15459
lines changed

8 files changed

+9603
-15459
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@
108108
"standard-version": "^9.5.0",
109109
"stylelint": "^14.9.1",
110110
"ts-jest": "^29.0.3",
111-
"typescript": "~4.5.2"
111+
"typescript": "~4.5.2",
112+
"rehype-raw": "~6.x"
112113
},
113114
"dependencies": {
114115
"@dtinsight/dt-utils": "^1.3.1",

pnpm-lock.yaml

Lines changed: 9453 additions & 15420 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/chat/content/index.tsx

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import React, { forwardRef, useImperativeHandle, useLayoutEffect, useRef, useSta
22
import classNames from 'classnames';
33

44
import { Message as MessageEntity, MessageStatus, Prompt as PromptEntity } from '../entity';
5-
import Message from '../message';
6-
import Prompt from '../prompt';
5+
import Message, { IMessageProps } from '../message';
6+
import Prompt, { IPromptProps } from '../prompt';
77
import { useContext } from '../useContext';
88
import './index.scss';
99

@@ -13,6 +13,8 @@ export interface IContentProps {
1313
scrollable?: boolean;
1414
onRegenerate?: (data: MessageEntity, prompt: PromptEntity) => void;
1515
onStop?: (data: MessageEntity, prompt: PromptEntity) => void;
16+
replacePrompt?: (promptProps: IPromptProps) => React.ReactNode;
17+
replaceMessage?: (messageProps: IMessageProps) => React.ReactNode;
1618
}
1719

1820
export interface IContentRef {
@@ -21,7 +23,7 @@ export interface IContentRef {
2123
}
2224

2325
const Content = forwardRef<IContentRef, IContentProps>(function (
24-
{ data, placeholder, scrollable = true, onRegenerate, onStop },
26+
{ data, placeholder, scrollable = true, onRegenerate, onStop, replacePrompt, replaceMessage },
2527
forwardedRef
2628
) {
2729
const { maxRegenerateCount, copy, regenerate } = useContext();
@@ -107,34 +109,41 @@ const Content = forwardRef<IContentRef, IContentProps>(function (
107109
{data.map((row, idx) => {
108110
const defaultRegenerate =
109111
idx === data.length - 1 && row.messages.length < maxRegenerateCount;
112+
const messageProps: IMessageProps = {
113+
prompt: row,
114+
data: row.messages,
115+
regenerate:
116+
typeof regenerate === 'function'
117+
? regenerate(row, idx, data)
118+
: regenerate ?? defaultRegenerate,
119+
copy,
120+
onRegenerate: (message) => onRegenerate?.(message, row),
121+
onStop: (message) => onStop?.(message, row),
122+
onLazyRendered: (renderFn) => {
123+
// 在触发懒加载之前判断是否在底部,如果是则加载完成后滚动到底部
124+
const scrolledToBottom = checkIfScrolledToBottom();
125+
renderFn().then(() => {
126+
window.requestAnimationFrame(() => {
127+
setIsStickyAtBottom(scrolledToBottom);
128+
if (scrolledToBottom && containerRef.current) {
129+
containerRef.current.scrollTop =
130+
containerRef.current.scrollHeight;
131+
}
132+
});
133+
});
134+
},
135+
};
136+
const promptProps: IPromptProps = {
137+
data: row,
138+
};
110139
return (
111140
<React.Fragment key={row.id}>
112-
<Prompt data={row} />
113-
<Message
114-
prompt={row}
115-
data={row.messages}
116-
regenerate={
117-
typeof regenerate === 'function'
118-
? regenerate(row, idx, data)
119-
: regenerate ?? defaultRegenerate
120-
}
121-
copy={copy}
122-
onRegenerate={(message) => onRegenerate?.(message, row)}
123-
onStop={(message) => onStop?.(message, row)}
124-
onLazyRendered={(renderFn) => {
125-
// 在触发懒加载之前判断是否在底部,如果是则加载完成后滚动到底部
126-
const scrolledToBottom = checkIfScrolledToBottom();
127-
renderFn().then(() => {
128-
window.requestAnimationFrame(() => {
129-
setIsStickyAtBottom(scrolledToBottom);
130-
if (scrolledToBottom && containerRef.current) {
131-
containerRef.current.scrollTop =
132-
containerRef.current.scrollHeight;
133-
}
134-
});
135-
});
136-
}}
137-
/>
141+
{replacePrompt ? replacePrompt(promptProps) : <Prompt data={row} />}
142+
{replaceMessage ? (
143+
replaceMessage(messageProps)
144+
) : (
145+
<Message {...messageProps} />
146+
)}
138147
</React.Fragment>
139148
);
140149
})}

src/chat/entity.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { immerable } from 'immer';
22

3+
import { BaseCommandItem, CurrentFileItem } from './input/type';
4+
35
/**
46
* 消息状态
57
*/
@@ -31,6 +33,8 @@ export type ConversationProperties = {
3133
createdAt?: Timestamp;
3234
title?: string;
3335
prompts?: Prompt[];
36+
commandList?: BaseCommandItem[];
37+
fileList?: CurrentFileItem[];
3438
};
3539

3640
export type PromptProperties = {
@@ -39,16 +43,22 @@ export type PromptProperties = {
3943
createdAt?: Timestamp;
4044
title: string;
4145
messages?: Message[];
46+
commandList?: BaseCommandItem[];
47+
fileList?: CurrentFileItem[];
4248
};
4349

44-
export type MessageProperties = {
50+
export interface MessageProperties {
4551
id: Id;
4652
assistantId?: string;
4753
creator?: string;
4854
createdAt?: Timestamp;
55+
// 离线使用到的字段
56+
taskType?: number;
4957
content?: string;
5058
status?: MessageStatus;
51-
};
59+
commandList?: BaseCommandItem[];
60+
fileList?: CurrentFileItem[];
61+
}
5262

5363
/**
5464
* 新对话
@@ -60,6 +70,8 @@ export abstract class Conversation {
6070
createdAt: Timestamp;
6171
title?: string;
6272
prompts: Prompt[];
73+
commandList?: BaseCommandItem[];
74+
fileList?: CurrentFileItem[];
6375

6476
[immerable] = true;
6577

@@ -69,6 +81,8 @@ export abstract class Conversation {
6981
this.createdAt = props.createdAt || new Date().valueOf();
7082
this.title = props.title;
7183
this.prompts = props.prompts || [];
84+
this.commandList = props.commandList;
85+
this.fileList = props.fileList;
7286
}
7387
}
7488

@@ -82,6 +96,8 @@ export abstract class Prompt {
8296
createdAt: Timestamp;
8397
title: string;
8498
messages: Message[];
99+
commandList?: BaseCommandItem[];
100+
fileList?: CurrentFileItem[];
85101

86102
[immerable] = true;
87103

@@ -91,6 +107,8 @@ export abstract class Prompt {
91107
this.createdAt = props.createdAt || new Date().valueOf();
92108
this.title = props.title;
93109
this.messages = props.messages || [];
110+
this.commandList = props.commandList;
111+
this.fileList = props.fileList;
94112
}
95113
}
96114

@@ -103,8 +121,12 @@ export abstract class Message {
103121
assistantId?: string;
104122
creator?: string;
105123
createdAt: Timestamp;
124+
// 离线使用到的字段
125+
taskType?: number;
106126
content: string;
107127
status: MessageStatus;
128+
commandList?: BaseCommandItem[];
129+
fileList?: CurrentFileItem[];
108130

109131
[immerable] = true;
110132

@@ -113,7 +135,11 @@ export abstract class Message {
113135
this.creator = props.creator;
114136
this.assistantId = props.assistantId;
115137
this.createdAt = props.createdAt || new Date().valueOf();
138+
// 离线使用到的字段
139+
this.taskType = props.taskType;
116140
this.content = props.content ?? '';
117141
this.status = props.status ?? MessageStatus.PENDING;
142+
this.commandList = props.commandList;
143+
this.fileList = props.fileList;
118144
}
119145
}

src/chat/input/type.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
export enum FILE_OPERATE_TYPE {
2+
INPUT = 'input',
3+
CLICK = 'CLICK',
4+
}
5+
export enum FileType {
6+
TASK = 1, // 任务
7+
}
8+
9+
export interface BaseCommandItem {
10+
name: string;
11+
description?: string;
12+
}
13+
// 前后端传输信息用的文件类型
14+
export interface FileItem {
15+
fileId?: string | number;
16+
fileType: FileType;
17+
fileName: string;
18+
isCurrentFile?: boolean;
19+
}
20+
export interface CurrentFileItem extends FileItem {
21+
sqlText: string; // 当前任务内的SQL
22+
sinkStr?: string; // 当前任务内的最新的结果表信息,JSON字符串
23+
sourceStr?: string; // 当前任务内的最新的源表信息,JSON字符串
24+
sideStr?: string; // 当前任务内的最新的维表信息,JSON字符串
25+
selectedSql?: string; // 当前任务内的选中的SQL片段
26+
beginLine?: number; // 当前任务内的选中的SQL片段开始行号
27+
endLine?: number; // 当前任务内的选中的SQL片段结束行号
28+
}
29+
export enum CommandType {
30+
COMMAND = 1, // 快捷命令
31+
FILE_REQUEST = 2, // 文件唤起
32+
}
33+
export enum QuickCommand {
34+
FIX = 'fix',
35+
EXPLAIN = 'explain',
36+
COMMENT = 'comment',
37+
CLEAR = 'clear',
38+
HELP = 'help',
39+
}
40+
// 前端渲染用的文件类型
41+
export interface FileItemRender extends BaseCommandItem {
42+
fileType: FileType;
43+
fileId?: string | number;
44+
insertType?: FILE_OPERATE_TYPE; // input: 输入框输入的命令,file: 从文件列表选择的命令
45+
isCurrentFile?: boolean; // 是否为当前文件
46+
extra?: any;
47+
sqlText: string;
48+
sinkStr?: string;
49+
sourceStr?: string;
50+
sideStr?: string;
51+
selectedSql?: string;
52+
beginLine?: number;
53+
endLine?: number;
54+
icon?: React.ReactNode; // 图标
55+
}
56+
57+
export interface CommandListItem {
58+
commandContent: string;
59+
commandType: CommandType;
60+
}

src/chat/markdown/index.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { memo, type PropsWithChildren, useEffect } from 'react';
22
import ReactMarkdown from 'react-markdown';
33
import { type ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
44
import classNames from 'classnames';
5+
import rehypeRaw from 'rehype-raw';
56
import remarkGfm from 'remark-gfm';
67

78
import Image from '../../image';
@@ -11,6 +12,7 @@ import './index.scss';
1112
type IMarkdownProps = {
1213
typing?: boolean;
1314
codeBlock?: Omit<ICodeBlockProps, 'children'>;
15+
isHtmlContent?: boolean;
1416
onMount?: () => void;
1517
} & ReactMarkdownOptions;
1618

@@ -23,21 +25,22 @@ export default memo(
2325
codeBlock,
2426
components,
2527
children,
28+
isHtmlContent = false,
2629
onMount,
2730
...rest
2831
}: PropsWithChildren<IMarkdownProps>) {
2932
useEffect(() => {
3033
onMount?.();
3134
}, []);
32-
35+
const mergedRehypePlugins = isHtmlContent ? [rehypeRaw, ...rehypePlugins] : rehypePlugins;
3336
return (
3437
<ReactMarkdown
3538
className={classNames(
3639
'dtc__aigc__markdown',
3740
typing && 'dtc__aigc__markdown--blink',
3841
className
3942
)}
40-
rehypePlugins={rehypePlugins}
43+
rehypePlugins={mergedRehypePlugins}
4144
remarkPlugins={[remarkGfm, ...remarkPlugins]}
4245
components={{
4346
code({ children }) {

src/chat/message/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import classNames from 'classnames';
1212
import Copy from '../../copy';
1313
import useIntersectionObserver from '../../useIntersectionObserver';
1414
import { Message as MessageEntity, MessageStatus, Prompt as PromptEntity } from '../entity';
15+
import { CurrentFileItem } from '../input/type';
1516
import Loading from '../loading';
1617
import Markdown from '../markdown';
1718
import Pagination from '../pagination';
1819
import { CopyOptions, useContext } from '../useContext';
1920
import './index.scss';
2021

21-
type IMessageProps = {
22+
export type IMessageProps = {
2223
prompt: PromptEntity;
2324
data: MessageEntity[];
2425
/**
@@ -32,6 +33,7 @@ type IMessageProps = {
3233
onRegenerate?: (data: MessageEntity) => void;
3334
onStop?: (data: MessageEntity) => void;
3435
onLazyRendered?: (cb: () => Promise<void>) => void;
36+
extraRender?: (fileList: CurrentFileItem[]) => React.ReactNode;
3537
};
3638

3739
export default function Message({
@@ -42,6 +44,7 @@ export default function Message({
4244
onRegenerate,
4345
onStop,
4446
onLazyRendered,
47+
extraRender,
4548
}: IMessageProps) {
4649
const divRef = useIntersectionObserver<HTMLDivElement>(handleObserverCb);
4750
const { components = {}, messageIcons, codeBlock, rehypePlugins, remarkPlugins } = useContext();
@@ -137,6 +140,7 @@ export default function Message({
137140
ref={divRef}
138141
>
139142
<Loading loading={loading}>
143+
{extraRender?.(record?.fileList || [])}
140144
{lazyRendered && (
141145
<Markdown
142146
typing={typing}

src/chat/prompt/index.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@ import React, { useMemo } from 'react';
22
import { Components } from 'react-markdown';
33
import classNames from 'classnames';
44

5-
import type { Prompt as PromptEntity } from '../entity';
5+
import { Prompt as PromptEntity } from '../entity';
6+
import { CurrentFileItem } from '../input/type';
67
import Markdown from '../markdown';
78
import { useContext } from '../useContext';
89
import './index.scss';
910

10-
type IPromptProps = {
11+
export type IPromptProps = {
1112
data?: PromptEntity;
1213
className?: string;
14+
extraRender?: (fileList: CurrentFileItem[]) => React.ReactNode;
1315
};
1416

15-
export default function Prompt({ data, className }: IPromptProps) {
17+
export default function Prompt({ data, className, extraRender }: IPromptProps) {
1618
const { components = {}, codeBlock } = useContext();
1719

1820
const composedComponents = useMemo(() => {
@@ -28,17 +30,23 @@ export default function Prompt({ data, className }: IPromptProps) {
2830
}, {});
2931
}, [components, data?.id]);
3032

33+
const fileHisList = useMemo(() => {
34+
const fileList = data?.fileList;
35+
return fileList;
36+
}, [data?.id, data]);
37+
3138
if (!data?.title) return null;
3239

3340
return (
3441
<section className={classNames('dtc__prompt__container', className)}>
3542
<div className="dtc__prompt__wrapper">
3643
<div className="dtc__prompt__content">
37-
<Markdown codeBlock={codeBlock} components={composedComponents}>
44+
<Markdown codeBlock={codeBlock} components={composedComponents} isHtmlContent>
3845
{data?.title || ''}
3946
</Markdown>
4047
</div>
4148
</div>
49+
{extraRender?.(fileHisList || [])}
4250
</section>
4351
);
4452
}

0 commit comments

Comments
 (0)