feat(@vben/web-antd): 新增聊天流和数据抓取功能

- 添加聊天流 API 和相关组件,实现 PPT 模板生成工具界面
- 新增数据抓取工具列表和抓取功能
- 优化模板选择和会话管理逻辑
- 实现消息列表和输入框功能
- 添加文件上传和下载支持
This commit is contained in:
Kven 2025-05-04 18:45:03 +08:00
parent 6a40314f5a
commit 7020672e57
11 changed files with 1611 additions and 102 deletions

View File

@ -26,6 +26,7 @@
"#/*": "./src/*"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",
@ -40,6 +41,8 @@
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vue-office/docx": "^1.6.3",
"@vue-office/pptx": "^1.0.1",
"@vueuse/core": "catalog:",
"ant-design-vue": "catalog:",
"ant-design-x-vue": "^1.1.2",

View File

@ -0,0 +1,52 @@
import { requestClient } from '#/api/request';
export namespace ChatflowApi {
export interface ChatParams {
appid: string;
}
export interface CompletionsBody {
userId: string;
conversationId: string;
content: string;
inputs: {
[key: string]: any;
};
files: [];
}
export interface ChatListBody {
userId: string;
lastId: string;
sortBy: string;
limit: string;
content?: string | undefined;
}
}
// 聊天流
export function sendChatflow(
params: ChatflowApi.ChatParams,
data: ChatflowApi.CompletionsBody,
) {
return requestClient.post(`/v1/chat/completions/${params.appid}`, data);
}
export function getChatList(
params: ChatflowApi.ChatParams,
data: ChatflowApi.ChatListBody,
) {
return requestClient.post(`/v1/chat/conversations/${params.appid}`, data);
}
// export function stopChatflow(data){
// return requestClient.post('/v1/chat/stopMessagesStream', data);
// };
//
// export function getChatflowList(data){
// return requestClient.post('/v1/chat/messages', data);
// };
//
// export function deleteChatflow(data){
// return requestClient.delete('/v1/chat/messages/suggested', data);
// };

View File

@ -1,3 +1,7 @@
export * from './auth';
export * from './chatflow';
export * from './menu';
export * from './repository';
export * from './server';
export * from './user';
export * from './workflow';

View File

@ -0,0 +1,40 @@
import { requestClient } from '#/api/request';
export namespace RepositoryApi {
export interface AppListParams {
mode?: string;
name?: string;
}
}
export const getAppList = (params: RepositoryApi.AppListParams) => {
return requestClient.get(`/v1/server/apps`, { params });
};
// export const getAppDetail = (id) => {
// return requestClient.get(`/v1/server/${id}`)
// }
//
// export const getAppKey = (id) => {
// return requestClient.get(`/v1/server/api-key/${id}`)
// }
//
// export const initAppKey = (id) => {
// return requestClient.post(`/v1/server/api-key/init/${id}`)
// }
//
// export const getRepositoryKey = (data) => {
// return requestClient.get(`/v1/server/api-key/dataset`)
// }
//
// export const initRepositoryKey = (data) => {
// return requestClient.post(`/v1/server/api-key/dataset/init`)
// }
//
// export const getAbleList = (data) => {
// return requestClient.get(`/v1/server/apps/able`)
// };
//
// export const toggleAble = (id) => {
// return requestClient.get(`/v1/server/app/${id}`)
// };

View File

@ -0,0 +1,39 @@
import { requestClient } from '#/api/request';
export namespace WorkflowApi {
export interface WorkflowParams {
appid: string;
}
export interface WorkflowRunBody {
conversationId: string;
userId: string;
inputs: {
[key: string]: any;
};
files: [];
}
}
// 工作流
export function sendWorkflow(
params: WorkflowApi.WorkflowParams,
data: WorkflowApi.WorkflowRunBody,
) {
return requestClient.post(`/v1/workflow/run/${params.appid}`, data);
}
// export function runWorkflow(data){
// return requestClient.post('/v1/workflow/run/stream', data);
// };
//
// export function stopWorkflow(data){
// return requestClient.patch('/v1/workflow/stop', { data });
// };
//
// export function getWorkflowMessage(data){
// return requestClient.get('/v1/workflow/info', { data });
// };
//
// export function getWorkflowLog(data){
// return requestClient.get('/v1/workflow/logs', { data });
// };

View File

@ -1,41 +1,615 @@
<script lang="ts" setup>
import type { WorkflowItem } from '@vben/common-ui';
<script setup lang="ts">
import type {
AttachmentsProps,
BubbleListProps,
ConversationsProps,
PromptsProps,
} from 'ant-design-x-vue';
import { useRouter } from 'vue-router';
import type { VNode } from 'vue';
import { WorkflowsView } from '@vben/common-ui';
import { SvgPPT, SvgSpider, SvgWord } from '@vben/icons';
import { computed, h, onMounted, ref, watch } from 'vue';
const items: WorkflowItem[] = [
{
icon: SvgSpider,
title: '数据抓取工具',
description: '自动抓取',
path: '/crawler',
import {
CloudUploadOutlined,
CommentOutlined,
CopyOutlined,
DeleteOutlined,
EllipsisOutlined,
FireOutlined,
HeartOutlined,
PaperClipOutlined,
PlusOutlined,
ReadOutlined,
ShareAltOutlined,
SmileOutlined,
SyncOutlined,
} from '@ant-design/icons-vue';
import VueOfficePptx from '@vue-office/pptx';
import {
Badge,
Button,
Flex,
message,
Space,
theme,
Typography,
} from 'ant-design-vue';
import {
Attachments,
Bubble,
Conversations,
Prompts,
Sender,
useXAgent,
useXChat,
Welcome,
} from 'ant-design-x-vue';
import { getChatList, sendWorkflow } from '#/api';
defineOptions({ name: 'PlaygroundIndependentSetup' });
const { token } = theme.useToken();
const styles = computed(() => {
return {
layout: {
width: '100%',
'min-width': '1000px',
height: '722px',
'border-radius': `${token.value.borderRadius}px`,
display: 'flex',
background: `${token.value.colorBgContainer}`,
'font-family': `AlibabaPuHuiTi, ${token.value.fontFamily}, sans-serif`,
},
menu: {
background: `${token.value.colorBgLayout}80`,
width: '280px',
height: '100%',
display: 'flex',
'flex-direction': 'column',
},
conversations: {
padding: '0 12px',
flex: 1,
'overflow-y': 'auto',
},
chat: {
height: '100%',
width: '100%',
'max-width': '700px',
margin: '0 auto',
'box-sizing': 'border-box',
display: 'flex',
'flex-direction': 'column',
padding: `${token.value.paddingLG}px`,
gap: '16px',
},
messages: {
flex: 1,
},
placeholder: {
'padding-top': '32px',
'text-align': 'left',
flex: 1,
},
sender: {
'box-shadow': token.value.boxShadow,
},
logo: {
display: 'flex',
height: '72px',
'align-items': 'center',
'justify-content': 'start',
padding: '0 24px',
'box-sizing': 'border-box',
},
'logo-img': {
width: '24px',
height: '24px',
display: 'inline-block',
},
'logo-span': {
display: 'inline-block',
margin: '0 8px',
'font-weight': 'bold',
color: token.value.colorText,
'font-size': '16px',
},
addBtn: {
background: '#1677ff0f',
border: '1px solid #1677ff34',
width: 'calc(100% - 24px)',
margin: '0 12px 24px 12px',
},
} as const;
});
const pptx = ref<any>('/static/43b7da46a6ca47e5a9d7710d8dcf499c.pptx');
const [messageApi, contextHolder] = message.useMessage();
const menuConfig: ConversationsProps['menu'] = (conversation) => ({
items: [
{
label: '删除此对话',
key: 'operation3',
icon: h(DeleteOutlined),
danger: true,
},
],
onClick: (menuInfo) => {
messageApi.info(`Click ${conversation.key} - ${menuInfo.key}`);
},
});
const acticeModeKey = ref('1');
//
const templates = ref([
{ label: '模板1', key: '1' },
{ label: '模板2', key: '2' },
{ label: '模板3', key: '3' },
{ label: '模板34', key: '4' },
{ label: '模板35', key: '5' },
{ label: '模板36', key: '7' },
{ label: '模板37', key: '8' },
{ label: '模板38', key: '9' },
]);
// const sleep = () => new Promise(resolve => setTimeout(resolve, 500))
function renderTitle(icon: VNode, title: string) {
return h(Space, { align: 'start' }, [icon, h('span', title)]);
}
const defaultConversationsItems = [
{
icon: SvgWord,
title: 'Word文档生成工具',
description: '自动生成word文档',
path: '/word',
},
{
icon: SvgPPT,
title: 'PPT生成工具',
description: '自动生成PPT文档',
path: '/ppt',
key: '0',
label: 'What is Ant Design X?',
},
];
const router = useRouter();
function handleGo(path: string) {
console.log('跳转到: ' + path);
// router.push(path);
const placeholderPromptsItems: PromptsProps['items'] = [
{
key: '1',
label: renderTitle(
h(FireOutlined, { style: { color: '#FF4D4F' } }),
'Hot Topics',
),
description: 'What are you interested in?',
children: [
{
key: '1-1',
description: `What's new in X?`,
},
{
key: '1-2',
description: `What's AGI?`,
},
{
key: '1-3',
description: `Where is the doc?`,
},
],
},
{
key: '2',
label: renderTitle(
h(ReadOutlined, { style: { color: '#1890FF' } }),
'Design Guide',
),
description: 'How to design a good product?',
children: [
{
key: '2-1',
icon: h(HeartOutlined),
description: `Know the well`,
},
{
key: '2-2',
icon: h(SmileOutlined),
description: `Set the AI role`,
},
{
key: '2-3',
icon: h(CommentOutlined),
description: `Express the feeling`,
},
],
},
];
const senderPromptsItems: PromptsProps['items'] = [
{
key: '1',
description: 'Hot Topics',
icon: h(FireOutlined, { style: { color: '#FF4D4F' } }),
},
{
key: '2',
description: 'Design Guide',
icon: h(ReadOutlined, { style: { color: '#1890FF' } }),
},
];
const roles: BubbleListProps['roles'] = {
ai: {
placement: 'start',
typing: { step: 5, interval: 20 },
styles: {
content: {
borderRadius: '16px',
},
},
},
local: {
placement: 'end',
variant: 'shadow',
},
};
// ==================== State ====================
const headerOpen = ref(false);
const content = ref('');
const conversationsItems = ref(defaultConversationsItems);
const activeKey = ref(defaultConversationsItems[0]?.key);
const attachedFiles = ref<AttachmentsProps['items']>([]);
const agentRequestLoading = ref(false);
// ==================== Runtime ====================
const [agent] = useXAgent({
request: async ({ message }, { onSuccess }) => {
agentRequestLoading.value = true;
const res = await sendWorkflow(
{
appid: 'baca08c1-e92b-4dc9-a445-3584803f54d4',
},
{
userId: '1562',
conversationId: '',
files: [],
inputs: {
declarationDoc: message,
},
},
);
agentRequestLoading.value = false;
if (res.data.data.outputs.result) {
onSuccess(`下载链接:${res.data.data.outputs.result}`);
const url = new URL(res.data.data.outputs.result);
pptx.value = url.pathname;
} else {
onSuccess('发生异常,可以输入更多信息再让我来回答或重试。');
}
},
});
const { onRequest, messages, setMessages } = useXChat({
agent: agent?.value,
});
watch(
activeKey,
() => {
if (activeKey.value !== undefined) {
setMessages([]);
}
},
{ immediate: true },
);
// ==================== Event ====================
function onSubmit(nextContent: string) {
if (!nextContent) return;
onRequest(nextContent);
// sendMessage(nextContent)
content.value = '';
}
const onPromptsItemClick: PromptsProps['onItemClick'] = (info) => {
onRequest(info.data.description as string);
};
function onAddConversation() {
conversationsItems.value = [
...conversationsItems.value,
{
key: `${conversationsItems.value.length}`,
label: `New Conversation ${conversationsItems.value.length}`,
},
];
activeKey.value = `${conversationsItems.value.length}`;
}
const onConversationClick: ConversationsProps['onActiveChange'] = (key) => {
activeKey.value = key;
};
const handleFileChange: AttachmentsProps['onChange'] = (info) =>
(attachedFiles.value = info.fileList);
// ==================== Nodes ====================
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) }),
]),
}),
h(Prompts, {
title: 'Do you want?',
items: placeholderPromptsItems,
styles: {
list: {
width: '100%',
},
item: {
flex: 1,
},
},
onItemClick: onPromptsItemClick,
}),
],
),
);
const items = computed<BubbleListProps['items']>(() => {
if (messages.value.length === 0) {
return [{ content: placeholderNode, variant: 'borderless' }];
}
return messages.value.map(({ id, message, status }) => ({
key: id,
loading: status === 'loading',
role: status === 'local' ? 'local' : 'ai',
content: message,
}));
});
//
//
const getConversationList = async () => {
const res = await getChatList(
{
appid: '363b9580-ae60-4a40-ae7d-ec434e86e326',
},
{
userId: '1562',
lastId: '4dfa17e1-364a-4c98-af5d-1109c8f28212',
sortBy: '',
limit: '5',
},
);
if (res.status === 200) {
conversationsItems.value = res.data.data.map((item: any) => {
return {
key: item.id,
label: item.name,
};
});
activeKey.value = res.data.data[0].id;
}
};
const showQuoteButton = ref(false);
const selectedText = ref('');
const quoteButtonStyle = ref({
position: 'absolute',
top: '0',
left: '0',
display: 'none',
});
const handleTextSelection = (event: any) => {
const selection = window.getSelection();
if (selection && selection.toString().trim() !== '') {
selectedText.value = selection.toString();
//
const mouseX = event.pageX;
const mouseY = event.pageY;
//
quoteButtonStyle.value = {
position: 'fixed',
top: `${mouseY}px`,
left: `${mouseX}px`,
display: 'block',
};
showQuoteButton.value = true;
} else {
showQuoteButton.value = false;
}
};
const handleQuoteClick = () => {
//
content.value = selectedText.value;
showQuoteButton.value = false;
};
// pptx
const pptStyle = ref({
height: 'calc(100vh - 150px)',
width: '40%',
margin: '30px 20px',
});
//
const handleResize = () => {
//
pptStyle.value = {
height: 'calc(100vh - 150px)', //
width: '40%', // 40%
margin: '30px 20px',
};
};
//
const onModeClick: ConversationsProps['onActiveChange'] = (key) => {
acticeModeKey.value = key;
};
const renderedHandler = () => {
console.warn('渲染完成');
};
const errorHandler = () => {
console.error('渲染失败');
};
onMounted(() => {
getConversationList();
window.addEventListener('resize', handleResize);
});
</script>
<template>
<div class="p-5">
<WorkflowsView :items="items" @click="handleGo" />
<contextHolder />
<div :style="styles.layout" style="height: calc(100vh - 70px)">
<div :style="styles.menu">
<!-- 🌟 Logo -->
<div :style="styles.logo">
<img
src="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*eco6RrQhxbMAAAAAAAAAAAAADgCCAQ/original"
draggable="false"
alt="logo"
:style="styles['logo-img']"
/>
<span :style="styles['logo-span']">PPT模板生成</span>
</div>
<!-- 🌟 添加会话 -->
<Button type="link" :style="styles.addBtn" @click="onAddConversation">
<PlusOutlined />
新建会话
</Button>
<!-- 🌟 会话管理 -->
<Conversations
:items="conversationsItems"
:style="styles.conversations"
:active-key="activeKey"
:menu="menuConfig"
@active-change="onConversationClick"
/>
<!-- 🌟 会话管理 -->
<Typography.Title :level="5" style="margin: 0 0 8px">
选择模板
</Typography.Title>
<Conversations
:items="templates"
:style="styles.conversations"
:active-key="acticeModeKey"
:menu="menuConfig"
@active-change="onModeClick"
/>
</div>
<div :style="styles.chat" style="margin-left: 20px">
<!-- 🌟 消息列表 -->
<Bubble.List
:items="items"
:roles="roles"
:style="styles.messages"
@mouseup="handleTextSelection"
>
<template #footer>
<Space :size="token.paddingXXS">
<Button type="text" size="small" :icon="h(SyncOutlined)" />
<Button type="text" size="small" :icon="h(CopyOutlined)" />
</Space>
</template>
</Bubble.List>
<a-button
v-if="showQuoteButton"
:style="quoteButtonStyle"
@click="handleQuoteClick"
>
引用
</a-button>
<!-- 🌟 提示词 -->
<Prompts :items="senderPromptsItems" @item-click="onPromptsItemClick" />
<!-- 🌟 输入框 -->
<Sender
:value="content"
:style="styles.sender"
:loading="agentRequestLoading"
@submit="onSubmit"
@change="(value) => (content = value)"
>
<template #prefix>
<Badge :dot="attachedFiles.length > 0 && !headerOpen">
<Button type="text" @click="() => (headerOpen = !headerOpen)">
<template #icon>
<PaperClipOutlined />
</template>
</Button>
</Badge>
</template>
<template #header>
<Sender.Header
title="Attachments"
:open="headerOpen"
:styles="{ content: { padding: 0 } }"
@open-change="(open) => (headerOpen = open)"
>
<Attachments
:before-upload="() => false"
:items="attachedFiles"
@change="handleFileChange"
>
<template #placeholder="type">
<Flex
v-if="type && type.type === 'inline'"
align="center"
justify="center"
vertical
gap="2"
>
<Typography.Text style="font-size: 30px; line-height: 1">
<CloudUploadOutlined />
</Typography.Text>
<Typography.Title
:level="5"
style="margin: 0; font-size: 14px; line-height: 1.5"
>
Upload files
</Typography.Title>
<Typography.Text type="secondary">
Click or drag files to this area to upload
</Typography.Text>
</Flex>
<Typography.Text v-if="type && type.type === 'drop'">
Drop file here
</Typography.Text>
</template>
</Attachments>
</Sender.Header>
</template>
</Sender>
</div>
<VueOfficePptx
:src="pptx"
:style="pptStyle"
@rendered="renderedHandler"
@error="errorHandler"
/>
</div>
</template>

View File

@ -1,41 +1,211 @@
<script lang="ts" setup>
import type { WorkflowItem } from '@vben/common-ui';
<script setup lang="js">
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { getAppList, sendWorkflow } from '#/api';
import { WorkflowsView } from '@vben/common-ui';
import { SvgPPT, SvgSpider, SvgWord } from '@vben/icons';
const items: WorkflowItem[] = [
//
const tools = ref([
{
icon: SvgSpider,
title: '数据抓取工具',
description: '自动抓取',
path: '/crawler',
id: 1,
name: '工具一',
description: '这是一个用于抓取网页数据的工具',
content: '工具一的具体内容:支持多种网页抓取规则配置。',
path: '/tools/tool1',
},
{
icon: SvgWord,
title: 'Word文档生成工具',
description: '自动生成word文档',
path: '/word',
id: 2,
name: '工具二',
description: '这是一个用于分析文本数据的工具',
content: '工具二的具体内容:支持自然语言处理和文本分析功能。',
path: '/tools/tool2',
},
{
icon: SvgPPT,
title: 'PPT生成工具',
description: '自动生成PPT文档',
path: '/ppt',
id: 3,
name: '工具三',
description: '这是一个用于数据清洗的工具',
content: '工具三的具体内容:支持数据去重、格式化等操作。',
path: '/tools/tool3',
},
];
const router = useRouter();
]);
function handleGo(path: string) {
console.log('跳转到: ' + path);
// router.push(path);
}
//
const selectedTool = ref(null);
//
const isFetching = ref(false);
const fetchStatus = ref('idle'); // idle | fetching | completed
//
const fetchResult = ref(null);
//
const selectTool = (tool) => {
selectedTool.value = tool;
fetchStatus.value = 'idle'; //
};
const getChatflowList = async () => {
try {
const res = await getAppList({
mode: '',
name: '',
});
tools.value = res.data;
} catch (error) {
console.error(error);
}
};
//
const startFetching = async () => {
if (!selectedTool.value) return;
//
isFetching.value = true;
fetchStatus.value = 'fetching';
try {
const res = await sendWorkflow(
{
appid: 'c736edd0-925d-4877-9223-56aab7342311',
},
{
userId: '1562',
conversationId: '',
files: [],
inputs: {},
},
);
//
fetchResult.value = res.data.data.outputs.result;
} catch (error) {
console.error(error);
}
//
isFetching.value = false;
fetchStatus.value = 'completed';
};
//
const downloadFile = () => {
if (!fetchResult.value) return;
// 使
const fileName = `${selectedTool.value.name}.txt`;
const fileContent = fetchResult.value;
// Blob
const blob = new Blob([fileContent], { type: 'text/plain' });
//
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = fileName;
//
link.click();
// URL
URL.revokeObjectURL(link.href);
};
onMounted(() => {
getChatflowList();
});
</script>
<template>
<div class="p-5">
<WorkflowsView :items="items" @click="handleGo" />
</div>
<a-layout>
<!-- 右侧部分开始 -->
<a-layout :style="{ height: '100vh' }">
<!-- 右侧页面主体开始 -->
<a-layout-content
:style="{
margin: '12px 10px',
padding: '18px',
background: '#fff',
minHeight: '280px',
borderRadius: '4px',
}"
>
<!-- 左右布局容器 -->
<a-row
:gutter="16"
:style="{ height: 'calc(100vh - 100px)', overflow: 'hidden' }"
>
<!-- 左边数据爬取工具列表 -->
<a-col
:span="6"
:style="{
height: '100%',
overflowY: 'auto',
borderRight: '1px solid #e8e8e8',
}"
>
<a-card
title="数据爬取工具列表"
:bordered="false"
:style="{ height: '100%' }"
>
<a-list item-layout="horizontal" :data-source="tools">
<template #renderItem="{ item }">
<a-list-item @click="selectTool(item)">
<a-list-item-meta>
<template #title>
<span>{{ item.name }}</span>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<!-- 右边所选工具的内容 -->
<a-col
:span="18"
:style="{
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
height: '100%',
}"
>
<a-card
v-if="selectedTool"
title="工具"
:bordered="false"
:style="{ marginBottom: '20px' }"
>
<p>{{ selectedTool.name }}</p>
<!-- 抓取按钮和状态 -->
<div style="float: right; margin-top: 20px">
<a-button
type="primary"
style="margin-right: 12px"
:loading="isFetching"
@click="startFetching"
>
开始抓取
</a-button>
<a-button
type="primary"
@click="downloadFile"
v-if="fetchStatus === 'completed'"
>
下载文件
</a-button>
</div>
</a-card>
<a-empty v-else description="请选择一个工具" />
</a-col>
</a-row>
</a-layout-content>
<!-- 右侧页面主体结束 -->
</a-layout>
<!-- 右侧部分结束 -->
</a-layout>
</template>

View File

@ -1,41 +1,635 @@
<script lang="ts" setup>
import type { WorkflowItem } from '@vben/common-ui';
<script setup lang="ts">
import type {
AttachmentsProps,
BubbleListProps,
ConversationsProps,
PromptsProps,
} from 'ant-design-x-vue';
import { useRouter } from 'vue-router';
import type { VNode } from 'vue';
import { WorkflowsView } from '@vben/common-ui';
import { SvgPPT, SvgSpider, SvgWord } from '@vben/icons';
import { computed, h, onMounted, ref, watch } from 'vue';
const items: WorkflowItem[] = [
{
icon: SvgSpider,
title: '数据抓取工具',
description: '自动抓取',
path: '/crawler',
import {
CloudUploadOutlined,
CommentOutlined,
DeleteOutlined,
EllipsisOutlined,
FireOutlined,
HeartOutlined,
PaperClipOutlined,
PlusOutlined,
ReadOutlined,
ShareAltOutlined,
SmileOutlined,
} from '@ant-design/icons-vue';
import VueOfficeDocx from '@vue-office/docx';
import {
Badge,
Button,
Flex,
message,
Space,
theme,
Typography,
} from 'ant-design-vue';
import {
Attachments,
Bubble,
Conversations,
Prompts,
Sender,
useXAgent,
useXChat,
Welcome,
} from 'ant-design-x-vue';
import { getChatList, sendChatflow } from '#/api';
import '@vue-office/docx/lib/index.css';
defineOptions({ name: 'PlaygroundIndependentSetup' });
const { token } = theme.useToken();
const styles = computed(() => {
return {
layout: {
width: '100%',
'min-width': '1000px',
height: '722px',
'border-radius': `${token.value.borderRadius}px`,
display: 'flex',
background: `${token.value.colorBgContainer}`,
'font-family': `AlibabaPuHuiTi, ${token.value.fontFamily}, sans-serif`,
},
menu: {
background: `${token.value.colorBgLayout}80`,
width: '280px',
height: '100%',
display: 'flex',
'flex-direction': 'column',
},
conversations: {
padding: '0 12px',
flex: 1,
'overflow-y': 'auto',
},
chat: {
height: '100%',
width: '100%',
'max-width': '700px',
margin: '0 auto',
'box-sizing': 'border-box',
display: 'flex',
'flex-direction': 'column',
padding: `${token.value.paddingLG}px`,
gap: '16px',
},
messages: {
flex: 1,
},
placeholder: {
'padding-top': '32px',
'text-align': 'left',
flex: 1,
},
sender: {
'box-shadow': token.value.boxShadow,
},
logo: {
display: 'flex',
height: '72px',
'align-items': 'center',
'justify-content': 'start',
padding: '0 24px',
'box-sizing': 'border-box',
},
'logo-img': {
width: '24px',
height: '24px',
display: 'inline-block',
},
'logo-span': {
display: 'inline-block',
margin: '0 8px',
'font-weight': 'bold',
color: token.value.colorText,
'font-size': '16px',
},
addBtn: {
background: '#1677ff0f',
border: '1px solid #1677ff34',
width: 'calc(100% - 24px)',
margin: '0 12px 24px 12px',
},
} as const;
});
const pptx = ref<any>('/static/e5230571-ef0b-4fa0-a91d-f05313e15c3f.docx');
const [messageApi, contextHolder] = message.useMessage();
// const footer = ref("<Space size={token.value.paddingXXS}> <Button type="text" size="small" icon={<SyncOutlined />} /> <Button type="text" size="small" icon={<CopyOutlined />} /> </Space>")
const menuConfig: ConversationsProps['menu'] = (conversation) => ({
items: [
// {
// label: 'Operation 1',
// key: 'operation1',
// icon: h(EditOutlined),
// },
// {
// label: 'Operation 2',
// key: 'operation2',
// icon: h(StopOutlined),
// disabled: true,
// },
{
label: '删除此对话',
key: 'operation3',
icon: h(DeleteOutlined),
danger: true,
},
],
onClick: (menuInfo) => {
messageApi.info(`Click ${conversation.key} - ${menuInfo.key}`);
},
});
// const sleep = () => new Promise(resolve => setTimeout(resolve, 500))
function renderTitle(icon: VNode, title: string) {
return h(Space, { align: 'start' }, [icon, h('span', title)]);
}
const defaultConversationsItems = [
{
icon: SvgWord,
title: 'Word文档生成工具',
description: '自动生成word文档',
path: '/word',
},
{
icon: SvgPPT,
title: 'PPT生成工具',
description: '自动生成PPT文档',
path: '/ppt',
key: '0',
label: 'What is Ant Design X?',
},
];
const router = useRouter();
function handleGo(path: string) {
console.log('跳转到: ' + path);
// router.push(path);
const placeholderPromptsItems: PromptsProps['items'] = [
{
key: '1',
label: renderTitle(
h(FireOutlined, { style: { color: '#FF4D4F' } }),
'Hot Topics',
),
description: 'What are you interested in?',
children: [
{
key: '1-1',
description: `What's new in X?`,
},
{
key: '1-2',
description: `What's AGI?`,
},
{
key: '1-3',
description: `Where is the doc?`,
},
],
},
{
key: '2',
label: renderTitle(
h(ReadOutlined, { style: { color: '#1890FF' } }),
'Design Guide',
),
description: 'How to design a good product?',
children: [
{
key: '2-1',
icon: h(HeartOutlined),
description: `Know the well`,
},
{
key: '2-2',
icon: h(SmileOutlined),
description: `Set the AI role`,
},
{
key: '2-3',
icon: h(CommentOutlined),
description: `Express the feeling`,
},
],
},
];
const senderPromptsItems: PromptsProps['items'] = [
{
key: '1',
description: 'Hot Topics',
icon: h(FireOutlined, { style: { color: '#FF4D4F' } }),
},
{
key: '2',
description: 'Design Guide',
icon: h(ReadOutlined, { style: { color: '#1890FF' } }),
},
];
const roles: BubbleListProps['roles'] = {
ai: {
placement: 'start',
typing: { step: 5, interval: 20 },
styles: {
content: {
borderRadius: '16px',
},
},
},
local: {
placement: 'end',
variant: 'shadow',
},
};
// ==================== State ====================
const headerOpen = ref(false);
const content = ref('');
const conversationsItems = ref(defaultConversationsItems);
const activeKey = ref(defaultConversationsItems[0]?.key);
const acticeModeKey = ref('1');
const attachedFiles = ref<AttachmentsProps['items']>([]);
const agentRequestLoading = ref(false);
//
const projectName = ref('');
// ==================== Runtime ====================
const [agent] = useXAgent({
request: async ({ message }, { onSuccess }) => {
agentRequestLoading.value = true;
const res = await sendChatflow(
{
appid: 'baca08c1-e92b-4dc9-a445-3584803f54d4',
},
{
userId: '1562',
conversationId: '',
files: [],
inputs: {
projectName: projectName.value,
},
content: message || '',
},
);
agentRequestLoading.value = false;
if (res.data.answer) {
onSuccess(res.data.answer);
const url = new URL(res.data.answer);
pptx.value = url.pathname;
} else {
onSuccess('发生异常,可以输入更多信息再让我来回答或重试。');
}
},
});
const { onRequest, messages, setMessages } = useXChat({
agent: agent?.value,
});
watch(
activeKey,
() => {
if (activeKey.value !== undefined) {
setMessages([]);
}
},
{ immediate: true },
);
// ==================== Event ====================
function onSubmit(nextContent: string) {
if (!nextContent) return;
if (projectName.value === '') {
message.error('请输入项目名称');
return;
}
onRequest(nextContent);
// sendMessage(nextContent)
content.value = '';
}
const onPromptsItemClick: PromptsProps['onItemClick'] = (info) => {
onRequest(info.data.description as string);
};
function onAddConversation() {
conversationsItems.value = [
...conversationsItems.value,
{
key: `${conversationsItems.value.length}`,
label: `New Conversation ${conversationsItems.value.length}`,
},
];
activeKey.value = `${conversationsItems.value.length}`;
}
const onConversationClick: ConversationsProps['onActiveChange'] = (key) => {
activeKey.value = key;
};
const handleFileChange: AttachmentsProps['onChange'] = (info) =>
(attachedFiles.value = info.fileList);
// ==================== Nodes ====================
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) }),
]),
}),
h(Prompts, {
title: 'Do you want?',
items: placeholderPromptsItems,
styles: {
list: {
width: '100%',
},
item: {
flex: 1,
},
},
onItemClick: onPromptsItemClick,
}),
],
),
);
const items = computed<BubbleListProps['items']>(() => {
if (messages.value.length === 0) {
return [{ content: placeholderNode, variant: 'borderless' }];
}
return messages.value.map(({ id, message, status }) => ({
key: id,
loading: status === 'loading',
role: status === 'local' ? 'local' : 'ai',
content: message,
}));
});
//
//
const getConversationList = async () => {
const res = await getChatList(
{
appid: '363b9580-ae60-4a40-ae7d-ec434e86e326',
},
{
userId: '1562',
lastId: '4dfa17e1-364a-4c98-af5d-1109c8f28212',
sortBy: '',
limit: '5',
},
);
if (res.status === 200) {
conversationsItems.value = res.data.data.map((item: any) => {
return {
key: item.id,
label: item.name,
};
});
activeKey.value = res.data.data[0].id;
}
};
const showQuoteButton = ref(false);
const selectedText = ref('');
const quoteButtonStyle = ref({
position: 'absolute',
top: '0',
left: '0',
display: 'none',
});
const handleTextSelection = (event: any) => {
const selection = window.getSelection();
if (selection && selection.toString().trim() !== '') {
selectedText.value = selection.toString();
//
const mouseX = event.pageX;
const mouseY = event.pageY;
//
quoteButtonStyle.value = {
position: 'fixed',
top: `${mouseY}px`,
left: `${mouseX}px`,
display: 'block',
};
showQuoteButton.value = true;
} else {
showQuoteButton.value = false;
}
};
const handleQuoteClick = () => {
//
content.value = selectedText.value;
showQuoteButton.value = false;
};
//
const templates = ref([
{ label: '模板1', key: '1' },
{ label: '模板2', key: '2' },
{ label: '模板3', key: '3' },
{ label: '模板34', key: '4' },
{ label: '模板35', key: '5' },
{ label: '模板36', key: '7' },
{ label: '模板37', key: '8' },
{ label: '模板38', key: '9' },
]);
//
const onModeClick: ConversationsProps['onActiveChange'] = (key) => {
acticeModeKey.value = key;
};
// pptx
const pptStyle = ref({
height: 'calc(100vh - 150px) !import',
width: '40%',
margin: '30px 20px',
});
//
const handleResize = () => {
//
pptStyle.value = {
height: 'calc(100vh - 150px)', //
width: '40%', // 40%
margin: '30px 20px',
};
};
const renderedHandler = () => {
console.warn('渲染完成');
};
const errorHandler = () => {
console.error('渲染失败');
};
onMounted(() => {
getConversationList();
window.addEventListener('resize', handleResize);
});
</script>
<template>
<div class="p-5">
<WorkflowsView :items="items" @click="handleGo" />
<contextHolder />
<div :style="styles.layout" style="height: calc(100vh - 70px)">
<div :style="styles.menu">
<!-- 🌟 Logo -->
<div :style="styles.logo">
<img
src="https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*eco6RrQhxbMAAAAAAAAAAAAADgCCAQ/original"
draggable="false"
alt="logo"
:style="styles['logo-img']"
/>
<span :style="styles['logo-span']">Word文档生成</span>
</div>
<!-- 🌟 添加会话 -->
<Button type="link" :style="styles.addBtn" @click="onAddConversation">
<PlusOutlined />
新建会话
</Button>
<!-- 🌟 会话管理 -->
<Conversations
:items="conversationsItems"
:style="styles.conversations"
:active-key="activeKey"
:menu="menuConfig"
@active-change="onConversationClick"
/>
<!-- 🌟 会话管理 -->
<Typography.Title :level="5" style="margin: 0 0 8px">
选择模板
</Typography.Title>
<Conversations
:items="templates"
:style="styles.conversations"
:active-key="acticeModeKey"
:menu="menuConfig"
@active-change="onModeClick"
/>
<!-- 🌟 项目名称输入 -->
<Typography.Title :level="5" style="margin: 16px 0 8px">
项目名称
</Typography.Title>
<a-input
v-model:value="projectName"
placeholder="请输入项目名称"
:style="{ margin: '10px 5%', padding: '0 8px', width: '90%' }"
/>
</div>
<div :style="styles.chat" style="margin-left: 20px">
<!-- 🌟 消息列表 -->
<Bubble.List
:items="items"
:roles="roles"
:style="styles.messages"
@mouseup="handleTextSelection"
/>
<a-button
v-if="showQuoteButton"
:style="quoteButtonStyle"
@click="handleQuoteClick"
>
引用
</a-button>
<!-- 🌟 提示词 -->
<Prompts :items="senderPromptsItems" @item-click="onPromptsItemClick" />
<!-- 🌟 输入框 -->
<Sender
:value="content"
:style="styles.sender"
:loading="agentRequestLoading"
@submit="onSubmit"
@change="(value) => (content = value)"
>
<template #prefix>
<Badge :dot="attachedFiles.length > 0 && !headerOpen">
<Button type="text" @click="() => (headerOpen = !headerOpen)">
<template #icon>
<PaperClipOutlined />
</template>
</Button>
</Badge>
</template>
<template #header>
<Sender.Header
title="Attachments"
:open="headerOpen"
:styles="{ content: { padding: 0 } }"
@open-change="(open) => (headerOpen = open)"
>
<Attachments
:before-upload="() => false"
:items="attachedFiles"
@change="handleFileChange"
>
<template #placeholder="type">
<Flex
v-if="type && type.type === 'inline'"
justify="center"
vertical
gap="2"
>
<Typography.Text style="font-size: 30px; line-height: 1">
<CloudUploadOutlined />
</Typography.Text>
<Typography.Title
:level="5"
style="margin: 0; font-size: 14px; line-height: 1.5"
>
Upload files
</Typography.Title>
<Typography.Text type="secondary">
Click or drag files to this area to upload
</Typography.Text>
</Flex>
<Typography.Text v-if="type && type.type === 'drop'">
Drop file here
</Typography.Text>
</template>
</Attachments>
</Sender.Header>
</template>
</Sender>
</div>
<VueOfficeDocx
:src="pptx"
:style="pptStyle"
@rendered="renderedHandler"
@error="errorHandler"
/>
</div>
</template>

View File

@ -13,6 +13,20 @@ export default defineConfig(async () => {
target: 'http://localhost:8081/api',
ws: true,
},
'/v1': {
target: 'http://localhost:8081/v1',
rewrite: (path) => path.replace(/^\/v1/, ''),
changeOrigin: true,
ws: true,
},
'/static/*.pptx': {
target: 'http://47.112.173.8:6802',
changeOrigin: true,
},
'/static/*.docx': {
target: 'http://47.112.173.8:6805',
changeOrigin: true,
},
},
},
},

View File

@ -595,6 +595,9 @@ importers:
apps/web-antd:
dependencies:
'@ant-design/icons-vue':
specifier: ^7.0.1
version: 7.0.1(vue@3.5.13(typescript@5.8.3))
'@vben/access':
specifier: workspace:*
version: link:../../packages/effects/access
@ -637,6 +640,12 @@ importers:
'@vben/utils':
specifier: workspace:*
version: link:../../packages/utils
'@vue-office/docx':
specifier: ^1.6.3
version: 1.6.3(vue-demi@0.14.10(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
'@vue-office/pptx':
specifier: ^1.0.1
version: 1.0.1(vue-demi@0.14.10(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
'@vueuse/core':
specifier: 'catalog:'
version: 13.1.0(vue@3.5.13(typescript@5.8.3))
@ -4170,6 +4179,26 @@ packages:
'@volar/typescript@2.4.13':
resolution: {integrity: sha512-Ukz4xv84swJPupZeoFsQoeJEOm7U9pqsEnaGGgt5ni3SCTa22m8oJP5Nng3Wed7Uw5RBELdLxxORX8YhJPyOgQ==}
'@vue-office/docx@1.6.3':
resolution: {integrity: sha512-Cs+3CAaRBOWOiW4XAhTwwxJ0dy8cPIf6DqfNvYcD3YACiLwO4kuawLF2IAXxyijhbuOeoFsfvoVbOc16A/4bZA==}
peerDependencies:
'@vue/composition-api': ^1.7.1
vue: ^3.5.13
vue-demi: ^0.14.6
peerDependenciesMeta:
'@vue/composition-api':
optional: true
'@vue-office/pptx@1.0.1':
resolution: {integrity: sha512-+V7Kctzl6f6+Yk4NaD/wQGRIkqLWcowe0jEhPexWQb8Oilbzt1OyhWRWcMsxNDTdrgm6aMLP+0/tmw27cxddMg==}
peerDependencies:
'@vue/composition-api': ^1.7.1
vue: ^3.5.13
vue-demi: ^0.14.6
peerDependenciesMeta:
'@vue/composition-api':
optional: true
'@vue/babel-helper-vue-transform-on@1.4.0':
resolution: {integrity: sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw==}
@ -13076,6 +13105,16 @@ snapshots:
path-browserify: 1.0.1
vscode-uri: 3.1.0
'@vue-office/docx@1.6.3(vue-demi@0.14.10(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))':
dependencies:
vue: 3.5.13(typescript@5.8.3)
vue-demi: 0.14.10(vue@3.5.13(typescript@5.8.3))
'@vue-office/pptx@1.0.1(vue-demi@0.14.10(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))':
dependencies:
vue: 3.5.13(typescript@5.8.3)
vue-demi: 0.14.10(vue@3.5.13(typescript@5.8.3))
'@vue/babel-helper-vue-transform-on@1.4.0': {}
'@vue/babel-plugin-jsx@1.4.0(@babel/core@7.26.10)':

View File

@ -1,25 +1,9 @@
{
"folders": [
{
"name": "@vben/backend-mock",
"path": "apps/backend-mock",
},
{
"name": "@vben/web-antd",
"path": "apps/web-antd",
},
{
"name": "@vben/web-ele",
"path": "apps/web-ele",
},
{
"name": "@vben/web-naive",
"path": "apps/web-naive",
},
{
"name": "@vben/docs",
"path": "docs",
},
{
"name": "@vben/commitlint-config",
"path": "internal/lint-configs/commitlint-config",
@ -156,10 +140,6 @@
"name": "@vben/utils",
"path": "packages/utils",
},
{
"name": "@vben/playground",
"path": "playground",
},
{
"name": "@vben/turbo-run",
"path": "scripts/turbo-run",