Skip to content

Commit 0b3a722

Browse files
Merlin218黄锦标
authored andcommitted
feat(AiSearchV2): Supports the new streaming message format and the latest AI-powered search
1 parent 31fae3c commit 0b3a722

File tree

45 files changed

+3141
-170
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+3141
-170
lines changed

frontend/magic-web/src/opensource/components/base/MagicCode/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const MagicCode = memo((props: MagicCodeProps) => {
4949

5050
const { appearance } = useThemeMode()
5151

52-
const { styles } = useStyles()
52+
const { styles, cx } = useStyles()
5353

5454
const { fontSize } = useFontSize()
5555

@@ -81,7 +81,7 @@ const MagicCode = memo((props: MagicCodeProps) => {
8181
<MagicButton
8282
hidden={!isHover || isStreaming}
8383
type="text"
84-
className={styles.copy}
84+
className={cx(styles.copy, "magic-code-copy")}
8585
onClick={setCopied}
8686
size="small"
8787
icon={
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// 打字机队列
2+
export class Typewriter {
3+
private queue: string[] = []
4+
5+
public consuming = false
6+
7+
private timer: NodeJS.Timeout | undefined
8+
9+
constructor(private onConsume: (str: string) => void) {}
10+
11+
// 输出速度动态控制
12+
dynamicSpeed() {
13+
const speed = 2000 / this.queue.length
14+
if (speed > 200) {
15+
return 200
16+
}
17+
return speed
18+
}
19+
20+
// 快速输出模式的速度控制
21+
fastSpeed() {
22+
const speed = 500 / this.queue.length
23+
if (speed > 50) {
24+
return 50
25+
}
26+
return speed < 10 ? 10 : speed // 确保至少有10ms的延迟
27+
}
28+
29+
// 添加字符串到队列
30+
add(str: string) {
31+
if (!str) return
32+
this.queue.push(...str.split(""))
33+
}
34+
35+
// 消费
36+
consume() {
37+
if (this.queue.length > 0) {
38+
const str = this.queue.shift()
39+
if (str) {
40+
this.onConsume(str)
41+
}
42+
}
43+
}
44+
45+
// 消费下一个
46+
next(fast = false) {
47+
this.consume()
48+
if (this.queue.length === 0) {
49+
this.consuming = false
50+
clearTimeout(this.timer)
51+
return
52+
}
53+
// 根据队列中字符的数量来设置消耗每一帧的速度,用定时器消耗
54+
this.timer = setTimeout(
55+
() => {
56+
this.consume()
57+
if (this.consuming) {
58+
this.next(fast)
59+
}
60+
},
61+
fast ? this.fastSpeed() : this.dynamicSpeed(),
62+
)
63+
}
64+
65+
// 开始消费队列
66+
start() {
67+
this.consuming = true
68+
this.next()
69+
}
70+
71+
// 结束消费队列
72+
done() {
73+
this.consuming = true
74+
clearTimeout(this.timer)
75+
// 不再一次性消费,而是以更快的速度逐个消费
76+
this.next(true)
77+
}
78+
79+
// 暂停
80+
stop() {
81+
this.consuming = false
82+
clearTimeout(this.timer)
83+
}
84+
85+
// 恢复
86+
resume() {
87+
this.consuming = true
88+
this.next()
89+
}
90+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useRef, useState } from "react"
2+
import { useMemoizedFn } from "ahooks"
3+
import { createStyles } from "antd-style"
4+
import { Typewriter } from "./TypeWriter"
5+
6+
const useStyles = createStyles(({ css }) => {
7+
return {
8+
typing: css`
9+
white-space: nowrap;
10+
border-right: 2px solid transparent;
11+
line-height: 10px;
12+
animation: typing 3s steps(15, end), blink-caret 0.5s step-end infinite;
13+
overflow: hidden;
14+
`,
15+
}
16+
})
17+
18+
export function useTyping(init: string = "") {
19+
const [content, setContent] = useState(init)
20+
21+
const { styles } = useStyles()
22+
23+
const writer = useRef(new Typewriter((str) => setContent((last) => last + str)))
24+
25+
const cursor = writer.current.consuming ? <span className={styles.typing} /> : null
26+
27+
return {
28+
add: useMemoizedFn(writer.current.add.bind(writer.current)),
29+
start: useMemoizedFn(writer.current.start.bind(writer.current)),
30+
stop: useMemoizedFn(writer.current.stop.bind(writer.current)),
31+
resume: useMemoizedFn(writer.current.resume.bind(writer.current)),
32+
done: useMemoizedFn(writer.current.done.bind(writer.current)),
33+
typing: writer.current.consuming,
34+
cursor,
35+
content,
36+
}
37+
}

frontend/magic-web/src/opensource/pages/chatNew/components/ChatMessageList/components/MessageFactory/components/AiSearch/components/EventTable.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ const EventTable = memo(({ events }: { events: AggregateAISearchCardEvent[] }) =
7575
columns={eventTableColumns}
7676
pagination={false}
7777
className={styles.eventTable}
78+
scroll={{ x: 600 }}
7879
/>
7980
)
8081
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { createStyles } from "antd-style"
2+
import { memo } from "react"
3+
4+
const useStyles = createStyles(({ css }) => {
5+
return {
6+
circleContaier: css`
7+
position: relative;
8+
width: 14px;
9+
height: 14px;
10+
`,
11+
circle: css`
12+
position: absolute;
13+
top: 50%;
14+
left: 50%;
15+
transform: translate(-50%, -50%);
16+
width: 8px;
17+
height: 8px;
18+
border-radius: 50%;
19+
background-color: #315cec;
20+
21+
@keyframes expand {
22+
from {
23+
opacity: 1;
24+
transform: translate(-50%, -50%) scale(0);
25+
}
26+
27+
to {
28+
opacity: 0;
29+
transform: translate(-50%, -50%) scale(3);
30+
}
31+
}
32+
33+
&::after {
34+
content: "";
35+
position: absolute;
36+
top: 50%;
37+
left: 50%;
38+
transform: translate(-50%, -50%);
39+
width: 100%;
40+
height: 100%;
41+
border-radius: 50%;
42+
background-color: #315cec90;
43+
animation: expand 2s ease-out infinite;
44+
}
45+
`,
46+
}
47+
})
48+
49+
const DotSpreading = memo(({ style }: { style?: React.CSSProperties }) => {
50+
const { styles } = useStyles()
51+
return (
52+
<div className={styles.circleContaier} style={style}>
53+
<div className={styles.circle} />
54+
</div>
55+
)
56+
})
57+
58+
export default DotSpreading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { AggregateAISearchCardEvent } from "@/types/chat/conversation_message"
2+
import { Table } from "antd"
3+
import { memo, useMemo } from "react"
4+
import { useTranslation } from "react-i18next"
5+
import { createStyles } from "antd-style"
6+
import MagicMarkdown from "@/opensource/pages/chatNew/components/ChatMessageList/components/MessageFactory/components/Markdown/EnhanceMarkdown"
7+
8+
const useStyles = createStyles(({ css, token }) => ({
9+
eventTable: css`
10+
border: 1px solid ${token.colorBorder};
11+
border-radius: 8px;
12+
overflow: hidden;
13+
14+
th[class*="magic-table-cell"] {
15+
background: ${token.magicColorScales.grey[0]} !important;
16+
}
17+
18+
td[class*="magic-table-cell"]:not(:last-child),
19+
th[class*="magic-table-cell"]:not(:last-child) {
20+
border-right: 1px solid ${token.colorBorder};
21+
}
22+
23+
th[class*="magic-table-cell"] {
24+
color: ${token.colorTextSecondary};
25+
font-size: 12px;
26+
font-weight: 600;
27+
line-height: 16px;
28+
}
29+
30+
td[class*="magic-table-cell"] {
31+
color: ${token.colorTextSecondary};
32+
font-size: 12px;
33+
font-weight: 400;
34+
line-height: 16px;
35+
}
36+
37+
tr:last-child {
38+
td[class*="magic-table-cell"] {
39+
border-bottom: none;
40+
}
41+
}
42+
`,
43+
}))
44+
45+
const EventTable = memo(({ events = [] }: { events?: AggregateAISearchCardEvent[] }) => {
46+
const { t } = useTranslation("interface")
47+
const { styles } = useStyles()
48+
49+
/** 事件表格列 */
50+
const eventTableColumns = useMemo(() => {
51+
return [
52+
{
53+
title: t("chat.aggregate_ai_search_card.eventName"),
54+
dataIndex: "name",
55+
minWidth: 100,
56+
render: (_: any, record: AggregateAISearchCardEvent) => {
57+
return <MagicMarkdown content={record.name} />
58+
},
59+
},
60+
{
61+
title: t("chat.aggregate_ai_search_card.eventTime"),
62+
dataIndex: "time",
63+
minWidth: 100,
64+
},
65+
{
66+
title: t("chat.aggregate_ai_search_card.eventDescription"),
67+
dataIndex: "description",
68+
},
69+
]
70+
}, [t])
71+
72+
return (
73+
<Table
74+
dataSource={events}
75+
columns={eventTableColumns}
76+
pagination={false}
77+
className={styles.eventTable}
78+
scroll={{ x: 600 }}
79+
/>
80+
)
81+
})
82+
83+
export default EventTable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import MagicMarkmap from "@/opensource/components/base/MagicMarkmap"
2+
import MagicMindMap from "@/opensource/components/base/MagicMindmap"
3+
import type { AggregateAISearchCardMindMapChildren } from "@/types/chat/conversation_message"
4+
import { Flex } from "antd"
5+
import { memo } from "react"
6+
import useStyles from "../styles"
7+
8+
interface MindMapProps {
9+
data?: AggregateAISearchCardMindMapChildren | string | null
10+
pptData?: string | null
11+
className?: string
12+
}
13+
14+
/**
15+
* 检查内容是否包含一级标题及以上
16+
* @param content 内容
17+
* @returns 是否包含一级标题及以上
18+
*/
19+
const checkMarkmapContent = (content: string) => {
20+
// 只有包含一级标题及以上才认为true,使用正则匹配
21+
return /^(#+)\s/.test(content)
22+
}
23+
24+
const MindMap = memo(({ data, pptData, className }: MindMapProps) => {
25+
const { styles, cx } = useStyles()
26+
27+
if (typeof data === "string") {
28+
if (!data || !checkMarkmapContent(data)) return null
29+
30+
return (
31+
<Flex vertical className={styles.mindmap}>
32+
<MagicMarkmap data={data} pptData={pptData} />
33+
</Flex>
34+
)
35+
}
36+
return (
37+
<Flex vertical className={cx(styles.mindmap, className)}>
38+
<MagicMindMap data-testid="mindmap" data={data} />
39+
</Flex>
40+
)
41+
})
42+
43+
export default MindMap

0 commit comments

Comments
 (0)