mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat(web): add specialized UI for all built-in tool calls
Replace generic JSON display with dedicated components for search_memory, search_inbox, web_fetch, send, react, get_contacts, email tools, browser tools, subagent tools, and use_skill.
This commit is contained in:
@@ -145,6 +145,13 @@
|
|||||||
"toolExecExit": "exit: {code}",
|
"toolExecExit": "exit: {code}",
|
||||||
"toolExecError": "Error",
|
"toolExecError": "Error",
|
||||||
"toolScheduleItems": "{count} items",
|
"toolScheduleItems": "{count} items",
|
||||||
|
"toolMemoryResults": "{count} memories",
|
||||||
|
"toolInboxResults": "{count} messages",
|
||||||
|
"toolWebFetchPreview": "Preview",
|
||||||
|
"toolContactsCount": "{count} contacts",
|
||||||
|
"toolEmailCount": "{count} emails",
|
||||||
|
"toolEmailAccounts": "{count} accounts",
|
||||||
|
"toolSubagentCount": "{count} subagents",
|
||||||
"unknownUser": "{platform} User",
|
"unknownUser": "{platform} User",
|
||||||
"files": "Files"
|
"files": "Files"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -141,6 +141,13 @@
|
|||||||
"toolExecExit": "退出: {code}",
|
"toolExecExit": "退出: {code}",
|
||||||
"toolExecError": "错误",
|
"toolExecError": "错误",
|
||||||
"toolScheduleItems": "{count} 条",
|
"toolScheduleItems": "{count} 条",
|
||||||
|
"toolMemoryResults": "{count} 条记忆",
|
||||||
|
"toolInboxResults": "{count} 条消息",
|
||||||
|
"toolWebFetchPreview": "预览",
|
||||||
|
"toolContactsCount": "{count} 个联系人",
|
||||||
|
"toolEmailCount": "{count} 封邮件",
|
||||||
|
"toolEmailAccounts": "{count} 个账户",
|
||||||
|
"toolSubagentCount": "{count} 个子代理",
|
||||||
"unknownUser": "{platform}用户",
|
"unknownUser": "{platform}用户",
|
||||||
"files": "文件管理"
|
"files": "文件管理"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,6 +27,42 @@
|
|||||||
v-else-if="scheduleTools.has(block.toolName)"
|
v-else-if="scheduleTools.has(block.toolName)"
|
||||||
:block="block"
|
:block="block"
|
||||||
/>
|
/>
|
||||||
|
<ToolCallMemory
|
||||||
|
v-else-if="block.toolName === 'search_memory'"
|
||||||
|
:block="block"
|
||||||
|
/>
|
||||||
|
<ToolCallInbox
|
||||||
|
v-else-if="block.toolName === 'search_inbox'"
|
||||||
|
:block="block"
|
||||||
|
/>
|
||||||
|
<ToolCallWebFetch
|
||||||
|
v-else-if="block.toolName === 'web_fetch'"
|
||||||
|
:block="block"
|
||||||
|
/>
|
||||||
|
<ToolCallMessage
|
||||||
|
v-else-if="messageTools.has(block.toolName)"
|
||||||
|
:block="block"
|
||||||
|
/>
|
||||||
|
<ToolCallContacts
|
||||||
|
v-else-if="block.toolName === 'get_contacts'"
|
||||||
|
:block="block"
|
||||||
|
/>
|
||||||
|
<ToolCallEmail
|
||||||
|
v-else-if="emailTools.has(block.toolName)"
|
||||||
|
:block="block"
|
||||||
|
/>
|
||||||
|
<ToolCallBrowser
|
||||||
|
v-else-if="browserTools.has(block.toolName)"
|
||||||
|
:block="block"
|
||||||
|
/>
|
||||||
|
<ToolCallSubagent
|
||||||
|
v-else-if="subagentTools.has(block.toolName)"
|
||||||
|
:block="block"
|
||||||
|
/>
|
||||||
|
<ToolCallSkill
|
||||||
|
v-else-if="block.toolName === 'use_skill'"
|
||||||
|
:block="block"
|
||||||
|
/>
|
||||||
<ToolCallGeneric
|
<ToolCallGeneric
|
||||||
v-else
|
v-else
|
||||||
:block="block"
|
:block="block"
|
||||||
@@ -42,6 +78,15 @@ import ToolCallList from './tool-call-list.vue'
|
|||||||
import ToolCallExec from './tool-call-exec.vue'
|
import ToolCallExec from './tool-call-exec.vue'
|
||||||
import ToolCallWebSearch from './tool-call-web-search.vue'
|
import ToolCallWebSearch from './tool-call-web-search.vue'
|
||||||
import ToolCallSchedule from './tool-call-schedule.vue'
|
import ToolCallSchedule from './tool-call-schedule.vue'
|
||||||
|
import ToolCallMemory from './tool-call-memory.vue'
|
||||||
|
import ToolCallInbox from './tool-call-inbox.vue'
|
||||||
|
import ToolCallWebFetch from './tool-call-web-fetch.vue'
|
||||||
|
import ToolCallMessage from './tool-call-message.vue'
|
||||||
|
import ToolCallContacts from './tool-call-contacts.vue'
|
||||||
|
import ToolCallEmail from './tool-call-email.vue'
|
||||||
|
import ToolCallBrowser from './tool-call-browser.vue'
|
||||||
|
import ToolCallSubagent from './tool-call-subagent.vue'
|
||||||
|
import ToolCallSkill from './tool-call-skill.vue'
|
||||||
import ToolCallGeneric from './tool-call-generic.vue'
|
import ToolCallGeneric from './tool-call-generic.vue'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -55,4 +100,21 @@ const scheduleTools = new Set([
|
|||||||
'update_schedule',
|
'update_schedule',
|
||||||
'delete_schedule',
|
'delete_schedule',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const messageTools = new Set(['send', 'react'])
|
||||||
|
|
||||||
|
const emailTools = new Set([
|
||||||
|
'send_email',
|
||||||
|
'list_email',
|
||||||
|
'read_email',
|
||||||
|
'list_email_accounts',
|
||||||
|
])
|
||||||
|
|
||||||
|
const browserTools = new Set(['browser_action', 'browser_observe'])
|
||||||
|
|
||||||
|
const subagentTools = new Set([
|
||||||
|
'query_subagent',
|
||||||
|
'list_subagents',
|
||||||
|
'delete_subagent',
|
||||||
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||||
|
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||||
|
class="size-3"
|
||||||
|
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||||
|
/>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'window-maximize']"
|
||||||
|
class="size-3 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<span class="font-mono font-medium text-xs text-muted-foreground">
|
||||||
|
{{ actionLabel }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="detail"
|
||||||
|
class="text-xs truncate text-foreground"
|
||||||
|
:title="detail"
|
||||||
|
>
|
||||||
|
{{ detail }}
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
v-if="block.done"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolDone') }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolRunning') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Collapsible
|
||||||
|
v-if="block.done && resultText"
|
||||||
|
v-model:open="resultOpen"
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'chevron-right']"
|
||||||
|
class="size-2.5 transition-transform"
|
||||||
|
:class="{ 'rotate-90': resultOpen }"
|
||||||
|
/>
|
||||||
|
{{ $t('chat.toolResult') }}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<pre class="px-3 pb-2 text-xs text-muted-foreground overflow-x-auto whitespace-pre-wrap break-all max-h-40 overflow-y-auto">{{ resultText }}</pre>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memoh/ui'
|
||||||
|
import type { ToolCallBlock } from '@/store/chat-list'
|
||||||
|
|
||||||
|
const props = defineProps<{ block: ToolCallBlock }>()
|
||||||
|
|
||||||
|
const resultOpen = ref(false)
|
||||||
|
|
||||||
|
const input = computed(() => props.block.input as Record<string, unknown> | undefined)
|
||||||
|
|
||||||
|
const actionLabel = computed(() => {
|
||||||
|
if (block.toolName === 'browser_action') {
|
||||||
|
return (input.value?.action as string) ?? 'browser_action'
|
||||||
|
}
|
||||||
|
return (input.value?.observe as string) ?? 'browser_observe'
|
||||||
|
})
|
||||||
|
|
||||||
|
const { block } = props
|
||||||
|
|
||||||
|
const detail = computed(() => {
|
||||||
|
const i = input.value
|
||||||
|
if (!i) return ''
|
||||||
|
return (i.url as string) ?? (i.selector as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
function resolveResult() {
|
||||||
|
if (!props.block.result) return null
|
||||||
|
const result = props.block.result as Record<string, unknown>
|
||||||
|
return (result.structuredContent as Record<string, unknown>) ?? result
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultText = computed(() => {
|
||||||
|
const r = resolveResult()
|
||||||
|
if (!r) return ''
|
||||||
|
// Skip displaying base64 image data
|
||||||
|
if (r.content && Array.isArray(r.content)) {
|
||||||
|
const texts = (r.content as Array<Record<string, unknown>>)
|
||||||
|
.filter((c) => c.type === 'text')
|
||||||
|
.map((c) => c.text as string)
|
||||||
|
if (texts.length) return texts.join('\n')
|
||||||
|
}
|
||||||
|
const { content: _c, ...rest } = r
|
||||||
|
const display = Object.keys(rest).length ? rest : r
|
||||||
|
try {
|
||||||
|
return JSON.stringify(display, null, 2)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return String(r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||||
|
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||||
|
class="size-3"
|
||||||
|
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||||
|
/>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'address-book']"
|
||||||
|
class="size-3 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<span class="font-mono font-medium text-xs text-muted-foreground">
|
||||||
|
get_contacts
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
v-if="block.done && contacts.length"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolContactsCount', { count: contacts.length }) }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else-if="block.done"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolDone') }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolRunning') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Collapsible
|
||||||
|
v-if="block.done && contacts.length"
|
||||||
|
v-model:open="contactsOpen"
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'chevron-right']"
|
||||||
|
class="size-2.5 transition-transform"
|
||||||
|
:class="{ 'rotate-90': contactsOpen }"
|
||||||
|
/>
|
||||||
|
{{ $t('chat.toolSearchResultsLabel') }}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<div class="px-3 pb-2 space-y-1.5">
|
||||||
|
<div
|
||||||
|
v-for="(item, i) in contacts"
|
||||||
|
:key="i"
|
||||||
|
class="flex items-center gap-2 text-xs"
|
||||||
|
>
|
||||||
|
<span class="text-foreground truncate">
|
||||||
|
{{ item.display_name || item.username || item.target }}
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
v-if="item.platform"
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] shrink-0"
|
||||||
|
>
|
||||||
|
{{ item.platform }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memoh/ui'
|
||||||
|
import type { ToolCallBlock } from '@/store/chat-list'
|
||||||
|
|
||||||
|
interface Contact {
|
||||||
|
route_id: string
|
||||||
|
platform: string
|
||||||
|
conversation_type: string
|
||||||
|
target: string
|
||||||
|
display_name: string
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{ block: ToolCallBlock }>()
|
||||||
|
|
||||||
|
const contactsOpen = ref(false)
|
||||||
|
|
||||||
|
const contacts = computed<Contact[]>(() => {
|
||||||
|
if (!props.block.done || !props.block.result) return []
|
||||||
|
const result = props.block.result as Record<string, unknown>
|
||||||
|
const sc = result.structuredContent as Record<string, unknown> | undefined
|
||||||
|
const items = (sc ?? result).contacts as Contact[] | undefined
|
||||||
|
return Array.isArray(items) ? items : []
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||||
|
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||||
|
class="size-3"
|
||||||
|
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||||
|
/>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'envelope']"
|
||||||
|
class="size-3 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- send_email -->
|
||||||
|
<template v-if="block.toolName === 'send_email'">
|
||||||
|
<span class="text-xs truncate text-foreground">
|
||||||
|
<span class="text-muted-foreground">→</span> {{ to }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="subject"
|
||||||
|
class="text-xs truncate text-muted-foreground"
|
||||||
|
:title="subject"
|
||||||
|
>
|
||||||
|
{{ subject }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- read_email -->
|
||||||
|
<template v-else-if="block.toolName === 'read_email'">
|
||||||
|
<span
|
||||||
|
v-if="readSubject"
|
||||||
|
class="text-xs truncate text-foreground"
|
||||||
|
>
|
||||||
|
{{ readSubject }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="font-mono font-medium text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
read_email
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- list_email / list_email_accounts -->
|
||||||
|
<template v-else>
|
||||||
|
<span class="font-mono font-medium text-xs text-muted-foreground">
|
||||||
|
{{ block.toolName }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Badge -->
|
||||||
|
<Badge
|
||||||
|
v-if="block.done && block.toolName === 'list_email' && emailTotal !== null"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolEmailCount', { count: emailTotal }) }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else-if="block.done && block.toolName === 'list_email_accounts' && accountCount !== null"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolEmailAccounts', { count: accountCount }) }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else-if="block.done"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolDone') }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolRunning') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- list_email collapsible -->
|
||||||
|
<Collapsible
|
||||||
|
v-if="block.done && block.toolName === 'list_email' && emails.length"
|
||||||
|
v-model:open="emailsOpen"
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'chevron-right']"
|
||||||
|
class="size-2.5 transition-transform"
|
||||||
|
:class="{ 'rotate-90': emailsOpen }"
|
||||||
|
/>
|
||||||
|
{{ $t('chat.toolSearchResultsLabel') }}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<div class="px-3 pb-2 space-y-1.5">
|
||||||
|
<div
|
||||||
|
v-for="(item, i) in emails"
|
||||||
|
:key="i"
|
||||||
|
class="flex flex-col gap-0.5"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs font-medium text-foreground truncate">{{ item.subject }}</span>
|
||||||
|
<span class="text-[10px] text-muted-foreground shrink-0 ml-auto">{{ item.received_at }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] text-muted-foreground truncate">{{ item.from }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
|
||||||
|
<!-- read_email collapsible -->
|
||||||
|
<Collapsible
|
||||||
|
v-if="block.done && block.toolName === 'read_email' && emailBody"
|
||||||
|
v-model:open="bodyOpen"
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'chevron-right']"
|
||||||
|
class="size-2.5 transition-transform"
|
||||||
|
:class="{ 'rotate-90': bodyOpen }"
|
||||||
|
/>
|
||||||
|
{{ $t('chat.toolWriteContent') }}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<pre class="px-3 pb-2 text-xs text-muted-foreground overflow-x-auto whitespace-pre-wrap break-all max-h-60 overflow-y-auto">{{ emailBody }}</pre>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memoh/ui'
|
||||||
|
import type { ToolCallBlock } from '@/store/chat-list'
|
||||||
|
|
||||||
|
interface EmailItem {
|
||||||
|
uid: number
|
||||||
|
from: string
|
||||||
|
subject: string
|
||||||
|
received_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{ block: ToolCallBlock }>()
|
||||||
|
|
||||||
|
const emailsOpen = ref(false)
|
||||||
|
const bodyOpen = ref(false)
|
||||||
|
|
||||||
|
const to = computed(() => {
|
||||||
|
const input = props.block.input as Record<string, unknown> | undefined
|
||||||
|
return (input?.to as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const subject = computed(() => {
|
||||||
|
const input = props.block.input as Record<string, unknown> | undefined
|
||||||
|
return (input?.subject as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
function resolveResult() {
|
||||||
|
if (!props.block.result) return null
|
||||||
|
const result = props.block.result as Record<string, unknown>
|
||||||
|
return (result.structuredContent as Record<string, unknown>) ?? result
|
||||||
|
}
|
||||||
|
|
||||||
|
const readSubject = computed(() => {
|
||||||
|
const r = resolveResult()
|
||||||
|
return (r?.subject as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const emailTotal = computed(() => {
|
||||||
|
const r = resolveResult()
|
||||||
|
if (!r) return null
|
||||||
|
const total = r.total
|
||||||
|
return typeof total === 'number' ? total : null
|
||||||
|
})
|
||||||
|
|
||||||
|
const accountCount = computed(() => {
|
||||||
|
const r = resolveResult()
|
||||||
|
if (!r) return null
|
||||||
|
const accounts = r.accounts as unknown[] | undefined
|
||||||
|
return Array.isArray(accounts) ? accounts.length : null
|
||||||
|
})
|
||||||
|
|
||||||
|
const emails = computed<EmailItem[]>(() => {
|
||||||
|
const r = resolveResult()
|
||||||
|
if (!r) return []
|
||||||
|
const items = r.emails as EmailItem[] | undefined
|
||||||
|
return Array.isArray(items) ? items : []
|
||||||
|
})
|
||||||
|
|
||||||
|
const emailBody = computed(() => {
|
||||||
|
const r = resolveResult()
|
||||||
|
return (r?.body as string) ?? ''
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||||
|
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||||
|
class="size-3"
|
||||||
|
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||||
|
/>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'inbox']"
|
||||||
|
class="size-3 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="query"
|
||||||
|
class="text-xs truncate text-foreground"
|
||||||
|
>
|
||||||
|
{{ query }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="font-mono font-medium text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
search_inbox
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
v-if="block.done && results.length"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolInboxResults', { count: results.length }) }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else-if="block.done"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolDone') }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolRunning') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Collapsible
|
||||||
|
v-if="block.done && results.length"
|
||||||
|
v-model:open="resultsOpen"
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'chevron-right']"
|
||||||
|
class="size-2.5 transition-transform"
|
||||||
|
:class="{ 'rotate-90': resultsOpen }"
|
||||||
|
/>
|
||||||
|
{{ $t('chat.toolSearchResultsLabel') }}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<div class="px-3 pb-2 space-y-1.5">
|
||||||
|
<div
|
||||||
|
v-for="(item, i) in results"
|
||||||
|
:key="i"
|
||||||
|
class="flex flex-col gap-0.5"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
v-if="item.header"
|
||||||
|
class="text-xs font-medium text-foreground truncate"
|
||||||
|
>
|
||||||
|
{{ item.header }}
|
||||||
|
</span>
|
||||||
|
<span class="text-[10px] text-muted-foreground shrink-0 ml-auto">
|
||||||
|
{{ item.created_at }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-muted-foreground line-clamp-2">
|
||||||
|
{{ item.content }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memoh/ui'
|
||||||
|
import type { ToolCallBlock } from '@/store/chat-list'
|
||||||
|
|
||||||
|
interface InboxResult {
|
||||||
|
id: string
|
||||||
|
source: string
|
||||||
|
header: string
|
||||||
|
content: string
|
||||||
|
is_read: boolean
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{ block: ToolCallBlock }>()
|
||||||
|
|
||||||
|
const resultsOpen = ref(false)
|
||||||
|
|
||||||
|
const query = computed(() => {
|
||||||
|
const input = props.block.input as Record<string, unknown> | undefined
|
||||||
|
return (input?.query as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = computed<InboxResult[]>(() => {
|
||||||
|
if (!props.block.done || !props.block.result) return []
|
||||||
|
const result = props.block.result as Record<string, unknown>
|
||||||
|
const sc = result.structuredContent as Record<string, unknown> | undefined
|
||||||
|
const items = (sc ?? result).results as InboxResult[] | undefined
|
||||||
|
return Array.isArray(items) ? items : []
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||||
|
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||||
|
class="size-3"
|
||||||
|
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||||
|
/>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'brain']"
|
||||||
|
class="size-3 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<span class="text-xs truncate text-foreground">
|
||||||
|
{{ query }}
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
v-if="block.done && results.length"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolMemoryResults', { count: results.length }) }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else-if="block.done"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolDone') }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolRunning') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Collapsible
|
||||||
|
v-if="block.done && results.length"
|
||||||
|
v-model:open="resultsOpen"
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'chevron-right']"
|
||||||
|
class="size-2.5 transition-transform"
|
||||||
|
:class="{ 'rotate-90': resultsOpen }"
|
||||||
|
/>
|
||||||
|
{{ $t('chat.toolSearchResultsLabel') }}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<div class="px-3 pb-2 space-y-1.5">
|
||||||
|
<div
|
||||||
|
v-for="(item, i) in results"
|
||||||
|
:key="i"
|
||||||
|
class="flex items-start gap-2"
|
||||||
|
>
|
||||||
|
<span class="text-xs text-muted-foreground whitespace-pre-wrap break-all flex-1">{{ item.memory }}</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] shrink-0"
|
||||||
|
>
|
||||||
|
{{ item.score.toFixed(2) }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memoh/ui'
|
||||||
|
import type { ToolCallBlock } from '@/store/chat-list'
|
||||||
|
|
||||||
|
interface MemoryResult {
|
||||||
|
id: string
|
||||||
|
memory: string
|
||||||
|
score: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{ block: ToolCallBlock }>()
|
||||||
|
|
||||||
|
const resultsOpen = ref(false)
|
||||||
|
|
||||||
|
const query = computed(() => {
|
||||||
|
const input = props.block.input as Record<string, unknown> | undefined
|
||||||
|
return (input?.query as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = computed<MemoryResult[]>(() => {
|
||||||
|
if (!props.block.done || !props.block.result) return []
|
||||||
|
const result = props.block.result as Record<string, unknown>
|
||||||
|
const sc = result.structuredContent as Record<string, unknown> | undefined
|
||||||
|
const items = (sc ?? result).results as MemoryResult[] | undefined
|
||||||
|
return Array.isArray(items) ? items : []
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||||
|
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||||
|
class="size-3"
|
||||||
|
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- send -->
|
||||||
|
<template v-if="block.toolName === 'send'">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'paper-plane']"
|
||||||
|
class="size-3 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="platform || target"
|
||||||
|
class="text-xs truncate text-foreground"
|
||||||
|
:title="`${platform} → ${target}`"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="platform"
|
||||||
|
class="text-muted-foreground"
|
||||||
|
>{{ platform }}</span>
|
||||||
|
<span v-if="platform && target"> → </span>
|
||||||
|
<span v-if="target">{{ target }}</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="text"
|
||||||
|
class="text-xs truncate text-muted-foreground"
|
||||||
|
:title="text"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- react -->
|
||||||
|
<template v-else>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'face-smile']"
|
||||||
|
class="size-3 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="emoji"
|
||||||
|
class="text-sm"
|
||||||
|
>
|
||||||
|
{{ emoji }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="block.done && action"
|
||||||
|
class="text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
{{ action }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
v-if="block.done"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolDone') }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolRunning') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Badge } from '@memoh/ui'
|
||||||
|
import type { ToolCallBlock } from '@/store/chat-list'
|
||||||
|
|
||||||
|
const props = defineProps<{ block: ToolCallBlock }>()
|
||||||
|
|
||||||
|
const platform = computed(() => {
|
||||||
|
const input = props.block.input as Record<string, unknown> | undefined
|
||||||
|
return (input?.platform as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const target = computed(() => {
|
||||||
|
const input = props.block.input as Record<string, unknown> | undefined
|
||||||
|
return (input?.target as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = computed(() => {
|
||||||
|
const input = props.block.input as Record<string, unknown> | undefined
|
||||||
|
return (input?.text as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const emoji = computed(() => {
|
||||||
|
const input = props.block.input as Record<string, unknown> | undefined
|
||||||
|
return (input?.emoji as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const action = computed(() => {
|
||||||
|
if (!props.block.done || !props.block.result) return ''
|
||||||
|
const result = props.block.result as Record<string, unknown>
|
||||||
|
const sc = result.structuredContent as Record<string, unknown> | undefined
|
||||||
|
return ((sc ?? result).action as string) ?? ''
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||||
|
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||||
|
class="size-3"
|
||||||
|
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||||
|
/>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'wand-magic-sparkles']"
|
||||||
|
class="size-3 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="skillName"
|
||||||
|
class="text-xs truncate text-foreground"
|
||||||
|
>
|
||||||
|
{{ skillName }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="font-mono font-medium text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
use_skill
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
v-if="block.done"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolDone') }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolRunning') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Badge } from '@memoh/ui'
|
||||||
|
import type { ToolCallBlock } from '@/store/chat-list'
|
||||||
|
|
||||||
|
const props = defineProps<{ block: ToolCallBlock }>()
|
||||||
|
|
||||||
|
const skillName = computed(() => {
|
||||||
|
const input = props.block.input as Record<string, unknown> | undefined
|
||||||
|
return (input?.skillName as string) ?? ''
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||||
|
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||||
|
class="size-3"
|
||||||
|
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||||
|
/>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'robot']"
|
||||||
|
class="size-3 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- query_subagent -->
|
||||||
|
<template v-if="block.toolName === 'query_subagent'">
|
||||||
|
<span
|
||||||
|
v-if="name"
|
||||||
|
class="font-mono font-medium text-xs text-foreground"
|
||||||
|
>
|
||||||
|
{{ name }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="query"
|
||||||
|
class="text-xs truncate text-muted-foreground"
|
||||||
|
:title="query"
|
||||||
|
>
|
||||||
|
{{ query }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- list_subagents / delete_subagent -->
|
||||||
|
<template v-else>
|
||||||
|
<span class="font-mono font-medium text-xs text-muted-foreground">
|
||||||
|
{{ block.toolName }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="block.toolName === 'delete_subagent' && deleteId"
|
||||||
|
class="text-xs truncate text-foreground"
|
||||||
|
>
|
||||||
|
{{ deleteId }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
v-if="block.done && block.toolName === 'list_subagents' && subagentCount !== null"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolSubagentCount', { count: subagentCount }) }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else-if="block.done"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolDone') }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolRunning') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- query_subagent result -->
|
||||||
|
<Collapsible
|
||||||
|
v-if="block.done && block.toolName === 'query_subagent' && subagentResult"
|
||||||
|
v-model:open="resultOpen"
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'chevron-right']"
|
||||||
|
class="size-2.5 transition-transform"
|
||||||
|
:class="{ 'rotate-90': resultOpen }"
|
||||||
|
/>
|
||||||
|
{{ $t('chat.toolResult') }}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<pre class="px-3 pb-2 text-xs text-muted-foreground overflow-x-auto whitespace-pre-wrap break-all max-h-40 overflow-y-auto">{{ subagentResult }}</pre>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memoh/ui'
|
||||||
|
import type { ToolCallBlock } from '@/store/chat-list'
|
||||||
|
|
||||||
|
const props = defineProps<{ block: ToolCallBlock }>()
|
||||||
|
|
||||||
|
const resultOpen = ref(false)
|
||||||
|
|
||||||
|
const name = computed(() => {
|
||||||
|
const input = props.block.input as Record<string, unknown> | undefined
|
||||||
|
return (input?.name as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const query = computed(() => {
|
||||||
|
const input = props.block.input as Record<string, unknown> | undefined
|
||||||
|
return (input?.query as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteId = computed(() => {
|
||||||
|
const input = props.block.input as Record<string, unknown> | undefined
|
||||||
|
return (input?.id as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
function resolveResult() {
|
||||||
|
if (!props.block.result) return null
|
||||||
|
const result = props.block.result as Record<string, unknown>
|
||||||
|
return (result.structuredContent as Record<string, unknown>) ?? result
|
||||||
|
}
|
||||||
|
|
||||||
|
const subagentCount = computed(() => {
|
||||||
|
const r = resolveResult()
|
||||||
|
if (!r) return null
|
||||||
|
const items = r.items as unknown[] | undefined
|
||||||
|
return Array.isArray(items) ? items.length : null
|
||||||
|
})
|
||||||
|
|
||||||
|
const subagentResult = computed(() => {
|
||||||
|
const r = resolveResult()
|
||||||
|
return (r?.result as string) ?? ''
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||||
|
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||||
|
class="size-3"
|
||||||
|
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||||
|
/>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'globe']"
|
||||||
|
class="size-3 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
v-if="url"
|
||||||
|
:href="url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-xs truncate text-primary hover:underline"
|
||||||
|
:title="url"
|
||||||
|
>
|
||||||
|
{{ url }}
|
||||||
|
</a>
|
||||||
|
<Badge
|
||||||
|
v-if="block.done && format"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ format }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else-if="block.done"
|
||||||
|
variant="secondary"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolDone') }}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else
|
||||||
|
variant="outline"
|
||||||
|
class="text-[10px] ml-auto shrink-0"
|
||||||
|
>
|
||||||
|
{{ $t('chat.toolRunning') }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Collapsible
|
||||||
|
v-if="block.done && preview"
|
||||||
|
v-model:open="previewOpen"
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="['fas', 'chevron-right']"
|
||||||
|
class="size-2.5 transition-transform"
|
||||||
|
:class="{ 'rotate-90': previewOpen }"
|
||||||
|
/>
|
||||||
|
{{ $t('chat.toolWebFetchPreview') }}
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<div class="px-3 pb-2 space-y-1">
|
||||||
|
<div
|
||||||
|
v-if="title"
|
||||||
|
class="text-xs font-medium text-foreground"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="excerpt"
|
||||||
|
class="text-[10px] text-muted-foreground italic"
|
||||||
|
>
|
||||||
|
{{ excerpt }}
|
||||||
|
</div>
|
||||||
|
<pre
|
||||||
|
v-if="contentPreview"
|
||||||
|
class="text-xs text-muted-foreground overflow-x-auto whitespace-pre-wrap break-all max-h-40 overflow-y-auto"
|
||||||
|
>{{ contentPreview }}</pre>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memoh/ui'
|
||||||
|
import type { ToolCallBlock } from '@/store/chat-list'
|
||||||
|
|
||||||
|
const props = defineProps<{ block: ToolCallBlock }>()
|
||||||
|
|
||||||
|
const previewOpen = ref(false)
|
||||||
|
|
||||||
|
const url = computed(() => {
|
||||||
|
const input = props.block.input as Record<string, unknown> | undefined
|
||||||
|
return (input?.url as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
function resolveResult() {
|
||||||
|
if (!props.block.result) return null
|
||||||
|
const result = props.block.result as Record<string, unknown>
|
||||||
|
return (result.structuredContent as Record<string, unknown>) ?? result
|
||||||
|
}
|
||||||
|
|
||||||
|
const format = computed(() => {
|
||||||
|
const r = resolveResult()
|
||||||
|
return (r?.format as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = computed(() => {
|
||||||
|
const r = resolveResult()
|
||||||
|
return (r?.title as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const excerpt = computed(() => {
|
||||||
|
const r = resolveResult()
|
||||||
|
return (r?.excerpt as string) ?? ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const contentPreview = computed(() => {
|
||||||
|
const r = resolveResult()
|
||||||
|
if (!r) return ''
|
||||||
|
const content = (r.content as string) ?? (r.textContent as string) ?? ''
|
||||||
|
return content.length > 500 ? `${content.slice(0, 500)}…` : content
|
||||||
|
})
|
||||||
|
|
||||||
|
const preview = computed(() => title.value || excerpt.value || contentPreview.value)
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user