Skip to content

Commit e777034

Browse files
authored
feat(chat): support render empty prompt (#575)
* feat(chat): support render empty prompt * test(useTyping): add unit test
1 parent de6d39d commit e777034

File tree

7 files changed

+79
-23
lines changed

7 files changed

+79
-23
lines changed

src/chat/__tests__/__snapshots__/markdown.test.tsx.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ exports[`Test Chat Markdown Match Snapshots: typing 1`] = `
2323
<div
2424
class="dtc__aigc__markdown dtc__aigc__markdown--blink test"
2525
>
26-
<p>
26+
<div>
2727
<code
2828
class="dtc__aigc__markdown__inlineCode"
2929
>
3030
inline code test
3131
</code>
32-
</p>
32+
</div>
3333
3434
3535
<div

src/chat/__tests__/__snapshots__/prompt.test.tsx.snap

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,6 @@ exports[`Test Chat Prompt Match Snapshots: default 1`] = `
2424
</DocumentFragment>
2525
`;
2626

27-
exports[`Test Chat Prompt Match Snapshots: empty 1`] = `
28-
<DocumentFragment>
29-
<section
30-
class="dtc__prompt__container"
31-
>
32-
<div
33-
class="dtc__prompt__wrapper"
34-
>
35-
<div
36-
class="dtc__prompt__content"
37-
>
38-
<div
39-
class="dtc__aigc__markdown"
40-
/>
41-
</div>
42-
</div>
43-
</section>
44-
</DocumentFragment>
45-
`;
27+
exports[`Test Chat Prompt Match Snapshots: empty 1`] = `<DocumentFragment />`;
28+
29+
exports[`Test Chat Prompt Match Snapshots: empty title 1`] = `<DocumentFragment />`;

src/chat/__tests__/prompt.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ describe('Test Chat Prompt', () => {
2626
expect(
2727
render(<Prompt className="test" data={generatePrompt()} />).asFragment()
2828
).toMatchSnapshot('default');
29+
const _empty = generatePrompt();
30+
_empty.title = '';
31+
expect(render(<Prompt className="test" data={_empty} />).asFragment()).toMatchSnapshot(
32+
'empty title'
33+
);
2934
});
3035

3136
it('Should support components', () => {

src/chat/markdown/index.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,17 @@ export default memo(
4848
hr() {
4949
return <hr color="#ebecf0" className="dtc__aigc__markdown__hr" />;
5050
},
51+
p: (data) => {
52+
// avoid validateDOMNesting error for div as a descendant of p
53+
if (data.node.children.every((child) => child.type === 'text')) {
54+
return <p>{data.children}</p>;
55+
} else {
56+
return <div>{data.children}</div>;
57+
}
58+
},
5159
...components,
5260
}}
61+
includeElementIndex
5362
{...rest}
5463
>
5564
{children}

src/chat/prompt/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export default function Prompt({ data, className }: IPromptProps) {
2828
}, {});
2929
}, [components, data?.id]);
3030

31+
if (!data?.title) return null;
32+
3133
return (
3234
<section className={classNames('dtc__prompt__container', className)}>
3335
<div className="dtc__prompt__wrapper">

src/useTyping/__tests__/useTyping.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,46 @@ describe('Test useTyping hook', () => {
6363
expect(result.current.isTyping).toBe(false);
6464
});
6565

66+
it('Should typing whole tag', () => {
67+
const { result, rerender } = renderHook((props: any) => {
68+
const [text, setText] = useState('');
69+
const ref = useRef(0);
70+
const typing = useTyping({
71+
onTyping(post) {
72+
setText(post);
73+
},
74+
});
75+
76+
useEffect(() => {
77+
if (props?.start) {
78+
typing.start();
79+
let p = 0;
80+
typing.push('<step type="1">');
81+
ref.current = window.setInterval(() => {
82+
typing.push(testText[p]);
83+
p++;
84+
if (p >= testText.length) {
85+
typing.close();
86+
window.clearInterval(ref.current);
87+
}
88+
}, 50);
89+
}
90+
91+
return () => {
92+
window.clearInterval(ref.current);
93+
};
94+
}, [props?.start]);
95+
96+
return { text, isTyping: typing.isTyping };
97+
});
98+
99+
// 开启打字机效果
100+
rerender({ start: true });
101+
102+
jest.advanceTimersByTime(50);
103+
expect(result.current.text).toBe('<step type="1">');
104+
});
105+
66106
it('Should waiting for new text', () => {
67107
const { result } = renderHook(() => {
68108
const [text, setText] = useState('');

src/useTyping/index.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,29 @@ export default function useTyping({ onTyping }: { onTyping: (post: string) => vo
3535
typingCountOnTime.current = Math.ceil(remainWordsLength / typingTimes);
3636
}
3737

38+
function getNextChunkPosition() {
39+
const rest = queue.current.slice(beginIndex.current);
40+
const chunk = rest.slice(0, typingCountOnTime.current);
41+
const validHTMLTagRegex = /<[a-zA-Z]{0,4}\s[^<]*>/;
42+
// 确保在 typing 的过程中,HTML 标签不被分割
43+
if (validHTMLTagRegex.test(rest) && !validHTMLTagRegex.test(chunk)) {
44+
const match = rest.match(validHTMLTagRegex)!;
45+
const tag = match[0];
46+
const index = rest.indexOf(tag);
47+
return beginIndex.current + index + tag.length;
48+
}
49+
return beginIndex.current + typingCountOnTime.current;
50+
}
51+
3852
function startTyping() {
3953
if (interval.current) return;
4054
interval.current = window.setInterval(() => {
4155
if (beginIndex.current < queue.current.length) {
4256
const str = queue.current;
43-
onTyping(str.slice(0, beginIndex.current + typingCountOnTime.current));
44-
beginIndex.current += typingCountOnTime.current;
57+
const idx = getNextChunkPosition();
58+
const next = str.slice(0, idx);
59+
onTyping(next);
60+
beginIndex.current = next.length;
4561
} else if (!isStart.current) {
4662
// 如果发送了全部的消息且信号关闭,则清空队列
4763
window.clearInterval(interval.current);

0 commit comments

Comments
 (0)