Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 10 additions & 28 deletions ui/src/conversation-panel/components/content/items/failure.vue
Original file line number Diff line number Diff line change
@@ -1,34 +1,16 @@
<template>
<div class="content-failure">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM8 4v4M8 10.5h.006" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
</svg>
<span>{{ content.content || '发生错误' }}</span>
</div>
</template>

<script setup lang="ts">
defineOptions({ name: 'ConversationFailureContent' })

defineProps<{
content: any
content: { content?: string | null }
}>()
</script>

<style scoped lang="scss">
.content-failure {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 12px;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 8px;
color: #ef4444;
font-size: 13px;
line-height: 1.5;
}
<template>
<div class="flex-align-center gap-2">
<MkIcon name="icon_warning_filled" class="text-danger!" />
<span>{{ content.content || '发生错误' }}</span>
</div>
</template>

.content-failure svg {
flex-shrink: 0;
margin-top: 1px;
}
</style>
<style scoped lang="scss"></style>
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ const submit = async () => {
</script>

<template>
<MdPreview class="!h-auto" editorId="conversation-text" :modelValue="leadingText || ''"></MdPreview>
<div class="mk-gray-card-lg space-y-4 border border-N300">
<MdPreview class="!h-auto" editorId="conversation-text" :modelValue="leadingText || ''"></MdPreview>
<MkDynamicsForm ref="dynamicsFormRef" :view="is_submit || !mainStore" :render-data="form_field_list" v-model="form_data" />
<MdPreview class="!h-auto" editorId="conversation-text" :modelValue="trailingText || ''"></MdPreview>
<el-button :type="is_submit ? 'info' : 'primary'" :disabled="is_submit || !mainStore" @click="submit">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const statusType = computed(() => {
return 'info'
})

const isFailed = computed(() => props.toolItem.status === 'FAIL' || props.toolItem.status === 'FAILURE')

// 参数为 JSON 字符串,尝试格式化展示,失败则原样展示
function tryFormatJson(value: unknown): string {
if (value == null) return ''
Expand Down Expand Up @@ -65,7 +67,7 @@ const formattedResult = computed(() => tryFormatJson(props.toolItem.content))
<span v-if="loading" class="mk-conversation-loading" />
<MkIcon name="icon_busy_outlined" />
<span class="min-w-0 max-w-[300px] truncate" :title="toolTitle">{{ toolTitle }}</span>
<el-tag v-if="toolItem.status" size="small" :type="statusType">{{ statusText }}</el-tag>
<el-tag v-if="isFailed" size="small" :type="statusType">{{ statusText }}</el-tag>
</template>

<div class="space-y-2 pb-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const props = defineProps<{ content: ChatContent }>()

// 相邻工具由 content-list 聚合,分组内按工具名选择专用展示。
const toolItems = computed(() => props.content.children ?? [])
const failedToolCount = computed(() => toolItems.value.filter((toolItem) => toolItem.status === 'FAIL' || toolItem.status === 'FAILURE').length)

// 工具名(小写)→ 专用渲染器;新增工具时与 tool/index.vue 同步
const toolRenderers: Dict<Component> = {
Expand Down Expand Up @@ -87,6 +88,11 @@ const isExpanded = computed({
<template #label>
<span v-if="groupStatus === 'running'" class="mk-conversation-loading" />
<span>调用了 {{ toolItems.length }} 个工具</span>
<span v-if="failedToolCount">
({{ failedToolCount }} 个
<span class="text-danger">失败</span>
)
</span>
</template>

<div v-show="toolItems.length" class="bg-white border rounded-xl">
Expand Down
148 changes: 41 additions & 107 deletions ui/src/conversation-panel/components/content/items/group/index.vue
Original file line number Diff line number Diff line change
@@ -1,50 +1,36 @@
<template>
<div class="content-group">
<!-- 分组头部:标题 + 状态,收起时附当前进展,点击展开/收起组内内容 -->
<button type="button" class="group-header" @click="isExpanded = !isExpanded">
<svg class="group-chevron" :class="{ open: isExpanded }" viewBox="0 0 16 16" fill="none">
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span class="group-name">{{ content.name }}</span>
<span v-if="!isExpanded && currentActivity" class="group-activity">{{ currentActivity }}</span>
<span v-if="statusClass" class="group-status" :class="statusClass">{{ statusText }}</span>
</button>

<div v-if="isExpanded && childContents.length" class="group-body">
<ContentList :content-list="childContents" />
</div>
</div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import type { ChatContent } from '@/api/types'
import ContentList from '@/conversation-panel/components/content-list/index.vue'

/**
* 通用分组:content.name 为标题,content.status 可选(不传不显示状态),
* content.children 为任意内容项(可再嵌套 GROUP),交由 ContentList 渲染。
* 执行中或无状态时默认展开,结束后默认收起,手动切换后以手动为准;收起时展示最新一条子项在做什么。
*/
const props = defineProps<{ content: any }>()
defineOptions({ name: 'ConversationGroupContent' })

const props = defineProps<{ content: ChatContent }>()

const childContents = computed<any[]>(() => props.content.children ?? [])
const childContents = computed(() => props.content.children ?? [])

const STATUS_TEXT: Record<string, string> = {
RUNNING: '执行中',
SUCCESS: '成功',
FAIL: '失败',
FAILURE: '失败',
}
const statusText = computed(() => STATUS_TEXT[props.content.status] ?? props.content.status)
const statusText = computed(() => STATUS_TEXT[props.content.status ?? ''] ?? props.content.status)

const statusClass = computed(() => {
const statusType = computed(() => {
const status = props.content.status
if (!status) return ''
if (status === 'SUCCESS') return 'success'
if (status === 'FAIL' || status === 'FAILURE') return 'error'
return 'running'
if (status === 'FAIL' || status === 'FAILURE') return 'danger'
return 'info'
})

const isFailed = computed(() => props.content.status === 'FAIL' || props.content.status === 'FAILURE')

const manualExpanded = ref<boolean>()
const isExpanded = computed({
get: () => manualExpanded.value ?? (!props.content.status || props.content.status === 'RUNNING'),
Expand All @@ -57,7 +43,7 @@ function tail(text: string) {
}

// 子项在做什么:工具取名称,正文取末尾,思考固定文案,子分组取标题,失败取错误信息
function describeChild(child: any): string {
function describeChild(child: ChatContent): string {
if (child.type === 'TOOL' || child.type === 'GROUP') return child.name ?? ''
if (child.type === 'TEXT' || child.type === 'FAILURE') return tail(child.content ?? '')
if (child.type === 'REASONING') return '思考中'
Expand All @@ -68,88 +54,36 @@ function describeChild(child: any): string {
const currentActivity = computed(() => {
const latestChild = childContents.value
.filter((child) => describeChild(child))
.reduce<any>((latest, child) => (!latest || (child.id ?? '').localeCompare(latest.id ?? '') >= 0 ? child : latest), undefined)
.reduce<ChatContent | undefined>((latest, child) => (!latest || (child.id ?? '').localeCompare(latest.id ?? '') >= 0 ? child : latest), undefined)
return latestChild ? describeChild(latestChild) : ''
})
</script>

<style scoped lang="scss">
.content-group {
margin-bottom: 8px;
}

.group-header {
display: flex;
align-items: center;
gap: 6px;
max-width: 100%;
padding: 4px 0;
border: none;
background: none;
cursor: pointer;
font-family: inherit;
font-size: 13px;
color: var(--t2, #606266);
text-align: left;
transition: color 0.15s;
}

.group-header:hover {
color: var(--t1, #303133);
}

.group-chevron {
width: 12px;
height: 12px;
flex-shrink: 0;
color: var(--t3, #909399);
transition: transform 0.15s;
}

.group-chevron.open {
transform: rotate(90deg);
}

.group-name {
flex-shrink: 0;
max-width: 60%;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.group-activity {
min-width: 0;
color: var(--t3, #909399);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.group-status {
flex-shrink: 0;
font-size: 11px;
padding: 1px 6px;
border-radius: 4px;
background: var(--hv, #f5f7fa);
}

.group-status.success {
color: #67c23a;
}

.group-status.error {
color: #f56c6c;
}

.group-status.running {
color: var(--t3, #909399);
}

.group-body {
margin: 4px 0 0 5px;
padding-left: 12px;
border-left: 2px solid var(--bd, #dcdfe6);
}
</style>
<template>
<!-- 展开或收起分组内容 -->
<MkCollapse
class="mb-2 w-full"
v-model:expanded="isExpanded"
indicator-position="after"
trigger-class="group text-N500! mb-2 w-fit max-w-full hover:text-N900!"
>
<template #indicator="{ expanded }">
<MkIcon
name="icon_right_outlined"
:size="12"
class="transition-transform text-N500! group-hover:text-N900!"
:class="{ 'rotate-90': expanded }"
/>
</template>
<template #label>
<span v-if="content.status === 'RUNNING'" class="mk-conversation-loading" />
<span class="min-w-0 max-w-[300px] truncate" :title="content.name">{{ content.name }}</span>
<span v-if="!isExpanded && currentActivity" class="min-w-0 truncate" :title="currentActivity">{{ currentActivity }}</span>
<el-tag v-if="isFailed" size="small" :type="statusType">{{ statusText }}</el-tag>
</template>

<div v-if="isExpanded && childContents.length" class="mt-1 border-l-2 border-N900/15 pl-3">
<ContentList :content-list="childContents" />
</div>
</MkCollapse>
</template>
78 changes: 44 additions & 34 deletions ui/src/conversation-panel/components/content/items/question.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type QuestionContent = { content?: string } & Partial<Record<AttachmentField, Qu

const props = defineProps<{ content: QuestionContent }>()

// 合并各类已发送附件,统一换行排列;不依赖输入区的上传状态。
// 非图片附件统一排列在上方,图片单独放在下方。
const attachmentFields = ['document_list', 'image_list', 'audio_list', 'video_list', 'other_list'] as const
const attachments = computed(() =>
attachmentFields.flatMap((field) =>
Expand All @@ -36,6 +36,12 @@ const attachments = computed(() =>
})),
),
)
const attachmentGroups = computed(() =>
[
{ type: 'file', files: attachments.value.filter((file) => file.field !== 'image_list') },
{ type: 'image', files: attachments.value.filter((file) => file.field === 'image_list') },
].filter((group) => group.files.length > 0),
)
const imageSources = computed(() => attachments.value.filter((file) => file.field === 'image_list' && file.src).map((file) => file.src))

function getAttachmentDescription(file: QuestionAttachment) {
Expand All @@ -46,42 +52,46 @@ function getAttachmentDescription(file: QuestionAttachment) {

<template>
<div class="mk-conversation-question-content group/question space-y-3 cursor-pointer">
<!-- 查看已发送附件,超出当前宽度时自动换行。 -->
<div v-if="attachments.length" class="flex-wrap max-w-full justify-end gap-2">
<template v-for="file in attachments" :key="file.key">
<div :class="file.field === 'image_list' ? 'shrink-0' : 'w-60 max-w-full'">
<!-- 查看图片 -->
<ImageUploadCard
v-if="file.field === 'image_list' && file.src"
:file="file"
:src="file.src"
:size="58"
:show-remove="false"
:preview-src-list="imageSources"
:initial-index="imageSources.indexOf(file.src)"
class="cursor-pointer"
/>
<!-- 播放音频 -->
<MkAudio v-else-if="file.field === 'audio_list'" :src="file.src" :name="file.name">
<FileUploadCard :file="file" :description="getAttachmentDescription(file)" :show-remove="false" class="cursor-pointer rounded-lg!" />
</MkAudio>
<!-- 播放视频 -->
<MkVideo v-else-if="file.field === 'video_list'" :src="file.src" :name="file.name">
<FileUploadCard :file="file" :description="getAttachmentDescription(file)" :show-remove="false" class="cursor-pointer rounded-lg!" />
</MkVideo>
<!-- 下载文件 -->
<FileUploadCard
v-else-if="file.field !== 'image_list'"
:file="file"
:description="getAttachmentDescription(file)"
:show-remove="false"
class="cursor-pointer rounded-lg!"
@click="file.src && downloadByURL(file.src, file.name)"
/>
<!-- 视频、音频和文档共用一行,图片在下方;超出当前宽度时自动换行。 -->
<div v-if="attachments.length" class="max-w-full space-y-2">
<template v-for="attachmentGroup in attachmentGroups" :key="attachmentGroup.type">
<div class="flex max-w-full flex-wrap justify-end gap-2">
<template v-for="file in attachmentGroup.files" :key="file.key">
<div :class="file.field === 'image_list' ? 'shrink-0' : 'w-60 max-w-full'">
<!-- 查看图片 -->
<ImageUploadCard
v-if="file.field === 'image_list' && file.src"
:file="file"
:src="file.src"
:size="58"
:show-remove="false"
:preview-src-list="imageSources"
:initial-index="imageSources.indexOf(file.src)"
class="cursor-pointer"
/>
<!-- 播放音频 -->
<MkAudio v-else-if="file.field === 'audio_list'" :src="file.src" :name="file.name">
<FileUploadCard :file="file" :description="getAttachmentDescription(file)" :show-remove="false" class="cursor-pointer rounded-lg!" />
</MkAudio>
<!-- 播放视频 -->
<MkVideo v-else-if="file.field === 'video_list'" :src="file.src" :name="file.name">
<FileUploadCard :file="file" :description="getAttachmentDescription(file)" :show-remove="false" class="cursor-pointer rounded-lg!" />
</MkVideo>
<!-- 下载文件 -->
<FileUploadCard
v-else-if="file.field !== 'image_list'"
:file="file"
:description="getAttachmentDescription(file)"
:show-remove="false"
class="cursor-pointer rounded-lg!"
@click="file.src && downloadByURL(file.src, file.name)"
/>
</div>
</template>
</div>
</template>
</div>
<div v-if="content.content" class="mk-gray-card-lg max-w-full py-3! whitespace-pre-wrap wrap-anywhere">{{ content.content }}</div>
<div v-if="content.content" class="mk-gray-card-lg ml-auto w-fit max-w-full py-3! whitespace-pre-wrap wrap-anywhere">{{ content.content }}</div>
<!-- TODO 用户的操作 -->
<div class="h-8 -mt-3 flex items-center justify-end invisible group-hover/question:visible">
<!-- 复制提问 -->
Expand Down
Loading
Loading