feat: 接入聊天历史界面与外链会话恢复
- 新增管理端与用户端聊天历史接口和页面 - 外链聊天支持访问令牌登录、身份保活与当前会话恢复 - 聊天执行链路切到统一 runtime 与 chatlog 查询接口
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { createApp, watchEffect } from 'vue';
|
||||
import { BubbleList, Sender, Thinking, XMarkdown } from 'vue-element-plus-x';
|
||||
import { BubbleList, Sender, XMarkdown } from 'vue-element-plus-x';
|
||||
|
||||
import { registerAccessDirective } from '@easyflow/access';
|
||||
import { registerLoadingDirective } from '@easyflow/common-ui';
|
||||
@@ -41,7 +41,6 @@ async function bootstrap(namespace: string) {
|
||||
app.component('ElBubbleList', BubbleList);
|
||||
app.component('ElSender', Sender);
|
||||
app.component('ElXMarkdown', XMarkdown);
|
||||
app.component('ElThinking', Thinking);
|
||||
|
||||
// 注册EasyFlow提供的v-loading和v-spinning指令
|
||||
registerLoadingDirective(app, {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ChatThinkingBlock } from '@easyflow/common-ui';
|
||||
import { IconifyIcon } from '@easyflow/icons';
|
||||
import { useUserStore } from '@easyflow/stores';
|
||||
|
||||
@@ -48,13 +49,14 @@ function getUserAvatar() {
|
||||
v-for="(chain, index) in item.chains"
|
||||
:key="chain.id || index"
|
||||
>
|
||||
<ElThinking
|
||||
<ChatThinkingBlock
|
||||
v-if="!('id' in chain)"
|
||||
v-model="chain.thinlCollapse"
|
||||
v-model:expanded="chain.thinkingExpanded"
|
||||
:content="chain.reasoning_content"
|
||||
:status="chain.thinkingStatus"
|
||||
class="chat-thinking-block-item"
|
||||
/>
|
||||
<ElCollapse v-else class="mb-2">
|
||||
<ElCollapse v-else class="chat-tool-panel">
|
||||
<ElCollapseItem :title="chain.name" :name="chain.id">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2 pl-5">
|
||||
@@ -90,41 +92,6 @@ function getUserAvatar() {
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- <ElThinking
|
||||
v-if="item.reasoning_content"
|
||||
v-model="item.thinlCollapse"
|
||||
:content="item.reasoning_content"
|
||||
:status="item.thinkingStatus"
|
||||
/> -->
|
||||
<!-- <ElCollapse v-if="item.tools" class="mb-2">
|
||||
<ElCollapseItem
|
||||
class="mb-2"
|
||||
v-for="tool in item.tools"
|
||||
:key="tool.id"
|
||||
:title="tool.name"
|
||||
:name="tool.id"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2 pl-5">
|
||||
<ElIcon size="16">
|
||||
<IconifyIcon icon="svg:wrench" />
|
||||
</ElIcon>
|
||||
<span>{{ tool.name }}</span>
|
||||
<template v-if="tool.status === 'TOOL_CALL'">
|
||||
<ElIcon size="16">
|
||||
<IconifyIcon icon="svg:spinner" />
|
||||
</ElIcon>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElIcon size="16" color="var(--el-color-success)">
|
||||
<CircleCheck />
|
||||
</ElIcon>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<ShowJson :value="tool.result" />
|
||||
</ElCollapseItem>
|
||||
</ElCollapse> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -162,23 +129,30 @@ function getUserAvatar() {
|
||||
--bubble-content-max-width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-thinking) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.el-thinking .content-wrapper) {
|
||||
--el-thinking-content-wrapper-width: 100%;
|
||||
|
||||
.chat-thinking-block-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-collapse) {
|
||||
:deep(.chat-tool-panel.el-collapse) {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-collapse-border-color);
|
||||
border-radius: 8px;
|
||||
border: 1px solid hsl(var(--divider-faint) / 0.26);
|
||||
border-radius: 14px;
|
||||
background: hsl(var(--surface-panel) / 0.7);
|
||||
box-shadow: inset 0 1px 0 hsl(var(--glass-border) / 0.2);
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__content) {
|
||||
:deep(.chat-tool-panel .el-collapse-item__wrap) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.chat-tool-panel .el-collapse-item__header) {
|
||||
min-height: 44px;
|
||||
padding-right: 14px;
|
||||
background: transparent;
|
||||
border-bottom-color: hsl(var(--divider-faint) / 0.16);
|
||||
}
|
||||
|
||||
:deep(.chat-tool-panel .el-collapse-item__content) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -49,14 +49,16 @@ defineExpose({
|
||||
|
||||
function getSessionList(resetSession = false) {
|
||||
api
|
||||
.get('/userCenter/botConversation/list', {
|
||||
.get('/userCenter/chatHistory/sessions', {
|
||||
params: {
|
||||
botId: props.bot.id,
|
||||
assistantId: props.bot.id,
|
||||
pageNumber: 1,
|
||||
pageSize: 100,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
sessionList.value = res.data;
|
||||
sessionList.value = res.data.records || [];
|
||||
if (resetSession) {
|
||||
currentSession.value = {};
|
||||
}
|
||||
@@ -92,15 +94,27 @@ function clickSession(session: any) {
|
||||
}
|
||||
function getMessageList() {
|
||||
api
|
||||
.get('/userCenter/botMessage/getMessages', {
|
||||
params: {
|
||||
botId: props.bot.id,
|
||||
conversationId: currentSession.value.id,
|
||||
},
|
||||
.get(`/userCenter/chatHistory/sessions/${currentSession.value.id}/messages`, {
|
||||
params: { pageNumber: 1, pageSize: 100 },
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
props.onMessageList?.(res.data);
|
||||
const records = Array.isArray(res.data?.records) ? [...res.data.records] : [];
|
||||
props.onMessageList?.(
|
||||
records.reverse().map((item: any) => ({
|
||||
key: String(item.id),
|
||||
role: item.senderRole === 'assistant' ? 'assistant' : 'user',
|
||||
content:
|
||||
item.senderRole === 'assistant'
|
||||
? String(item.contentText || '').replace(/^Final Answer:\s*/i, '')
|
||||
: item.contentText,
|
||||
placement: item.senderRole === 'assistant' ? 'start' : 'end',
|
||||
created: item.created,
|
||||
chains: Array.isArray(item.contentPayload?.chains)
|
||||
? item.contentPayload.chains
|
||||
: undefined,
|
||||
})),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -125,12 +139,8 @@ const updateLoading = ref(false);
|
||||
function updateTitle() {
|
||||
updateLoading.value = true;
|
||||
api
|
||||
.get('/userCenter/botConversation/updateConversation', {
|
||||
params: {
|
||||
botId: props.bot.id,
|
||||
conversationId: currentSession.value.id,
|
||||
.post(`/userCenter/chatHistory/sessions/${currentSession.value.id}/rename`, {
|
||||
title: currentSession.value.title,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
updateLoading.value = false;
|
||||
@@ -150,12 +160,7 @@ function remove(row: any) {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.get('/userCenter/botConversation/deleteConversation', {
|
||||
params: {
|
||||
botId: props.bot.id,
|
||||
conversationId: row.id,
|
||||
},
|
||||
})
|
||||
.post(`/userCenter/chatHistory/sessions/${row.id}/delete`)
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
@@ -175,6 +180,11 @@ function remove(row: any) {
|
||||
},
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function openRenameDialog(row: any) {
|
||||
currentSession.value = { ...row };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -230,7 +240,7 @@ function remove(row: any) {
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ formatCreatedTime(conversation.created) }}
|
||||
{{ formatCreatedTime(conversation.lastMessageAt || conversation.accessAt || conversation.created) }}
|
||||
</span>
|
||||
<ElDropdown
|
||||
:class="
|
||||
@@ -249,7 +259,7 @@ function remove(row: any) {
|
||||
@mouseenter="handleMouseEvent(conversation.id)"
|
||||
@mouseleave="handleMouseEvent()"
|
||||
>
|
||||
<ElDropdownItem @click="dialogVisible = true">
|
||||
<ElDropdownItem @click="openRenameDialog(conversation)">
|
||||
<ElButton link :icon="Edit">编辑</ElButton>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChatThinkingBlockStatus } from '@easyflow/common-ui';
|
||||
import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
|
||||
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
|
||||
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
@@ -16,8 +16,8 @@ import ChatFileUploader from '#/components/upload/ChatFileUploader.vue';
|
||||
|
||||
type Think = {
|
||||
reasoning_content?: string;
|
||||
thinkingStatus?: ThinkingStatus;
|
||||
thinlCollapse?: boolean;
|
||||
thinkingExpanded?: boolean;
|
||||
thinkingStatus?: ChatThinkingBlockStatus;
|
||||
};
|
||||
|
||||
type Tool = {
|
||||
@@ -152,7 +152,7 @@ function sendMessage() {
|
||||
if (index === -1) {
|
||||
chains.push({
|
||||
thinkingStatus: 'thinking',
|
||||
thinlCollapse: true,
|
||||
thinkingExpanded: false,
|
||||
reasoning_content: delta,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -92,20 +92,6 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ChatHistoryShare',
|
||||
path: '/share/:id',
|
||||
component: () => import('#/views/chatHistory/share/index.vue'),
|
||||
meta: {
|
||||
title: '分享',
|
||||
noBasicLayout: true,
|
||||
hideInMenu: true,
|
||||
hideInTab: true,
|
||||
hideInBreadcrumb: true,
|
||||
ignoreAccess: true,
|
||||
loaded: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export { coreRoutes, fallbackNotFoundRoute };
|
||||
|
||||
@@ -28,15 +28,20 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
icon: 'svg:chat-history',
|
||||
order: 80,
|
||||
title: '聊天记录',
|
||||
title: '聊天历史',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ChatHistoryDetails',
|
||||
path: '/chatHistory/:id',
|
||||
component: () => import('#/views/chatHistory/details/index.vue'),
|
||||
redirect: (to) => ({
|
||||
path: '/chatHistory',
|
||||
query: {
|
||||
sessionId: String(to.params.id || ''),
|
||||
},
|
||||
}),
|
||||
meta: {
|
||||
title: '聊天记录',
|
||||
title: '聊天历史',
|
||||
hideInMenu: true,
|
||||
hideInTab: true,
|
||||
hideInBreadcrumb: true,
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { IconifyIcon } from '@easyflow/icons';
|
||||
import { copyToClipboard } from '@easyflow/utils';
|
||||
|
||||
import { ArrowLeft, Delete, MoreFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElContainer,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElHeader,
|
||||
ElMain,
|
||||
ElMessage,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { ChatBubbleList } from '#/components/chat';
|
||||
import { router } from '#/router';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const ids = reactive({
|
||||
botId: '',
|
||||
conversationId: '',
|
||||
});
|
||||
const conversationInfo = ref<any>();
|
||||
const messageList = ref<any[]>([]);
|
||||
const loading = ref(true);
|
||||
|
||||
onMounted(() => {
|
||||
if (route.params.id) {
|
||||
ids.conversationId = route.params.id as string;
|
||||
getConversationDetails();
|
||||
}
|
||||
});
|
||||
|
||||
function getConversationDetails() {
|
||||
api
|
||||
.get('/userCenter/botConversation/detail', {
|
||||
params: {
|
||||
id: ids.conversationId,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
conversationInfo.value = res.data;
|
||||
ids.botId = res.data.botId;
|
||||
getMessageList();
|
||||
}
|
||||
});
|
||||
}
|
||||
function getMessageList() {
|
||||
api
|
||||
.get('/userCenter/botMessage/getMessages', {
|
||||
params: ids,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
messageList.value = res.data;
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
async function handleShare() {
|
||||
const shareLink = import.meta.env.DEV
|
||||
? `${location.origin}/share/${ids.conversationId}`
|
||||
: `${location.origin}/#/share/${ids.conversationId}`;
|
||||
const { success, error } = await copyToClipboard(shareLink);
|
||||
|
||||
if (success) {
|
||||
ElMessage.success('分享链接复制成功!');
|
||||
} else {
|
||||
ElMessage.error(error);
|
||||
}
|
||||
}
|
||||
async function handleDelete() {
|
||||
const [, res] = await tryit(api.post)('/userCenter/botConversation/remove', {
|
||||
id: ids.conversationId,
|
||||
});
|
||||
|
||||
if (res && res.errorCode === 0) {
|
||||
ElMessage.success('删除成功');
|
||||
router.back();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElContainer class="bg-background h-full">
|
||||
<ElHeader height="100px" class="border-border border-b !pr-10">
|
||||
<div class="flex h-full w-full items-center justify-between">
|
||||
<!-- Left -->
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton
|
||||
link
|
||||
style="font-size: 20px"
|
||||
:icon="ArrowLeft"
|
||||
@click="router.back()"
|
||||
/>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-lg font-medium">{{
|
||||
conversationInfo?.title
|
||||
}}</span>
|
||||
<div
|
||||
v-if="conversationInfo?.bot.title"
|
||||
class="text-foreground/70 rounded bg-[var(--el-fill-color-light)] p-1 text-xs"
|
||||
>
|
||||
{{ conversationInfo.bot.title }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-foreground/50 text-sm">{{
|
||||
conversationInfo?.created
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right -->
|
||||
<div class="flex items-center gap-5">
|
||||
<ElButton link style="font-size: 20px" @click="handleShare">
|
||||
<template #icon>
|
||||
<IconifyIcon icon="svg:share" />
|
||||
</template>
|
||||
</ElButton>
|
||||
<ElDropdown>
|
||||
<ElButton link style="font-size: 20px" :icon="MoreFilled" />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownItem
|
||||
style="color: var(--el-color-danger)"
|
||||
:icon="Delete"
|
||||
@click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</ElDropdownItem>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</ElHeader>
|
||||
<ElMain class="relative" v-loading="loading">
|
||||
<div
|
||||
class="absolute bottom-5 left-1/2 top-5 w-full max-w-[1000px] -translate-x-1/2"
|
||||
>
|
||||
<ChatBubbleList
|
||||
:bot="conversationInfo?.bot"
|
||||
:messages="messageList"
|
||||
:editable="false"
|
||||
:open-editor="() => {}"
|
||||
/>
|
||||
</div>
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
:deep(.el-bubble-list) {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-bubble-content-wrapper .el-bubble-content) {
|
||||
--bubble-content-max-width: calc(100% - 52px);
|
||||
}
|
||||
</style>
|
||||
@@ -1,177 +1,367 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Delete, MoreFilled, Search } from '@element-plus/icons-vue';
|
||||
import { Delete, Edit, Search } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElContainer,
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElDrawer,
|
||||
ElEmpty,
|
||||
ElHeader,
|
||||
ElInput,
|
||||
ElMain,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElPagination,
|
||||
ElSelect,
|
||||
ElSpace,
|
||||
ElText,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
|
||||
const listTitles = ['聊天助理名称', '话题', '创建时间', '操作'];
|
||||
import { ChatBubbleList } from '#/components/chat';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const assistantList = ref<any[]>([]);
|
||||
const queryParams = ref<any>({});
|
||||
const pageRef = ref();
|
||||
|
||||
onMounted(() => {
|
||||
getAssistantList();
|
||||
const assistantList = ref<any[]>([]);
|
||||
const sessions = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const queryParams = ref({
|
||||
assistantId: undefined as number | undefined,
|
||||
keyword: '',
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
|
||||
async function getAssistantList() {
|
||||
api
|
||||
.get('/userCenter/bot/list', {
|
||||
params: { ...queryParams.value, status: 1 },
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
assistantList.value = res.data.map((item: any) => ({
|
||||
label: item.title,
|
||||
value: item.id,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
function search() {
|
||||
pageRef.value.setQuery({ ...queryParams.value, status: 1 });
|
||||
}
|
||||
function toDetail(record: any) {
|
||||
router.push({ path: `/chatHistory/${record.id}` });
|
||||
}
|
||||
async function handleDelete(id: string) {
|
||||
const [, res] = await tryit(api.post)('/userCenter/botConversation/remove', {
|
||||
id,
|
||||
});
|
||||
const pageState = ref({
|
||||
total: 0,
|
||||
});
|
||||
|
||||
if (res && res.errorCode === 0) {
|
||||
search();
|
||||
ElMessage.success('删除成功');
|
||||
const drawerVisible = ref(false);
|
||||
const drawerLoading = ref(false);
|
||||
const currentSession = ref<any>();
|
||||
const messageList = ref<any[]>([]);
|
||||
const messagePage = ref({
|
||||
total: 0,
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
|
||||
const filteredSessions = computed(() => {
|
||||
const keyword = queryParams.value.keyword.trim().toLowerCase();
|
||||
if (!keyword) {
|
||||
return sessions.value;
|
||||
}
|
||||
return sessions.value.filter((item) => {
|
||||
const title = String(item.title || '').toLowerCase();
|
||||
const preview = String(item.lastMessagePreview || '').toLowerCase();
|
||||
const assistantName = String(item.assistantName || '').toLowerCase();
|
||||
return title.includes(keyword) || preview.includes(keyword) || assistantName.includes(keyword);
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([fetchAssistants(), fetchSessions()]);
|
||||
const sessionId = route.query.sessionId ? String(route.query.sessionId) : '';
|
||||
if (sessionId) {
|
||||
await openSession(sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.query.sessionId,
|
||||
async (sessionId) => {
|
||||
if (!sessionId) {
|
||||
drawerVisible.value = false;
|
||||
currentSession.value = undefined;
|
||||
messageList.value = [];
|
||||
return;
|
||||
}
|
||||
if (!currentSession.value || String(currentSession.value.id) !== String(sessionId)) {
|
||||
await openSession(String(sessionId));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
async function fetchAssistants() {
|
||||
const [, res] = await tryit(api.get)('/userCenter/bot/list', {
|
||||
params: { status: 1 },
|
||||
});
|
||||
if (res?.errorCode === 0) {
|
||||
assistantList.value = (res.data || []).map((item: any) => ({
|
||||
label: item.title,
|
||||
value: item.id,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSessions() {
|
||||
loading.value = true;
|
||||
const [, res] = await tryit(api.get)('/userCenter/chatHistory/sessions', {
|
||||
params: {
|
||||
assistantId: queryParams.value.assistantId,
|
||||
pageNumber: queryParams.value.pageNumber,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
},
|
||||
});
|
||||
loading.value = false;
|
||||
if (res?.errorCode === 0) {
|
||||
sessions.value = res.data?.records || [];
|
||||
pageState.value.total = res.data?.total || 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function openSession(sessionId: string | number) {
|
||||
drawerLoading.value = true;
|
||||
const [, summaryRes] = await tryit(api.get)(`/userCenter/chatHistory/sessions/${sessionId}`);
|
||||
if (summaryRes?.errorCode !== 0) {
|
||||
drawerLoading.value = false;
|
||||
return;
|
||||
}
|
||||
currentSession.value = summaryRes.data;
|
||||
messageList.value = [];
|
||||
messagePage.value = {
|
||||
total: 0,
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
};
|
||||
drawerVisible.value = true;
|
||||
await loadMessages(true);
|
||||
drawerLoading.value = false;
|
||||
if (String(route.query.sessionId || '') !== String(sessionId)) {
|
||||
router.replace({
|
||||
path: '/chatHistory',
|
||||
query: { ...route.query, sessionId: String(sessionId) },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMessages(reset = false) {
|
||||
if (!currentSession.value?.id) {
|
||||
return;
|
||||
}
|
||||
const nextPageNumber = reset ? 1 : messagePage.value.pageNumber + 1;
|
||||
const [, res] = await tryit(api.get)(
|
||||
`/userCenter/chatHistory/sessions/${currentSession.value.id}/messages`,
|
||||
{
|
||||
params: {
|
||||
pageNumber: nextPageNumber,
|
||||
pageSize: messagePage.value.pageSize,
|
||||
},
|
||||
},
|
||||
);
|
||||
if (res?.errorCode !== 0) {
|
||||
return;
|
||||
}
|
||||
const normalized = normalizeMessages(res.data?.records || []);
|
||||
if (reset) {
|
||||
messageList.value = normalized;
|
||||
} else {
|
||||
messageList.value = [...normalized, ...messageList.value];
|
||||
}
|
||||
messagePage.value.total = res.data?.total || 0;
|
||||
messagePage.value.pageNumber = nextPageNumber;
|
||||
}
|
||||
|
||||
function normalizeMessages(records: any[]) {
|
||||
return [...records]
|
||||
.reverse()
|
||||
.map((item: any) => ({
|
||||
key: String(item.id),
|
||||
role: item.senderRole === 'assistant' ? 'assistant' : 'user',
|
||||
content:
|
||||
item.senderRole === 'assistant'
|
||||
? String(item.contentText || '').replace(/^Final Answer:\s*/i, '')
|
||||
: item.contentText,
|
||||
placement: item.senderRole === 'assistant' ? 'start' : 'end',
|
||||
created: item.created,
|
||||
chains: Array.isArray(item.contentPayload?.chains)
|
||||
? item.contentPayload.chains
|
||||
: undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
drawerVisible.value = false;
|
||||
currentSession.value = undefined;
|
||||
messageList.value = [];
|
||||
router.replace({ path: '/chatHistory', query: {} });
|
||||
}
|
||||
|
||||
function changePage(pageNumber: number) {
|
||||
queryParams.value.pageNumber = pageNumber;
|
||||
fetchSessions();
|
||||
}
|
||||
|
||||
async function renameSession(session: any) {
|
||||
const [, promptRes] = await tryit(ElMessageBox.prompt)(
|
||||
'请输入新的会话名称',
|
||||
'重命名会话',
|
||||
{
|
||||
inputValue: session.title || '',
|
||||
inputPlaceholder: '请输入会话名称',
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
},
|
||||
);
|
||||
const value = promptRes?.value?.trim();
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const [, res] = await tryit(api.post)(`/userCenter/chatHistory/sessions/${session.id}/rename`, {
|
||||
title: value,
|
||||
});
|
||||
if (res?.errorCode === 0) {
|
||||
ElMessage.success('重命名成功');
|
||||
if (currentSession.value?.id === session.id) {
|
||||
currentSession.value.title = value;
|
||||
}
|
||||
await fetchSessions();
|
||||
}
|
||||
}
|
||||
|
||||
function deleteSession(session: any) {
|
||||
ElMessageBox.confirm('删除后将不再出现在聊天历史中,是否继续?', '删除会话', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
}).then(async () => {
|
||||
const [, res] = await tryit(api.post)(`/userCenter/chatHistory/sessions/${session.id}/delete`, {});
|
||||
if (res?.errorCode === 0) {
|
||||
ElMessage.success('删除成功');
|
||||
if (currentSession.value?.id === session.id) {
|
||||
closeDrawer();
|
||||
}
|
||||
await fetchSessions();
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function formatTime(value?: string) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
const time = new Date(value);
|
||||
if (Number.isNaN(time.getTime())) {
|
||||
return value;
|
||||
}
|
||||
return `${time.getMonth() + 1}-${time.getDate()} ${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElContainer class="bg-background-deep h-full">
|
||||
<ElHeader class="!h-auto !p-8 !pb-0">
|
||||
<ElSpace direction="vertical" :size="24" alignment="flex-start">
|
||||
<h1 class="text-2xl font-medium">聊天记录</h1>
|
||||
<div class="flex items-center gap-5">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-nowrap text-sm">聊天助理</span>
|
||||
<ElSelect
|
||||
clearable
|
||||
v-model="queryParams.botId"
|
||||
:options="assistantList"
|
||||
placeholder="请选择聊天助理"
|
||||
@change="search"
|
||||
/>
|
||||
</div>
|
||||
<ElHeader class="!h-auto !px-8 !pb-0 !pt-8">
|
||||
<ElSpace direction="vertical" :size="20" alignment="flex-start" class="w-full">
|
||||
<div>
|
||||
<h1 class="text-2xl font-medium">聊天历史</h1>
|
||||
<p class="text-foreground/60 mt-2 text-sm">
|
||||
查看最近会话,并在右侧抽屉中回溯完整聊天内容。
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex w-full flex-wrap items-center gap-4">
|
||||
<ElSelect
|
||||
v-model="queryParams.assistantId"
|
||||
clearable
|
||||
placeholder="筛选聊天助理"
|
||||
:options="assistantList"
|
||||
class="!w-[220px]"
|
||||
@change="fetchSessions"
|
||||
/>
|
||||
<ElInput
|
||||
placeholder="搜索关键词"
|
||||
v-model="queryParams.title"
|
||||
@keyup.enter="search"
|
||||
@change="search"
|
||||
v-model="queryParams.keyword"
|
||||
class="max-w-[320px]"
|
||||
placeholder="搜索标题或最近消息"
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
</div>
|
||||
</ElSpace>
|
||||
</ElHeader>
|
||||
<ElMain class="!px-8">
|
||||
<ElContainer class="bg-background rounded-lg p-5">
|
||||
<ElHeader
|
||||
class="dark:bg-accent grid grid-cols-[repeat(3,minmax(0,1fr))_120px] place-items-center rounded-lg bg-[#f7f9fd] !p-0"
|
||||
height="54px"
|
||||
>
|
||||
<span
|
||||
class="text-accent-foreground text-sm"
|
||||
v-for="title in listTitles"
|
||||
:key="title"
|
||||
|
||||
<ElMain class="!px-8 !pb-8">
|
||||
<div class="bg-background border-border min-h-full rounded-2xl border p-5">
|
||||
<div v-if="filteredSessions.length > 0" class="flex flex-col gap-3">
|
||||
<button
|
||||
v-for="item in filteredSessions"
|
||||
:key="item.id"
|
||||
class="border-border hover:border-primary/30 hover:bg-accent/40 flex w-full items-start justify-between gap-4 rounded-2xl border px-5 py-4 text-left transition-colors"
|
||||
@click="openSession(item.id)"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
</ElHeader>
|
||||
<ElMain class="!p-0">
|
||||
<div class="flex flex-col items-center gap-5">
|
||||
<div class="w-full">
|
||||
<PageData
|
||||
page-url="/userCenter/botConversation/pageList"
|
||||
ref="pageRef"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<div
|
||||
class="text-foreground/90 grid h-[60px] grid-cols-[repeat(3,minmax(0,1fr))_120px] place-items-center text-sm hover:bg-[var(--el-fill-color-light)]"
|
||||
v-for="record in pageList"
|
||||
:key="record.id"
|
||||
>
|
||||
<ElText truncated>{{ record.bot.title }}</ElText>
|
||||
<ElText line-clamp="2">{{ record.title }}</ElText>
|
||||
<span>{{ record.created }}</span>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton
|
||||
class="[--el-font-weight-primary:400]"
|
||||
link
|
||||
type="primary"
|
||||
@click="toDetail(record)"
|
||||
>
|
||||
查看详情
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown>
|
||||
<ElButton :icon="MoreFilled" link />
|
||||
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem>
|
||||
<ElButton
|
||||
link
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@click="handleDelete(record.id)"
|
||||
>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</PageData>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="truncate text-base font-medium">{{ item.title || '未命名会话' }}</span>
|
||||
<ElTag size="small" effect="plain">{{ item.assistantName || '聊天助理' }}</ElTag>
|
||||
</div>
|
||||
<div class="text-foreground/65 mt-2 line-clamp-2 text-sm">
|
||||
{{ item.lastMessagePreview || '暂无消息内容' }}
|
||||
</div>
|
||||
<div class="text-foreground/50 mt-3 flex flex-wrap items-center gap-4 text-xs">
|
||||
<span>最近发送人:{{ item.lastSenderName || '未知' }}</span>
|
||||
<span>消息数:{{ item.messageCount || 0 }}</span>
|
||||
<span>活跃时间:{{ formatTime(item.lastMessageAt || item.accessAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
<div class="flex items-center gap-2">
|
||||
<ElButton link :icon="Edit" @click.stop="renameSession(item)">重命名</ElButton>
|
||||
<ElButton link type="danger" :icon="Delete" @click.stop="deleteSession(item)">删除</ElButton>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<ElEmpty v-else description="暂无聊天历史" />
|
||||
|
||||
<div class="mt-6 flex justify-end" v-if="pageState.total > queryParams.pageSize">
|
||||
<ElPagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:current-page="queryParams.pageNumber"
|
||||
:page-size="queryParams.pageSize"
|
||||
:total="pageState.total"
|
||||
@current-change="changePage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
|
||||
<ElDrawer
|
||||
v-model="drawerVisible"
|
||||
:title="currentSession?.title || '聊天详情'"
|
||||
size="760px"
|
||||
destroy-on-close
|
||||
@close="closeDrawer"
|
||||
>
|
||||
<div v-loading="drawerLoading" class="flex h-full flex-col">
|
||||
<div class="border-border mb-4 flex items-center justify-between rounded-2xl border px-4 py-3">
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-base font-medium">{{ currentSession?.title || '聊天详情' }}</div>
|
||||
<div class="text-foreground/55 mt-1 text-sm">
|
||||
{{ currentSession?.assistantName || '聊天助理' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-foreground/50 text-xs">
|
||||
{{ formatTime(currentSession?.lastMessageAt || currentSession?.accessAt) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div class="mb-4 flex justify-center">
|
||||
<ElButton
|
||||
v-if="messageList.length < messagePage.total"
|
||||
text
|
||||
type="primary"
|
||||
@click="loadMessages(false)"
|
||||
>
|
||||
加载更早消息
|
||||
</ElButton>
|
||||
</div>
|
||||
<ChatBubbleList
|
||||
:bot="{ icon: '', title: currentSession?.assistantName || '' }"
|
||||
:messages="messageList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ElDrawer>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.el-select {
|
||||
--el-select-width: 165px;
|
||||
}
|
||||
|
||||
.el-select.bot-select {
|
||||
--el-select-width: 343px;
|
||||
}
|
||||
|
||||
.el-select :deep(.el-select__wrapper) {
|
||||
--el-border-radius-base: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { ElContainer, ElHeader, ElMain } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { ChatBubbleList } from '#/components/chat';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const ids = reactive({
|
||||
botId: '',
|
||||
conversationId: '',
|
||||
});
|
||||
const conversationInfo = ref<any>();
|
||||
const messageList = ref<any[]>([]);
|
||||
const loading = ref(true);
|
||||
const logoUrl = `${import.meta.env.BASE_URL || '/'}logo.svg`;
|
||||
|
||||
onMounted(() => {
|
||||
if (route.params.id) {
|
||||
ids.conversationId = route.params.id as string;
|
||||
getConversationDetails();
|
||||
}
|
||||
});
|
||||
|
||||
function getConversationDetails() {
|
||||
api
|
||||
.get('/userCenter/botConversation/detail', {
|
||||
params: {
|
||||
id: ids.conversationId,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
conversationInfo.value = res.data;
|
||||
ids.botId = res.data.botId;
|
||||
getMessageList();
|
||||
}
|
||||
});
|
||||
}
|
||||
function getMessageList() {
|
||||
api
|
||||
.get('/userCenter/botMessage/getMessages', {
|
||||
params: ids,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
messageList.value = res.data;
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full px-12 py-8 max-sm:p-3">
|
||||
<ElContainer class="bg-background h-full">
|
||||
<ElHeader
|
||||
height="80px"
|
||||
class="rounded-xl bg-[#F8F8F9] !pr-9 max-sm:!h-16 max-sm:!pr-3"
|
||||
>
|
||||
<div class="flex h-full w-full items-center justify-between">
|
||||
<!-- Left -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-lg font-medium max-sm:text-base">{{
|
||||
conversationInfo?.title
|
||||
}}</span>
|
||||
<div
|
||||
v-if="conversationInfo?.bot.title"
|
||||
class="text-foreground/70 rounded bg-[#ECECEE] p-1 text-xs"
|
||||
>
|
||||
{{ conversationInfo.bot.title }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-foreground/50 text-sm max-sm:text-xs">{{
|
||||
conversationInfo?.created
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Right -->
|
||||
<img :src="logoUrl" class="w-40 max-sm:w-28" />
|
||||
</div>
|
||||
</ElHeader>
|
||||
<ElMain class="relative max-sm:mt-2 max-sm:!p-0" v-loading="loading">
|
||||
<div
|
||||
class="absolute bottom-5 left-1/2 top-5 w-full max-w-[1000px] -translate-x-1/2 max-sm:bottom-0 max-sm:top-0"
|
||||
>
|
||||
<ChatBubbleList
|
||||
class="relative mx-auto h-full max-w-[1000px]"
|
||||
:bot="conversationInfo?.bot"
|
||||
:messages="messageList"
|
||||
:editable="false"
|
||||
:open-editor="() => {}"
|
||||
/>
|
||||
</div>
|
||||
</ElMain>
|
||||
</ElContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
:deep(.el-bubble-list) {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-bubble-content-wrapper .el-bubble-content) {
|
||||
--bubble-content-max-width: calc(100% - 52px);
|
||||
}
|
||||
|
||||
@media not all and (min-width: 640px) {
|
||||
:deep(.el-bubble) {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-avatar) {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
:deep(.el-bubble-content-wrapper .el-bubble-content) {
|
||||
--bubble-content-max-width: calc(100% - 38px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,279 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import type { ChatThinkingBlockProps } from './types';
|
||||
|
||||
defineOptions({
|
||||
name: 'ChatThinkingBlock',
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<ChatThinkingBlockProps>(), {
|
||||
content: '',
|
||||
disabled: false,
|
||||
emptyBehavior: 'hide',
|
||||
expanded: false,
|
||||
label: '',
|
||||
readonly: false,
|
||||
status: 'end',
|
||||
summary: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:expanded': [boolean];
|
||||
}>();
|
||||
|
||||
const normalizedContent = computed(() =>
|
||||
String(props.content || '')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/^\s*\n+/, '')
|
||||
.trimEnd(),
|
||||
);
|
||||
|
||||
const shouldRender = computed(
|
||||
() =>
|
||||
normalizedContent.value.length > 0 || props.emptyBehavior === 'placeholder',
|
||||
);
|
||||
|
||||
const expandedModel = computed({
|
||||
get: () => props.expanded,
|
||||
set: (value: boolean) => emit('update:expanded', value),
|
||||
});
|
||||
|
||||
const computedLabel = computed(() => {
|
||||
if (props.label) {
|
||||
return props.label;
|
||||
}
|
||||
if (props.status === 'thinking') {
|
||||
return '思考中';
|
||||
}
|
||||
if (props.status === 'error') {
|
||||
return '思考异常';
|
||||
}
|
||||
return '已思考';
|
||||
});
|
||||
|
||||
const computedSummary = computed(() => {
|
||||
if (props.summary) {
|
||||
return props.summary;
|
||||
}
|
||||
const source = normalizedContent.value
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.find(Boolean);
|
||||
if (!source) {
|
||||
return props.emptyBehavior === 'placeholder' ? '暂无思考内容' : '';
|
||||
}
|
||||
return source.length > 56 ? `${source.slice(0, 56)}...` : source;
|
||||
});
|
||||
|
||||
const canToggle = computed(
|
||||
() => !props.disabled && normalizedContent.value.length > 0,
|
||||
);
|
||||
|
||||
function toggleExpanded() {
|
||||
if (!canToggle.value) {
|
||||
return;
|
||||
}
|
||||
expandedModel.value = !expandedModel.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="shouldRender"
|
||||
class="chat-thinking-block"
|
||||
:class="[
|
||||
`is-${status}`,
|
||||
{
|
||||
'is-disabled': disabled,
|
||||
'is-expanded': expandedModel,
|
||||
'is-readonly': readonly,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="chat-thinking-block__trigger"
|
||||
:disabled="!canToggle"
|
||||
@click="toggleExpanded"
|
||||
>
|
||||
<span class="chat-thinking-block__leading">
|
||||
<span class="chat-thinking-block__indicator" aria-hidden="true"></span>
|
||||
<span class="chat-thinking-block__label">{{ computedLabel }}</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="!expandedModel && computedSummary"
|
||||
class="chat-thinking-block__summary"
|
||||
>
|
||||
{{ computedSummary }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="chat-thinking-block__chevron"
|
||||
:class="{ 'is-open': expandedModel }"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
|
||||
<transition name="chat-thinking-block__body-transition">
|
||||
<div
|
||||
v-if="expandedModel && normalizedContent"
|
||||
class="chat-thinking-block__body"
|
||||
>
|
||||
<div class="chat-thinking-block__content">
|
||||
{{ normalizedContent }}
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chat-thinking-block {
|
||||
border: 1px solid hsl(var(--divider-faint) / 0.18);
|
||||
border-radius: 16px;
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
hsl(var(--glass-tint) / 0.48) 0%,
|
||||
hsl(var(--surface-panel) / 0.74) 100%
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 1px 0 hsl(var(--glass-border) / 0.24),
|
||||
0 10px 24px -24px hsl(var(--foreground) / 0.18);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.chat-thinking-block__trigger {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 12px;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: inherit;
|
||||
transition: background-color 0.18s ease;
|
||||
}
|
||||
|
||||
.chat-thinking-block__trigger:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-thinking-block__trigger:not(:disabled):hover {
|
||||
background: hsl(var(--surface-contrast-soft) / 0.34);
|
||||
}
|
||||
|
||||
.chat-thinking-block__trigger:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.chat-thinking-block__leading {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chat-thinking-block__indicator {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: hsl(var(--text-muted) / 0.74);
|
||||
}
|
||||
|
||||
.chat-thinking-block.is-thinking .chat-thinking-block__indicator {
|
||||
background: hsl(var(--primary) / 0.82);
|
||||
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
|
||||
animation: chat-thinking-pulse 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.chat-thinking-block.is-error .chat-thinking-block__indicator {
|
||||
background: hsl(var(--destructive) / 0.86);
|
||||
box-shadow: 0 0 0 4px hsl(var(--destructive) / 0.1);
|
||||
}
|
||||
|
||||
.chat-thinking-block__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
color: hsl(var(--text-strong));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-thinking-block__summary {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
color: hsl(var(--text-muted));
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-thinking-block__chevron {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-right: 1.5px solid hsl(var(--text-muted));
|
||||
border-bottom: 1.5px solid hsl(var(--text-muted));
|
||||
transform: rotate(45deg) translateY(-1px);
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
.chat-thinking-block__chevron.is-open {
|
||||
transform: rotate(225deg) translateY(-1px);
|
||||
}
|
||||
|
||||
.chat-thinking-block__body {
|
||||
padding: 0 12px 12px;
|
||||
}
|
||||
|
||||
.chat-thinking-block__content {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: hsl(var(--surface-panel) / 0.72);
|
||||
font-size: 12px;
|
||||
line-height: 1.68;
|
||||
color: hsl(var(--text-secondary));
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.chat-thinking-block.is-disabled {
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
.chat-thinking-block__body-transition-enter-active,
|
||||
.chat-thinking-block__body-transition-leave-active {
|
||||
transition:
|
||||
opacity 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
}
|
||||
|
||||
.chat-thinking-block__body-transition-enter-from,
|
||||
.chat-thinking-block__body-transition-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
@keyframes chat-thinking-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 0 7px hsl(var(--primary) / 0.04);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as ChatThinkingBlock } from './ChatThinkingBlock.vue';
|
||||
export type {
|
||||
ChatThinkingBlockProps,
|
||||
ChatThinkingBlockStatus,
|
||||
} from './types';
|
||||
@@ -0,0 +1,12 @@
|
||||
export type ChatThinkingBlockStatus = 'end' | 'error' | 'thinking';
|
||||
|
||||
export interface ChatThinkingBlockProps {
|
||||
content?: string;
|
||||
disabled?: boolean;
|
||||
emptyBehavior?: 'hide' | 'placeholder';
|
||||
expanded?: boolean;
|
||||
label?: string;
|
||||
readonly?: boolean;
|
||||
status?: ChatThinkingBlockStatus;
|
||||
summary?: string;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './api-component';
|
||||
export * from './captcha';
|
||||
export * from './chat-thinking';
|
||||
export * from './col-page';
|
||||
export * from './count-to';
|
||||
export * from './ellipsis-text';
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="18px" height="16px" viewBox="0 0 18 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>聊天助理备份 7</title>
|
||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="聊天记录" transform="translate(-29, -463)">
|
||||
<g id="编组-12备份-2" transform="translate(0, 3)">
|
||||
<g id="编组-32备份-2" transform="translate(12, 444)">
|
||||
<g id="聊天助理备份-7" transform="translate(16, 14)">
|
||||
<rect id="矩形" x="0" y="0" width="20" height="20"></rect>
|
||||
<path d="M15,2.8 C15.8836556,2.8 16.6836556,3.1581722 17.2627417,3.7372583 C17.8418278,4.3163444 18.2,5.1163444 18.2,6 L18.2,17.2 L5,17.2 C4.1163444,17.2 3.3163444,16.8418278 2.7372583,16.2627417 C2.1581722,15.6836556 1.8,14.8836556 1.8,14 L1.8,6 C1.8,5.1163444 2.1581722,4.3163444 2.7372583,3.7372583 C3.3163444,3.1581722 4.1163444,2.8 5,2.8 Z" id="形状结合" stroke="currentColor" stroke-width="1.6"></path>
|
||||
<polyline id="路径-2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" points="8.8275 6.93789684 8.8275 10.9003453 12.5 10.9003453"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.25"
|
||||
>
|
||||
<path d="M3 3.4h7.9A1.9 1.9 0 0 1 12.8 5.3v3.5a1.9 1.9 0 0 1-1.9 1.9H7.2l-2.4 2V10.7H4.9A1.9 1.9 0 0 1 3 8.8V3.4Z"/>
|
||||
<path d="M5.3 5.9h2.2"/>
|
||||
<circle cx="9.6" cy="7.2" r="1.95"/>
|
||||
<path d="M9.6 6.2v1.2l.85.58"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 432 B |
@@ -10,6 +10,7 @@ const SvgDownloadIcon = createIconifyIcon('svg:download');
|
||||
const SvgCardIcon = createIconifyIcon('svg:card');
|
||||
const SvgBellIcon = createIconifyIcon('svg:bell');
|
||||
const SvgCakeIcon = createIconifyIcon('svg:cake');
|
||||
const SvgChatHistoryIcon = createIconifyIcon('svg:chat-history');
|
||||
const SvgAntdvLogoIcon = createIconifyIcon('svg:antdv-logo');
|
||||
const SvgGithubIcon = createIconifyIcon('svg:github');
|
||||
const SvgGoogleIcon = createIconifyIcon('svg:google');
|
||||
@@ -27,6 +28,7 @@ export {
|
||||
SvgBellIcon,
|
||||
SvgCakeIcon,
|
||||
SvgCardIcon,
|
||||
SvgChatHistoryIcon,
|
||||
SvgDingDingIcon,
|
||||
SvgDownloadIcon,
|
||||
SvgGithubIcon,
|
||||
|
||||
Reference in New Issue
Block a user