Skip to content

Commit ecc05dc

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

18 files changed

+9939
-15532
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
"react-markdown": "~8.0.6",
127127
"react-resizable": "^3.0.5",
128128
"react-syntax-highlighter": "~15.5.0",
129+
"rehype-raw": "^7.0.0",
129130
"remark-gfm": "~3.0.1",
130131
"shortid": "^2.2.16",
131132
"showdown": "^1.9.0"

pnpm-lock.yaml

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

src/chat/__tests__/content.test.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ jest.mock('../prompt', () => {
3636
return (props: any) => <pre data-testid="fakePrompt">{JSON.stringify(props)}</pre>;
3737
});
3838

39+
const CustomPrompt = (props: any) => (
40+
<pre className="custom-prompt-replaced" data-testid="fakeCustomPrompt">
41+
{JSON.stringify(props)}
42+
</pre>
43+
);
44+
const CustomMessage = (props: any) => (
45+
<pre className="custom-message-replaced" data-testid="fakeCustomMessage">
46+
{JSON.stringify(props)}
47+
</pre>
48+
);
3949
function generatePrompt() {
4050
const prompt = new BasePrompt({
4151
id: '1',
@@ -192,4 +202,20 @@ describe('Test Chat Content', () => {
192202
expect(fn).toBeCalledTimes(1);
193203
expect(fn).lastCalledWith({ top: 200, left: 0, behavior: 'instant' });
194204
});
205+
it('Should support replacePrompt and replaceMessage', () => {
206+
const { container, getByTestId } = render(
207+
<Content
208+
data={[generatePrompt()]}
209+
replacePrompt={(promptProps) => <CustomPrompt data={promptProps} />}
210+
replaceMessage={(messageProps) => <CustomMessage data={messageProps} />}
211+
/>
212+
);
213+
expect(getByTestId('fakeCustomPrompt')).toBeInTheDocument();
214+
const ele1 = container.querySelector('.custom-prompt-replaced')!;
215+
expect(ele1.textContent).not.toBeNull();
216+
217+
expect(getByTestId('fakeCustomMessage')).toBeInTheDocument();
218+
const ele2 = container.querySelector('.custom-message-replaced')!;
219+
expect(ele2.textContent).not.toBeNull();
220+
});
195221
});

src/chat/__tests__/markdown.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { cleanup, render } from '@testing-library/react';
55
import Markdown from '../markdown';
66

77
jest.mock('remark-gfm', () => () => {});
8+
jest.mock('rehype-raw', () => () => {});
89

910
const markdown = `
1011
# title

src/chat/__tests__/message.test.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import '@testing-library/jest-dom/extend-expect';
77

88
import { Message as MessageEntity, MessageStatus, Prompt as PromptEntity } from '../entity';
99
import Message from '../message';
10-
import Chat from '..';
10+
import Chat from '../';
1111

1212
jest.mock('remark-gfm', () => () => {});
13+
jest.mock('rehype-raw', () => () => {});
1314

1415
class BasePrompt extends PromptEntity {}
1516
class BaseMessage extends MessageEntity {}
@@ -301,4 +302,28 @@ describe('Test Chat Message', () => {
301302
expect(ele.dataset.messageid).toBe('1');
302303
expect(ele.dataset.promptid).toBe('1');
303304
});
305+
306+
it('Should support extraRender', () => {
307+
const prompt = generatePrompt();
308+
prompt.messages[0].status = MessageStatus.DONE;
309+
const { container, getByTestId } = render(
310+
<Message
311+
prompt={prompt}
312+
data={prompt.messages}
313+
extraRender={
314+
<div
315+
className="dtc__message__extra__render"
316+
data-testid="fakeMessageExtraRender"
317+
>
318+
ExtraDom
319+
</div>
320+
}
321+
/>
322+
);
323+
expect(getByTestId('fakeMessageExtraRender')).toBeInTheDocument();
324+
const nodeList = container.querySelectorAll<HTMLDivElement>('.dtc__message__extra__render');
325+
const ele = nodeList?.item(nodeList?.length - 1);
326+
expect(ele).not.toBeNull();
327+
expect(ele?.textContent).toBe('ExtraDom');
328+
});
304329
});

src/chat/__tests__/prompt.test.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { render } from '@testing-library/react';
44

55
import { Prompt as PromptEntity } from '../entity';
66
import Prompt from '../prompt';
7-
import Chat from '..';
7+
import Chat from '../';
88

99
jest.mock('remark-gfm', () => () => {});
10+
jest.mock('rehype-raw', () => () => {});
1011
class BasePrompt extends PromptEntity {}
1112

1213
function generatePrompt() {
@@ -57,4 +58,22 @@ describe('Test Chat Prompt', () => {
5758

5859
expect(getByTestId('fakeCode').dataset.promptid).toBe('1');
5960
});
61+
62+
it('Should support extraRender', () => {
63+
const data = generatePrompt();
64+
const { container } = render(
65+
<Prompt
66+
data={data}
67+
extraRender={
68+
<div className="dtc__prompt__extra__render" data-testid="fakePromptExtraRender">
69+
PromptExtraDom
70+
</div>
71+
}
72+
/>
73+
);
74+
const nodeList = container.querySelectorAll<HTMLDivElement>('.dtc__prompt__extra__render');
75+
const ele = nodeList?.item(nodeList?.length - 1);
76+
expect(ele).not.toBeNull();
77+
expect(ele?.textContent).toBe('PromptExtraDom');
78+
});
6079
});

src/chat/content/index.tsx

Lines changed: 42 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,45 @@ 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 ? (
142+
replacePrompt(promptProps)
143+
) : (
144+
<Prompt {...promptProps} />
145+
)}
146+
{replaceMessage ? (
147+
replaceMessage(messageProps)
148+
) : (
149+
<Message {...messageProps} />
150+
)}
138151
</React.Fragment>
139152
);
140153
})}

src/chat/demos/markdown.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ SELECT * FROM table_name;
3434
| 单元格 | 单元格 | 单元格 |
3535
| 单元格 | 单元格 | 单元格 |
3636
| 单元格 | 单元格 | 单元格 |
37+
<span style="color: red;">自定义颜色</span>
3738
`;
3839

3940
export default function () {
40-
return <Chat.Markdown>{children}</Chat.Markdown>;
41+
return <Chat.Markdown isHtmlContent>{children}</Chat.Markdown>;
4142
}

src/chat/demos/message.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { Message, MessageStatus, Prompt } from 'dt-react-component/chat/entity';
55

66
class BasicPrompt extends Prompt {}
77
class BasicMessage extends Message {}
8-
8+
const CustomRender = () => {
9+
return <div>CustomRender</div>;
10+
};
911
export default function () {
1012
const [status, setStatus] = useState<MessageStatus>(MessageStatus.DONE);
1113

@@ -42,6 +44,7 @@ export default function () {
4244
regenerate
4345
onStop={() => setStatus(MessageStatus.STOPPED)}
4446
onRegenerate={() => console.log('regenerate')}
47+
extraRender={<CustomRender />}
4548
/>
4649
</>
4750
);

src/chat/demos/prompt.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { Chat } from 'dt-react-component';
44
import { Prompt } from 'dt-react-component/chat/entity';
55

66
const updateReducer = (num: number): number => (num + 1) % 1_000_000;
7-
7+
const CustomRender = () => {
8+
return <div>CustomRender</div>;
9+
};
810
export default function () {
911
const [value, setValue] = useState<string | undefined>('');
1012
const [, update] = useReducer(updateReducer, 0);
@@ -17,7 +19,7 @@ export default function () {
1719

1820
return (
1921
<Space direction="vertical" style={{ width: '100%' }}>
20-
<Chat.Prompt data={prompt.current} />
22+
<Chat.Prompt data={prompt.current} extraRender={<CustomRender />} />
2123
<Space>
2224
<Chat.Input
2325
value={value}

0 commit comments

Comments
 (0)