feat(@vben/common-ui): 优化 AI 工具模块功能和路由

- 修改路由路径,统一为 /ai前缀
- 新增表单弹窗组件,用于输入项目信息
- 优化聊天消息展示逻辑,支持文档预览和下载
- 新增个人页面路由和组件
- 调整模板数据结构和样式
This commit is contained in:
Kven 2025-05-16 23:02:11 +08:00
parent 9cead8075b
commit ad79e5113e
15 changed files with 378 additions and 204 deletions

View File

@ -40,6 +40,13 @@ export namespace ChatflowApi {
conversationId: string; conversationId: string;
appId: string; appId: string;
} }
export interface ChatMessageParams {
userId: string;
conversationId: string;
firstId: string;
limit: string;
}
} }
// 聊天流 // 聊天流
@ -78,3 +85,10 @@ export function deleteChatflow(data: ChatflowApi.deleteParams) {
export function getChatParameters(appId: string) { export function getChatParameters(appId: string) {
return requestClient.get(`/v1/chat/parameters/${appId}`); return requestClient.get(`/v1/chat/parameters/${appId}`);
} }
export function getChatflowMessage(
appId: string,
data: ChatflowApi.ChatMessageParams,
) {
return requestClient.post(`/v1/chat/messages/${appId}`, data);
}

View File

@ -5,5 +5,6 @@ export * from './log';
export * from './menu'; export * from './menu';
export * from './role'; export * from './role';
export * from './server'; export * from './server';
export * from './spider';
export * from './user'; export * from './user';
export * from './workflow'; export * from './workflow';

View File

@ -0,0 +1,18 @@
import { requestClient } from '#/api/request';
// 全国公共资源爬虫
export function runSpider(data: any) {
return requestClient.post(`/spider/run`, { data });
}
export function getSpiderStatus() {
return requestClient.get(`/spider/status`);
}
export function getSpiderLogs() {
return requestClient.get(`/v1/workflow/list`);
}
export function stopSpider() {
return requestClient.get(`/spider/stop`);
}

View File

@ -15,7 +15,7 @@ const routes: RouteRecordRaw[] = [
children: [ children: [
{ {
name: 'spider', name: 'spider',
path: '/aiflow/spider', path: '/ai/spider',
component: () => import('#/views/spider/index.vue'), component: () => import('#/views/spider/index.vue'),
meta: { meta: {
icon: SvgSpider, icon: SvgSpider,
@ -26,7 +26,7 @@ const routes: RouteRecordRaw[] = [
}, },
{ {
name: 'word', name: 'word',
path: '/aiflow/word', path: '/ai/word',
component: () => import('#/views/word/index.vue'), component: () => import('#/views/word/index.vue'),
meta: { meta: {
icon: SvgWord, icon: SvgWord,
@ -37,7 +37,7 @@ const routes: RouteRecordRaw[] = [
}, },
{ {
name: 'ppt', name: 'ppt',
path: '/aiflow/ppt', path: '/ai/ppt',
component: () => import('#/views/ppt/index.vue'), component: () => import('#/views/ppt/index.vue'),
meta: { meta: {
icon: SvgPPT, icon: SvgPPT,

View File

@ -11,19 +11,19 @@ const items: WorkflowItem[] = [
icon: SvgSpider, icon: SvgSpider,
title: '数据抓取工具', title: '数据抓取工具',
description: '自动抓取', description: '自动抓取',
path: '/spider', path: '/ai/spider',
}, },
{ {
icon: SvgWord, icon: SvgWord,
title: 'Word文档生成工具', title: 'Word文档生成工具',
description: '自动生成word文档', description: '自动生成word文档',
path: '/word', path: '/ai/word',
}, },
{ {
icon: SvgPPT, icon: SvgPPT,
title: 'PPT生成工具', title: 'PPT生成工具',
description: '自动生成PPT文档', description: '自动生成PPT文档',
path: '/ppt', path: '/ai/ppt',
}, },
]; ];
const router = useRouter(); const router = useRouter();

View File

@ -5,7 +5,7 @@ import { onMounted, reactive, ref } from 'vue';
import { PptListView, PptWorkView } from '@vben/common-ui'; import { PptListView, PptWorkView } from '@vben/common-ui';
import { message } from 'ant-design-vue'; import { notification } from 'ant-design-vue';
import { getWorkflowInfo, getWorkflowList, sendWorkflow } from '#/api'; import { getWorkflowInfo, getWorkflowList, sendWorkflow } from '#/api';
@ -58,7 +58,11 @@ async function handleClick(item: PPTTempItem) {
} }
async function handleClickMode(item: PPTTempItem) { async function handleClickMode(item: PPTTempItem) {
message.success(`已选取${item.name}为模板`); notification.success({
message: `${item.name}`,
description: '已选取',
duration: 3,
});
temp = item; temp = item;
} }

View File

@ -7,8 +7,8 @@ import { SpiderListView, SpiderWorkView } from '@vben/common-ui';
import { notification } from 'ant-design-vue'; import { notification } from 'ant-design-vue';
import { getWorkflowInfo, getWorkflowList, sendWorkflow } from '#/api'; import { getWorkflowInfo, getWorkflowList, runSpider } from '#/api';
// sendWorkflow
interface ResultItem { interface ResultItem {
key: number; key: number;
role: 'ai' | 'user'; role: 'ai' | 'user';
@ -82,7 +82,7 @@ async function handleClickMode(item: PPTTempItem) {
<SpiderWorkView <SpiderWorkView
title="目标网址:" title="目标网址:"
:item="temp" :item="temp"
:run-spider="sendWorkflow" :run-spider="runSpider"
:item-message="itemMessage" :item-message="itemMessage"
/> />
</div> </div>

View File

@ -9,11 +9,19 @@ import { message, notification } from 'ant-design-vue';
import { import {
deleteChatflow, deleteChatflow,
getChatflowMessage,
getChatList, getChatList,
getChatParameters, getChatParameters,
sendChatflow, sendChatflow,
} from '#/api'; } from '#/api';
interface ResultItem {
key: number;
role: 'ai' | 'user';
content: string;
footer?: any;
}
let temp = reactive<WordTempItem>({ let temp = reactive<WordTempItem>({
id: 'baca08c1-e92b-4dc9-a445-3584803f54d4', id: 'baca08c1-e92b-4dc9-a445-3584803f54d4',
name: '海南职创申报书生成', name: '海南职创申报书生成',
@ -54,11 +62,12 @@ function handleClickMode(item: WordTempItem) {
temp = item; temp = item;
getParameters(temp.id); getParameters(temp.id);
} }
const itemMessage = ref<ResultItem[]>([]);
async function deleteLog(item: any) { async function deleteLog(item: any) {
const res = await deleteChatflow({ const res = await deleteChatflow({
appId: temp.id, appId: temp.id,
id: item, conversationId: item,
userId: '1562', userId: '1562',
}); });
if (res.code === 0) { if (res.code === 0) {
@ -74,9 +83,33 @@ async function getParameters(id: string) {
params.value = res.suggestedQuestions; params.value = res.suggestedQuestions;
} }
function handleClick(item: WordTempItem) { async function handleClick(item: string) {
message.error('暂不支持查看历史'); const res = await getChatflowMessage(temp.id, {
temp = item; userId: '1562',
firstId: '',
conversationId: item,
limit: '',
});
itemMessage.value = [];
if (res.data.length > 0) {
res.data.forEach((msg) => {
if (msg.inputs) {
itemMessage.value.push({
key: itemMessage.value.length + 1,
role: 'user', // 'user' or 'ai'
content: msg.inputs.projectName,
footer: msg.footer,
});
}
if (msg.answer) {
itemMessage.value.push({
key: itemMessage.value.length + 1,
role: 'ai',
content: msg.answer,
});
}
});
}
} }
onMounted(() => { onMounted(() => {
@ -101,6 +134,7 @@ onMounted(() => {
:item="temp" :item="temp"
:params-data="params" :params-data="params"
:run-chatflow="sendChatflow" :run-chatflow="sendChatflow"
:item-message="itemMessage"
/> />
</div> </div>
</template> </template>

View File

@ -101,7 +101,7 @@ const openKeys = ref([]);
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
mode="vertical" mode="vertical"
class="mode" class="mode"
:items="items" :items="itemsData"
@click="handleMenuClick" @click="handleMenuClick"
/> />
<!-- 🌟 添加会话 --> <!-- 🌟 添加会话 -->

View File

@ -37,7 +37,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
const data = drawerApi.getData<Record<string, any>>(); const data = drawerApi.getData<Record<string, any>>();
if (data) { if (data) {
isLoading.value = true; // isLoading.value = true; //
pptx.value = `/pptx/${data}`; // docx pptx.value = `/pptx/${data}`; // pptx
} }
// url.value = drawerApi.getData<Record<string, any>>(); // url.value = drawerApi.getData<Record<string, any>>();
} }

View File

@ -14,8 +14,8 @@ import {
PaperClipOutlined, PaperClipOutlined,
UserOutlined, UserOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { Badge, Button, Flex, Typography } from 'ant-design-vue'; import { Badge, Button, Flex, Space, Typography } from 'ant-design-vue';
import { Attachments, Bubble, Sender } from 'ant-design-x-vue'; import { Attachments, Bubble, Sender, Welcome } from 'ant-design-x-vue';
import WordPreview from '../word/word-preview.vue'; import WordPreview from '../word/word-preview.vue';
@ -342,7 +342,7 @@ watch(
h( h(
Button, Button,
{ {
size: 'nomarl', size: 'normal',
type: 'primary', type: 'primary',
style: { style: {
marginLeft: '10px', marginLeft: '10px',
@ -373,8 +373,22 @@ watch(
<div class="layout"> <div class="layout">
<PreviewDrawer /> <PreviewDrawer />
<div class="chat"> <div class="chat">
<Space
v-if="resultItems.length === 0"
direction="vertical"
size:16
style="flex: 1; padding-top: 20px"
>
<Welcome
variant="borderless"
icon="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*s5sNRo5LjfQAAAAAAAAAAAAADgCCAQ/fmt.webp"
title="欢迎使用PPT自动生成"
description="请选择模板列表中需要生成文件的模板,输入参数后开始生成生成文件。"
/>
</Space>
<!-- 🌟 消息列表 --> <!-- 🌟 消息列表 -->
<Bubble.List <Bubble.List
v-else
variant="shadow" variant="shadow"
:typing="true" :typing="true"
:items="resultItems" :items="resultItems"

View File

@ -37,12 +37,14 @@ const conversationsItems = ref(defaultConversationsItems);
const activeKey = ref(defaultConversationsItems.value[0]?.key); const activeKey = ref(defaultConversationsItems.value[0]?.key);
const itemsData = ref([ const itemsData = ref([
{ {
key: 'a2a55334-a111-45e6-942f-9f3f70af8826', key: '77c068fd-d5b6-4c33-97d8-db5511a09b26',
label: '全国公共资源交易平台_信息爬取', label: '全国公共资源交易平台_信息爬取',
title: '全国公共资源交易平台_信息爬取',
}, },
{ {
key: 'c736edd0-925d-4877-9223-56aab7342311', key: 'c736edd0-925d-4877-9223-56aab7342311',
label: '广州公共资源交易中心_信息获取', label: '广州公共资源交易中心_信息获取',
title: '全国公共资源交易平台_信息爬取',
}, },
]); ]);
@ -71,24 +73,6 @@ const onConversationClick: ConversationsProps['onActiveChange'] = (key) => {
</script> </script>
<template> <template>
<!-- <Card style="height: calc(100vh - 120px); overflow-y: auto">-->
<!-- <CardHeader class="py-4">-->
<!-- <CardTitle class="text-lg">{{ title }}</CardTitle>-->
<!-- <CardDescription>请选择需要爬取的网站</CardDescription>-->
<!-- </CardHeader>-->
<!-- <CardContent class="flex flex-wrap p-5 pt-0">-->
<!-- <ul class="divide-border w-full divide-y" role="list">-->
<!-- <li-->
<!-- v-for="item in items"-->
<!-- :key="item.id"-->
<!-- @click="$emit('click', item)"-->
<!-- class="flex cursor-pointer justify-between gap-x-6 py-5"-->
<!-- >-->
<!-- {{ item.name }}-->
<!-- </li>-->
<!-- </ul>-->
<!-- </CardContent>-->
<!-- </Card>-->
<div class="menu"> <div class="menu">
<div class="addBtn">数据信息列表</div> <div class="addBtn">数据信息列表</div>
<Menu <Menu
@ -96,12 +80,12 @@ const onConversationClick: ConversationsProps['onActiveChange'] = (key) => {
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
mode="vertical" mode="vertical"
class="mode" class="mode"
:items="items" :items="itemsData"
@click="handleMenuClick" @click="handleMenuClick"
/> />
<!-- 🌟 添加会话 --> <!-- 🌟 添加会话 -->
<!-- <Button type="link" class="addBtn">会话</Button>--> <!-- <Button type="link" class="addBtn">会话</Button>-->
<div class="addBtn">会话</div> <div class="addBtn">获取记录</div>
<!-- 🌟 添加会话 --> <!-- 🌟 添加会话 -->
<!-- 🌟 会话管理 --> <!-- 🌟 会话管理 -->

View File

@ -9,7 +9,6 @@ import { h, ref, watch } from 'vue';
import { useVbenDrawer } from '@vben-core/popup-ui'; import { useVbenDrawer } from '@vben-core/popup-ui';
import { import {
Button,
Card, Card,
CardContent, CardContent,
CardFooter, CardFooter,
@ -18,8 +17,15 @@ import {
} from '@vben-core/shadcn-ui'; } from '@vben-core/shadcn-ui';
import { UserOutlined } from '@ant-design/icons-vue'; import { UserOutlined } from '@ant-design/icons-vue';
import { Flex, message, RangePicker } from 'ant-design-vue'; import {
import { Attachments, Bubble } from 'ant-design-x-vue'; Button,
Flex,
message,
notification,
RangePicker,
Space,
} from 'ant-design-vue';
import { Attachments, Bubble, Welcome } from 'ant-design-x-vue';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import SpiderPreview from './spider-preview.vue'; import SpiderPreview from './spider-preview.vue';
@ -78,40 +84,7 @@ const props = withDefaults(defineProps<Props>(), {
}), }),
}); });
// const styles = computed(() => {
// return {
// 'placeholder': {
// 'padding-top': '32px',
// 'text-align': 'left',
// 'flex': 1,
// },
// } as const
// })
// const placeholderNode = computed(() => h(
// Space,
// { direction: "vertical", size: 16, style: styles.value.placeholder },
// [
// h(
// Welcome,
// {
// variant: "borderless",
// icon: "https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*s5sNRo5LjfQAAAAAAAAAAAAADgCCAQ/fmt.webp",
// title: "Hello, I'm Ant Design X",
// description: "Base on Ant Design, AGI product interface solution, create a better intelligent vision~",
// extra: h(Space, {}, [h(Button, { icon: h(ShareAltOutlined) }), h(Button, { icon: h(EllipsisOutlined) })]),
// }
// ),
// ]
// ))
const resultItems = ref<ResultItem[]>([]); const resultItems = ref<ResultItem[]>([]);
// const items = computed<BubbleListProps['items']>(() => {
// if (resultItems.value.length === 0) {
// return [{ content: placeholderNode, variant: 'borderless' }]
// }
// return resultItems
// })
const [PreviewDrawer, previewDrawerApi] = useVbenDrawer({ const [PreviewDrawer, previewDrawerApi] = useVbenDrawer({
// //
@ -162,24 +135,36 @@ const startFetching = async () => {
} }
try { try {
notification.info({
message: '正在获取中...',
duration: 3,
});
const res = await props.runSpider( const res = await props.runSpider(
{ {
appid: props.item.id,
},
{
userId: '1562',
conversationId: '',
files: [],
inputs: {
publish_start_time, publish_start_time,
publish_end_time, publish_end_time,
llm_api_key: '77c068fd-d5b6-4c33-97d8-db5511a09b26',
}, },
}, // {
// appid: props.item.id,
// },
// {
// userId: '1562',
// conversationId: '',
// files: [],
// inputs: {
// publish_start_time,
// publish_end_time,
// },
// },
); );
if (res.data.outputs.result) { if (res.data.outputs.result) {
// //
fetchResult.value = res.data.outputs.result; fetchResult.value = res.data.outputs.result;
message.success('抓取成功'); notification.success({
message: '获取成功',
duration: 3,
});
const { result } = res.data.outputs; const { result } = res.data.outputs;
const filename = ref(''); const filename = ref('');
@ -209,7 +194,7 @@ const startFetching = async () => {
h( h(
Button, Button,
{ {
size: 'nomarl', size: 'normal',
type: 'primary', type: 'primary',
style: { style: {
marginLeft: '10px', marginLeft: '10px',
@ -217,7 +202,7 @@ const startFetching = async () => {
onClick: () => { onClick: () => {
// <a> // <a>
const link = document.createElement('a'); const link = document.createElement('a');
link.href = result; // link.href = msg.content; //
link.download = filename; // link.download = filename; //
document.body.append(link); // <a> document.body.append(link); // <a>
link.click(); // link.click(); //
@ -234,31 +219,13 @@ const startFetching = async () => {
message.error('抓取无结果'); message.error('抓取无结果');
} }
} catch (error) { } catch (error) {
console.error(error); message.error(`${error}`);
} }
// //
isFetching.value = false; isFetching.value = false;
}; };
//
const downloadFile = () => {
if (!fetchResult.value || !fetchResult.value.startsWith('http')) {
message.error('无效的文件链接');
return;
}
const fileUrl = fetchResult.value;
const fileName = decodeURIComponent(
fileUrl.slice(Math.max(0, fileUrl.lastIndexOf('/') + 1)),
);
const link = document.createElement('a');
link.href = fileUrl;
link.download = fileName;
link.click();
};
// //
const roles: BubbleListProps['roles'] = { const roles: BubbleListProps['roles'] = {
user: { user: {
@ -333,7 +300,7 @@ watch(
h( h(
Button, Button,
{ {
size: 'nomarl', size: 'normal',
type: 'primary', type: 'primary',
onClick: () => { onClick: () => {
openPreviewDrawer('right', filename); openPreviewDrawer('right', filename);
@ -344,7 +311,7 @@ watch(
h( h(
Button, Button,
{ {
size: 'nomarl', size: 'normal',
type: 'primary', type: 'primary',
style: { style: {
marginLeft: '10px', marginLeft: '10px',
@ -373,10 +340,27 @@ watch(
<template> <template>
<!-- style="flex-direction: column"--> <!-- style="flex-direction: column"-->
<div class="flex h-full" style="width: 70%"> <div
class="flex h-full"
style="flex-direction: column; width: 70%; padding-top: 20px"
>
<PreviewDrawer /> <PreviewDrawer />
<!-- <PreviewDrawer />--> <!-- <PreviewDrawer />-->
<Space
v-if="resultItems.length === 0"
direction="vertical"
size:16
style="flex: 1"
>
<Welcome
variant="borderless"
icon="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*s5sNRo5LjfQAAAAAAAAAAAAADgCCAQ/fmt.webp"
title="欢迎使用AI平台信息获取"
description="请选择数据信息列表中需要获取信息的链接,选取时间后开始爬取获取信息。"
/>
</Space>
<Bubble.List <Bubble.List
v-else
variant="shadow" variant="shadow"
:typing="true" :typing="true"
:items="resultItems" :items="resultItems"
@ -403,13 +387,13 @@ watch(
> >
{{ isFetching ? '抓取中...' : '开始抓取' }} {{ isFetching ? '抓取中...' : '开始抓取' }}
</Button> </Button>
<Button <!-- <Button-->
class="mx-2" <!-- class="mx-2"-->
:disabled="fetchStatus !== 'completed'" <!-- :disabled="fetchStatus !== 'completed'"-->
@click="downloadFile" <!-- @click="downloadFile"-->
> <!-- >-->
下载文件 <!-- 下载文件-->
</Button> <!-- </Button>-->
</CardFooter> </CardFooter>
</Card> </Card>
</div> </div>

View File

@ -96,7 +96,7 @@ const openKeys = ref([]);
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
mode="vertical" mode="vertical"
class="mode" class="mode"
:items="items" :items="itemsData"
@click="handleMenuClick" @click="handleMenuClick"
/> />
<!-- 🌟 添加会话 --> <!-- 🌟 添加会话 -->

View File

@ -11,7 +11,8 @@ import type { WordTempItem } from './typing';
import { computed, h, ref, watch } from 'vue'; import { computed, h, ref, watch } from 'vue';
import { useVbenDrawer } from '@vben-core/popup-ui'; import { useVbenForm } from '@vben-core/form-ui';
import { useVbenDrawer, useVbenModal } from '@vben-core/popup-ui';
import { import {
CloudUploadOutlined, CloudUploadOutlined,
@ -19,7 +20,14 @@ import {
UserOutlined, UserOutlined,
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
// import type { VNode } from 'vue'; // import type { VNode } from 'vue';
import { Badge, Button, Flex, message, Typography } from 'ant-design-vue'; import {
Badge,
Button,
Flex,
message,
Space,
Typography,
} from 'ant-design-vue';
import { import {
Attachments, Attachments,
Bubble, Bubble,
@ -27,15 +35,21 @@ import {
Sender, Sender,
useXAgent, useXAgent,
useXChat, useXChat,
Welcome,
} from 'ant-design-x-vue'; } from 'ant-design-x-vue';
import markdownit from 'markdown-it'; import markdownit from 'markdown-it';
import WordPreview from './word-preview.vue'; import WordPreview from './word-preview.vue';
interface ResultItem {
key: number;
role: 'ai' | 'user';
content: string;
footer?: any;
}
interface WorkflowParams { interface WorkflowParams {
appid: string; appid: string;
} }
interface WorkflowContext { interface WorkflowContext {
userId: string; userId: string;
conversationId: string; conversationId: string;
@ -45,7 +59,6 @@ interface WorkflowContext {
}; };
files: []; files: [];
} }
interface WorkflowResult { interface WorkflowResult {
conversationId: string; conversationId: string;
answer: {}; answer: {};
@ -55,8 +68,8 @@ interface WorkflowResult {
}; };
}; };
} }
interface Props { interface Props {
itemMessage?: ResultItem;
item?: WordTempItem; item?: WordTempItem;
paramsData?: {}; paramsData?: {};
runChatflow?: ( runChatflow?: (
@ -69,6 +82,7 @@ defineOptions({ name: 'PlaygroundIndependentSetup' });
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
item: () => null, item: () => null,
itemMessage: () => null,
paramsData: () => null, paramsData: () => null,
runChatflow: () => async () => ({ runChatflow: () => async () => ({
data: { data: {
@ -81,13 +95,6 @@ const props = withDefaults(defineProps<Props>(), {
// const { token } = theme.useToken(); // const { token } = theme.useToken();
interface ResultItem {
key: number;
role: 'ai' | 'user';
content: string;
footer?: any;
}
// markdown // markdown
const md = markdownit({ html: true, breaks: true }); const md = markdownit({ html: true, breaks: true });
@ -231,11 +238,20 @@ watch(
); );
// ==================== Event ==================== // ==================== Event ====================
// function onSubmit(nextContent: string) {
// if (!nextContent) return; // .docx URL
// onRequest(nextContent); function isDocxURL(str: string): boolean {
// content.value = ''; return str.endsWith('.docx');
// } }
// 使 /static/
function extractDocxFilename(url: string): null | string {
const match = url.match(/\/static\/([a-f0-9-]+\.docx)$/i);
if (match) {
return match[1]; // 9e1c421e-991c-411f-8176-6350a97e70f3.docx
}
return null;
}
const startFetching = async () => { const startFetching = async () => {
// //
@ -264,6 +280,9 @@ const startFetching = async () => {
files: [], files: [],
inputs: { inputs: {
projectName: projectName.value, projectName: projectName.value,
projectContext: '无',
keyAvoidTechOrKeyword: '无',
userInitialInnovationPoint: '无',
}, },
content: content.value || '', content: content.value || '',
}, },
@ -272,20 +291,6 @@ const startFetching = async () => {
const { answer } = res; const { answer } = res;
conversationId.value = res.conversationId; conversationId.value = res.conversationId;
// .docx URL
function isDocxURL(str: string): boolean {
return str.endsWith('.docx');
}
// 使 /static/
function extractDocxFilename(url: string): null | string {
const match = url.match(/\/static\/([a-f0-9-]+\.docx)$/i);
if (match) {
return match[1]; // 9e1c421e-991c-411f-8176-6350a97e70f3.docx
}
return null;
}
const filename = ref(''); const filename = ref('');
if (answer && isDocxURL(answer)) { if (answer && isDocxURL(answer)) {
@ -332,7 +337,7 @@ const startFetching = async () => {
resultItems.value.push({ resultItems.value.push({
key: resultItems.value.length + 1, key: resultItems.value.length + 1,
role: 'ai', role: 'ai',
content: res.answer.trim(), content: res.answer,
}); });
} }
@ -346,6 +351,79 @@ const startFetching = async () => {
fetchStatus.value = 'completed'; fetchStatus.value = 'completed';
}; };
function openFormModal() {
formModalApi
.setData({
//
values: {
projectName: 'abc',
projectContext: '123',
keyAvoidTechOrKeyWord: 'abc',
userInitialInnovationPoint: 'abc',
},
})
.open();
}
const [Form, formApi] = useVbenForm({
handleSubmit: startFetching,
schema: [
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'projectName',
label: '项目名称',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'projectContext',
label: '项目背景',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'keyAvoidTechOrKeyWord',
label: '规避关键词',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'userInitialInnovationPoint',
label: '初步创新点',
},
],
showDefaultActions: false,
});
const [Modal, formModalApi] = useVbenModal({
fullscreenButton: false,
onCancel() {
formModalApi.close();
},
onConfirm: async () => {
await formApi.validateAndSubmitForm();
// modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const { values } = formModalApi.getData<Record<string, any>>();
if (values) {
formApi.setValues(values);
}
}
},
title: '内嵌表单示例',
});
const onPromptsItemClick: PromptsProps['onItemClick'] = (info) => { const onPromptsItemClick: PromptsProps['onItemClick'] = (info) => {
content.value = info.data.description; content.value = info.data.description;
}; };
@ -353,67 +431,110 @@ const onPromptsItemClick: PromptsProps['onItemClick'] = (info) => {
const handleFileChange: AttachmentsProps['onChange'] = (info) => const handleFileChange: AttachmentsProps['onChange'] = (info) =>
(attachedFiles.value = info.fileList); (attachedFiles.value = info.fileList);
// ==================== Nodes ==================== // itemMessage resultItems
// const placeholderNode = computed(() => watch(
// h( () => props.itemMessage,
// Space, (newVal) => {
// { direction: 'vertical', size: 16, style: value.placeholder }, resultItems.value = [];
// [ if (newVal && newVal.length > 0) {
// h(Welcome, { newVal.forEach((msg) => {
// variant: 'borderless', if (msg.role === 'user') {
// icon: 'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*s5sNRo5LjfQAAAAAAAAAAAAADgCCAQ/fmt.webp', resultItems.value.push({
// title: "Hello, I'm Ant Design X", key: resultItems.value.length + 1,
// description: role: msg.role, // 'user' or 'ai'
// 'Base on Ant Design, AGI product interface solution, create a better intelligent vision~', content: msg.content,
// extra: h(Space, {}, [ footer: msg.footer,
// h(Button, { icon: h(ShareAltOutlined) }), });
// h(Button, { icon: h(EllipsisOutlined) }), } else {
// ]), const filename = ref('');
// }),
// h(Prompts, {
// title: 'Do you want?',
// items: placeholderPromptsItems,
// styles: {
// list: {
// width: '100%',
// },
// item: {
// flex: 1,
// },
// },
// onItemClick: onPromptsItemClick,
// }),
// ],
// ),
// );
// const items = computed<BubbleListProps['items']>(() => { if (msg.content && isDocxURL(msg.content)) {
// // if (messages.value.length === 0) { filename.value = extractDocxFilename(msg.content);
// // return [{ content: placeholderNode, variant: 'borderless' }]; resultItems.value.push({
// // } key: resultItems.value.length + 1,
// return messages.value.map(({ id, message, status }) => ({ role: msg.role, // 'user' or 'ai'
// key: id, content: '文档已生成',
// loading: status === 'loading', footer: h(Flex, null, [
// role: status === 'local' ? 'local' : 'ai', h(
// content: message, Button,
// })); {
// }); size: 'normal',
type: 'primary',
onClick: () => {
openPreviewDrawer('right', filename);
},
},
'文档预览',
),
h(
Button,
{
size: 'normal',
type: 'primary',
style: {
marginLeft: '10px',
},
onClick: () => {
// <a>
const link = document.createElement('a');
link.href = msg.content; //
link.download = filename; //
document.body.append(link); // <a>
link.click(); //
link.remove(); // <a>
},
},
'文档下载',
),
]),
});
} else {
resultItems.value.push({
key: resultItems.value.length + 1,
role: msg.role, // 'user' or 'ai'
content: msg.content,
});
}
}
});
}
},
{ deep: true },
);
</script> </script>
<template> <template>
<div class="layout"> <div class="layout">
<PreviewDrawer /> <PreviewDrawer />
<Modal>
<Form />
</Modal>
<div class="chat"> <div class="chat">
<a-space class="w-full" direction="vertical" style="padding-top: 20px"> <Space
<a-input v-if="resultItems.length === 0"
v-model:value="projectName" direction="vertical"
required size:16
placeholder="项目名称" style="flex: 1; padding-top: 20px"
:status="inputStatus" >
<Welcome
variant="borderless"
icon="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*s5sNRo5LjfQAAAAAAAAAAAAADgCCAQ/fmt.webp"
title="欢迎使用AI申报书WORD生成"
description="请选择模板列表中需要生成文件的模板,输入参数后开始生成生成内容或文件。"
/> />
</a-space> </Space>
<!-- <a-space class="w-full" direction="vertical" style="padding-top: 20px">-->
<!-- <a-input-->
<!-- v-model:value="projectName"-->
<!-- required-->
<!-- placeholder="项目名称"-->
<!-- :status="inputStatus"-->
<!-- />-->
<!-- </a-space>-->
<!-- 🌟 消息列表 --> <!-- 🌟 消息列表 -->
<Bubble.List <Bubble.List
v-else
:items="resultItems" :items="resultItems"
:roles="roles" :roles="roles"
style="flex: 1" style="flex: 1"
@ -432,7 +553,7 @@ const handleFileChange: AttachmentsProps['onChange'] = (info) =>
:value="content" :value="content"
class="sender" class="sender"
:loading="agentRequestLoading" :loading="agentRequestLoading"
@submit="startFetching" @submit="openFormModal"
@change="(value) => (content = value)" @change="(value) => (content = value)"
> >
<template #prefix> <template #prefix>