Skip to content

Commit d2b2963

Browse files
authored
feat(chatmessage): add codeblock click callback (#332)
* feat(chatmessage): add codeblock click callback * feat(chatengine): toolcallname append
1 parent a687958 commit d2b2963

File tree

9 files changed

+73
-30
lines changed

9 files changed

+73
-30
lines changed

src/chat-engine/adapters/agui/event-mapper.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable class-methods-use-this */
22
import type { AIMessageContent, SSEChunkData, ToolCall } from '../../type';
3-
import { EventType, isStateEvent,isTextMessageEvent, isThinkingEvent, isToolCallEvent } from './events';
3+
import { EventType, isStateEvent, isTextMessageEvent, isThinkingEvent, isToolCallEvent } from './events';
44
import { stateManager } from './state-manager';
55
import {
66
addToReasoningData,
@@ -301,7 +301,8 @@ export class AGUIEventMapper {
301301
parentMessageId: event.parentMessageId || '',
302302
};
303303

304-
const toolCallContent = createToolCallContent(this.toolCallMap[event.toolCallId], 'pending');
304+
// 每个toocallstart都会开始一个新的内容块,使用append(添加新的工具调用,使用不同的渲染组件)
305+
const toolCallContent = createToolCallContent(this.toolCallMap[event.toolCallId], 'pending', 'append');
305306

306307
if (this.reasoningContext.active) {
307308
// Reasoning 模式:添加 toolcall 到 reasoning.data
@@ -312,7 +313,8 @@ export class AGUIEventMapper {
312313
return createReasoningContent(data, 'streaming', 'merge', false);
313314
}
314315
// 独立模式:返回独立的工具调用内容块
315-
return { ...toolCallContent, strategy: 'append' };
316+
// 通过 type (toolcall-${toolCallName}) + strategy 来控制是否合并
317+
return toolCallContent;
316318
}
317319

318320
/**
@@ -408,7 +410,10 @@ export class AGUIEventMapper {
408410
const currentIndex = this.reasoningContext.currentDataIndex;
409411
if (currentIndex >= 0 && this.reasoningContext.currentData[currentIndex]) {
410412
const currentContent = this.reasoningContext.currentData[currentIndex];
411-
if (currentContent.type === 'toolcall') {
413+
const currentType = currentContent.type;
414+
415+
// 检查 type 是否匹配(toolcall-${toolCallName})
416+
if (currentType.startsWith('toolcall')) {
412417
const updatedContent = {
413418
...currentContent,
414419
data: this.toolCallMap[toolCallId],
@@ -427,6 +432,7 @@ export class AGUIEventMapper {
427432
return null;
428433
}
429434
// 独立模式:返回独立的 toolcall 更新
435+
// 通过相同的 type (toolcall-${toolCallName}) 来实现 merge
430436
return createToolCallContent(this.toolCallMap[toolCallId], status, 'merge');
431437
}
432438

src/chat-engine/adapters/agui/utils.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -206,15 +206,17 @@ export function createThinkingContent(
206206
* 创建 toolcall 类型的 AIMessageContent
207207
* @param toolCall 工具调用数据
208208
* @param status 状态
209-
* @param strategy 策略
209+
* @param strategy 策略(可选,会根据 toolCallName 自动判断)
210210
* @returns toolcall 类型的 AIMessageContent
211211
*/
212212
export function createToolCallContent(
213213
toolCall: any,
214214
status: 'pending' | 'streaming' | 'complete' = 'pending',
215215
strategy: 'append' | 'merge' = 'append',
216216
): any {
217-
return createAIMessageContent('toolcall', toolCall, status, strategy);
217+
// 根据 toolCallName 生成唯一的 type
218+
const type = `toolcall-${toolCall.toolCallName}`;
219+
return createAIMessageContent(type, toolCall, status, strategy);
218220
}
219221

220222
/**
@@ -393,14 +395,15 @@ export function convertReasoningMessages(reasoningMessages: any[]): any[] {
393395
if (msg.toolCalls && msg.toolCalls.length > 0) {
394396
msg.toolCalls.forEach((toolCall: any) => {
395397
const toolResult = toolCallMap.get(toolCall.id)?.result || '';
398+
const toolCallData = {
399+
toolCallId: toolCall.id,
400+
toolCallName: toolCall.function.name,
401+
args: toolCall.function.arguments,
402+
result: toolResult,
403+
};
396404
reasoningData.push({
397-
type: 'toolcall',
398-
data: {
399-
toolCallId: toolCall.id,
400-
toolCallName: toolCall.function.name,
401-
args: toolCall.function.arguments,
402-
result: toolResult,
403-
},
405+
type: `toolcall-${toolCall.function.name}`,
406+
data: toolCallData,
404407
status: 'complete',
405408
});
406409
});
@@ -456,14 +459,16 @@ export function processToolCalls(toolCalls: any[], toolCallMap: Map<string, any>
456459
};
457460
}
458461

462+
const toolCallData = {
463+
toolCallId: toolCall.id,
464+
toolCallName: toolCall.function.name,
465+
args: toolCall.function.arguments,
466+
result: toolResult,
467+
};
468+
459469
return {
460-
type: 'toolcall' as const,
461-
data: {
462-
toolCallId: toolCall.id,
463-
toolCallName: toolCall.function.name,
464-
args: toolCall.function.arguments,
465-
result: toolResult,
466-
},
470+
type: `toolcall-${toolCall.function.name}` as const,
471+
data: toolCallData,
467472
};
468473
});
469474
}

src/chat-engine/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -457,15 +457,16 @@ export default class ChatEngine implements IChatEngine {
457457
// const processed = this.messageProcessor.processContentUpdate(lastContent, rawChunk);
458458
// this.messageStore.appendContent(messageId, processed);
459459

460-
let targetIndex;
460+
let targetIndex: number;
461461
// 作为新的内容块追加
462462
if (rawChunk?.strategy === 'append') {
463463
targetIndex = -1;
464464
} else {
465-
// 合并/替换到现有同类型内容中
465+
// merge 策略:按 type 查找最后一个匹配的类型
466+
// 通过 type (如 toolcall-${toolCallName}) 来定位要更新的内容块
466467
targetIndex = message.content.findIndex((content: AIMessageContent) => content.type === rawChunk.type);
467468
if (targetIndex !== -1) {
468-
// 找到最后一个匹配的类型
469+
// 找到最后一个匹配的类型(从后往前查找)
469470
for (let i = message.content.length - 1; i >= 0; i--) {
470471
if (message.content[i].type === rawChunk.type) {
471472
targetIndex = i;

src/chat-engine/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export function isAttachmentContent(content: UserMessageContent): content is Att
115115
}
116116

117117
export function isToolCallContent(content: AIMessageContent): content is ToolCallContent {
118-
return content.type === 'toolcall';
118+
return content.type.startsWith('toolcall');
119119
}
120120

121121
export function isReasoningContent(content) {

src/chat-message/chat-item.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,22 @@ export default class ChatItem extends Component<ChatMessageProps> {
122122

123123
ready(): void {
124124
setExportparts(this);
125+
126+
// 监听代码复制事件
127+
this.addEventListener('code_copy', this.handleCodeCopy as EventListener);
128+
}
129+
130+
uninstall(): void {
131+
// 清理事件监听
132+
this.removeEventListener('code_copy', this.handleCodeCopy as EventListener);
125133
}
126134

135+
private handleCodeCopy = (event: CustomEvent) => {
136+
event.stopPropagation();
137+
const { code, lang } = event.detail;
138+
this.handleClickAction('codeCopy' as TdChatMessageActionName, { code, lang });
139+
};
140+
127141
private renderMessageHeader() {
128142
const { name, datetime } = this.props;
129143
return (

src/chat-message/md/chat-md-code.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,21 @@ export default class ChatMDCode extends Component<TdChatCodeProps> {
6262
}
6363

6464
clickCopyHandler = () => {
65+
const code = this.props['data-code'] || '';
66+
const lang = this.props['data-lang'];
67+
68+
// 派发事件到外层
69+
this.fire(
70+
'code_copy',
71+
{ code, lang },
72+
{
73+
bubbles: true,
74+
composed: true,
75+
},
76+
);
77+
6578
navigator.clipboard
66-
.writeText(this.props['data-code'] || '')
79+
.writeText(code)
6780
.then(() => {
6881
this.msgInstance = MessagePlugin.success('复制成功');
6982
})

src/chat-message/type.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type TdChatContentSuggestionProps = {
2929

3030
export type TdChatMessageVariant = 'base' | 'text' | 'outline';
3131

32-
export type TdChatMessageActionName = TdChatActionsName | 'searchResult' | 'searchItem' | 'suggestion';
32+
export type TdChatMessageActionName = TdChatActionsName | 'searchResult' | 'searchItem' | 'suggestion' | 'codeCopy';
3333
export interface TdChatMessageAction {
3434
name: TdChatMessageActionName;
3535
render: TNode;

src/chatbot/_example/basic.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { TdChatMessageConfigItem } from 'tdesign-web-components/chatbot';
88
import type { TdAttachmentItem } from 'tdesign-web-components/filecard';
99

1010
import Chatbot from '../chat';
11+
import mdContent from '../mock/testMarkdown.md?raw';
1112

1213
// 天气扩展类型定义
1314
declare module '../../chat-engine/type' {
@@ -164,8 +165,8 @@ const mockData: ChatMessagesData[] = [
164165
role: 'assistant',
165166
content: [
166167
{
167-
type: 'text',
168-
data: '出错了',
168+
type: 'markdown',
169+
data: mdContent,
169170
},
170171
],
171172
},
@@ -454,6 +455,9 @@ export default class BasicChat extends Component {
454455
event.stopPropagation();
455456
console.log('searchItem', content);
456457
},
458+
codeCopy: (data) => {
459+
console.log('codeCopy', data);
460+
},
457461
},
458462
chatContentProps: {
459463
search: {

src/range-input/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import './style/index.js';
22

33
import _RangeInput from './RangeInput.jsx';
4-
import _RangeInputPopup from './RangeInputPopup';
4+
// import _RangeInputPopup from './RangeInputPopup';
55

66
export type { RangeInputProps } from './RangeInput.jsx';
77
export * from './type';
88

99
export const RangeInput = _RangeInput;
10-
export const RangeInputPopup = _RangeInputPopup;
10+
// export const RangeInputPopup = _RangeInputPopup;
1111
export default RangeInput;

0 commit comments

Comments
 (0)