2222 @item-click =" handlePromptItemClick"
2323 ></tr-prompts >
2424 </div >
25- <tr-bubble-provider v-else :content-renderers =" contentRenderers" >
26- <tr-bubble-list :items =" messages" :roles =" roles" auto-scroll class =" robot-bubble-list" > </tr-bubble-list >
25+ <tr-bubble-provider v-else :content-renderer-matches =" contentRendererMatches" >
26+ <tr-bubble-list
27+ :messages =" messages"
28+ :role-configs =" roleConfigs"
29+ :content-resolver =" resolveMessageContent"
30+ auto-scroll
31+ class =" robot-bubble-list"
32+ >
33+ <template #content-footer =" { messages } " >
34+ <div v-if =" showAborted && messages[0]?.aborted" class =" aborted" >已中止</div >
35+ </template >
36+ </tr-bubble-list >
2737 </tr-bubble-provider >
2838 </div >
2939
3949 :showWordLimit =" false"
4050 @submit =" handleSendMessage"
4151 @cancel =" handleAbortRequest"
42- :allowFiles =" selectedAttachments.length < 1 && props.allowFiles"
43- uploadTooltip =" 支持上传1张图片"
44- @files-selected =" handleSingleFilesSelected"
4552 >
4653 <template #header v-if =" selectedAttachments .length > 0 " >
4754 <div >
5562 </tr-attachments >
5663 </div >
5764 </template >
58- <template #footer-left >
65+ <template #footer >
5966 <slot name =" footer-left" ></slot >
6067 </template >
68+ <template #footer-right >
69+ <VoiceButton :speech-config =" { lang: ' zh-CN' , continuous: false } " />
70+ <UploadButton
71+ v-if =" selectedAttachments .length < 1 && props .allowFiles "
72+ accept="image/*"
73+ :multiple =" false "
74+ @select =" handleSingleFilesSelected "
75+ />
76+ </template >
6177 </tr-sender >
6278 </div >
6379 </template >
@@ -74,11 +90,17 @@ import {
7490 TrSender ,
7591 TrWelcome ,
7692 TrAttachments ,
93+ UploadButton ,
94+ VoiceButton ,
95+ BubbleRenderers ,
96+ BubbleRendererMatchPriority ,
7797 type BubbleRoleConfig ,
7898 type PromptProps ,
79- type RawFileAttachment
99+ type RawFileAttachment ,
100+ type BubbleContentRendererMatch
80101} from ' @opentiny/tiny-robot'
81- import { type ChatMessage , GeneratingStatus } from ' @opentiny/tiny-robot-kit'
102+ import { type ChatMessage } from ' @opentiny/tiny-robot-kit'
103+ import { GeneratingStatus } from ' ../../constants/status'
82104import { LoadingRenderer , MarkdownRenderer , ImgRenderer } from ' ../renderers'
83105import { useNotify } from ' @opentiny/tiny-engine-meta-register'
84106
@@ -91,10 +113,15 @@ const props = defineProps({
91113 type: Function
92114 },
93115 status: { type: String },
116+ chatMode: { type: String },
94117 allowFiles: {
95118 type: Boolean ,
96119 default: false
97120 },
121+ showAborted: {
122+ type: Boolean ,
123+ default: true
124+ },
98125 bubbleRenderers: {
99126 type: Object as PropType <Record <string , Component >>,
100127 default : () => ({})
@@ -113,6 +140,7 @@ const robotVisible = defineModel<boolean>('show', { required: true })
113140const fullscreen = defineModel <boolean >(' fullscreen' )
114141const inputMessage = defineModel <string >(' input' , { required: true })
115142const messages = defineModel <ChatMessage []>(' messages' , { required: true })
143+ const senderRef = ref <InstanceType <typeof TrSender > | null >(null )
116144
117145watch (
118146 () => props .allowFiles ,
@@ -123,6 +151,71 @@ watch(
123151 }
124152)
125153
154+ const contentRendererMatches = computed <BubbleContentRendererMatch []>(() => [
155+ {
156+ priority: BubbleRendererMatchPriority .LOADING ,
157+ find : (message ) => Boolean (message .loading ),
158+ renderer: LoadingRenderer
159+ },
160+ ... Object .entries (props .bubbleRenderers ).map (([type , renderer ]) => ({
161+ priority: BubbleRendererMatchPriority .NORMAL ,
162+ find : (_message : any , content : any ) => content ?.type === type ,
163+ renderer
164+ })),
165+ {
166+ priority: BubbleRendererMatchPriority .NORMAL ,
167+ find : (message : any , content : any ) => content ?.type === ' tool' && message .tool_calls ?.length ,
168+ renderer: BubbleRenderers .Tools
169+ },
170+ {
171+ priority: BubbleRendererMatchPriority .NORMAL ,
172+ find : (message : any , content : any ) =>
173+ ! message .loading && message .content && (! content ?.type || [' markdown' , ' text' ].includes (content .type )),
174+ renderer: MarkdownRenderer
175+ },
176+ {
177+ priority: BubbleRendererMatchPriority .NORMAL ,
178+ find : (message : any ) => message ?.content ?.[0 ]?.type === ' img' || message ?.content ?.[0 ]?.type === ' image' ,
179+ renderer: ImgRenderer
180+ }
181+ ])
182+
183+ const isAgentMessage = (message : any ) => {
184+ const hasAgentContent = message .renderContent ?.some ((item : any ) => {
185+ return item .type === ' agent-content' || item .type === ' agent-loading'
186+ })
187+ return message .metadata ?.chatMode === ' agent' || hasAgentContent
188+ }
189+
190+ const resolveAgentRenderContent = (message : any ) => {
191+ if (! isAgentMessage (message ) || message .role !== ' assistant' ) {
192+ return message .renderContent
193+ }
194+
195+ const isLastMessage = messages .value .at (- 1 ) === message
196+ const isGenerating = Boolean (message .loading ) || (isLastMessage && GeneratingStatus .includes (props .status as any ))
197+ const renderContent = isGenerating
198+ ? message .renderContent
199+ : message .renderContent .filter ((item : any ) => item .type !== ' agent-loading' )
200+ const agentContents = renderContent .filter ((item : any ) => item .type === ' agent-content' )
201+ const finalStatus = agentContents .findLast ((item : any ) => [' success' , ' failed' , ' fix' ].includes (item .status ))?.status
202+
203+ return renderContent .map ((item : any ) => {
204+ if (item .type !== ' agent-content' || isGenerating ) {
205+ return item
206+ }
207+
208+ if (! item .status || item .status === ' loading' ) {
209+ return {
210+ ... item ,
211+ status: finalStatus || message .metadata ?.agentStatus || ' failed'
212+ }
213+ }
214+
215+ return item
216+ })
217+ }
218+
126219// 处理文件选择事件
127220const handleSingleFilesSelected = (files : File [] | null , retry = false ) => {
128221 if (! files ?.length ) return
@@ -178,32 +271,40 @@ const getSvgIcon = (name: string, style?: CSSProperties) => {
178271const aiAvatar = getSvgIcon (' AI' )
179272const welcomeIcon = getSvgIcon (' AI' , { fontSize: ' 44px' })
180273
181- const contentRenderers = computed (() => ({
182- markdown: MarkdownRenderer ,
183- loading: LoadingRenderer ,
184- img: ImgRenderer ,
185- ... props .bubbleRenderers
186- }))
274+ const resolveMessageContent = (message : any ) => {
275+ if (Array .isArray (message .renderContent ) && message .renderContent .length > 0 ) {
276+ return resolveAgentRenderContent (message )
277+ }
187278
188- const roles: Record <string , BubbleRoleConfig > = {
279+ if (isAgentMessage (message ) && message .role === ' assistant' && message .content ) {
280+ const agentStatus = [' success' , ' failed' , ' fix' ].includes (message .metadata ?.agentStatus )
281+ ? message .metadata .agentStatus
282+ : ' failed'
283+ return [
284+ {
285+ type: ' agent-content' ,
286+ status: agentStatus ,
287+ content: message .content
288+ }
289+ ]
290+ }
291+
292+ return message .content
293+ }
294+
295+ const roleConfigs: Record <string , BubbleRoleConfig > = {
189296 assistant: {
190297 placement: ' start' ,
191- avatar: aiAvatar ,
192- contentRenderer: MarkdownRenderer ,
193- customContentField: ' renderContent'
298+ avatar: aiAvatar
194299 },
195300 user: {
196- placement: ' end' ,
197- contentRenderer: MarkdownRenderer ,
198- customContentField: ' renderContent'
301+ placement: ' end'
199302 },
200303 system: {
201304 hidden: true
202305 }
203306}
204307
205- const senderRef = ref <InstanceType <typeof TrSender > | null >(null )
206-
207308// 发送消息
208309const handleSendMessage = async (content : string ) => {
209310 const messageContent = content || inputMessage .value
@@ -256,6 +357,7 @@ const handleSendMessage = async (content: string) => {
256357 }
257358 messages .value .push (userMessage )
258359 inputMessage .value = ' '
360+ senderRef .value ?.clear ()
259361 selectedAttachments .value = []
260362 emit (' sendMessage' )
261363}
@@ -371,13 +473,13 @@ const handlePromptItemClick = (ev: unknown, item: { description?: string }) => {
371473 }
372474 }
373475 :deep([data- role= ' user' ]) {
374- --tr-bubble-content -bg : var (--tr-color-primary-light );
476+ --tr-bubble-box -bg : var (--tr-color-primary-light );
375477 }
376478 }
377479
378480 & .fullscreen {
379481 :deep([data- role= ' assistant' ]) {
380- --tr-bubble-content -bg : transparent ;
482+ --tr-bubble-box -bg : transparent ;
381483 .tr-bubble__content {
382484 padding : 8px 0 0 ;
383485 }
@@ -491,4 +593,10 @@ const handlePromptItemClick = (ev: unknown, item: { description?: string }) => {
491593.robot-bubble-list {
492594 height : 100% ;
493595}
596+
597+ .aborted {
598+ margin-top : 6px ;
599+ font-size : 12px ;
600+ opacity : 0.7 ;
601+ }
494602 </style >
0 commit comments