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
328 changes: 208 additions & 120 deletions apps/chat/template/agent_simple.py

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions ui/src/conversation-panel/components/header/simple-debug/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { useConversationListStore } from '../../../left-sidebar/conversation-list/index'
import HistoryPopover from '../../history-popover/index.vue'

// 左栏放外部设置表单,开合沿用会话列表 store 的侧栏状态。
const { appInfo, leftSideOpen, toggleLeftSide, newConversation } = useConversationListStore()
</script>

<template>
<header class="flex-align-center h-header shrink-0 gap-1 px-3">
<MkTooltip :content="leftSideOpen ? '收起设置' : '展开设置'">
<!-- 收起或展开左侧设置表单 -->
<el-button text @click="toggleLeftSide()">
<MkIcon name="icon_sidebar_outlined" :size="18" class="text-N900!" />
</el-button>
</MkTooltip>
<el-divider direction="vertical" class="mx-1!" />
<ApplicationIcon :icon="appInfo?.icon" :size="24" class="shrink-0" />
<h4 class="ml-1 min-w-0 flex-1 truncate" :title="appInfo?.name">{{ appInfo?.name }}</h4>
<MkTooltip content="新建对话">
<!-- 新建对话 -->
<el-button text @click="newConversation()">
<MkIcon name="icon_new-chat_outlined" :size="18" />
</el-button>
</MkTooltip>
<HistoryPopover />
</header>
</template>
128 changes: 128 additions & 0 deletions ui/src/conversation-panel/components/history-popover/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessageBox, type ScrollbarDirection } from 'element-plus'
import type { Conversation } from '../../core/types'
import { useConversationListStore } from '../../left-sidebar/conversation-list/index'
import { relativeTimeFormat } from '@/utils/time'
import { MsgConfirm } from '@/utils/message'

defineOptions({ name: 'HistoryPopover' })

const conversationList = useConversationListStore()
const visible = ref(false)
const clearing = ref(false)

// 每次打开重新加载第一页,滚动到底部时续载。
const handleShow = () => conversationList.loadConversations()
const handleEndReached = (direction: ScrollbarDirection) => {
if (direction === 'bottom') void conversationList.loadMore()
}

// 历史时间优先使用更新时间,缺少时间的记录不显示占位文案。
const conversationTime = (conversation: Conversation) => relativeTimeFormat(conversation.update_time || conversation.create_time)

/* 切换会话后收起浮层。 */
function handleSelectConversation(conversation: Conversation) {
conversationList.selectConversation(conversation.id)
visible.value = false
}

/* 重命名:去除首尾空格后提交,取消或请求失败保留原名称。 */
function handleRenameConversation(conversation: Conversation) {
ElMessageBox.prompt('', '重命名', {
inputValue: conversation.abstract || '',
inputPattern: /\S/,
inputErrorMessage: '请输入对话名称',
confirmButtonText: '确定',
cancelButtonText: '取消',
})
.then(({ value }) => conversationList.renameChat(conversation.id, value.trim()))
.catch(() => {})
}

function handleDeleteConversation(conversation: Conversation) {
MsgConfirm('确认删除该对话?', '删除后无法恢复。')
.then(() => conversationList.deleteChat(conversation.id))
.catch(() => {})
}

/*
* 批量清空:会话接口未提供批量删除,先加载全部分页,切到新对话后逐条删除;
* 中途失败时保留未删除的记录。
*/
function handleClearConversations() {
if (clearing.value) return
MsgConfirm('确认清空全部历史对话?', '清空后无法恢复。', { confirmButtonText: '清空' })
.then(async () => {
clearing.value = true
while (conversationList.hasMore.value) await conversationList.loadMore()
conversationList.newConversation()
for (const { id } of [...conversationList.conversations.value]) await conversationList.deleteChat(id)
})
.catch(() => {})
.finally(() => {
clearing.value = false
})
}
</script>

<template>
<el-popover v-model:visible="visible" trigger="click" placement="bottom-end" :width="280" popper-class="p-2!" @show="handleShow">
<template #reference>
<span class="inline-flex">
<MkTooltip content="历史对话">
<!-- 打开历史对话 -->
<el-button text>
<MkIcon name="icon_history_outlined" :size="18" />
</el-button>
</MkTooltip>
</span>
</template>
<div class="flex-between pl-2">
<h6 class="text-N600">历史对话</h6>
<MkTooltip content="清空">
<!-- 批量清空历史对话 -->
<el-button text :loading="clearing" :disabled="!conversationList.conversations.value.length" @click="handleClearConversations">
<MkIcon name="icon_delete-trash_outlined" />
</el-button>
</MkTooltip>
</div>
<el-scrollbar max-height="360px" class="mt-1" @end-reached="handleEndReached">
<div v-if="!conversationList.conversations.value.length" class="py-4 text-center text-N600">暂无历史记录</div>
<div v-else class="space-y-1">
<!-- 切换历史对话 -->
<MkListItem
v-for="conversation in conversationList.conversations.value"
:key="conversation.id"
:active="conversationList.currentChatId.value === conversation.id"
@click="handleSelectConversation(conversation)"
>
<template #default="{ active }">
<span class="min-w-0 flex-1 truncate" :title="conversation.abstract || '新对话'">
{{ conversation.abstract || '新对话' }}
</span>
<!-- 选中、悬停或聚焦列表行时隐藏时间,显示内置更多操作。 -->
<span
class="-mr-6 shrink-0 text-sm font-normal text-N500 group-hover:invisible group-focus-within:invisible"
:class="{ invisible: active }"
>
{{ conversationTime(conversation) }}
</span>
</template>
<template #action-dropdown>
<!-- 重命名历史对话 -->
<MkDropdownItem @click="handleRenameConversation(conversation)">
<template #icon><MkIcon name="icon_edit_outlined" /></template>
<span>重命名</span>
</MkDropdownItem>
<!-- 删除历史对话 -->
<MkDropdownItem @click="handleDeleteConversation(conversation)">
<template #icon><MkIcon name="icon_delete-trash_outlined" /></template>
<span>删除</span>
</MkDropdownItem>
</template>
</MkListItem>
</div>
</el-scrollbar>
</el-popover>
</template>
164 changes: 164 additions & 0 deletions ui/src/conversation-panel/view/simple-debug/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, provide, ref, toRef, useTemplateRef, watch } from 'vue'
import type { AxiosProgressEvent } from 'axios'
import type { ApplicationDetail } from '@/api/types'
import FileApi from '@/api/admin/file'
import { FILE_SOURCE_TYPE } from '@/api/enums'
import ConversationLayout from '../../core/layout/index.vue'
import ChatPanel from '../../main/chat-panel/index.vue'
import ExecutionDetail from '../../right-sidebar/execution-detail/index.vue'
import SimpleDebugHeader from '../../components/header/simple-debug/index.vue'
import { CONVERSATION_LIST_KEY } from '../../left-sidebar/conversation-list/index'
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 { createDebugConversation } from '../debug/index'

defineOptions({ name: 'SimpleDebugConversation' })

const props = defineProps<{
/** 已保存的智能体详情;调试接口执行已保存、未发布的配置,页面保存后更新。 */
application?: ApplicationDetail | null
}>()

defineSlots<{
/** 左侧设置表单,收起时保留实例。 */
setting?(): unknown
/** 标题栏下方的页面提示,如设置未保存。 */
notice?(): unknown
}>()

// 与工作流调试共用装配:Admin 调试接口、流式处理与各组件 store。
const bundle = createDebugConversation(toRef(props, 'application'))

// 动态表单上传使用当前会话,兼容文件字段的地址包装与取消操作。
provide('upload', (file: File, onProgress?: (percent: number, event: AxiosProgressEvent) => void) => {
const upload = FileApi.postUploadFile(
file,
bundle.list.currentChatId.value,
FILE_SOURCE_TYPE.CHAT,
typeof onProgress === 'function' ? onProgress : undefined,
)
return Object.assign(
upload.request.then((url) => ({ data: url })),
{ abort: upload.abort },
)
})

provide(CONVERSATION_LIST_KEY, bundle.list)
provide(MESSAGE_LIST_KEY, bundle.msgs)
provide(MESSAGE_INPUT_KEY, bundle.input)
provide(EXECUTION_DETAIL_KEY, bundle.detail)

// 左栏放设置表单而非会话列表,开合复用会话列表 store 的 leftSideOpen;历史对话由标题栏浮层切换。
// 响应式按面板自身宽度判断(嵌在 Admin 页面内,窗口宽度不代表可用宽度):窄时 drawer,宽时 push。
const SETTING_WIDTH = 520
const DETAIL_WIDTH = 420
// 两侧同时挤压展开时,对话区保留的最小宽度。
const MIN_CHAT_WIDTH = 400
const BREAKPOINT = 1000
const panelRef = useTemplateRef<HTMLElement>('panelRef')
const panelWidth = ref(BREAKPOINT)
const narrow = computed(() => panelWidth.value < BREAKPOINT)
const sideMode = computed(() => (narrow.value ? 'drawer' : 'push'))
const canPushBoth = computed(() => panelWidth.value >= SETTING_WIDTH + DETAIL_WIDTH + MIN_CHAT_WIDTH)

// 宽窄切换时同步开合:宽屏展开设置,窄屏收起设置和执行详情。
watch(
narrow,
(isNarrow) => {
bundle.list.leftSideOpen.value = !isNarrow
if (isNarrow) bundle.detail.closeRightSide()
},
{ immediate: true },
)

// 挤压态放不下两侧时互相让位:后展开的一侧收起对侧;面板变窄时保留设置、收起执行详情。
watch(
() => bundle.detail.rightSideOpen.value,
(open) => {
if (open && !narrow.value && !canPushBoth.value) bundle.list.leftSideOpen.value = false
},
)
watch(
() => bundle.list.leftSideOpen.value,
(open) => {
if (open && !narrow.value && !canPushBoth.value) bundle.detail.closeRightSide()
},
)
watch(canPushBoth, (fits) => {
if (!fits && !narrow.value && bundle.list.leftSideOpen.value) bundle.detail.closeRightSide()
})

const resizeObserver = new ResizeObserver(([entry]) => {
if (entry) panelWidth.value = entry.contentRect.width
})
onMounted(() => {
if (!panelRef.value) return
// 首次绘制前同步测量,避免窄屏先以展开态渲染再收起。
panelWidth.value = panelRef.value.clientWidth
resizeObserver.observe(panelRef.value)
})
onBeforeUnmount(() => {
resizeObserver.disconnect()
bundle.msgs.cancel()
})

// 遮罩只在抽屉态出现,点击收起设置和执行详情。
const closeDrawers = () => {
bundle.list.leftSideOpen.value = false
bundle.detail.closeRightSide()
}

// 页面保存、发布校验失败时展开设置,显示错误字段。
defineExpose({ openSetting: () => (bundle.list.leftSideOpen.value = true) })
</script>

<template>
<!-- 内嵌调试:左侧设置表单 + 主面板 + 右侧执行详情 -->
<div ref="panelRef" class="mk-simple-debug h-full">
<ConversationLayout
:left-open="bundle.list.leftSideOpen.value"
:right-open="bundle.detail.rightSideOpen.value"
:left-mode="sideMode"
:right-mode="sideMode"
:left-width="SETTING_WIDTH"
:right-width="DETAIL_WIDTH"
@mask-click="closeDrawers"
>
<template #left><slot name="setting" /></template>
<template #main>
<ChatPanel>
<template #header>
<SimpleDebugHeader />
<slot name="notice" />
</template>
</ChatPanel>
</template>
<template #right><ExecutionDetail /></template>
</ConversationLayout>
</div>
</template>

<style lang="scss">
@use '../../index.scss';

/* 设置表单使用白底;两侧抽屉沿用挤压宽度并限制占比,避免把对话区完全覆盖。 */
.mk-simple-debug .mk-conversation-side-panel {
&.left {
background: var(--color-white);
}

&.drawer {
max-width: 90%;

.mk-conversation-side-panel__inner {
width: 100%;
}
}

&.right.drawer {
--mk-conversation-side-panel-width: var(--mk-chat-right-width);
}
}
</style>
4 changes: 2 additions & 2 deletions ui/src/styles/STYLE_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ Element Plus 覆盖、主题变量和应用样式。Chat 入口还显式引入 E

## 对话模块公共样式

`src/conversation-panel/index.scss` 是对话模块公共样式入口,由模块内的
`view/chat/index.vue` 和 `view/debug/index.vue` 在 `<style lang="scss">` 中通过
`src/conversation-panel/index.scss` 是对话模块公共样式入口,由模块内各
`view/*/index.vue` 在 `<style lang="scss">` 中通过
`@use '../../index.scss';` 引入,不在脚本或应用入口重复引入。
仅放置对话模块内复用的样式,公共类名使用 `conversation-panel-` 前缀,标题栏、主面板、侧栏和遮罩
使用 `mk-conversation-` 前缀;其他组件专属布局、定位和交互样式继续保留在所属组件的 scoped
Expand Down
10 changes: 8 additions & 2 deletions ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,14 @@

### 简易智能体设置

`application-detail/setting/` 使用可折叠配置表单和调试预览占位双栏布局,仅模型设置和知识库
通过 `:expanded="true"` 默认展开,未绑定更新事件时由组件内部切换状态。通过详情上下文按
`application-detail/setting/` 使用可折叠配置表单和调试对话双栏布局,仅模型设置和知识库
通过 `:expanded="true"` 默认展开,未绑定更新事件时由组件内部切换状态。
页面整体使用 `conversation-panel/view/simple-debug/index.vue`,与工作流调试共用 Admin 调试装配,
执行已保存、未发布的配置;页面传入已保存详情,草稿与详情不一致时通过 `notice` 插槽提示先保存。
设置表单放入 `setting` 插槽,作为对话布局左栏:面板宽度不小于 1000px 时以 520px 挤压展开,
更窄时改为抽屉并默认收起;收起时保留表单实例,保存、发布校验失败时调用 `openSetting()` 展开。
执行详情与设置使用相同模式,宽度 420px;挤压态放不下两侧和 400px 对话区时,后展开的一侧收起对侧。
标题栏提供设置收起/展开、新建对话和历史对话浮层(切换、重命名、删除、清空)。通过详情上下文按
Workspace/System 资源管理范围保存、发布,发布前先保存当前配置;草稿独立深拷贝,保存失败
保留输入,保存成功后用返回详情回填。详情返回的模型别名统一回填,空语音模型 ID 提交
`null`,简易设置不提交详情中的画布或只读字段,保留范围外配置。
Expand Down
Loading
Loading