Skip to content
Merged

feat: #7672

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
18 changes: 16 additions & 2 deletions ui/src/conversation-panel/components/content/items/question.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, inject } from 'vue'
import MkAudio from '@/components/global/mk-media-preview/MkAudio.vue'
import MkVideo from '@/components/global/mk-media-preview/MkVideo.vue'
import FileUploadCard from '@/components/mk-drag-upload/FileUploadCard.vue'
Expand All @@ -9,8 +9,16 @@ import { formatFileSize } from '@/utils/number'
import { downloadByURL } from '@/utils/common'
import { copyText } from '@/utils/clipboard'

import { MESSAGE_LIST_KEY } from '../../message-list/index'
import { CONVERSATION_LIST_KEY } from '../../../left-sidebar/conversation-list/index'

defineOptions({ name: 'ConversationQuestionContent' })

// 复用对话的分享选择入口,回答生成中禁用。
const conversationList = inject(CONVERSATION_LIST_KEY, null)
const messageList = inject(MESSAGE_LIST_KEY, null)
const canShare = computed(() => Boolean(messageList?.messages.value.length && !messageList.streamLoading.value))

interface QuestionAttachment {
file_id?: string
name: string
Expand Down Expand Up @@ -93,11 +101,17 @@ function getAttachmentDescription(file: QuestionAttachment) {
</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">
<div class="h-8 -mt-3 flex items-center justify-end gap-1 invisible group-hover/question:visible">
<!-- 复制提问 -->
<el-button text @click="copyText(content.content)">
<MkIcon name="icon_copy_outlined" />
</el-button>
<!-- 进入分享模式 -->
<MkTooltip content="分享">
<el-button text :disabled="!canShare" @click="conversationList?.switchShared(true)">
<MkIcon name="icon_share_outlined" />
</el-button>
</MkTooltip>
</div>
</div>
</template>
21 changes: 19 additions & 2 deletions ui/src/conversation-panel/components/message-footer/index.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, inject } from 'vue'
// import Feedback from './feedback/index.vue'
import type { ChatContent } from '@/api/types'
import { copyText } from '@/utils/clipboard'
import { datetimeFormat } from '@/utils/time'
import type { ChatMessage, ChatRecordMeta } from '../../core/types'
import { useExecutionDetailStore } from '../../right-sidebar/execution-detail/index'

import { MESSAGE_LIST_KEY } from '../message-list/index'
import { CONVERSATION_LIST_KEY } from '../../left-sidebar/conversation-list/index'

const props = defineProps<{ message: ChatMessage }>()

// 复用对话的分享选择入口,回答生成中禁用。
const conversationList = inject(CONVERSATION_LIST_KEY, null)
const messageList = inject(MESSAGE_LIST_KEY, null)
const canShare = computed(() => Boolean(messageList?.messages.value.length && !messageList.streamLoading.value))

const detail = useExecutionDetailStore()

const EMPTY: ChatRecordMeta = {
Expand Down Expand Up @@ -68,8 +76,17 @@ const openExecutionDetail = () => {
<MkIcon name="icon_copy_outlined" />
</el-button>
</MkTooltip>
<!-- 进入分享模式 -->
<span>
<MkTooltip content="分享">
<el-button text :disabled="!canShare" @click="conversationList?.switchShared(true)">
<MkIcon name="icon_share_outlined" />
</el-button>
</MkTooltip>
</span>

<!-- <Feedback :message="message" /> -->
<span v-if="meta.createTime" class="text-N600">{{ datetimeFormat(meta.createTime) }}</span>
<span v-if="meta.createTime" class="text-N600 ml-2">{{ datetimeFormat(meta.createTime) }}</span>
</div>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { computed, inject, ref, type InjectionKey, type Ref } from 'vue'
import { v7 as uuidv7 } from 'uuid'
import type { SidePanelMode } from '../../core/layout/types'
import type { Conversation, ConversationAccount, ConversationAppInfo } from '../../core/types'

// 由 Chat / Debug 入口注入本组件需要的会话接口。
Expand Down Expand Up @@ -27,9 +28,13 @@ export function createConversationListStore(deps: ConversationListDeps) {
const pageSize = 50
const currentPage = ref(1)
const hasMore = ref(true)
// 布局模式由 View 创建后绑定,分享入口统一在抽屉模式下收起左栏。
let leftSideMode: Readonly<Ref<SidePanelMode>> | undefined
const bindLeftSideMode = (mode: Readonly<Ref<SidePanelMode>>) => (leftSideMode = mode)
const isShared = ref<boolean>(false)
const switchShared=(share:boolean)=>{
isShared.value=share
const switchShared = (share: boolean) => {
isShared.value = share
if (share && leftSideMode?.value === 'drawer') leftSideOpen.value = false
}
const loadConversations = async (page = 1) => {
try {
Expand Down Expand Up @@ -103,6 +108,7 @@ export function createConversationListStore(deps: ConversationListDeps) {
return {
isShared,
switchShared,
bindLeftSideMode,
conversations,
currentChatId,
currentConversation,
Expand Down
23 changes: 20 additions & 3 deletions ui/src/conversation-panel/left-sidebar/conversation-list/index.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { MsgConfirm } from '@/utils/message'
import RenameConversation from '../../components/rename/index.vue'
import { relativeTimeFormat } from '@/utils/time'
import type { Conversation } from '../../core/types'
import { useConversationListStore } from './index'
import AccountMenu from '../account-menu/index.vue'

import { useMessageListStore } from '../../components/message-list/index'
import { useViewType, VIEW_TYPE } from '../../view/index'

defineOptions({ name: 'ConversationList' })

const { appInfo, conversations, currentChatId, selectConversation, newConversation, deleteChat, toggleLeftSide, hasMore, loadMore } =
const viewType = useViewType()
const msgs = useMessageListStore()
const canShare = computed(() => msgs.messages.value.length > 0 && !msgs.streamLoading.value)

const { appInfo, conversations, currentChatId, switchShared, selectConversation, newConversation, deleteChat, toggleLeftSide, hasMore, loadMore } =
useConversationListStore()

// 历史时间优先使用更新时间,缺少时间的记录不显示占位文案。
Expand Down Expand Up @@ -99,10 +106,20 @@ function handleClearConversations() {
<el-button text><MkIcon name="icon_more_outlined" /></el-button>
<template #dropdown>
<MkDropdownMenu>
<!-- 分享当前激活对话 -->
<MkDropdownItem
v-if="viewType === VIEW_TYPE.CHAT && currentChatId === conversation.id"
:disabled="!canShare"
@click="switchShared(true)"
>
<template #icon><MkIcon name="icon_share_outlined" /></template>
<span>分享</span>
</MkDropdownItem>
<!-- 重命名 -->
<RenameConversation :conversation="conversation" />

<!-- 删除历史对话 -->
<MkDropdownItem divided @click="handleDeleteConversation(conversation)">
<MkDropdownItem @click="handleDeleteConversation(conversation)">
<template #icon><MkIcon name="icon_delete-trash_outlined" /></template>
<span>删除</span>
</MkDropdownItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'
import ConversationApi from '@/api/chat/conversation'
import type { PortalConversationSummary } from '@/api/types'
import { relativeTimeFormat } from '@/utils/time'
import { useConversationListStore } from '../conversation-list/index'
import { usePortalConversationListStore } from './index'
import RenameConversation from '../../components/rename/index.vue'

import { useMessageListStore } from '../../components/message-list/index'

defineOptions({ name: 'HistoryConversationDrawer' })
const props = defineProps<{ applicationId: string }>()
const visible = defineModel<boolean>({ required: true })
const portalList = usePortalConversationListStore()
const conversationList = useConversationListStore()
const msgs = useMessageListStore()
const canShare = computed(() => msgs.messages.value.length > 0 && !msgs.streamLoading.value)

// 开启当前对话的分享选择,并关闭历史抽屉。
const handleShareConversation = () => {
if (!canShare.value) return
conversationList.switchShared(true)
visible.value = false
}
const selectedApplicationId = ref(props.applicationId)
const keyword = ref('')
const conversations = ref<PortalConversationSummary[]>([])
Expand Down Expand Up @@ -89,14 +100,20 @@ const handleDeleteConversation = (conversation: PortalConversationSummary) => {
<el-button text><MkIcon name="icon_more_outlined" /></el-button>
<template #dropdown>
<MkDropdownMenu>
<!-- 分享当前激活对话 -->
<MkDropdownItem v-if="isActiveConversation(conversation)" :disabled="!canShare" @click="handleShareConversation">
<template #icon><MkIcon name="icon_share_outlined" /></template>
<span>分享</span>
</MkDropdownItem>
<!-- 重命名 -->
<RenameConversation
:conversation="conversation"
:application-id="selectedApplicationId"
@renamed="conversation.abstract = $event"
/>

<!-- 删除历史对话 -->
<MkDropdownItem divided @click="handleDeleteConversation(conversation)">
<MkDropdownItem @click="handleDeleteConversation(conversation)">
<template #icon><MkIcon name="icon_delete-trash_outlined" /></template>
<span>删除</span>
</MkDropdownItem>
Expand Down
3 changes: 2 additions & 1 deletion ui/src/conversation-panel/view/chat/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ provide(SHARE_PANEL_CONTENT_KEY, {
...bundle.msgs,
title: computed(() => bundle.list.currentConversation.value?.abstract || '新建对话'),
})
provide(VIEW_KEY,VIEW_TYPE.CHAT)
provide(VIEW_KEY, VIEW_TYPE.CHAT)
// HTML 内容 iframe 内 chatUserProfile() 取当前对话用户
provide(CHAT_USER_PROFILE_KEY, createChatUserProfileGetter())

Expand All @@ -50,6 +50,7 @@ const BREAKPOINT = 768
const isMobile = ref(window.innerWidth < BREAKPOINT)
const onResize = () => (isMobile.value = window.innerWidth < BREAKPOINT)
const leftMode = computed(() => (isMobile.value ? 'drawer' : 'push'))
bundle.list.bindLeftSideMode(leftMode)
const rightMode = computed(() => (isMobile.value ? 'drawer' : 'push'))
// 左栏为抽屉即手机端
provide(DEVICE_KEY, createDeviceType(leftMode))
Expand Down
5 changes: 2 additions & 3 deletions ui/src/conversation-panel/view/portal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import { useRoute, useRouter } from 'vue-router'
import ConversationApi from '@/api/chat/conversation'
import PortalApi from '@/api/chat/portal'
import { createPortalConversationListStore, type PortalConversationListStore } from '../../left-sidebar/portal-conversation-list/index'
import { createChatConversation, type ChatConversationRouting } from '../chat/index'
import type { ConversationBundle } from '../types'
import { createChatConversation, type ChatConversationBundle, type ChatConversationRouting } from '../chat/index'

export interface PortalConversationBundle extends ConversationBundle {
export interface PortalConversationBundle extends ChatConversationBundle {
portalList: PortalConversationListStore
}

Expand Down
5 changes: 4 additions & 1 deletion ui/src/conversation-panel/view/portal/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { MESSAGE_LIST_KEY } from '../../components/message-list/index'
import { MESSAGE_INPUT_KEY } from '../../components/message-input/index'
import { EXECUTION_DETAIL_KEY } from '../../right-sidebar/execution-detail/index'
import { PORTAL_CONVERSATION_LIST_KEY } from '../../left-sidebar/portal-conversation-list/index'
import { SHARE_PANEL_KEY } from '../../main/share-panel/index'
import { createPortalConversation } from './index'
import { createChatUserProfileGetter } from '../chat/index'
import { VIEW_KEY, VIEW_TYPE, DEVICE_KEY, CHAT_USER_PROFILE_KEY, createDeviceType } from '../index.ts'
Expand All @@ -40,15 +41,17 @@ provide(CONVERSATION_LIST_KEY, bundle.list)
provide(MESSAGE_LIST_KEY, bundle.msgs)
provide(MESSAGE_INPUT_KEY, bundle.input)
provide(EXECUTION_DETAIL_KEY, bundle.detail)
provide(SHARE_PANEL_KEY, bundle.share)
provide(PORTAL_CONVERSATION_LIST_KEY, bundle.portalList)
provide(VIEW_KEY,VIEW_TYPE.PORTAL)
provide(VIEW_KEY, VIEW_TYPE.PORTAL)
// HTML 内容 iframe 内 chatUserProfile() 取当前对话用户
provide(CHAT_USER_PROFILE_KEY, createChatUserProfileGetter())
// 响应式:窄屏 drawer,宽屏 push
const BREAKPOINT = 768
const isMobile = ref(window.innerWidth < BREAKPOINT)
const onResize = () => (isMobile.value = window.innerWidth < BREAKPOINT)
const leftMode = computed(() => (isMobile.value ? 'drawer' : 'push'))
bundle.list.bindLeftSideMode(leftMode)
const rightMode = computed(() => (isMobile.value ? 'drawer' : 'push'))
// 左栏为抽屉即手机端
provide(DEVICE_KEY, createDeviceType(leftMode))
Expand Down
4 changes: 4 additions & 0 deletions ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ MCP 配置入口使用独立权限。导出 Skills 权限已定义,知识库
`WebKnowledgeDrawer`、`LarkKnowledgeDialog`、`WorkflowKnowledgeDialog`。
`KnowledgeBaseForm` 负责名称、描述及 Embedding 选择,内部按范围查询模型;`mode` 默认为 `create`,
未选择时默认首个可用模型,刷新不覆盖用户选择;基础设置传入 `mode="setting"`,不自动选择模型,保留详情空值。
基础设置在详情回填后单独校验向量模型,未配置时立即显示必填提示。
创建浮层接收完整 Knowledge API,工作流创建使用其 `postKnowledgeWorkflow`。
导入创建成功后先刷新基础资料权限与列表,再提示设置向量模型并向量化文档;确认后按知识库类型进入当前范围的基础设置页,关闭提示保留在当前页。
“导出知识库”先弹窗确认是否同时导出源文件,默认不勾选;确认后通过当前范围 API 导出,成功关闭、失败保留选项,关闭后清理选择。文档 Excel、ZIP 仍直接导出。
Expand Down Expand Up @@ -501,6 +502,9 @@ View 负责路由、资源与模式、图加载、保存/发布/调试编排
全部智能体与选择抽屉复用卡片,抽屉每次打开重新查询,搜索状态独立。
门户历史侧栏仅展示有历史的智能体及最近 5 条会话,剩余数量取分组总数;“查看更多”打开历史对话抽屉,
支持切换智能体、摘要搜索、滚动分页及打开/删除会话,删除后同步刷新侧栏。
单应用历史列表与门户历史抽屉仅在当前激活会话的菜单显示分享入口;无消息或回答生成中不可分享。
门户复用 Chat 分享 Store,抽屉点击分享后关闭并进入消息选择模式。
Chat/Portal 视图将左栏布局模式绑定到会话列表 Store;开启分享时,抽屉模式自动收起左栏。

### 对话消息反馈

Expand Down
19 changes: 3 additions & 16 deletions ui/src/views/chat/share/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, provide, ref, watch } from 'vue'
import { computed, onMounted, provide, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import ConversationApi from '@/api/chat/conversation'
import type { ChatShareDetail } from '@/api/types'
Expand All @@ -13,7 +13,6 @@ const route = useRoute()
const shareId = computed(() => String(route.params.shareId || ''))
const detail = ref<ChatShareDetail | null>(null)
const loading = ref(false)
const failed = ref(false)

// 公开接口按时间正序返回全部记录,不请求原会话或加载更多历史。
const messages = computed<ChatMessage[]>(() =>
Expand All @@ -40,24 +39,12 @@ provide(SHARE_PANEL_CONTENT_KEY, {
loadMore: async () => false,
})

// 请求序号隔离切换链接及卸载后的响应,防止旧记录覆盖当前页面。
let requestVersion = 0
const loadShare = async () => {
const version = ++requestVersion
detail.value = null
loading.value = true
failed.value = false
try {
const response = await ConversationApi.getShareConversation(shareId.value)
if (version === requestVersion) detail.value = response
} catch {
if (version === requestVersion) failed.value = true
} finally {
if (version === requestVersion) loading.value = false
}
await ConversationApi.getShareConversation(shareId.value)
}
watch(shareId, loadShare, { immediate: true })
onBeforeUnmount(() => requestVersion++)
onMounted(loadShare)
</script>

<template>
Expand Down
9 changes: 6 additions & 3 deletions ui/src/views/knowledge-detail/setting/base/index.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { perm } from '@/permission'
import type { ResourceDetailPageProps } from '@/layout/ResourceDetailLayout.vue'
import { computed, reactive, ref, useTemplateRef, watch } from 'vue'
import { computed, nextTick, reactive, ref, useTemplateRef, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { cloneDeep } from 'lodash'
import { KNOWLEDGE_TYPE } from '@/api/enums'
Expand All @@ -10,7 +10,7 @@ import { KNOWLEDGE_TYPE_LABELS, KNOWLEDGE_TYPE_MAP } from '@/constants/knowledge
import { MsgConfirm, MsgSuccess } from '@/utils/message'
import KnowledgeBaseForm from '@/views/knowledge/create-knowledge/components/KnowledgeBaseForm.vue'
import { useKnowledgeDetailContext } from '../../context'
import {isWorkspaceSharedResource, isSystemResource, isSystemSharedResource } from '@/utils/resource-context'
import { isWorkspaceSharedResource, isSystemResource, isSystemSharedResource } from '@/utils/resource-context'
/**
* 知识库「设置」权限判断
*/
Expand Down Expand Up @@ -61,7 +61,7 @@ const knowledgeTypeDescriptions = {
// 等待异步详情与基本信息表单挂载完成后回填,不直接修改详情中的配置。
watch(
[knowledge, baseFormRef],
([detail, baseForm]) => {
async ([detail, baseForm]) => {
if (!detail || !baseForm) return
Object.assign(baseForm.form, {
name: detail.name,
Expand All @@ -80,6 +80,9 @@ watch(
file_size_limit: detail.file_size_limit ?? 100,
})
settingFormRef.value?.clearValidate()
// 回填后校验向量模型,进入设置时即提示尚未配置的模型。
await nextTick()
await baseForm.validateEmbeddingModel()
},
{ immediate: true, flush: 'post' },
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ function validate() {
return formRef.value?.validate().catch(() => false) ?? Promise.resolve(false)
}

function validateEmbeddingModel() {
return formRef.value?.validateField('embedding_model_id').catch(() => false) ?? Promise.resolve(false)
}

function reset() {
Object.assign(form, {
name: '',
Expand Down Expand Up @@ -65,7 +69,7 @@ function loadModelOptions() {
}

onMounted(() => loadModelOptions())
defineExpose({ form, validate, reset })
defineExpose({ form, validate, validateEmbeddingModel, reset })
</script>

<template>
Expand Down
Loading