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
4 changes: 3 additions & 1 deletion ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ Element Plus 的 `v-infinite-scroll`。组件通过 `v-model` 管理已经加载
串行加载历史。内容不足一屏时继续补页,失败或未新增数据时停止自动补页。

默认插槽放消息,`empty` 插槽放欢迎语或空状态,加载期间不显示空状态。
`prepend` 插槽放消息与空状态共用的前置内容,位于顶部加载提示之后,首次加载期间隐藏;
是否等待历史全部加载后展示由调用方控制。
首次加载使用整块遮罩;有消息时顶部预留状态行,加载期间使用 `mk-conversation-loading`
展示加载动画。失败后可再次主动上滑重试,流式回复状态由消息列表自行渲染。
历史消息前插后按滚动高度差补偿阅读位置,不要求消息设置额外的 DOM 属性。
Expand All @@ -254,7 +256,7 @@ Element Plus 的 `v-infinite-scroll`。组件通过 `v-model` 管理已经加载
公开 `scrollToBottom()` 强制定位到底部、`followBottom()` 仅在用户位于底部附近时跟随。
聊天列表订阅流式进度后调用 `followBottom()`;只读日志无需订阅。
`message-list` 和 `log-message-list` 使用该容器,保留各自的消息正文与底部信息;聊天列表
自行展示流式圆点,欢迎语和日志空状态放入 `empty` 插槽。日志列表不注入发送能力。
自行展示流式圆点,开场白放入 `prepend` 插槽,日志空状态放入 `empty` 插槽。日志列表不注入发送能力。
消息 Store 的 `loadMore()` 在新增历史记录时返回 `true`,空页、失败或未执行时返回 `false`,
直接传给滚动容器。新会话及重置时 `hasMore` 为 `false`;已有会话主动加载首页,成功后再更新
`hasMore`。首页失败后通过重新加载首页重试,不从下一页继续。
Expand Down
4 changes: 2 additions & 2 deletions ui/src/constants/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ export const APPLICATION_DISPLAY_SETTING_DEFAULT: Readonly<ApplicationDisplaySet
disclaimer_value: '「以上内容均由 AI 生成,仅供参考和借鉴」',
custom_theme: { theme_color: '', header_font_color: '#1f2329' },
float_location: { x: { type: 'right', value: 0 }, y: { type: 'bottom', value: 30 } },
show_avatar: true,
show_user_avatar: true,
show_avatar: false,
show_user_avatar: false,
}
13 changes: 10 additions & 3 deletions ui/src/conversation-panel/components/log-message-list/index.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed } from 'vue'
import ContentList from '../content-list/index.vue'
import MessageFooter from '../message-footer/index.vue'
import ReverseInfiniteScroll from '../../core/reverse-infinite-scroll/index.vue'
import { computed } from 'vue'
import { useLogMessageListStore } from './index'

const { appInfo, messages, loading, hasMore, loadMore } = useLogMessageListStore()
Expand All @@ -17,11 +17,18 @@ const showUserAvatar = computed(() => appInfo.value?.show_user_avatar)
<template v-for="(msg, i) in messages" :key="msg.id || i">
<!-- 用户提问靠右:头像独占一行在上,提问容器在下方靠右收缩 -->
<div v-if="msg.role === 'USER'" class="flex flex-col items-end">
<ApplicationIcon v-if="showUserAvatar" :icon="userAvatar" :size="24" class="mb-2" />
<div v-if="showUserAvatar">
<el-avatar v-if="userAvatar" class="bg-transparent!" shape="square" :size="24">
<img :src="userAvatar" alt="" />
</el-avatar>
<el-avatar v-else :size="24" :style="{ background: appInfo?.custom_theme?.theme_color || 'var(--mk-primary-gradient)' }">
<img src="@/assets/mk_icon_user_gradient.svg" alt="" style="width: 50%" />
</el-avatar>
</div>
<ContentList :content-list="msg.content" />
</div>
<div v-else>
<ApplicationIcon v-if="showAvatar" :icon="avatar" :size="24" class="shrink-0 mr-2" />
<ApplicationIcon v-if="showAvatar" :icon="avatar" :size="24" class="shrink-0" />
<ContentList :content-list="msg.content" />
<!-- 助手消息底部信息栏(知识来源/执行详情/tokens/耗时) -->
<MessageFooter v-if="msg.role === 'ASSISTANT' && msg.record" :message="msg" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ defineProps<{

<template>
<!-- 发送快捷问题 -->
<div
class="quick-question mt-2 w-fit max-w-full rounded-lg border border-N300 bg-white px-3 py-1.5 leading-5 break-all"
:class="sendMessage ? 'cursor-pointer hover:bg-N100' : 'text-N500'"
@click="sendMessage?.(question)"
>
<el-button size="large" class="mt-2 rounded-xl!" plain @click="sendMessage?.(question)">
{{ question }}
</div>
</el-button>
</template>
69 changes: 23 additions & 46 deletions ui/src/conversation-panel/components/message-list/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import PrologueContent from '../prologue-content/index.vue'
import { formatPrologue } from '../prologue-content/index'
import ReverseInfiniteScroll from '../../core/reverse-infinite-scroll/index.vue'
import { useMessageListStore } from './index'
import { te } from 'element-plus/es/locale/index.mjs'

defineProps<{ appName?: string }>()

Expand All @@ -20,29 +19,37 @@ const prologue = computed(() => formatPrologue(appInfo.value?.prologue ?? ''))
const offProgress = onStreamProgress(() => void scrollRef.value?.followBottom())
onBeforeUnmount(offProgress)
const avatar = computed(() => appInfo.value?.avatar)
const user_avatar = computed(() => appInfo.value?.user_avatar)
const show_avatar = computed(() => appInfo.value?.show_avatar)
const show_user_avatar = computed(() => appInfo.value?.show_user_avatar)
const userAvatar = computed(() => appInfo.value?.user_avatar)
const showAvatar = computed(() => appInfo.value?.show_avatar)
const showUserAvatar = computed(() => appInfo.value?.show_user_avatar)
</script>

<template>
<ReverseInfiniteScroll ref="scrollRef" content-class="p-4" :loading="loading" :has-more="hasMore"
:count="messages.length" :load-more="loadMore">
<!-- 开场白固定在会话最前,历史全部加载后才显示,避免夹在已加载的消息之间 -->
<div v-if="prologue && !hasMore">
<ApplicationIcon v-if="show_avatar" :icon="avatar" :size="24" class="shrink-0 mr-2" />
<PrologueContent :prologue="prologue" :app-info="appInfo" />
</div>
<ReverseInfiniteScroll ref="scrollRef" content-class="p-4" :loading="loading" :has-more="hasMore" :count="messages.length" :load-more="loadMore">
<template #prepend>
<!-- 开场白在空会话或历史全部加载后显示,固定在会话最前 -->
<div v-if="prologue && (!messages.length || !hasMore)">
<ApplicationIcon v-if="showAvatar" :icon="avatar" :size="24" class="shrink-0" />
<PrologueContent :prologue="prologue" :app-info="appInfo" />
</div>
</template>
<template v-for="(msg, i) in messages" :key="msg.id || i">
<div>
<!-- 用户提问靠右:头像独占一行在上,提问容器在下方靠右收缩 -->
<div v-if="msg.role === 'USER'" class="flex flex-col items-end">
<ApplicationIcon v-if="show_user_avatar" :icon="user_avatar" :size="24" class="mb-2" />
<div v-if="msg.role === 'USER'">
<div v-if="showUserAvatar">
<el-avatar v-if="userAvatar" class="bg-transparent!" shape="square" :size="24">
<img :src="userAvatar" alt="" />
</el-avatar>
<el-avatar v-else :size="24" :style="{ background: appInfo?.custom_theme?.theme_color || 'var(--mk-primary-gradient)' }">
<img src="@/assets/mk_icon_user_gradient.svg" alt="" style="width: 50%" />
</el-avatar>
</div>
<ContentList :content-list="msg.content" />
<!-- 这块写用户的操作 -->
<!-- TODO 这块写用户的操作 -->
</div>
<template v-else>
<ApplicationIcon v-if="show_avatar" :icon="avatar" :size="24" class="shrink-0 mr-2" />
<ApplicationIcon v-if="showAvatar" :icon="avatar" :size="24" class="shrink-0" />
<ContentList :content-list="msg.content" />
<!-- 助手消息结束后显示底部信息栏(知识来源/执行详情/tokens/耗时) -->
<MessageFooter v-if="msg.role === 'ASSISTANT' && (msg.write_ed || msg.record)" :message="msg" />
Expand All @@ -53,37 +60,7 @@ const show_user_avatar = computed(() => appInfo.value?.show_user_avatar)
<div v-if="streamLoading">
<span class="mk-conversation-loading" />
</div>

<template #empty>
<!-- 有开场白时按首条助手消息展示,否则显示欢迎语 -->
<div v-if="prologue">
<ApplicationIcon v-if="show_avatar" :icon="avatar" :size="24" class="shrink-0 mr-2" />
<PrologueContent :prologue="prologue" :app-info="appInfo" />
</div>
</template>
</ReverseInfiniteScroll>
</template>

<style scoped lang="scss">
.welcome {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 32px 16px;
}

.welcome-title {
font-size: 18px;
font-weight: 600;
color: var(--t1, #303133);
margin-bottom: 8px;
}

.welcome-sub {
font-size: 14px;
color: var(--t3, #909399);
}
</style>
<style scoped lang="scss"></style>
10 changes: 6 additions & 4 deletions ui/src/conversation-panel/core/reverse-infinite-scroll/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useReverseInfiniteScroll, type ReverseInfiniteScrollProps } from './ind
defineOptions({ name: 'ReverseInfiniteScroll' })
const props = defineProps<ReverseInfiniteScrollProps>()
defineSlots<{
prepend?: () => unknown
default?: () => unknown
empty?: () => unknown
}>()
Expand All @@ -17,11 +18,12 @@ defineExpose({ scrollToBottom, followBottom })
<div v-loading="initialLoading" class="min-h-0 flex-1 overflow-hidden">
<el-scrollbar ref="scrollbarRef" wrap-class="[overflow-anchor:none]" view-class="min-h-full flex-column" @scroll="onScroll">
<div class="min-h-full flex-1 flex-column" :class="contentClass">
<!-- 固定预留状态行,加载提示切换时不改变消息位置。 -->
<div v-if="loadingMore" class="flex-center py-4 shrink-0">
<span class="mk-conversation-loading" />
</div>
<slot v-if="!initialLoading" name="prepend" />
<template v-if="count > 0">
<!-- 固定预留状态行,加载提示切换时不改变消息位置。 -->
<div v-if="loadingMore" class="flex-center py-4 shrink-0">
<span class="mk-conversation-loading" />
</div>
<slot />
</template>
<slot v-else-if="!loading" name="empty" />
Expand Down
4 changes: 4 additions & 0 deletions ui/src/styles/element-plus.scss
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@
&--small {
padding: 5px 11px;
}
&--large {
font-size: 14px !important;
padding: 10px 11px !important;
}
&:not(.is-link, .is-text, .is-circle) {
min-width: 80px;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,15 @@ import type { ApplicationDisplaySetting } from '@/api/types'
import { MsgError, MsgSuccess } from '@/utils/message'
import { useApplicationDetailContext } from '../../context'
import DisplaySettingPreview from './components/DisplaySettingPreview.vue'
import { APPLICATION_DISPLAY_SETTING_DEFAULT } from '@/constants/application'

defineOptions({ name: 'XpackDisplaySettingDialog' })

const { application, applicationId, resourceApi } = useApplicationDetailContext()
const visible = ref(false)
const loading = ref(false)

// 展示草稿与恢复默认;只在保存时提交,取消不回写配置。
const defaultSetting: ApplicationDisplaySetting = {
show_source: false,
show_exec: false,
language: '',
show_history: true,
draggable: true,
show_share: true,
icon: '',
chat_background: '',
avatar: '',
float_icon: '',
user_avatar: '',
disclaimer: false,
disclaimer_value: '「以上内容均由 AI 生成,仅供参考和借鉴」',
custom_theme: { theme_color: '', header_font_color: '#1f2329' },
float_location: { x: { type: 'right', value: 0 }, y: { type: 'bottom', value: 30 } },
show_avatar: true,
show_user_avatar: false,
}
const displayForm = ref<ApplicationDisplaySetting>(cloneDeep(defaultSetting))
const displayForm = ref<ApplicationDisplaySetting>(cloneDeep(APPLICATION_DISPLAY_SETTING_DEFAULT))
type ImageField = 'icon' | 'chat_background' | 'avatar' | 'user_avatar' | 'float_icon'
const imageFields: {
key: ImageField
Expand Down Expand Up @@ -69,14 +50,14 @@ function loadSetting() {
.getAccessToken(applicationId.value)
.then((config) => {
// 旧版接口可能省略展示字段,嵌套配置分别补齐默认值。
const setting = cloneDeep(defaultSetting)
const setting: ApplicationDisplaySetting = cloneDeep(APPLICATION_DISPLAY_SETTING_DEFAULT)
for (const key of Object.keys(setting) as (keyof ApplicationDisplaySetting)[]) {
if (config[key] != null) Object.assign(setting, { [key]: cloneDeep(config[key]) })
}
setting.custom_theme = { ...defaultSetting.custom_theme, ...config.custom_theme }
setting.custom_theme = { ...APPLICATION_DISPLAY_SETTING_DEFAULT.custom_theme, ...config.custom_theme }
setting.float_location = {
x: { ...defaultSetting.float_location.x, ...config.float_location?.x },
y: { ...defaultSetting.float_location.y, ...config.float_location?.y },
x: { ...APPLICATION_DISPLAY_SETTING_DEFAULT.float_location.x, ...config.float_location?.x },
y: { ...APPLICATION_DISPLAY_SETTING_DEFAULT.float_location.y, ...config.float_location?.y },
}
displayForm.value = setting
})
Expand Down Expand Up @@ -118,12 +99,12 @@ function handleImageChange(file: UploadFile, field: ImageField) {
function handleRestoreDefault() {
if (loading.value) return
releaseImagePreviews()
displayForm.value = cloneDeep(defaultSetting)
displayForm.value = cloneDeep(APPLICATION_DISPLAY_SETTING_DEFAULT)
}

function handleClosed() {
releaseImagePreviews()
displayForm.value = cloneDeep(defaultSetting)
displayForm.value = cloneDeep(APPLICATION_DISPLAY_SETTING_DEFAULT)
}

// 对照 V2 协议提交文件、已有图片 URL 和嵌套 JSON,失败保留当前草稿。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ defineExpose({ open })
<MkDialog v-model="visible" :title="groupForm.id ? '重命名用户组' : '创建用户组'" @closed="resetData">
<el-form ref="formRef" :model="groupForm" :rules="formRules" label-position="top" @submit.prevent>
<el-form-item label="用户组名称" prop="name">
<el-input v-model="groupForm.name" maxlength="128" placeholder="请输入用户组名称" show-word-limit @keyup.enter="submit" />
<el-input v-model="groupForm.name" maxlength="128" placeholder="请输入用户组名称" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ defineExpose({ open })
<MkDialog v-model="visible" :title="groupForm.id ? '重命名用户组' : '创建用户组'" @closed="resetData">
<el-form ref="formRef" :model="groupForm" :rules="formRules" label-position="top" require-asterisk-position="right" @submit.prevent>
<el-form-item label="用户组名称" prop="name">
<el-input v-model="groupForm.name" maxlength="128" placeholder="请输入用户组名称" show-word-limit @keyup.enter="submit" />
<el-input v-model="groupForm.name" maxlength="128" placeholder="请输入用户组名称" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
Expand Down
Loading