Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"resize-observer-polyfill": "^1.5.1",
"standard-version": "^9.5.0",
"stylelint": "^14.9.1",
"ts-jest": "^29.0.3",
"ts-jest": "29.0.3",
"typescript": "~4.5.2"
},
"dependencies": {
Expand All @@ -126,8 +126,9 @@
"react-markdown": "~8.0.6",
"react-resizable": "^3.0.5",
"react-syntax-highlighter": "~15.5.0",
"rehype-raw": "^6.0.0",
"remark-gfm": "~3.0.1",
"shortid": "^2.2.16",
"shortid": "2.2.16",
"showdown": "^1.9.0"
},
"config": {
Expand Down
26,773 changes: 10,513 additions & 16,260 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions src/chat/__tests__/content.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ jest.mock('../prompt', () => {
return (props: any) => <pre data-testid="fakePrompt">{JSON.stringify(props)}</pre>;
});

const CustomPrompt = (props: any) => (
<pre className="custom-prompt-replaced" data-testid="fakeCustomPrompt">
{JSON.stringify(props)}
</pre>
);
const CustomMessage = (props: any) => (
<pre className="custom-message-replaced" data-testid="fakeCustomMessage">
{JSON.stringify(props)}
</pre>
);
function generatePrompt() {
const prompt = new BasePrompt({
id: '1',
Expand Down Expand Up @@ -192,4 +202,20 @@ describe('Test Chat Content', () => {
expect(fn).toBeCalledTimes(1);
expect(fn).lastCalledWith({ top: 200, left: 0, behavior: 'instant' });
});
it('Should support replacePrompt and replaceMessage', () => {
const { container, getByTestId } = render(
<Content
data={[generatePrompt()]}
replacePrompt={(promptProps) => <CustomPrompt data={promptProps} />}
replaceMessage={(messageProps) => <CustomMessage data={messageProps} />}
/>
);
expect(getByTestId('fakeCustomPrompt')).toBeInTheDocument();
const ele1 = container.querySelector('.custom-prompt-replaced')!;
expect(ele1.textContent).not.toBeNull();

expect(getByTestId('fakeCustomMessage')).toBeInTheDocument();
const ele2 = container.querySelector('.custom-message-replaced')!;
expect(ele2.textContent).not.toBeNull();
});
});
1 change: 1 addition & 0 deletions src/chat/__tests__/markdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { cleanup, render } from '@testing-library/react';
import Markdown from '../markdown';

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

const markdown = `
# title
Expand Down
27 changes: 26 additions & 1 deletion src/chat/__tests__/message.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import '@testing-library/jest-dom/extend-expect';

import { Message as MessageEntity, MessageStatus, Prompt as PromptEntity } from '../entity';
import Message from '../message';
import Chat from '..';
import Chat from '../';

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

class BasePrompt extends PromptEntity {}
class BaseMessage extends MessageEntity {}
Expand Down Expand Up @@ -301,4 +302,28 @@ describe('Test Chat Message', () => {
expect(ele.dataset.messageid).toBe('1');
expect(ele.dataset.promptid).toBe('1');
});

it('Should support extraRender', () => {
const prompt = generatePrompt();
prompt.messages[0].status = MessageStatus.DONE;
const { container, getByTestId } = render(
<Message
prompt={prompt}
data={prompt.messages}
extraRender={
<div
className="dtc__message__extra__render"
data-testid="fakeMessageExtraRender"
>
ExtraDom
</div>
}
/>
);
expect(getByTestId('fakeMessageExtraRender')).toBeInTheDocument();
const nodeList = container.querySelectorAll<HTMLDivElement>('.dtc__message__extra__render');
const ele = nodeList?.item(nodeList?.length - 1);
expect(ele).not.toBeNull();
expect(ele?.textContent).toBe('ExtraDom');
});
});
21 changes: 20 additions & 1 deletion src/chat/__tests__/prompt.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { render } from '@testing-library/react';

import { Prompt as PromptEntity } from '../entity';
import Prompt from '../prompt';
import Chat from '..';
import Chat from '../';

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

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

expect(getByTestId('fakeCode').dataset.promptid).toBe('1');
});

it('Should support extraRender', () => {
const data = generatePrompt();
const { container } = render(
<Prompt
data={data}
extraRender={
<div className="dtc__prompt__extra__render" data-testid="fakePromptExtraRender">
PromptExtraDom
</div>
}
/>
);
const nodeList = container.querySelectorAll<HTMLDivElement>('.dtc__prompt__extra__render');
const ele = nodeList?.item(nodeList?.length - 1);
expect(ele).not.toBeNull();
expect(ele?.textContent).toBe('PromptExtraDom');
});
});
71 changes: 42 additions & 29 deletions src/chat/content/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React, { forwardRef, useImperativeHandle, useLayoutEffect, useRef, useSta
import classNames from 'classnames';

import { Message as MessageEntity, MessageStatus, Prompt as PromptEntity } from '../entity';
import Message from '../message';
import Prompt from '../prompt';
import Message, { IMessageProps } from '../message';
import Prompt, { IPromptProps } from '../prompt';
import { useContext } from '../useContext';
import './index.scss';

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

export interface IContentRef {
Expand All @@ -21,7 +23,7 @@ export interface IContentRef {
}

const Content = forwardRef<IContentRef, IContentProps>(function (
{ data, placeholder, scrollable = true, onRegenerate, onStop },
{ data, placeholder, scrollable = true, onRegenerate, onStop, replacePrompt, replaceMessage },
forwardedRef
) {
const { maxRegenerateCount, copy, regenerate } = useContext();
Expand Down Expand Up @@ -107,34 +109,45 @@ const Content = forwardRef<IContentRef, IContentProps>(function (
{data.map((row, idx) => {
const defaultRegenerate =
idx === data.length - 1 && row.messages.length < maxRegenerateCount;
const messageProps: IMessageProps = {
prompt: row,
data: row.messages,
regenerate:
typeof regenerate === 'function'
? regenerate(row, idx, data)
: regenerate ?? defaultRegenerate,
copy,
onRegenerate: (message) => onRegenerate?.(message, row),
onStop: (message) => onStop?.(message, row),
onLazyRendered: (renderFn) => {
// 在触发懒加载之前判断是否在底部,如果是则加载完成后滚动到底部
const scrolledToBottom = checkIfScrolledToBottom();
renderFn().then(() => {
window.requestAnimationFrame(() => {
setIsStickyAtBottom(scrolledToBottom);
if (scrolledToBottom && containerRef.current) {
containerRef.current.scrollTop =
containerRef.current.scrollHeight;
}
});
});
},
};
const promptProps: IPromptProps = {
data: row,
};
return (
<React.Fragment key={row.id}>
<Prompt data={row} />
<Message
prompt={row}
data={row.messages}
regenerate={
typeof regenerate === 'function'
? regenerate(row, idx, data)
: regenerate ?? defaultRegenerate
}
copy={copy}
onRegenerate={(message) => onRegenerate?.(message, row)}
onStop={(message) => onStop?.(message, row)}
onLazyRendered={(renderFn) => {
// 在触发懒加载之前判断是否在底部,如果是则加载完成后滚动到底部
const scrolledToBottom = checkIfScrolledToBottom();
renderFn().then(() => {
window.requestAnimationFrame(() => {
setIsStickyAtBottom(scrolledToBottom);
if (scrolledToBottom && containerRef.current) {
containerRef.current.scrollTop =
containerRef.current.scrollHeight;
}
});
});
}}
/>
{replacePrompt ? (
replacePrompt(promptProps)
) : (
<Prompt {...promptProps} />
)}
{replaceMessage ? (
replaceMessage(messageProps)
) : (
<Message {...messageProps} />
)}
</React.Fragment>
);
})}
Expand Down
3 changes: 2 additions & 1 deletion src/chat/demos/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ SELECT * FROM table_name;
| 单元格 | 单元格 | 单元格 |
| 单元格 | 单元格 | 单元格 |
| 单元格 | 单元格 | 单元格 |
<span style="color: red;">自定义颜色</span>
`;

export default function () {
return <Chat.Markdown>{children}</Chat.Markdown>;
return <Chat.Markdown isHtmlContent>{children}</Chat.Markdown>;
}
5 changes: 4 additions & 1 deletion src/chat/demos/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { Message, MessageStatus, Prompt } from 'dt-react-component/chat/entity';

class BasicPrompt extends Prompt {}
class BasicMessage extends Message {}

const CustomRender = () => {
return <div>CustomRender</div>;
};
export default function () {
const [status, setStatus] = useState<MessageStatus>(MessageStatus.DONE);

Expand Down Expand Up @@ -42,6 +44,7 @@ export default function () {
regenerate
onStop={() => setStatus(MessageStatus.STOPPED)}
onRegenerate={() => console.log('regenerate')}
extraRender={<CustomRender />}
/>
</>
);
Expand Down
6 changes: 4 additions & 2 deletions src/chat/demos/prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { Chat } from 'dt-react-component';
import { Prompt } from 'dt-react-component/chat/entity';

const updateReducer = (num: number): number => (num + 1) % 1_000_000;

const CustomRender = () => {
return <div>CustomRender</div>;
};
export default function () {
const [value, setValue] = useState<string | undefined>('');
const [, update] = useReducer(updateReducer, 0);
Expand All @@ -17,7 +19,7 @@ export default function () {

return (
<Space direction="vertical" style={{ width: '100%' }}>
<Chat.Prompt data={prompt.current} />
<Chat.Prompt data={prompt.current} extraRender={<CustomRender />} />
<Space>
<Chat.Input
value={value}
Expand Down
30 changes: 28 additions & 2 deletions src/chat/entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { immerable } from 'immer';

import { BaseCommandItem, CurrentFileItem } from './input/type';

/**
* 消息状态
*/
Expand Down Expand Up @@ -31,6 +33,8 @@ export type ConversationProperties = {
createdAt?: Timestamp;
title?: string;
prompts?: Prompt[];
commandList?: BaseCommandItem[];
fileList?: CurrentFileItem[];
};

export type PromptProperties = {
Expand All @@ -39,16 +43,22 @@ export type PromptProperties = {
createdAt?: Timestamp;
title: string;
messages?: Message[];
commandList?: BaseCommandItem[];
fileList?: CurrentFileItem[];
};

export type MessageProperties = {
export interface MessageProperties {
id: Id;
assistantId?: string;
creator?: string;
createdAt?: Timestamp;
// 离线使用到的字段
taskType?: number;
content?: string;
status?: MessageStatus;
};
commandList?: BaseCommandItem[];
fileList?: CurrentFileItem[];
}

/**
* 新对话
Expand All @@ -60,6 +70,8 @@ export abstract class Conversation {
createdAt: Timestamp;
title?: string;
prompts: Prompt[];
commandList?: BaseCommandItem[];
fileList?: CurrentFileItem[];

[immerable] = true;

Expand All @@ -69,6 +81,8 @@ export abstract class Conversation {
this.createdAt = props.createdAt || new Date().valueOf();
this.title = props.title;
this.prompts = props.prompts || [];
this.commandList = props.commandList;
this.fileList = props.fileList;
}
}

Expand All @@ -82,6 +96,8 @@ export abstract class Prompt {
createdAt: Timestamp;
title: string;
messages: Message[];
commandList?: BaseCommandItem[];
fileList?: CurrentFileItem[];

[immerable] = true;

Expand All @@ -91,6 +107,8 @@ export abstract class Prompt {
this.createdAt = props.createdAt || new Date().valueOf();
this.title = props.title;
this.messages = props.messages || [];
this.commandList = props.commandList;
this.fileList = props.fileList;
}
}

Expand All @@ -103,8 +121,12 @@ export abstract class Message {
assistantId?: string;
creator?: string;
createdAt: Timestamp;
// 离线使用到的字段
taskType?: number;
content: string;
status: MessageStatus;
commandList?: BaseCommandItem[];
fileList?: CurrentFileItem[];

[immerable] = true;

Expand All @@ -113,7 +135,11 @@ export abstract class Message {
this.creator = props.creator;
this.assistantId = props.assistantId;
this.createdAt = props.createdAt || new Date().valueOf();
// 离线使用到的字段
this.taskType = props.taskType;
this.content = props.content ?? '';
this.status = props.status ?? MessageStatus.PENDING;
this.commandList = props.commandList;
this.fileList = props.fileList;
}
}
Loading
Loading