feat:【antd】【ai】chat 增加联网搜索的迁移
This commit is contained in:
@@ -57,6 +57,7 @@ const conversationInAbortController = ref<any>(); // 对话进行中 abort 控
|
|||||||
const inputTimeout = ref<any>(); // 处理输入中回车的定时器
|
const inputTimeout = ref<any>(); // 处理输入中回车的定时器
|
||||||
const prompt = ref<string>(); // prompt
|
const prompt = ref<string>(); // prompt
|
||||||
const enableContext = ref<boolean>(true); // 是否开启上下文
|
const enableContext = ref<boolean>(true); // 是否开启上下文
|
||||||
|
const enableWebSearch = ref<boolean>(false); // 是否开启联网搜索
|
||||||
// 接收 Stream 消息
|
// 接收 Stream 消息
|
||||||
const receiveMessageFullText = ref('');
|
const receiveMessageFullText = ref('');
|
||||||
const receiveMessageDisplayedText = ref('');
|
const receiveMessageDisplayedText = ref('');
|
||||||
@@ -353,6 +354,7 @@ async function doSendMessageStream(userMessage: AiChatMessageApi.ChatMessage) {
|
|||||||
userMessage.content,
|
userMessage.content,
|
||||||
conversationInAbortController.value,
|
conversationInAbortController.value,
|
||||||
enableContext.value,
|
enableContext.value,
|
||||||
|
enableWebSearch.value,
|
||||||
async (res: any) => {
|
async (res: any) => {
|
||||||
const { code, data, msg } = JSON.parse(res.data);
|
const { code, data, msg } = JSON.parse(res.data);
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
@@ -590,9 +592,15 @@ onMounted(async () => {
|
|||||||
placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)"
|
placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="flex justify-between pb-0 pt-1">
|
<div class="flex justify-between pb-0 pt-1">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center gap-3">
|
||||||
<Switch v-model:checked="enableContext" />
|
<div class="flex items-center">
|
||||||
<span class="ml-1 text-sm text-gray-400">上下文</span>
|
<Switch v-model:checked="enableContext" size="small" />
|
||||||
|
<span class="ml-1 text-sm text-gray-400">上下文</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Switch v-model:checked="enableWebSearch" size="small" />
|
||||||
|
<span class="ml-1 text-sm text-gray-400">联网搜索</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { deleteChatMessage } from '#/api/ai/chat/message';
|
|||||||
import { MarkdownView } from '#/components/markdown-view';
|
import { MarkdownView } from '#/components/markdown-view';
|
||||||
|
|
||||||
import MessageKnowledge from './knowledge.vue';
|
import MessageKnowledge from './knowledge.vue';
|
||||||
|
import MessageWebSearch from './web-search.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
conversation: {
|
conversation: {
|
||||||
@@ -136,6 +137,10 @@ onMounted(async () => {
|
|||||||
:content="item.content"
|
:content="item.content"
|
||||||
/>
|
/>
|
||||||
<MessageKnowledge v-if="item.segments" :segments="item.segments" />
|
<MessageKnowledge v-if="item.segments" :segments="item.segments" />
|
||||||
|
<MessageWebSearch
|
||||||
|
v-if="item.webSearchPages"
|
||||||
|
:web-search-pages="item.webSearchPages"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 flex flex-row">
|
<div class="mt-2 flex flex-row">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { AiChatMessageApi } from '#/api/ai/chat/message';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenDrawer } from '@vben/common-ui';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
webSearchPages?: AiChatMessageApi.WebSearchPage[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isExpanded = ref(false); // 默认收起
|
||||||
|
const selectedResult = ref<AiChatMessageApi.WebSearchPage | null>(null); // 选中的搜索结果
|
||||||
|
const iconLoadError = ref<Record<number, boolean>>({}); // 记录图标加载失败
|
||||||
|
|
||||||
|
const [Drawer, drawerApi] = useVbenDrawer({
|
||||||
|
title: '联网搜索详情',
|
||||||
|
closable: true,
|
||||||
|
footer: true,
|
||||||
|
onCancel() {
|
||||||
|
drawerApi.close();
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
if (selectedResult.value?.url) {
|
||||||
|
window.open(selectedResult.value.url, '_blank');
|
||||||
|
}
|
||||||
|
drawerApi.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 切换展开/收起 */
|
||||||
|
function toggleExpanded() {
|
||||||
|
isExpanded.value = !isExpanded.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点击搜索结果 */
|
||||||
|
function handleClick(result: AiChatMessageApi.WebSearchPage) {
|
||||||
|
selectedResult.value = result;
|
||||||
|
drawerApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 图标加载失败处理 */
|
||||||
|
function handleIconError(index: number) {
|
||||||
|
iconLoadError.value[index] = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="webSearchPages && webSearchPages.length > 0" class="mt-2.5">
|
||||||
|
<!-- 标题栏:可点击展开/收起 -->
|
||||||
|
<div
|
||||||
|
class="mb-2 flex cursor-pointer items-center justify-between text-sm text-gray-600 transition-colors hover:text-blue-500"
|
||||||
|
@click="toggleExpanded"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<IconifyIcon icon="lucide:search" :size="14" />
|
||||||
|
<span>联网搜索结果 ({{ webSearchPages.length }} 条)</span>
|
||||||
|
</div>
|
||||||
|
<IconifyIcon
|
||||||
|
:icon="isExpanded ? 'lucide:chevron-up' : 'lucide:chevron-down'"
|
||||||
|
class="text-xs transition-transform duration-200"
|
||||||
|
:size="12"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 可展开的搜索结果列表 -->
|
||||||
|
<div
|
||||||
|
v-show="isExpanded"
|
||||||
|
class="flex flex-col gap-2 transition-all duration-200 ease-in-out"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(page, index) in webSearchPages"
|
||||||
|
:key="index"
|
||||||
|
class="cursor-pointer rounded-md bg-white p-2.5 transition-all hover:bg-blue-50"
|
||||||
|
@click="handleClick(page)"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<!-- 网站图标 -->
|
||||||
|
<div class="mt-0.5 h-4 w-4 flex-shrink-0">
|
||||||
|
<img
|
||||||
|
v-if="page.icon && !iconLoadError[index]"
|
||||||
|
:src="page.icon"
|
||||||
|
:alt="page.name"
|
||||||
|
class="h-full w-full rounded-sm object-contain"
|
||||||
|
@error="handleIconError(index)"
|
||||||
|
/>
|
||||||
|
<IconifyIcon
|
||||||
|
v-else
|
||||||
|
icon="lucide:link"
|
||||||
|
class="h-full w-full text-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<!-- 网站名称 -->
|
||||||
|
<div class="mb-1 truncate text-xs text-gray-400">
|
||||||
|
{{ page.name }}
|
||||||
|
</div>
|
||||||
|
<!-- 主标题 -->
|
||||||
|
<div
|
||||||
|
class="mb-1 line-clamp-2 text-sm font-medium leading-snug text-blue-600"
|
||||||
|
>
|
||||||
|
{{ page.title }}
|
||||||
|
</div>
|
||||||
|
<!-- 描述 -->
|
||||||
|
<div class="mb-1 line-clamp-2 text-xs leading-snug text-gray-600">
|
||||||
|
{{ page.snippet }}
|
||||||
|
</div>
|
||||||
|
<!-- URL -->
|
||||||
|
<div class="truncate text-xs text-green-700">
|
||||||
|
{{ page.url }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 联网搜索详情 Drawer -->
|
||||||
|
<Drawer class="w-[600px]" cancel-text="关闭" confirm-text="访问原文">
|
||||||
|
<div v-if="selectedResult">
|
||||||
|
<!-- 标题区域 -->
|
||||||
|
<div class="mb-4 flex items-start gap-3">
|
||||||
|
<div class="mt-0.5 h-6 w-6 flex-shrink-0">
|
||||||
|
<img
|
||||||
|
v-if="selectedResult.icon"
|
||||||
|
:src="selectedResult.icon"
|
||||||
|
:alt="selectedResult.name"
|
||||||
|
class="h-full w-full rounded-sm object-contain"
|
||||||
|
/>
|
||||||
|
<IconifyIcon
|
||||||
|
v-else
|
||||||
|
icon="lucide:link"
|
||||||
|
class="h-full w-full text-gray-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="mb-2 text-lg font-bold text-gray-900">
|
||||||
|
{{ selectedResult.title }}
|
||||||
|
</div>
|
||||||
|
<div class="mb-1 text-sm text-gray-500">
|
||||||
|
{{ selectedResult.name }}
|
||||||
|
</div>
|
||||||
|
<div class="break-all text-sm text-green-700">
|
||||||
|
{{ selectedResult.url }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- 简短描述 -->
|
||||||
|
<div>
|
||||||
|
<div class="mb-2 text-sm font-semibold text-gray-900">简短描述</div>
|
||||||
|
<div
|
||||||
|
class="rounded-lg bg-gray-50 p-3 text-sm leading-relaxed text-gray-700"
|
||||||
|
>
|
||||||
|
{{ selectedResult.snippet }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 内容摘要 -->
|
||||||
|
<div v-if="selectedResult.summary">
|
||||||
|
<div class="mb-2 text-sm font-semibold text-gray-900">内容摘要</div>
|
||||||
|
<div
|
||||||
|
class="max-h-[50vh] overflow-y-auto whitespace-pre-wrap rounded-lg bg-gray-50 p-3 text-sm leading-relaxed text-gray-900"
|
||||||
|
>
|
||||||
|
{{ selectedResult.summary }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user