mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat(web): add sunagent, history, skills page
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -308,6 +308,7 @@
|
||||
"mcp": "MCP",
|
||||
"subagents": "Subagents",
|
||||
"history": "History",
|
||||
"skills": "Skills",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"container": {
|
||||
@@ -442,6 +443,64 @@
|
||||
"noConversations": "No history conversations found",
|
||||
"idCopied": "ID copied to clipboard",
|
||||
"deleteConfirm": "Are you sure you want to delete this memory? This cannot be undone."
|
||||
},
|
||||
"skills": {
|
||||
"title": "Skills",
|
||||
"addSkill": "New Skill",
|
||||
"emptyTitle": "No Skills",
|
||||
"emptyDescription": "Click above to create a new skill",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Enter skill description",
|
||||
"content": "Content",
|
||||
"contentPlaceholder": "Enter skill content/prompt",
|
||||
"deleteConfirm": "Are you sure you want to delete this skill?",
|
||||
"deleteSuccess": "Skill deleted",
|
||||
"deleteFailed": "Failed to delete skill",
|
||||
"saveSuccess": "Skill saved",
|
||||
"saveFailed": "Failed to save skill",
|
||||
"loadFailed": "Failed to load skills"
|
||||
},
|
||||
"history": {
|
||||
"title": "History",
|
||||
"loadFailed": "Failed to load history",
|
||||
"empty": "No history messages",
|
||||
"loadMore": "Load More",
|
||||
"messageCount": "{count} messages",
|
||||
"selectAll": "Select all",
|
||||
"deleteSelectedConfirm": "Are you sure you want to delete the selected {count} message(s)? This will clear all history and cannot be undone.",
|
||||
"deleteSuccess": "Messages deleted",
|
||||
"deleteFailed": "Failed to delete messages",
|
||||
"role": {
|
||||
"user": "User",
|
||||
"assistant": "Assistant",
|
||||
"tool": "Tool",
|
||||
"system": "System"
|
||||
},
|
||||
"filter": "Filter by role",
|
||||
"filterAll": "All",
|
||||
"expandContent": "Expand",
|
||||
"collapseContent": "Collapse"
|
||||
},
|
||||
"subagents": {
|
||||
"title": "Subagents",
|
||||
"subtitle": "Manage autonomous subagents for this bot.",
|
||||
"add": "Create Subagent",
|
||||
"emptyTitle": "No Subagents",
|
||||
"emptyDescription": "Click above to create a subagent",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Enter subagent description",
|
||||
"deleteConfirm": "Are you sure you want to delete this subagent?",
|
||||
"deleteSuccess": "Subagent deleted",
|
||||
"deleteFailed": "Failed to delete subagent",
|
||||
"saveSuccess": "Subagent saved",
|
||||
"saveFailed": "Failed to save subagent",
|
||||
"loadFailed": "Failed to load subagents",
|
||||
"viewContext": "View Context",
|
||||
"contextTitle": "Subagent Context",
|
||||
"contextEmpty": "No context messages",
|
||||
"messagesCount": "{count} messages",
|
||||
"usage": "Usage",
|
||||
"skills": "Skills"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +304,7 @@
|
||||
"mcp": "MCP",
|
||||
"subagents": "子智能体",
|
||||
"history": "对话历史",
|
||||
"skills": "技能",
|
||||
"settings": "设置"
|
||||
},
|
||||
"container": {
|
||||
@@ -438,6 +439,64 @@
|
||||
"noConversations": "暂无历史对话",
|
||||
"idCopied": "ID 已复制",
|
||||
"deleteConfirm": "确定要删除这条记忆吗?此操作无法撤销。"
|
||||
},
|
||||
"skills": {
|
||||
"title": "技能",
|
||||
"addSkill": "新建技能",
|
||||
"emptyTitle": "暂无技能",
|
||||
"emptyDescription": "点击上方按钮创建新技能",
|
||||
"description": "描述",
|
||||
"descriptionPlaceholder": "输入技能描述",
|
||||
"content": "内容",
|
||||
"contentPlaceholder": "输入技能内容/提示词",
|
||||
"deleteConfirm": "确定要删除这个技能吗?",
|
||||
"deleteSuccess": "技能已删除",
|
||||
"deleteFailed": "删除技能失败",
|
||||
"saveSuccess": "技能已保存",
|
||||
"saveFailed": "保存技能失败",
|
||||
"loadFailed": "加载技能失败"
|
||||
},
|
||||
"history": {
|
||||
"title": "对话历史",
|
||||
"loadFailed": "加载历史消息失败",
|
||||
"empty": "暂无历史消息",
|
||||
"loadMore": "加载更多",
|
||||
"messageCount": "{count} 条消息",
|
||||
"selectAll": "全选",
|
||||
"deleteSelectedConfirm": "确定要删除选中的 {count} 条消息吗?此操作将清空所有历史记录,且无法撤销。",
|
||||
"deleteSuccess": "消息已删除",
|
||||
"deleteFailed": "删除消息失败",
|
||||
"role": {
|
||||
"user": "用户",
|
||||
"assistant": "助手",
|
||||
"tool": "工具",
|
||||
"system": "系统"
|
||||
},
|
||||
"filter": "按角色筛选",
|
||||
"filterAll": "全部",
|
||||
"expandContent": "展开",
|
||||
"collapseContent": "收起"
|
||||
},
|
||||
"subagents": {
|
||||
"title": "子智能体",
|
||||
"subtitle": "管理当前 Bot 的自治子智能体。",
|
||||
"add": "创建子智能体",
|
||||
"emptyTitle": "暂无子智能体",
|
||||
"emptyDescription": "点击上方按钮创建子智能体",
|
||||
"description": "描述",
|
||||
"descriptionPlaceholder": "输入子智能体描述",
|
||||
"deleteConfirm": "确定要删除这个子智能体吗?",
|
||||
"deleteSuccess": "子智能体已删除",
|
||||
"deleteFailed": "删除子智能体失败",
|
||||
"saveSuccess": "子智能体已保存",
|
||||
"saveFailed": "保存子智能体失败",
|
||||
"loadFailed": "加载子智能体失败",
|
||||
"viewContext": "查看上下文",
|
||||
"contextTitle": "子智能体上下文",
|
||||
"contextEmpty": "暂无上下文消息",
|
||||
"messagesCount": "{count} 条消息",
|
||||
"usage": "用量",
|
||||
"skills": "技能"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-lg font-medium">
|
||||
{{ $t('bots.history.title') }}
|
||||
</h3>
|
||||
<Badge
|
||||
v-if="messages.length"
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ $t('bots.history.messageCount', { count: messages.length }) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<NativeSelect
|
||||
v-model="roleFilter"
|
||||
class="h-9 w-32 text-sm"
|
||||
>
|
||||
<option value="">
|
||||
{{ $t('bots.history.filterAll') }}
|
||||
</option>
|
||||
<option value="user">
|
||||
{{ $t('bots.history.role.user') }}
|
||||
</option>
|
||||
<option value="assistant">
|
||||
{{ $t('bots.history.role.assistant') }}
|
||||
</option>
|
||||
<option value="tool">
|
||||
{{ $t('bots.history.role.tool') }}
|
||||
</option>
|
||||
<option value="system">
|
||||
{{ $t('bots.history.role.system') }}
|
||||
</option>
|
||||
</NativeSelect>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="isLoading"
|
||||
@click="handleRefresh"
|
||||
>
|
||||
<Spinner
|
||||
v-if="isLoading"
|
||||
class="mr-2 size-4"
|
||||
/>
|
||||
{{ $t('common.refresh') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div
|
||||
v-if="isLoading && messages.length === 0"
|
||||
class="flex items-center justify-center py-8 text-sm text-muted-foreground"
|
||||
>
|
||||
<Spinner class="mr-2" />
|
||||
{{ $t('common.loading') }}
|
||||
</div>
|
||||
|
||||
<!-- Empty -->
|
||||
<div
|
||||
v-else-if="!isLoading && messages.length === 0"
|
||||
class="flex flex-col items-center justify-center py-12 text-center"
|
||||
>
|
||||
<div class="rounded-full bg-muted p-3 mb-4">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'message']"
|
||||
class="size-6 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ $t('bots.history.empty') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<template v-else>
|
||||
<MessageList :messages="pagedMessages" />
|
||||
|
||||
<!-- Pagination -->
|
||||
<div
|
||||
v-if="totalPages > 1"
|
||||
class="flex items-center justify-between pt-4"
|
||||
>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{{ paginationSummary }}
|
||||
</span>
|
||||
<Pagination
|
||||
:total="filteredMessages.length"
|
||||
:items-per-page="PAGE_SIZE"
|
||||
:sibling-count="1"
|
||||
:page="currentPage"
|
||||
show-edges
|
||||
@update:page="currentPage = $event"
|
||||
>
|
||||
<PaginationContent v-slot="{ items }">
|
||||
<PaginationFirst />
|
||||
<PaginationPrevious />
|
||||
<template
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
>
|
||||
<PaginationEllipsis
|
||||
v-if="item.type === 'ellipsis'"
|
||||
:index="index"
|
||||
/>
|
||||
<PaginationItem
|
||||
v-else
|
||||
:value="item.value"
|
||||
/>
|
||||
</template>
|
||||
<PaginationNext />
|
||||
<PaginationLast />
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toast } from 'vue-sonner'
|
||||
import {
|
||||
Button, Badge, Spinner, NativeSelect,
|
||||
Pagination, PaginationContent, PaginationEllipsis,
|
||||
PaginationFirst, PaginationItem, PaginationLast,
|
||||
PaginationNext, PaginationPrevious,
|
||||
} from '@memoh/ui'
|
||||
import MessageList from './message-list.vue'
|
||||
import {
|
||||
getBotsByBotIdMessages,
|
||||
} from '@memoh/sdk'
|
||||
import type { MessageMessage } from '@memoh/sdk'
|
||||
import { resolveApiErrorMessage } from '@/utils/api-error'
|
||||
|
||||
const props = defineProps<{
|
||||
botId: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const isLoading = ref(false)
|
||||
const messages = ref<MessageMessage[]>([])
|
||||
const roleFilter = ref('')
|
||||
const currentPage = ref(1)
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
const filteredMessages = computed(() => {
|
||||
if (!roleFilter.value) return messages.value
|
||||
return messages.value.filter(m => m.role === roleFilter.value)
|
||||
})
|
||||
|
||||
const totalPages = computed(() => Math.ceil(filteredMessages.value.length / PAGE_SIZE))
|
||||
|
||||
const pagedMessages = computed(() => {
|
||||
const start = (currentPage.value - 1) * PAGE_SIZE
|
||||
return filteredMessages.value.slice(start, start + PAGE_SIZE)
|
||||
})
|
||||
|
||||
const paginationSummary = computed(() => {
|
||||
const total = filteredMessages.value.length
|
||||
if (total === 0) return ''
|
||||
const start = (currentPage.value - 1) * PAGE_SIZE + 1
|
||||
const end = Math.min(currentPage.value * PAGE_SIZE, total)
|
||||
return `${start}-${end} / ${total}`
|
||||
})
|
||||
|
||||
watch(roleFilter, () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
|
||||
async function fetchAllHistory() {
|
||||
if (!props.botId) return
|
||||
isLoading.value = true
|
||||
messages.value = []
|
||||
|
||||
try {
|
||||
let before: string | undefined
|
||||
let hasMore = true
|
||||
|
||||
while (hasMore) {
|
||||
const { data } = await getBotsByBotIdMessages({
|
||||
path: { bot_id: props.botId },
|
||||
query: { limit: 100, before },
|
||||
throwOnError: true,
|
||||
})
|
||||
const items = data?.items || []
|
||||
if (items.length === 0) {
|
||||
hasMore = false
|
||||
} else {
|
||||
messages.value.push(...items)
|
||||
before = items[items.length - 1]?.created_at
|
||||
hasMore = items.length >= 100
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(resolveApiErrorMessage(error, t('bots.history.loadFailed')))
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRefresh() {
|
||||
currentPage.value = 1
|
||||
await fetchAllHistory()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchAllHistory()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium">
|
||||
{{ $t('bots.skills.title') }}
|
||||
</h3>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
@click="handleCreate"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'plus']"
|
||||
class="mr-2"
|
||||
/>
|
||||
{{ $t('bots.skills.addSkill') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="flex items-center justify-center py-8 text-sm text-muted-foreground"
|
||||
>
|
||||
<Spinner class="mr-2" />
|
||||
{{ $t('common.loading') }}
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
v-else-if="!skills.length"
|
||||
class="flex flex-col items-center justify-center py-12 text-center"
|
||||
>
|
||||
<div class="rounded-full bg-muted p-3 mb-4">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'bolt']"
|
||||
class="size-6 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium">
|
||||
{{ $t('bots.skills.emptyTitle') }}
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
{{ $t('bots.skills.emptyDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Skills Grid -->
|
||||
<div
|
||||
v-else
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||
>
|
||||
<Card
|
||||
v-for="skill in skills"
|
||||
:key="skill.name"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<CardTitle
|
||||
class="text-base truncate"
|
||||
:title="skill.name"
|
||||
>
|
||||
{{ skill.name }}
|
||||
</CardTitle>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="size-8 p-0"
|
||||
:title="$t('common.edit')"
|
||||
@click="handleEdit(skill)"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'pen-to-square']"
|
||||
class="size-3.5"
|
||||
/>
|
||||
</Button>
|
||||
<ConfirmPopover
|
||||
:message="$t('bots.skills.deleteConfirm')"
|
||||
:loading="isDeleting && deletingName === skill.name"
|
||||
@confirm="handleDelete(skill.name)"
|
||||
>
|
||||
<template #trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="size-8 p-0 text-destructive hover:text-destructive"
|
||||
:disabled="isDeleting"
|
||||
:title="$t('common.delete')"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'trash']"
|
||||
class="size-3.5"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
</ConfirmPopover>
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription
|
||||
class="line-clamp-2"
|
||||
:title="skill.description"
|
||||
>
|
||||
{{ skill.description || '-' }}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="pb-4 grow">
|
||||
<div class="rounded-md bg-muted p-2 text-xs font-mono text-muted-foreground line-clamp-4 break-all">
|
||||
{{ skill.content }}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Edit Dialog -->
|
||||
<Dialog v-model:open="isDialogOpen">
|
||||
<DialogContent class="sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ isEditing ? $t('common.edit') : $t('bots.skills.addSkill') }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4 py-4">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ $t('common.name') }}</Label>
|
||||
<Input
|
||||
v-model="draftSkill.name"
|
||||
:placeholder="$t('common.namePlaceholder')"
|
||||
:disabled="isEditing || isSaving"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>{{ $t('bots.skills.description') }}</Label>
|
||||
<Input
|
||||
v-model="draftSkill.description"
|
||||
:placeholder="$t('bots.skills.descriptionPlaceholder')"
|
||||
:disabled="isSaving"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>{{ $t('bots.skills.content') }}</Label>
|
||||
<Textarea
|
||||
v-model="draftSkill.content"
|
||||
:placeholder="$t('bots.skills.contentPlaceholder')"
|
||||
:disabled="isSaving"
|
||||
class="min-h-[150px] font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose as-child>
|
||||
<Button
|
||||
variant="outline"
|
||||
:disabled="isSaving"
|
||||
>
|
||||
{{ $t('common.cancel') }}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
:disabled="!canSave || isSaving"
|
||||
@click="handleSave"
|
||||
>
|
||||
<Spinner
|
||||
v-if="isSaving"
|
||||
class="mr-2 size-4"
|
||||
/>
|
||||
{{ $t('common.save') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toast } from 'vue-sonner'
|
||||
import {
|
||||
Button, Card, CardHeader, CardTitle, CardDescription, CardContent,
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose,
|
||||
Input, Textarea, Label, Spinner,
|
||||
} from '@memoh/ui'
|
||||
import ConfirmPopover from '@/components/confirm-popover/index.vue'
|
||||
import {
|
||||
getBotsByBotIdContainerSkills,
|
||||
postBotsByBotIdContainerSkills,
|
||||
deleteBotsByBotIdContainerSkills,
|
||||
type HandlersSkillItem,
|
||||
} from '@memoh/sdk'
|
||||
import { resolveApiErrorMessage } from '@/utils/api-error'
|
||||
|
||||
const props = defineProps<{
|
||||
botId: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const isLoading = ref(false)
|
||||
const isSaving = ref(false)
|
||||
const isDeleting = ref(false)
|
||||
const deletingName = ref('')
|
||||
const skills = ref<HandlersSkillItem[]>([])
|
||||
|
||||
const isDialogOpen = ref(false)
|
||||
const isEditing = ref(false)
|
||||
const draftSkill = ref<HandlersSkillItem>({
|
||||
name: '',
|
||||
description: '',
|
||||
content: '',
|
||||
})
|
||||
|
||||
const canSave = computed(() => {
|
||||
return (draftSkill.value.name || '').trim() && (draftSkill.value.content || '').trim()
|
||||
})
|
||||
|
||||
async function fetchSkills() {
|
||||
if (!props.botId) return
|
||||
isLoading.value = true
|
||||
try {
|
||||
const { data } = await getBotsByBotIdContainerSkills({
|
||||
path: { bot_id: props.botId },
|
||||
throwOnError: true,
|
||||
})
|
||||
skills.value = data.skills || []
|
||||
} catch (error) {
|
||||
toast.error(resolveApiErrorMessage(error, t('bots.skills.loadFailed')))
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
isEditing.value = false
|
||||
draftSkill.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
content: '',
|
||||
}
|
||||
isDialogOpen.value = true
|
||||
}
|
||||
|
||||
function handleEdit(skill: HandlersSkillItem) {
|
||||
isEditing.value = true
|
||||
draftSkill.value = {
|
||||
name: skill.name || '',
|
||||
description: skill.description || '',
|
||||
content: skill.content || '',
|
||||
metadata: skill.metadata,
|
||||
}
|
||||
isDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!canSave.value) return
|
||||
isSaving.value = true
|
||||
try {
|
||||
await postBotsByBotIdContainerSkills({
|
||||
path: { bot_id: props.botId },
|
||||
body: {
|
||||
skills: [{
|
||||
name: draftSkill.value.name?.trim(),
|
||||
description: draftSkill.value.description?.trim(),
|
||||
content: draftSkill.value.content?.trim(),
|
||||
metadata: draftSkill.value.metadata,
|
||||
}],
|
||||
},
|
||||
throwOnError: true,
|
||||
})
|
||||
toast.success(t('bots.skills.saveSuccess'))
|
||||
isDialogOpen.value = false
|
||||
await fetchSkills()
|
||||
} catch (error) {
|
||||
toast.error(resolveApiErrorMessage(error, t('bots.skills.saveFailed')))
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(name?: string) {
|
||||
if (!name) return
|
||||
isDeleting.value = true
|
||||
deletingName.value = name
|
||||
try {
|
||||
await deleteBotsByBotIdContainerSkills({
|
||||
path: { bot_id: props.botId },
|
||||
body: {
|
||||
names: [name],
|
||||
},
|
||||
throwOnError: true,
|
||||
})
|
||||
toast.success(t('bots.skills.deleteSuccess'))
|
||||
await fetchSkills()
|
||||
} catch (error) {
|
||||
toast.error(resolveApiErrorMessage(error, t('bots.skills.deleteFailed')))
|
||||
} finally {
|
||||
isDeleting.value = false
|
||||
deletingName.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSkills()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,460 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium">
|
||||
{{ $t('bots.subagents.title') }}
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ $t('bots.subagents.subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
@click="handleCreate"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'plus']"
|
||||
class="mr-2"
|
||||
/>
|
||||
{{ $t('bots.subagents.add') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="flex items-center justify-center py-8 text-sm text-muted-foreground"
|
||||
>
|
||||
<Spinner class="mr-2" />
|
||||
{{ $t('common.loading') }}
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
v-else-if="!subagents.length"
|
||||
class="flex flex-col items-center justify-center py-12 text-center"
|
||||
>
|
||||
<div class="rounded-full bg-muted p-3 mb-4">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'robot']"
|
||||
class="size-6 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium">
|
||||
{{ $t('bots.subagents.emptyTitle') }}
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
{{ $t('bots.subagents.emptyDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Subagents List -->
|
||||
<div
|
||||
v-else
|
||||
class="space-y-4"
|
||||
>
|
||||
<Card
|
||||
v-for="agent in subagents"
|
||||
:key="agent.id"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<CardHeader class="pb-3">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="space-y-1 min-w-0">
|
||||
<CardTitle class="text-lg flex items-center gap-2">
|
||||
{{ agent.name }}
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="font-normal text-xs font-mono"
|
||||
>
|
||||
{{ agent.id }}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>{{ agent.description || '-' }}</CardDescription>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="mr-2"
|
||||
@click="handleViewContext(agent)"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'eye']"
|
||||
class="mr-2"
|
||||
/>
|
||||
{{ $t('bots.subagents.viewContext') }}
|
||||
<Badge
|
||||
v-if="agent.messages && agent.messages.length"
|
||||
variant="secondary"
|
||||
class="ml-1.5 text-[10px]"
|
||||
>
|
||||
{{ agent.messages.length }}
|
||||
</Badge>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="size-8 p-0"
|
||||
:title="$t('common.edit')"
|
||||
@click="handleEdit(agent)"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'pen-to-square']"
|
||||
class="size-3.5"
|
||||
/>
|
||||
</Button>
|
||||
<ConfirmPopover
|
||||
:message="$t('bots.subagents.deleteConfirm')"
|
||||
:loading="isDeleting && deletingId === agent.id"
|
||||
@confirm="handleDelete(agent.id)"
|
||||
>
|
||||
<template #trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="size-8 p-0 text-destructive hover:text-destructive"
|
||||
:disabled="isDeleting"
|
||||
:title="$t('common.delete')"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'trash']"
|
||||
class="size-3.5"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
</ConfirmPopover>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="pb-4 space-y-3">
|
||||
<!-- Skills -->
|
||||
<div
|
||||
v-if="agent.skills && agent.skills.length > 0"
|
||||
class="space-y-1"
|
||||
>
|
||||
<span class="text-xs font-medium text-muted-foreground uppercase">{{ $t('bots.subagents.skills') }}</span>
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
<Badge
|
||||
v-for="skill in agent.skills"
|
||||
:key="skill"
|
||||
variant="secondary"
|
||||
>
|
||||
{{ skill }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage summary -->
|
||||
<div
|
||||
v-if="hasUsageData(agent.usage)"
|
||||
class="space-y-1"
|
||||
>
|
||||
<span class="text-xs font-medium text-muted-foreground uppercase">{{ $t('bots.subagents.usage') }}</span>
|
||||
<div class="flex flex-wrap items-center gap-3 mt-1 text-xs text-muted-foreground">
|
||||
<span
|
||||
v-for="(val, key) in flattenUsage(agent.usage)"
|
||||
:key="key"
|
||||
class="inline-flex items-center gap-1 bg-muted px-2 py-0.5 rounded"
|
||||
>
|
||||
<span class="font-medium">{{ key }}:</span> {{ val }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="flex items-center gap-4 text-xs text-muted-foreground pt-2">
|
||||
<span v-if="agent.created_at">
|
||||
{{ $t('common.createdAt') }}: {{ formatDateTime(agent.created_at) }}
|
||||
</span>
|
||||
<span v-if="agent.messages">
|
||||
{{ $t('bots.subagents.messagesCount', { count: agent.messages.length }) }}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Edit Dialog -->
|
||||
<Dialog v-model:open="isDialogOpen">
|
||||
<DialogContent class="sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ isEditing ? $t('common.edit') : $t('bots.subagents.add') }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4 py-4">
|
||||
<div class="space-y-2">
|
||||
<Label>{{ $t('common.name') }}</Label>
|
||||
<Input
|
||||
v-model="draftAgent.name"
|
||||
:placeholder="$t('common.namePlaceholder')"
|
||||
:disabled="isSaving"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>{{ $t('bots.subagents.description') }}</Label>
|
||||
<Textarea
|
||||
v-model="draftAgent.description"
|
||||
:placeholder="$t('bots.subagents.descriptionPlaceholder')"
|
||||
:disabled="isSaving"
|
||||
class="min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose as-child>
|
||||
<Button
|
||||
variant="outline"
|
||||
:disabled="isSaving"
|
||||
>
|
||||
{{ $t('common.cancel') }}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
:disabled="!canSave || isSaving"
|
||||
@click="handleSave"
|
||||
>
|
||||
<Spinner
|
||||
v-if="isSaving"
|
||||
class="mr-2 size-4"
|
||||
/>
|
||||
{{ $t('common.save') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- Context Dialog -->
|
||||
<Dialog v-model:open="isContextDialogOpen">
|
||||
<DialogContent class="max-w-3xl max-h-[80vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{{ $t('bots.subagents.contextTitle') }}
|
||||
<Badge
|
||||
v-if="contextMessages.length"
|
||||
variant="secondary"
|
||||
class="ml-2 text-xs"
|
||||
>
|
||||
{{ $t('bots.subagents.messagesCount', { count: contextMessages.length }) }}
|
||||
</Badge>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<!-- Usage info -->
|
||||
<div
|
||||
v-if="hasUsageData(contextUsage)"
|
||||
class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'chart-bar']"
|
||||
class="size-3"
|
||||
/>
|
||||
<span
|
||||
v-for="(val, key) in flattenUsage(contextUsage)"
|
||||
:key="key"
|
||||
class="bg-muted px-2 py-0.5 rounded"
|
||||
>
|
||||
{{ key }}: {{ val }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Messages list -->
|
||||
<ScrollArea class="flex-1 min-h-0 mt-2">
|
||||
<div
|
||||
v-if="contextMessages.length === 0"
|
||||
class="flex flex-col items-center justify-center py-8 text-center"
|
||||
>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ $t('bots.subagents.contextEmpty') }}
|
||||
</p>
|
||||
</div>
|
||||
<MessageList
|
||||
v-else
|
||||
:messages="contextMessages"
|
||||
/>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter class="pt-4">
|
||||
<DialogClose as-child>
|
||||
<Button variant="outline">
|
||||
{{ $t('common.cancel') }}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toast } from 'vue-sonner'
|
||||
import {
|
||||
Button, Card, CardHeader, CardTitle, CardDescription, CardContent,
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose,
|
||||
Input, Textarea, Label, Spinner, Badge, ScrollArea
|
||||
} from '@memoh/ui'
|
||||
import ConfirmPopover from '@/components/confirm-popover/index.vue'
|
||||
import MessageList from './message-list.vue'
|
||||
import {
|
||||
getBotsByBotIdSubagents,
|
||||
postBotsByBotIdSubagents,
|
||||
putBotsByBotIdSubagentsById,
|
||||
deleteBotsByBotIdSubagentsById,
|
||||
type SubagentSubagent
|
||||
} from '@memoh/sdk'
|
||||
import { resolveApiErrorMessage } from '@/utils/api-error'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
|
||||
const props = defineProps<{
|
||||
botId: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const isLoading = ref(false)
|
||||
const isSaving = ref(false)
|
||||
const isDeleting = ref(false)
|
||||
const deletingId = ref('')
|
||||
const subagents = ref<SubagentSubagent[]>([])
|
||||
|
||||
const isDialogOpen = ref(false)
|
||||
const isEditing = ref(false)
|
||||
const draftAgent = ref<SubagentSubagent>({
|
||||
name: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
const isContextDialogOpen = ref(false)
|
||||
const contextMessages = ref<Array<Record<string, unknown>>>([])
|
||||
const contextUsage = ref<Record<string, unknown>>({})
|
||||
|
||||
const canSave = computed(() => {
|
||||
return (draftAgent.value.name || '').trim() && (draftAgent.value.description || '').trim()
|
||||
})
|
||||
|
||||
function hasUsageData(usage: unknown): boolean {
|
||||
if (!usage) return false
|
||||
if (typeof usage === 'object' && Object.keys(usage as object).length > 0) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function flattenUsage(usage: unknown): Record<string, unknown> {
|
||||
if (!usage || typeof usage !== 'object') return {}
|
||||
return usage as Record<string, unknown>
|
||||
}
|
||||
|
||||
async function fetchSubagents() {
|
||||
if (!props.botId) return
|
||||
isLoading.value = true
|
||||
try {
|
||||
const { data } = await getBotsByBotIdSubagents({
|
||||
path: { bot_id: props.botId },
|
||||
throwOnError: true,
|
||||
})
|
||||
subagents.value = data.items || []
|
||||
} catch (error) {
|
||||
toast.error(resolveApiErrorMessage(error, t('bots.subagents.loadFailed')))
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
isEditing.value = false
|
||||
draftAgent.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
}
|
||||
isDialogOpen.value = true
|
||||
}
|
||||
|
||||
function handleEdit(agent: SubagentSubagent) {
|
||||
isEditing.value = true
|
||||
draftAgent.value = {
|
||||
id: agent.id,
|
||||
name: agent.name || '',
|
||||
description: agent.description || '',
|
||||
metadata: agent.metadata,
|
||||
}
|
||||
isDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!canSave.value) return
|
||||
isSaving.value = true
|
||||
try {
|
||||
if (isEditing.value && draftAgent.value.id) {
|
||||
await putBotsByBotIdSubagentsById({
|
||||
path: { bot_id: props.botId, id: draftAgent.value.id },
|
||||
body: {
|
||||
name: draftAgent.value.name?.trim(),
|
||||
description: draftAgent.value.description?.trim(),
|
||||
metadata: draftAgent.value.metadata,
|
||||
},
|
||||
throwOnError: true,
|
||||
})
|
||||
} else {
|
||||
await postBotsByBotIdSubagents({
|
||||
path: { bot_id: props.botId },
|
||||
body: {
|
||||
name: draftAgent.value.name?.trim(),
|
||||
description: draftAgent.value.description?.trim(),
|
||||
metadata: draftAgent.value.metadata,
|
||||
skills: [],
|
||||
messages: [],
|
||||
},
|
||||
throwOnError: true,
|
||||
})
|
||||
}
|
||||
toast.success(t('bots.subagents.saveSuccess'))
|
||||
isDialogOpen.value = false
|
||||
await fetchSubagents()
|
||||
} catch (error) {
|
||||
toast.error(resolveApiErrorMessage(error, t('bots.subagents.saveFailed')))
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id?: string) {
|
||||
if (!id) return
|
||||
isDeleting.value = true
|
||||
deletingId.value = id
|
||||
try {
|
||||
await deleteBotsByBotIdSubagentsById({
|
||||
path: { bot_id: props.botId, id },
|
||||
throwOnError: true,
|
||||
})
|
||||
toast.success(t('bots.subagents.deleteSuccess'))
|
||||
await fetchSubagents()
|
||||
} catch (error) {
|
||||
toast.error(resolveApiErrorMessage(error, t('bots.subagents.deleteFailed')))
|
||||
} finally {
|
||||
isDeleting.value = false
|
||||
deletingId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function handleViewContext(agent: SubagentSubagent) {
|
||||
const msgs = agent.messages || []
|
||||
contextMessages.value = msgs.map((m: Record<string, unknown>, idx: number) => ({
|
||||
id: String(idx),
|
||||
role: (m.role as string) || 'unknown',
|
||||
content: m.content,
|
||||
sender_display_name: m.sender_display_name as string | undefined,
|
||||
platform: m.platform as string | undefined,
|
||||
created_at: m.created_at as string | undefined,
|
||||
usage: m.usage,
|
||||
metadata: m.metadata as Record<string, unknown> | undefined,
|
||||
}))
|
||||
contextUsage.value = (agent.usage as Record<string, unknown>) || {}
|
||||
isContextDialogOpen.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSubagents()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="(msg, idx) in messages"
|
||||
:key="msg.id || idx"
|
||||
class="group relative rounded-lg border p-3 transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<!-- Role Icon -->
|
||||
<div
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-full"
|
||||
:class="roleIconClass(msg.role)"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="roleIcon(msg.role)"
|
||||
class="size-3.5 text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="min-w-0 flex-1 space-y-1.5">
|
||||
<!-- Top row -->
|
||||
<div class="flex flex-wrap items-center gap-2 text-sm">
|
||||
<Badge
|
||||
:variant="roleBadgeVariant(msg.role)"
|
||||
class="text-xs font-medium"
|
||||
>
|
||||
{{ roleLabel(msg.role) }}
|
||||
</Badge>
|
||||
<span
|
||||
v-if="msg.sender_display_name"
|
||||
class="font-medium truncate max-w-[200px]"
|
||||
>
|
||||
{{ msg.sender_display_name }}
|
||||
</span>
|
||||
<Badge
|
||||
v-if="msg.platform"
|
||||
variant="outline"
|
||||
class="text-[10px] uppercase h-5"
|
||||
>
|
||||
{{ msg.platform }}
|
||||
</Badge>
|
||||
<span class="text-xs text-muted-foreground ml-auto shrink-0">
|
||||
{{ formatTime(msg.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Message content -->
|
||||
<div
|
||||
class="text-sm leading-relaxed"
|
||||
:class="{ 'font-mono text-xs': msg.role === 'tool' || msg.role === 'system' }"
|
||||
>
|
||||
<div
|
||||
class="whitespace-pre-wrap wrap-break-word"
|
||||
:class="{ 'line-clamp-4': !expandedIds.includes(msgKey(msg, idx)) }"
|
||||
>
|
||||
{{ formatContent(msg.content) }}
|
||||
</div>
|
||||
<button
|
||||
v-if="isContentLong(msg.content)"
|
||||
class="mt-1 text-xs text-primary hover:underline"
|
||||
@click="toggleExpand(msgKey(msg, idx))"
|
||||
>
|
||||
{{ expandedIds.includes(msgKey(msg, idx)) ? collapseLabel : expandLabel }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Usage -->
|
||||
<div
|
||||
v-if="hasUsage(msg.usage)"
|
||||
class="flex items-center gap-3 text-xs text-muted-foreground pt-1"
|
||||
>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'chart-bar']"
|
||||
class="size-3"
|
||||
/>
|
||||
{{ formatUsage(msg.usage) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Badge } from '@memoh/ui'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
|
||||
export interface MessageItem {
|
||||
id?: string
|
||||
role?: string
|
||||
content?: unknown
|
||||
sender_display_name?: string
|
||||
sender_avatar_url?: string
|
||||
platform?: string
|
||||
created_at?: string
|
||||
usage?: unknown
|
||||
metadata?: Record<string, unknown>
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
messages: MessageItem[]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const expandedIds = ref<string[]>([])
|
||||
|
||||
const expandLabel = computed(() => t('bots.history.expandContent'))
|
||||
const collapseLabel = computed(() => t('bots.history.collapseContent'))
|
||||
|
||||
function msgKey(msg: MessageItem, idx: number): string {
|
||||
return msg.id || String(idx)
|
||||
}
|
||||
|
||||
function toggleExpand(id: string) {
|
||||
if (expandedIds.value.includes(id)) {
|
||||
expandedIds.value = expandedIds.value.filter(v => v !== id)
|
||||
} else {
|
||||
expandedIds.value = [...expandedIds.value, id]
|
||||
}
|
||||
}
|
||||
|
||||
function roleIcon(role?: string): [string, string] {
|
||||
switch (role) {
|
||||
case 'user': return ['fas', 'user']
|
||||
case 'assistant': return ['fas', 'robot']
|
||||
case 'tool': return ['fas', 'wrench']
|
||||
case 'system': return ['fas', 'laptop-code']
|
||||
default: return ['fas', 'circle-question']
|
||||
}
|
||||
}
|
||||
|
||||
function roleIconClass(role?: string): string {
|
||||
switch (role) {
|
||||
case 'user': return 'bg-blue-500'
|
||||
case 'assistant': return 'bg-emerald-500'
|
||||
case 'tool': return 'bg-amber-500'
|
||||
case 'system': return 'bg-slate-500'
|
||||
default: return 'bg-gray-400'
|
||||
}
|
||||
}
|
||||
|
||||
function roleBadgeVariant(role?: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||
switch (role) {
|
||||
case 'user': return 'default'
|
||||
case 'assistant': return 'secondary'
|
||||
case 'tool': return 'outline'
|
||||
case 'system': return 'outline'
|
||||
default: return 'outline'
|
||||
}
|
||||
}
|
||||
|
||||
function roleLabel(role?: string): string {
|
||||
const key = `bots.history.role.${role || 'system'}`
|
||||
const val = t(key)
|
||||
return val !== key ? val : (role || 'unknown')
|
||||
}
|
||||
|
||||
function formatTime(val?: string): string {
|
||||
return formatDateTime(val, { fallback: '-' })
|
||||
}
|
||||
|
||||
function formatContent(content: unknown): string {
|
||||
if (!content) return ''
|
||||
try {
|
||||
if (Array.isArray(content)) {
|
||||
const decoder = new TextDecoder()
|
||||
const str = decoder.decode(new Uint8Array(content as number[]))
|
||||
try {
|
||||
const parsed = JSON.parse(str)
|
||||
if (typeof parsed === 'string') return parsed
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
return str
|
||||
}
|
||||
}
|
||||
if (typeof content === 'string') return content
|
||||
if (typeof content === 'object') return JSON.stringify(content, null, 2)
|
||||
return String(content)
|
||||
} catch {
|
||||
return String(content)
|
||||
}
|
||||
}
|
||||
|
||||
function isContentLong(content: unknown): boolean {
|
||||
const text = formatContent(content)
|
||||
return text.length > 300 || text.split('\n').length > 4
|
||||
}
|
||||
|
||||
function hasUsage(usage: unknown): boolean {
|
||||
if (!usage) return false
|
||||
if (Array.isArray(usage) && usage.length > 0) return true
|
||||
if (typeof usage === 'object' && Object.keys(usage as object).length > 0) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function formatUsage(usage: unknown): string {
|
||||
if (!usage) return ''
|
||||
try {
|
||||
if (Array.isArray(usage)) {
|
||||
const decoder = new TextDecoder()
|
||||
const str = decoder.decode(new Uint8Array(usage as number[]))
|
||||
try {
|
||||
const parsed = JSON.parse(str)
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
const parts: string[] = []
|
||||
for (const [k, v] of Object.entries(parsed)) {
|
||||
parts.push(`${k}: ${v}`)
|
||||
}
|
||||
return parts.join(' | ')
|
||||
}
|
||||
return str
|
||||
} catch {
|
||||
return str
|
||||
}
|
||||
}
|
||||
if (typeof usage === 'object') {
|
||||
const parts: string[] = []
|
||||
for (const [k, v] of Object.entries(usage as Record<string, unknown>)) {
|
||||
parts.push(`${k}: ${v}`)
|
||||
}
|
||||
return parts.join(' | ')
|
||||
}
|
||||
return String(usage)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -128,6 +128,9 @@
|
||||
<TabsTrigger value="history">
|
||||
{{ $t('bots.tabs.history') }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="skills">
|
||||
{{ $t('bots.tabs.skills') }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="settings">
|
||||
{{ $t('bots.tabs.settings') }}
|
||||
</TabsTrigger>
|
||||
@@ -499,13 +502,19 @@
|
||||
value="subagents"
|
||||
class="mt-6"
|
||||
>
|
||||
<!-- TODO: Subagents content -->
|
||||
<BotSubagents :bot-id="botId" />
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="history"
|
||||
class="mt-6"
|
||||
>
|
||||
<!-- TODO: History content -->
|
||||
<BotHistory :bot-id="botId" />
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="skills"
|
||||
class="mt-6"
|
||||
>
|
||||
<BotSkills :bot-id="botId" />
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="settings"
|
||||
@@ -614,6 +623,9 @@ import BotSettings from './components/bot-settings.vue'
|
||||
import BotChannels from './components/bot-channels.vue'
|
||||
import BotMcp from './components/bot-mcp.vue'
|
||||
import BotMemory from './components/bot-memory.vue'
|
||||
import BotSkills from './components/bot-skills.vue'
|
||||
import BotHistory from './components/bot-history.vue'
|
||||
import BotSubagents from './components/bot-subagents.vue'
|
||||
import { resolveApiErrorMessage } from '@/utils/api-error'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
import { useAvatarInitials } from '@/composables/useAvatarInitials'
|
||||
|
||||
Reference in New Issue
Block a user