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
2 changes: 1 addition & 1 deletion ui/src/assets/iconfont.js

Large diffs are not rendered by default.

19 changes: 15 additions & 4 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,17 +539,18 @@ Logo 数据统一从 Theme Store 获取。
### MkDragUpload

入口 `mk-drag-upload/index.vue`,内部按单/多文件分离实现。`v-model` 为 `DragUploadFile[]`,
兼容 UploadUserFile,支持 file_id、errMsg、canRetry;类型从组件 `types.ts` 导入。
兼容 UploadUserFile,支持 file_id、errMsg;类型从组件 `types.ts` 导入。

- `multiple` 默认 false;单文件展示卡片和更换入口,多文件保留拖拽区、文件夹选择与列表。
- 多文件 `mode` 默认 `file`,使用文件图标和上传进度;`mode="image"` 按选择顺序展示 104×104 的
`el-image` 方块,不使用 Card,不展示汇总、文件名或状态文案。等待时中央显示空进度条,上传时显示实际进度,
成功或失败后隐藏进度条。右上角提供删除,未上传成功时
在原图上叠加灰色遮罩,成功后移除;网络失败和大小超限使用红框,仅网络失败在中央提供重试。
失败且有错误信息时通过 MkTooltip 展示文件名及失败原因,其他情况禁用提示。内部仍维护上传状态与 uploadingCount,本地缩略图 URL 在文件移除
在原图上叠加灰色遮罩,成功后移除;网络失败和大小超限使用红框,失败时中央显示警告图标,
在其右侧提供重试图标,两个图标整体居中。仅悬停警告图标且有错误信息时通过 MkTooltip
展示文件名及失败原因,重试图标单独提示“重试”,两个提示均定位到图片上方,图片其他区域不提示。内部仍维护上传状态与 uploadingCount,本地缩略图 URL 在文件移除
和组件卸载时释放。单文件不使用 mode。
- `accept`、`limit` 控制格式/数量,多文件 `sizeLimit` 默认 100MB;格式、空文件、重复选择移除,
大小超限保留不可重试 fail 文件,图片模式保留原图和红框;网络失败可重试。
大小超限保留 fail 文件,图片模式保留原图和红框;所有失败均提供重试,重试仍执行原有文件校验。
超量发出 exceed,DS_Store 格式不支持时静默移除。
- `dragText`、`selectText`、`tipText`、`replaceText` 覆盖文案;download 作用域插槽提供下载操作。
- 多文件通过 `inject('upload')` 获取 `DragUploadHandler`,返回 `{ request: Promise<string>, abort }`。
Expand All @@ -559,6 +560,16 @@ Logo 数据统一从 Theme Store 获取。
- 暴露 clearFiles()、handleRemove(file)、响应式 uploadingCount;公开选择状态 ready/uploading/success/fail。
页面提供具体上传请求与业务校验,不通过展示插槽控制数量限制。

同目录的 `FileUploadCard.vue` 和 `ImageUploadCard.vue` 可显式导入,供上传列表、对话输入与已发送附件复用。
两者接收只读 `UploadCardFile` 展示数据,统一呈现进度和失败红框,通过 `retry`、`remove` 事件通知使用方,
不请求接口、不管理文件或预览 URL 生命周期。`percentage` 可覆盖文件内的进度,
失败时均显示重试入口,`showRemove` 控制删除入口,默认显示;`disabled` 统一禁用操作。
已发送附件传入 success 状态并设置 `showRemove=false`。
文件卡片通过 `description` 覆盖辅助文案,`name`/`description` 作用域插槽可自定义文字内容;
图片卡片的 `size` 控制缩略图宽高(默认 104px),`iconSize` 控制状态图标大小(默认 16px),
`src` 由使用方提供,`showError` 控制失败警告图标,默认显示。
图片卡片可通过 `previewSrcList` 启用图片预览,`initialIndex` 指定初始索引(默认 0),预览挂载到 body。

### PythonCodeEditor

入口 `codemirror-editor/python.vue`。
Expand Down
64 changes: 64 additions & 0 deletions ui/src/components/mk-drag-upload/FileUploadCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script setup lang="ts">
import { computed } from 'vue'
import { getFileIconUrl } from '@/utils/icon'
import { formatFileSize } from '@/utils/number'
import type { UploadCardFile } from './types'

defineOptions({ name: 'FileUploadCard' })
const props = withDefaults(
defineProps<{ file: UploadCardFile; description?: string; percentage?: number; disabled?: boolean; showRemove?: boolean }>(),
{
disabled: false,
showRemove: true,
},
)
const emit = defineEmits<{ retry: []; remove: [] }>()
defineSlots<{
name?: (props: { file: UploadCardFile }) => unknown
description?: (props: { file: UploadCardFile; percentage: number }) => unknown
}>()

const percentage = computed(() => props.percentage ?? props.file.percentage ?? 0)
</script>

<template>
<el-card shadow="never" class="small relative overflow-hidden!" :class="{ 'border-danger!': file.status === 'fail' }">
<div class="flex-align-center gap-3">
<img :src="getFileIconUrl(file.name)" alt="" class="size-8 shrink-0" />
<div class="min-w-0 flex-1">
<slot name="name" :file="file">
<p class="truncate" :title="file.name">{{ file.name }}</p>
</slot>
<div class="flex-align-center gap-2 text-sm">
<span class="min-w-0 text-N500">
<slot name="description" :file="file" :percentage="percentage">
<template v-if="description !== undefined">{{ description }}</template>
<template v-else>
<template v-if="file.status === 'uploading'">{{ formatFileSize(((file.size ?? 0) * percentage) / 100) }} /</template>
{{ formatFileSize(file.size) }}
</template>
</slot>
</span>
<span v-if="file.status === 'fail'" class="truncate text-danger" :title="file.errMsg">{{ file.errMsg }}</span>
</div>
</div>
<div v-if="showRemove || file.status === 'fail'" class="flex shrink-0 gap-1">
<!-- 重新上传失败附件 -->
<el-button v-if="file.status === 'fail'" :disabled="disabled" text title="重新上传" @click.stop="emit('retry')">
<MkIcon name="icon_refresh_outlined" />
</el-button>
<!-- 删除已选附件 -->
<el-button v-if="showRemove" :disabled="disabled" text title="删除文件" @click.stop="emit('remove')">
<MkIcon name="icon_delete-trash_outlined" />
</el-button>
</div>
</div>
<el-progress
v-if="file.status === 'uploading'"
:percentage="percentage"
:stroke-width="4"
:show-text="false"
class="absolute! inset-x-0 bottom-0"
/>
</el-card>
</template>
66 changes: 66 additions & 0 deletions ui/src/components/mk-drag-upload/ImageUploadCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { UploadCardFile } from './types'

defineOptions({ name: 'ImageUploadCard' })
const props = withDefaults(
defineProps<{
file: UploadCardFile
src?: string
size?: number
iconSize?: number
percentage?: number
disabled?: boolean
showError?: boolean
showRemove?: boolean
previewSrcList?: string[]
initialIndex?: number
}>(),
{ size: 104, iconSize: 16, disabled: false, showError: true, showRemove: true, initialIndex: 0 },
)
const emit = defineEmits<{ retry: []; remove: [] }>()
const percentage = computed(() => props.percentage ?? props.file.percentage ?? 0)
</script>

<template>
<div class="group relative shrink-0 overflow-hidden rounded-md" :style="{ width: `${size}px`, height: `${size}px` }">
<!-- 查看图片,未提供预览列表时仅展示缩略图。 -->
<el-image
:src="src"
:alt="file.name"
:preview-src-list="previewSrcList"
:initial-index="initialIndex"
:scale="0.8"
fit="cover"
preview-teleported
class="h-full w-full border"
:class="{ 'border-danger!': file.status === 'fail' }"
/>
<div v-if="file.status !== 'success'" class="pointer-events-none absolute inset-0 bg-N900/30" />
<div v-if="file.status === 'ready' || file.status === 'uploading'" class="absolute-center w-3/5">
<el-progress :percentage="file.status === 'uploading' ? percentage : 0" :stroke-width="4" :show-text="false" />
</div>
<div v-if="file.status === 'fail'" class="absolute-center flex-align-center gap-3">
<MkTooltip v-if="showError" :content="`${file.name}:${file.errMsg}`" :disabled="!file.errMsg" placement="top" :offset="size / 2">
<span class="flex-center"><MkIcon name="icon_warning_filled" class="text-danger!" :size="iconSize" /></span>
</MkTooltip>
<!-- 重新上传失败图片 -->
<MkTooltip content="重新上传" placement="top" :offset="size / 2">
<button type="button" class="flex-center cursor-pointer disabled:cursor-not-allowed" :disabled="disabled" @click.stop="emit('retry')">
<MkIcon name="icon_refresh_outlined" class="text-white!" :size="iconSize" />
</button>
</MkTooltip>
</div>
<!-- 删除已选图片 -->
<button
v-if="showRemove"
type="button"
class="group-hover-visible mk-remove-button disabled:cursor-not-allowed"
title="删除图片"
:disabled="disabled"
@click.stop="emit('remove')"
>
<MkIcon name="icon_close_bold_outlined" class="text-white!" :size="6" />
</button>
</div>
</template>
78 changes: 13 additions & 65 deletions ui/src/components/mk-drag-upload/MultipleFileUpload.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import type { UploadFile, UploadFiles, UploadInstance, UploadRawFile, UploadUser
import type { DragUploadFile, DragUploadHandler, DragUploadMode } from './types'
import FileApi from '@/api/admin/file'
import uploadImage from '@/assets/mk_icon_upload.svg'
import { getFileIconUrl } from '@/utils/icon'
import { formatFileSize } from '@/utils/number'
import { MsgWarning } from '@/utils/message'
import FileUploadCard from './FileUploadCard.vue'
import ImageUploadCard from './ImageUploadCard.vue'

defineOptions({ name: 'MultipleFileUpload' })

Expand Down Expand Up @@ -94,7 +94,6 @@ function validateFile(file: DragUploadFile) {
file.status = 'fail'
file.percentage = 0
file.errMsg = `大小超限`
file.canRetry = false
return false
}
const acceptedTypes = props.accept
Expand Down Expand Up @@ -127,7 +126,6 @@ function handleUpload(file: DragUploadFile) {
file.status = 'uploading'
file.percentage = 0
file.errMsg = ''
file.canRetry = false
const uid = file.uid
// Promise 链同时覆盖上传接口同步抛错和异步请求失败。
void Promise.resolve()
Expand All @@ -149,7 +147,6 @@ function handleUpload(file: DragUploadFile) {
if (file.status !== 'uploading') return
file.status = 'fail'
file.errMsg = '网络失败'
file.canRetry = true
})
.finally(() => uploadRequests.delete(uid))
}
Expand Down Expand Up @@ -204,17 +201,17 @@ const uploadSummary = computed(() => {
summary.completed = summary.success + summary.error
return summary
})
const retryFiles = computed(() => fileList.value.filter((file) => file.status === 'fail' && file.canRetry))
const retryFiles = computed(() => fileList.value.filter((file) => file.status === 'fail'))
const sortedFiles = computed(() => [...fileList.value].sort((first, second) => getFileStatusOrder(first) - getFileStatusOrder(second)))

function getFileStatusOrder(file: DragUploadFile) {
if (file.status === 'fail') return file.canRetry ? 0 : 1
if (file.status === 'fail') return 0
if (file.status === 'uploading') return 2
return 3
}

function handleRetry(file: DragUploadFile) {
if (props.disabled || file.status !== 'fail' || !file.canRetry) return
if (props.disabled || file.status !== 'fail' || !validateFile(file)) return
if (upload) handleUpload(file)
else emit('retry', file)
}
Expand Down Expand Up @@ -302,67 +299,18 @@ defineExpose({ clearFiles, handleRemove })
</div>
<div v-if="mode === 'image'" class="mt-4 flex flex-wrap gap-3">
<template v-for="file in fileList" :key="file.uid ?? file.name">
<MkTooltip :content="`${file.name}:${file.errMsg}`" :disabled="file.status !== 'fail' || !file.errMsg" placement="top">
<div class="group relative shrink-0 overflow-hidden rounded-md">
<el-image
:src="(file.raw ? imagePreviewUrls.get(file.raw) : undefined) ?? file.url"
fit="cover"
class="size-26"
:class="{ 'border-danger!': file.status === 'fail' }"
></el-image>

<div v-if="file.status !== 'success'" class="pointer-events-none absolute inset-0 bg-N900/30" />
<div v-if="file.status === 'ready' || file.status === 'uploading'" class="absolute-center w-16">
<el-progress :percentage="file.status === 'uploading' ? (file.percentage ?? 0) : 0" :stroke-width="4" :show-text="false" />
</div>
<!-- 重试网络失败的图片 -->
<div v-if="file.status === 'fail' && file.canRetry" class="absolute-center cursor-pointer" @click="handleRetry(file)">
<MkIcon name="icon_refresh_outlined" class="text-white!" />
</div>
<!-- 删除已选图片 -->
<div class="group-hover-visible mk-remove-button" @click="handleRemove(file)">
<MkIcon name="icon_close_bold_outlined" class="text-white!" :size="6" />
</div>
</div>
</MkTooltip>
<ImageUploadCard
:file="file"
:src="(file.raw ? imagePreviewUrls.get(file.raw) : undefined) ?? file.url"
:disabled="disabled"
@retry="handleRetry(file)"
@remove="handleRemove(file)"
/>
</template>
</div>
<div v-else class="grid grid-cols-1 gap-3 md:grid-cols-2">
<template v-for="file in sortedFiles" :key="file.uid ?? file.name">
<el-card class="small relative" shadow="never" :class="{ 'border-danger!': file.status === 'fail' }">
<div class="flex-align-center gap-3">
<img :src="getFileIconUrl(file.name)" alt="" class="size-8 shrink-0" />
<div class="min-w-0 flex-1">
<p class="truncate" :title="file.name">{{ file.name }}</p>
<div class="flex-align-center gap-2 text-sm">
<span class="shrink-0 text-N500">
<template v-if="file.status === 'uploading'">{{ formatFileSize(((file.size ?? 0) * (file.percentage ?? 0)) / 100) }} /</template>
{{ formatFileSize(file.size) }}
</span>
<span v-if="file.status === 'fail'" class="text-danger">
{{ file.errMsg }}
</span>
</div>
</div>
<div class="flex shrink-0 gap-1">
<!-- 重试当前失败文件 -->
<el-button v-if="file.status === 'fail' && file.canRetry" :disabled="disabled" text @click="handleRetry(file)">
<MkIcon name="icon_refresh_outlined" />
</el-button>
<!-- 删除已选文件 -->
<el-button :disabled="disabled" text title="删除文件" @click="handleRemove(file)">
<MkIcon name="icon_delete-trash_outlined" />
</el-button>
</div>
</div>
<el-progress
v-if="file.status === 'uploading'"
:percentage="file.percentage ?? 0"
:stroke-width="4"
:show-text="false"
class="absolute inset-x-0 bottom-0"
/>
</el-card>
<FileUploadCard :file="file" :disabled="disabled" @retry="handleRetry(file)" @remove="handleRemove(file)" />
</template>
</div>
</template>
Expand Down
4 changes: 3 additions & 1 deletion ui/src/components/mk-drag-upload/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import type { UploadRawFile, UploadUserFile } from 'element-plus'
export interface DragUploadFile extends UploadUserFile {
file_id?: string
errMsg?: string
canRetry?: boolean
}

/** 上传卡片的只读展示数据,不依赖具体上传接口。 */
export type UploadCardFile = Pick<DragUploadFile, 'name' | 'size' | 'status' | 'percentage' | 'errMsg'>

/** 由调用方提供具体上传接口,组件管理进度、完成状态和取消。 */
export type DragUploadHandler = (file: UploadRawFile, onProgress: (percent: number) => void) => { request: Promise<string>; abort: () => void }

Expand Down
4 changes: 4 additions & 0 deletions ui/src/constants/CONSTANT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ API 枚举值。例如角色类型和登录方式以 `src/api/enums` 为唯一
`knowledge.ts` 的 `KNOWLEDGE_SEARCH_MODE_OPTIONS` 统一维护向量、全文和混合检索的值、文案及说明,
召回测试页与工作流知识库检索节点直接复用,选项值引用 `KNOWLEDGE_SEARCH_MODE`。

`knowledge.ts` 的 `KNOWLEDGE_IMAGE_EXTENSIONS` 维护知识库上传和召回测试支持的图片扩展名,
`KNOWLEDGE_IMAGE_ACCEPT` 和 `KNOWLEDGE_IMAGE_TYPE_LABEL` 由它派生,供文件选择、校验与提示使用。
此配置独立于聊天、工作流等场景使用的通用文件类型配置。

`schedule.ts` 的 `SCHEDULE_OPTION` 统一提供触发器、长期记忆与知识库定时同步的周期级联选项,
包含周期文案、执行时间和间隔数值;使用方只读,配置回填与校验仍由各自组件维护。
周期类型统一使用 `api/enums/schedule.ts` 的 `SCHEDULE_TYPE`,经 `@/api/enums` 导入。
Expand Down
5 changes: 5 additions & 0 deletions ui/src/constants/knowledge.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { KNOWLEDGE_TYPE, KNOWLEDGE_SYNC_TYPE, KNOWLEDGE_SEARCH_MODE } from '@/api/enums'
import type { KnowledgeType } from '@/api/types'

/** 知识库图片上传与召回测试共用的格式、文件选择范围和展示文案。 */
export const KNOWLEDGE_IMAGE_EXTENSIONS: readonly string[] = ['JPG', 'JPEG', 'PNG', 'WEBP', 'BMP']
export const KNOWLEDGE_IMAGE_ACCEPT = KNOWLEDGE_IMAGE_EXTENSIONS.map((extension) => `.${extension.toLowerCase()}`).join(',')
export const KNOWLEDGE_IMAGE_TYPE_LABEL = KNOWLEDGE_IMAGE_EXTENSIONS.join('、')

/** 前端知识库类型标识。 */
export const KNOWLEDGE_TYPE_KEY = {
BASE: 'BASE',
Expand Down
21 changes: 14 additions & 7 deletions ui/src/conversation-panel/components/content/index.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
<template>
<component v-if="typeRenderers[content.type]" :is="typeRenderers[content.type]" :content="content" />
<TextItem v-else :content="content" />
</template>

<script setup lang="ts">
import { computed, type Component } from 'vue'
import TextItem from './items/text.vue'
import ReasoningItem from './items/reasoning.vue'
import ToolItem from './items/tool.vue'
import FormItem from './items/form.vue'
import FailureItem from './items/failure.vue'
import QuestionItem from './items/question.vue'

const typeRenderers: Record<string, any> = {
defineOptions({ name: 'ConversationContent' })

const props = defineProps<{
content: { type: string; [key: string]: unknown }
}>()

const typeRenderers: Partial<Record<string, Component>> = {
TEXT: TextItem,
REASONING: ReasoningItem,
TOOL: ToolItem,
Expand All @@ -20,5 +22,10 @@ const typeRenderers: Record<string, any> = {
QUESTION: QuestionItem,
}

defineProps<{ content: any }>()
// 未识别的内容类型继续按正文展示。
const contentRenderer = computed(() => typeRenderers[props.content.type] ?? TextItem)
</script>

<template>
<component :is="contentRenderer" :content="content" />
</template>
Loading
Loading