feat(@vben/common-ui): 优化AI工作流模块

- 添加 AI 工作流相关的路由和页面组件
- 实现 AI 平台信息获取、WORD 生成和 PPT 生成等功能
-优化模板选择和历史记录展示逻辑
- 新增聊天参数获取和删除功能
- 更新相关 API接口
This commit is contained in:
Kven 2025-05-16 17:44:56 +08:00
parent dcafd5e3b0
commit 9cead8075b
19 changed files with 1780 additions and 1795 deletions

View File

@ -34,6 +34,12 @@ export namespace ChatflowApi {
createAt: string;
updatedAt: string;
}
export interface deleteParams {
userId: string;
conversationId: string;
appId: string;
}
}
// 聊天流
@ -65,6 +71,10 @@ export async function getChatList(
// return requestClient.post('/v1/chat/messages', data);
// };
//
// export function deleteChatflow(data){
// return requestClient.delete('/v1/chat/messages/suggested', data);
// };
export function deleteChatflow(data: ChatflowApi.deleteParams) {
return requestClient.delete('/v1/chat/conversation', { params: data });
}
export function getChatParameters(appId: string) {
return requestClient.get(`/v1/chat/parameters/${appId}`);
}

View File

@ -49,5 +49,7 @@ export function getWorkflowInfo(data: WorkflowApi.WorkflowLogParams) {
}
export function getWorkflowList(params: WorkflowApi.WorkflowLogParams) {
return requestClient.get(`/v1/workflow/list/${params.appid}`);
return requestClient.get(`/v1/workflow/list/${params.appid}`, {
params,
});
}

View File

@ -0,0 +1,53 @@
import type { RouteRecordRaw } from 'vue-router';
import { HugeAi, SvgPPT, SvgSpider, SvgWord } from '@vben/icons';
const routes: RouteRecordRaw[] = [
{
name: 'aiflow',
path: '/aiflow',
meta: {
icon: HugeAi,
title: 'AI工作流',
order: 4,
authority: ['system'],
},
children: [
{
name: 'spider',
path: '/aiflow/spider',
component: () => import('#/views/spider/index.vue'),
meta: {
icon: SvgSpider,
title: 'AI平台信息获取',
order: 1,
authority: ['dify:server:init'],
},
},
{
name: 'word',
path: '/aiflow/word',
component: () => import('#/views/word/index.vue'),
meta: {
icon: SvgWord,
title: 'AI申报书WORD生成',
order: 2,
authority: ['dify:chat:send'],
},
},
{
name: 'ppt',
path: '/aiflow/ppt',
component: () => import('#/views/ppt/index.vue'),
meta: {
icon: SvgPPT,
title: 'PPT自动生成',
order: 3,
authority: ['dify:workflow:run'],
},
},
],
},
];
export default routes;

View File

@ -1,19 +0,0 @@
import type { RouteRecordRaw } from 'vue-router';
import { SvgPPT } from '@vben/icons';
const routes: RouteRecordRaw[] = [
{
name: 'ppt',
path: '/ppt',
component: () => import('#/views/ppt/index.vue'),
meta: {
icon: SvgPPT,
title: 'PPT自动生成',
order: 3,
authority: ['dify:workflow:run'],
},
},
];
export default routes;

View File

@ -1,19 +0,0 @@
import type { RouteRecordRaw } from 'vue-router';
import { SvgSpider } from '@vben/icons';
const routes: RouteRecordRaw[] = [
{
name: 'spider',
path: '/spider',
component: () => import('#/views/spider/index.vue'),
meta: {
icon: SvgSpider,
title: '数据爬取',
order: 1,
authority: ['dify:server:init'],
},
},
];
export default routes;

View File

@ -1,19 +0,0 @@
import type { RouteRecordRaw } from 'vue-router';
import { SvgWord } from '@vben/icons';
const routes: RouteRecordRaw[] = [
{
name: 'word',
path: '/word',
component: () => import('#/views/word/index.vue'),
meta: {
icon: SvgWord,
title: 'Word自动生成',
order: 2,
authority: ['dify:chat:send'],
},
},
];
export default routes;

View File

@ -3,7 +3,7 @@ import type { PPTTempItem } from '@vben/common-ui';
import { onMounted, reactive, ref } from 'vue';
import { PptHistoryView, PptListView, PptWorkView } from '@vben/common-ui';
import { PptListView, PptWorkView } from '@vben/common-ui';
import { message } from 'ant-design-vue';
@ -11,7 +11,7 @@ import { getWorkflowInfo, getWorkflowList, sendWorkflow } from '#/api';
let temp = reactive<PPTTempItem>({
id: 'ee3889b6-50fa-463e-b956-3b93447727fc',
name: '从可研申报书生成科技项目PPT',
name: '海南科技项目可研报告PPT生成',
});
interface ResultItem {
key: number;
@ -29,7 +29,6 @@ const getLogs = async (appid: string) => {
const res = await getWorkflowList({
appid,
limit: 5,
page: 1,
});
hitsory.value = res;
loading.value = false;
@ -69,24 +68,32 @@ onMounted(() => {
</script>
<template>
<div class="pr-5" style="height: calc(100vh - 80px)">
<div class="flex h-full flex-col lg:flex-row">
<div class="mr-4 w-full lg:w-1/6">
<PptHistoryView
:loading="loading"
<div class="layout">
<div class="lg:w-1/6">
<PptListView
title="选择模板"
:items="hitsory"
title="运行历史"
:loading="loading"
@click-mode="handleClickMode"
@click="handleClick"
/>
<PptListView title="选择模板" @click="handleClickMode" />
</div>
<div class="h-full w-full lg:w-5/6">
<PptWorkView
:item="temp"
:run-workflow="sendWorkflow"
:item-message="itemMessage"
/>
</div>
</div>
</div>
</template>
<style scoped lang="less">
.layout {
width: 100%;
min-width: 1400px;
height: 100%;
display: flex;
background: hsl(216deg 21.74% 95.49%);
font-family: AlibabaPuHuiTi, v-deep(var(--ant-font-family)), sans-serif;
border-radius: v-deep(var(--ant-border-radius));
}
</style>

View File

@ -1,615 +0,0 @@
<script setup lang="ts">
import type {
AttachmentsProps,
BubbleListProps,
ConversationsProps,
PromptsProps,
} from 'ant-design-x-vue';
import type { VNode } from 'vue';
import { computed, h, onMounted, ref, watch } from 'vue';
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 = [
{
key: '0',
label: 'What is Ant Design X?',
},
];
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: '1563',
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: '1563',
lastId: '',
sortBy: '',
limit: '5',
},
);
if (res.status === 200) {
conversationsItems.value = res.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>
<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,58 +1,101 @@
<script lang="ts" setup>
import type { SpiderItem } from '@vben/common-ui';
import type { PPTTempItem } from '@vben/common-ui';
import { ref } from 'vue';
import { SpiderListView, SpiderWorkView } from '@vben/common-ui';
import { sendWorkflow } from '#/api';
import { notification } from 'ant-design-vue';
const spiderList = ref<SpiderItem[]>([
{
id: 'a2a55334-a111-45e6-942f-9f3f70af8826',
name: '全国公共资源交易平台_数据爬取',
},
{
id: 'c736edd0-925d-4877-9223-56aab7342311',
name: '广州公共资源交易中心',
},
]);
const spider = ref<SpiderItem>();
import { getWorkflowInfo, getWorkflowList, sendWorkflow } from '#/api';
// const getFlowList = async () => {
// const res = await getAppList({});
// if (res) {
// spiderList.value = res;
// loading.value = false;
// }
// };
interface ResultItem {
key: number;
role: 'ai' | 'user';
content: string;
footer?: any;
}
interface WordFlowItem {
id: string;
workflowRunId: string;
taskId: string;
userId: string;
appId: string;
}
function handleClick(item: SpiderItem) {
spider.value = item;
const hitsory = ref([]);
const loading = ref(true);
const temp = ref();
const itemMessage = ref<ResultItem[]>([]);
const getLogs = async (appid: string) => {
loading.value = true;
const res = await getWorkflowList({
appid,
limit: 5,
});
hitsory.value = res;
loading.value = false;
};
async function handleClick(item: WordFlowItem) {
const res = await getWorkflowInfo({
appid: item.appId,
workflowRunId: item.workflowRunId,
});
itemMessage.value = [];
if (res.outputs) {
itemMessage.value.push({
key: itemMessage.value.length + 1,
role: 'ai',
content: res.outputs.result,
});
}
}
async function handleClickMode(item: PPTTempItem) {
notification.success({
message: `${item.name}`,
description: '已选取',
duration: 3,
});
temp.value = item;
getLogs(item.id);
}
// onMounted(() => {
// getFlowList();
// getLogs(spiderList.value[0].id);
// });
</script>
<template>
<div class="px-5">
<div class="mt-5 flex flex-col lg:flex-row">
<div class="mr-4 w-full lg:w-1/5">
<div class="layout">
<div class="lg:w-1/5">
<SpiderListView
:items="spiderList"
title="数据爬取工具"
title="选择模板"
:items="hitsory"
:loading="loading"
@click-mode="handleClickMode"
@click="handleClick"
/>
</div>
<div class="w-full lg:w-4/5">
<SpiderWorkView
:item="spider"
:run-spider="sendWorkflow"
title="目标网址:"
:item="temp"
:run-spider="sendWorkflow"
:item-message="itemMessage"
/>
</div>
</div>
</div>
</template>
<style scoped lang="less">
.layout {
width: 100%;
min-width: 1400px;
height: 100%;
display: flex;
background: hsl(216deg 21.74% 95.49%);
font-family: AlibabaPuHuiTi, v-deep(var(--ant-font-family)), sans-serif;
border-radius: v-deep(var(--ant-border-radius));
}
</style>

View File

@ -3,19 +3,25 @@ import type { WordTempItem } from '@vben/common-ui';
import { onMounted, reactive, ref } from 'vue';
import { WordHistoryView, WordListView, WordWorkView } from '@vben/common-ui';
import { WordListView, WordWorkView } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { message, notification } from 'ant-design-vue';
import { getChatList, sendChatflow } from '#/api';
import {
deleteChatflow,
getChatList,
getChatParameters,
sendChatflow,
} from '#/api';
let temp = reactive<WordTempItem>({
id: 'baca08c1-e92b-4dc9-a445-3584803f54d4',
name: '职业创新申报书',
name: '海南职创申报书生成',
});
const hitsory = ref([]);
const loading = ref(true);
const params = ref({});
const getLogs = async (appid: string) => {
loading.value = true;
@ -40,8 +46,32 @@ const getLogs = async (appid: string) => {
};
function handleClickMode(item: WordTempItem) {
message.success(`已选取${item.name}为模板`);
notification.success({
message: `${item.name}`,
description: '已选取',
duration: 3,
});
temp = item;
getParameters(temp.id);
}
async function deleteLog(item: any) {
const res = await deleteChatflow({
appId: temp.id,
id: item,
userId: '1562',
});
if (res.code === 0) {
message.success('删除成功');
getLogs(temp.id);
} else {
message.error('删除失败');
}
}
async function getParameters(id: string) {
const res = await getChatParameters(id);
params.value = res.suggestedQuestions;
}
function handleClick(item: WordTempItem) {
@ -51,24 +81,38 @@ function handleClick(item: WordTempItem) {
onMounted(() => {
getLogs(temp.id);
getParameters(temp.id);
});
</script>
<template>
<div class="pr-5" style="height: calc(100vh - 80px)">
<div class="flex h-full flex-col lg:flex-row">
<div class="mr-4 w-full lg:w-1/6">
<WordHistoryView
:loading="loading"
<div class="layout">
<div class="lg:w-1/6">
<WordListView
title="选择模板"
:items="hitsory"
title="运行历史"
:loading="loading"
@click-mode="handleClickMode"
@click="handleClick"
@delete="deleteLog"
/>
<WordListView title="选择模板" @click="handleClickMode" />
</div>
<div class="h-full w-full lg:w-5/6">
<WordWorkView :item="temp" :run-chatflow="sendChatflow" />
</div>
</div>
<WordWorkView
:item="temp"
:params-data="params"
:run-chatflow="sendChatflow"
/>
</div>
</template>
<style scoped lang="less">
.layout {
width: 100%;
min-width: 1400px;
height: 100%;
display: flex;
background: hsl(216deg 21.74% 95.49%);
font-family: AlibabaPuHuiTi, v-deep(var(--ant-font-family)), sans-serif;
border-radius: v-deep(var(--ant-border-radius));
}
</style>

View File

@ -1,635 +0,0 @@
<script setup lang="ts">
import type {
AttachmentsProps,
BubbleListProps,
ConversationsProps,
PromptsProps,
} from 'ant-design-x-vue';
import type { VNode } from 'vue';
import { computed, h, onMounted, ref, watch } from 'vue';
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 = [
{
key: '0',
label: 'What is Ant Design X?',
},
];
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>
<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

@ -1,38 +1,210 @@
<script setup lang="ts">
import type { PPTTempItem } from './typing';
import type { ConversationsProps } from 'ant-design-x-vue';
import { Card, CardContent, CardHeader, CardTitle } from '@vben-core/shadcn-ui';
import type { PptHistoryItem } from './typing';
import { computed, ref } from 'vue';
// import { Card, CardContent, CardHeader, CardTitle } from '@vben-core/shadcn-ui';
import { Menu } from 'ant-design-vue';
import { Conversations } from 'ant-design-x-vue';
interface Props {
items?: PptHistoryItem[];
title: string;
loading: boolean;
}
defineOptions({
name: 'PptListView',
name: 'WordListView',
});
defineEmits(['click']);
const props = withDefaults(defineProps<Props>(), {
items: () => [],
});
const emit = defineEmits(['click', 'clickMode']);
const defaultConversationsItems = computed(() => {
return props.items.map((item) => {
return {
key: item.id,
label: item.id,
};
});
});
const conversationsItems = ref(defaultConversationsItems);
const activeKey = ref(defaultConversationsItems.value[0]?.key);
const items: PPTTempItem[] = [
const itemsData = ref([
{
id: 'ee3889b6-50fa-463e-b956-3b93447727fc',
name: '从可研申报书生成科技项目PPT',
key: 'baca08c1-e92b-4dc9-a445-3584803f54d4',
label: '海南职创申报书生成',
title: '海南职创申报书生成',
},
];
]);
// const items2: ItemType[] = reactive([
// getItem(
// '',
// 'grp',
// null,
// [getItem('', 'baca08c1-e92b-4dc9-a445-3584803f54d4')],
// 'group',
// ),
// ]);
const handleMenuClick = (item: { key: string }) => {
const selectedItem = itemsData.value.find((i) => i.key === item.key);
if (selectedItem) {
// title -> name, key -> id
const transformedItem = {
name: selectedItem.title,
id: selectedItem.key,
};
emit('clickMode', transformedItem); //
}
};
const onConversationClick: ConversationsProps['onActiveChange'] = (key) => {
activeKey.value = key;
const matchedItem = props.items.find((item) => item.id === key);
if (matchedItem) {
emit('click', matchedItem);
} else {
emit('click', null); //
}
};
// const menuConfig: ConversationsProps['menu'] = (conversation) => ({
// items: [
// {
// label: '',
// key: conversation.key,
// icon: h(DeleteOutlined),
// danger: true,
// },
// ],
// onClick: (menuInfo) => {
// emit('delete', menuInfo.key)
// },
// });
const selectedKeys = ref([]);
const openKeys = ref([]);
</script>
<template>
<Card style="height: 50vh; overflow-y: auto; border-radius: 0">
<CardHeader class="py-4">
<CardTitle class="text-lg">选择模板</CardTitle>
</CardHeader>
<CardContent class="flex flex-wrap p-5 pt-0">
<ul class="divide-border w-full divide-y" role="list">
<li
v-for="(item, index) in items"
:key="index"
@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="addBtn">模板</div>
<Menu
v-model:open-keys="openKeys"
v-model:selected-keys="selectedKeys"
mode="vertical"
class="mode"
:items="items"
@click="handleMenuClick"
/>
<!-- 🌟 添加会话 -->
<!-- <Button type="link" class="addBtn">会话</Button>-->
<div class="addBtn">会话</div>
<!-- 🌟 添加会话 -->
<!-- 🌟 会话管理 -->
<Conversations
:items="conversationsItems"
class="conversations"
:active-key="activeKey"
@active-change="onConversationClick"
/>
</div>
</template>
<style scoped lang="less">
.layout {
width: 100%;
min-width: 1400px;
height: 100%;
display: flex;
background: hsl(216deg 21.74% 95.49%);
font-family: AlibabaPuHuiTi, v-deep(var(--ant-font-family)), sans-serif;
border-radius: v-deep(var(--ant-border-radius));
}
.menu {
background: #ffffff;
width: 200px;
height: 100%;
display: flex;
flex-direction: column;
}
.mode {
overflow-y: auto;
}
.conversations {
padding: 0 12px;
height: 400px;
overflow-y: auto;
}
.chat {
height: 100%;
width: 100%;
max-width: 1000px;
max-height: 900px;
margin: 0 auto;
box-sizing: border-box;
display: flex;
flex-direction: column;
padding: 12px;
gap: 16px;
}
//.messages {
// flex: 1;
//}
.placeholder {
padding-top: 32px;
text-align: left;
flex: 1;
}
.sender {
background: #ffffff;
width: 100%;
box-shadow: v-deep(var(--ant-box-shadow));
}
.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: v-deep(var(--ant-color-text));
font-size: 16px;
}
.addBtn {
float: left;
//border: 1px solid #1677ff34;
color: #666666;
width: calc(100% - 24px);
margin: 12px 12px 24px 12px;
}
</style>

View File

@ -1,21 +1,30 @@
<script setup lang="ts">
import type { BubbleListProps } from 'ant-design-x-vue';
import type { AttachmentsProps, BubbleListProps } from 'ant-design-x-vue';
import type { DrawerPlacement } from '@vben/common-ui';
import type { DrawerPlacement } from '@vben-core/popup-ui';
import type { PPTTempItem } from './typing';
import { h, ref, watch } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { useVbenDrawer } from '@vben-core/popup-ui';
import { Card } from '@vben-core/shadcn-ui';
import {
CloudUploadOutlined,
PaperClipOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
import { Badge, Button, Flex, Typography } from 'ant-design-vue';
import { Attachments, Bubble, Sender } from 'ant-design-x-vue';
import { UserOutlined } from '@ant-design/icons-vue';
import { Button, Flex } from 'ant-design-vue';
import { Attachments, BubbleList, Sender } from 'ant-design-x-vue';
import WordPreview from '../word/word-preview.vue';
import PptPreview from './ppt-perview.vue';
interface ResultItem {
key: number;
role: 'ai' | 'user';
content: string;
footer?: any;
}
interface WorkflowParams {
appid: string;
@ -36,13 +45,6 @@ interface WorkflowResult {
};
}
interface ResultItem {
key: number;
role: 'ai' | 'user';
content: string;
footer?: any;
}
interface Props {
itemMessage?: ResultItem;
item?: PPTTempItem;
@ -52,9 +54,7 @@ interface Props {
) => Promise<WorkflowResult>;
}
defineOptions({
name: 'SpiderWorkView',
});
defineOptions({ name: 'PlaygroundIndependentSetup' });
const props = withDefaults(defineProps<Props>(), {
itemMessage: () => null,
@ -68,12 +68,81 @@ const props = withDefaults(defineProps<Props>(), {
}),
});
// const { token } = theme.useToken();
const roles: BubbleListProps['roles'] = {
user: {
placement: 'end',
typing: false,
avatar: { icon: h(UserOutlined), style: { background: '#87d068' } },
},
ai: {
placement: 'start',
typing: false,
style: {
maxWidth: 600,
marginInlineEnd: 44,
},
styles: {
footer: {
width: '100%',
},
},
avatar: { icon: h(UserOutlined), style: { background: '#fde3cf' } },
},
file: {
placement: 'start',
avatar: { icon: h(UserOutlined), style: { visibility: 'hidden' } },
variant: 'borderless',
messageRender: (items: any) =>
h(
Flex,
{ vertical: true, gap: 'middle' },
items.map((item: any) =>
h(Attachments.FileCard, { key: item.uid, item }),
),
),
},
};
// ==================== State ====================
const headerOpen = ref(false);
const content = ref('');
const attachedFiles = ref<AttachmentsProps['items']>([]);
const agentRequestLoading = ref(false);
const fetchStatus = ref('');
const resultItems = ref<ResultItem[]>([]);
const fetchResult = ref('');
// ==================== Runtime ====================
// const [agent] = useXAgent({
// request: async ({ message }, { onSuccess }) => {
// agentRequestLoading.value = true;
// await sleep();
// agentRequestLoading.value = false;
// onSuccess(`Mock success return. You said: ${message}`);
// },
// });
//
// const { } = useXChat({
// agent: agent.value,
// });
// watch(
// activeKey,
// () => {
// if (activeKey.value !== undefined) {
// setMessages([]);
// }
// },
// { immediate: true },
// );
const [PreviewDrawer, previewDrawerApi] = useVbenDrawer({
//
connectedComponent: PptPreview,
connectedComponent: WordPreview,
// placement: 'left',
});
function openPreviewDrawer(
placement: DrawerPlacement = 'right',
filename?: string,
@ -82,6 +151,11 @@ function openPreviewDrawer(
previewDrawerApi.setState({ placement }).setData(fileData).open();
}
// ==================== Event ====================
const handleFileChange: AttachmentsProps['onChange'] = (info) =>
(attachedFiles.value = info.fileList);
// .pptx URL
function isPptxURL(str: string): boolean {
return str.endsWith('.pptx');
@ -97,8 +171,13 @@ function extractPptxFilename(url: string): null | string {
}
const startFetching = async () => {
isFetching.value = true;
agentRequestLoading.value = true;
fetchStatus.value = 'fetching';
resultItems.value.push({
key: resultItems.value.length + 1,
role: 'user',
content: content.value,
});
try {
const res = await props.runWorkflow(
@ -110,12 +189,12 @@ const startFetching = async () => {
conversationId: '',
files: [],
inputs: {
declarationDoc: value.value,
declarationDoc: content.value,
},
},
);
const { result } = res.data.outputs;
value.value = '';
content.value = '';
const filename = ref('');
@ -168,51 +247,55 @@ const startFetching = async () => {
}
//
isFetching.value = false;
agentRequestLoading.value = false;
fetchStatus.value = 'completed';
};
const roles: BubbleListProps['roles'] = {
user: {
placement: 'end',
typing: false,
avatar: { icon: h(UserOutlined), style: { background: '#87d068' } },
},
ai: {
placement: 'start',
typing: false,
style: {
maxWidth: 600,
marginInlineEnd: 44,
},
styles: {
footer: {
width: '100%',
},
},
avatar: { icon: h(UserOutlined), style: { background: '#fde3cf' } },
},
file: {
placement: 'start',
avatar: { icon: h(UserOutlined), style: { visibility: 'hidden' } },
variant: 'borderless',
messageRender: (items: any) =>
h(
Flex,
{ vertical: true, gap: 'middle' },
items.map((item: any) =>
h(Attachments.FileCard, { key: item.uid, item }),
),
),
},
};
// ==================== Nodes ====================
// const placeholderNode = computed(() =>
// h(
// Space,
// { direction: 'vertical', size: 16, style: 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 isFetching = ref(false);
const fetchResult = ref('');
const fetchStatus = ref('');
const value = ref('');
const resultItems = ref<ResultItem[]>([]);
// 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,
// }));
// });
// itemMessage resultItems
watch(
@ -287,30 +370,162 @@ watch(
</script>
<template>
<div style="display: flex; flex-direction: column" class="h-full">
<div class="layout">
<PreviewDrawer />
<div style="flex: 1; padding: 20px; overflow-y: auto">
<BubbleList
<div class="chat">
<!-- 🌟 消息列表 -->
<Bubble.List
variant="shadow"
:roles="roles"
:typing="true"
:items="resultItems"
:roles="roles"
style="flex: 1"
/>
</div>
<Card class="w-full">
<!-- 🌟 输入框 -->
<Sender
v-model:value="value"
:loading="isFetching"
:disabled="isFetching"
:auto-size="{ minRows: 1, maxRows: 12 }"
:value="content"
class="sender"
:loading="agentRequestLoading"
:disabled="agentRequestLoading"
@submit="startFetching"
/>
</Card>
@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>
</div>
</template>
<style scoped>
.markdown-content p:empty {
display: none;
<style scoped lang="less">
.layout {
width: 100%;
min-width: 1000px;
height: 100%;
display: flex;
background: hsl(216deg 21.74% 95.49%);
font-family: AlibabaPuHuiTi, v-deep(var(--ant-font-family)), sans-serif;
border-radius: v-deep(var(--ant-border-radius));
}
.menu {
background: #ffffff;
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: 1000px;
max-height: 900px;
margin: 0 auto;
box-sizing: border-box;
display: flex;
flex-direction: column;
padding: 12px;
gap: 16px;
}
//.messages {
// flex: 1;
//}
.placeholder {
padding-top: 32px;
text-align: left;
flex: 1;
}
.sender {
background: #ffffff;
box-shadow: v-deep(var(--ant-box-shadow));
}
.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: v-deep(var(--ant-color-text));
font-size: 16px;
}
.addBtn {
background: #1677ff0f;
border: 1px solid #1677ff34;
width: calc(100% - 24px);
margin: 12px 12px 24px 12px;
}
</style>

View File

@ -1,45 +1,205 @@
<script setup lang="ts">
import type { ConversationsProps } from 'ant-design-x-vue';
import type { SpiderItem } from './typing';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@vben-core/shadcn-ui';
import { computed, ref } from 'vue';
import { Menu } from 'ant-design-vue';
import { Conversations } from 'ant-design-x-vue';
interface Props {
items?: SpiderItem[];
spiderList?: SpiderItem[];
title: string;
}
defineOptions({
name: 'SpiderListView',
});
withDefaults(defineProps<Props>(), {
const props = withDefaults(defineProps<Props>(), {
items: () => [],
spiderList: () => [],
});
defineEmits(['click']);
const emit = defineEmits(['click', 'clickMode']);
const selectedKeys = ref([]);
const openKeys = ref([]);
const defaultConversationsItems = computed(() => {
return props.items.map((item) => {
return {
key: item.id,
label: item.id,
};
});
});
const conversationsItems = ref(defaultConversationsItems);
const activeKey = ref(defaultConversationsItems.value[0]?.key);
const itemsData = ref([
{
key: 'a2a55334-a111-45e6-942f-9f3f70af8826',
label: '全国公共资源交易平台_信息爬取',
},
{
key: 'c736edd0-925d-4877-9223-56aab7342311',
label: '广州公共资源交易中心_信息获取',
},
]);
const handleMenuClick = (item: { key: string }) => {
const selectedItem = itemsData.value.find((i) => i.key === item.key);
if (selectedItem) {
// title -> name, key -> id
const transformedItem = {
name: selectedItem.title,
id: selectedItem.key,
};
emit('clickMode', transformedItem); //
}
};
const onConversationClick: ConversationsProps['onActiveChange'] = (key) => {
activeKey.value = key;
const matchedItem = props.items.find((item) => item.id === key);
if (matchedItem) {
emit('click', matchedItem);
} else {
emit('click', null); //
}
};
</script>
<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>
<!-- <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="addBtn">数据信息列表</div>
<Menu
v-model:open-keys="openKeys"
v-model:selected-keys="selectedKeys"
mode="vertical"
class="mode"
:items="items"
@click="handleMenuClick"
/>
<!-- 🌟 添加会话 -->
<!-- <Button type="link" class="addBtn">会话</Button>-->
<div class="addBtn">会话</div>
<!-- 🌟 添加会话 -->
<!-- 🌟 会话管理 -->
<Conversations
:items="conversationsItems"
class="conversations"
:active-key="activeKey"
@active-change="onConversationClick"
/>
</div>
</template>
<style scoped lang="less">
.layout {
width: 100%;
min-width: 1400px;
height: 100%;
display: flex;
background: hsl(216deg 21.74% 95.49%);
font-family: AlibabaPuHuiTi, v-deep(var(--ant-font-family)), sans-serif;
border-radius: v-deep(var(--ant-border-radius));
}
.menu {
background: #ffffff;
width: 200px;
height: 100%;
display: flex;
flex-direction: column;
}
.mode {
overflow-y: auto;
}
.conversations {
padding: 0 12px;
height: 400px;
overflow-y: auto;
}
.chat {
height: 100%;
width: 100%;
max-width: 1000px;
max-height: 900px;
margin: 0 auto;
box-sizing: border-box;
display: flex;
flex-direction: column;
padding: 12px;
gap: 16px;
}
//.messages {
// flex: 1;
//}
.placeholder {
padding-top: 32px;
text-align: left;
flex: 1;
}
.sender {
background: #ffffff;
width: 100%;
box-shadow: v-deep(var(--ant-box-shadow));
}
.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: v-deep(var(--ant-color-text));
font-size: 16px;
}
.addBtn {
float: left;
//border: 1px solid #1677ff34;
color: #666666;
width: calc(100% - 24px);
margin: 12px 12px 24px 12px;
}
</style>

View File

@ -0,0 +1,56 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import VueOfficeDocx from '@vue-office/docx';
import { message } from 'ant-design-vue';
import '@vue-office/docx/lib/index.css';
// const url = ref('');
const isLoading = ref(false); //
const docx = ref<any>(`/docx/027c6b7c-fea6-4964-839b-27857c4d3181.docx`);
const pptStyle = ref({
height: 'calc(100vh - 100px)',
width: '100%',
margin: 'auto',
});
const renderedHandler = () => {
isLoading.value = false; //
message.success('渲染完成');
};
const errorHandler = () => {
isLoading.value = false; //
message.error('渲染失败');
};
const [Drawer, drawerApi] = useVbenDrawer({
onCancel() {
drawerApi.close();
},
onClosed() {
drawerApi.setState({ overlayBlur: 0, placement: 'right' });
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = drawerApi.getData<Record<string, any>>();
if (data) {
isLoading.value = false; //
docx.value = `/docx/${data}`; // docx
}
// url.value = drawerApi.getData<Record<string, any>>();
}
},
});
</script>
<template>
<Drawer class="w-[880px]" title="文档预览" :footer="false">
<div v-if="isLoading" class="loading-overlay">正在加载文档请稍候...</div>
<VueOfficeDocx
:src="docx"
:style="pptStyle"
@rendered="renderedHandler"
@error="errorHandler"
/>
</Drawer>
</template>

View File

@ -1,8 +1,13 @@
<script setup lang="ts">
import type { BubbleListProps } from 'ant-design-x-vue';
import type { DrawerPlacement } from '@vben-core/popup-ui';
import type { SpiderItem } from './typing';
import { ref } from 'vue';
import { h, ref, watch } from 'vue';
import { useVbenDrawer } from '@vben-core/popup-ui';
import {
Button,
Card,
@ -12,12 +17,12 @@ import {
CardTitle,
} from '@vben-core/shadcn-ui';
import { message, RangePicker } from 'ant-design-vue';
import { UserOutlined } from '@ant-design/icons-vue';
import { Flex, message, RangePicker } from 'ant-design-vue';
import { Attachments, Bubble } from 'ant-design-x-vue';
import dayjs, { Dayjs } from 'dayjs';
// import {Attachments, BubbleList, type BubbleListProps} from "ant-design-x-vue";
// import {UserOutlined} from "@ant-design/icons-vue";
// import PptPreview from "../ppt/ppt-perview.vue";
// import {type DrawerPlacement, useVbenDrawer} from "@vben-core/popup-ui";
import SpiderPreview from './spider-preview.vue';
interface SpiderParams {
appid: string;
@ -38,14 +43,15 @@ interface SpiderResult {
};
}
// interface ResultItem {
// key: number;
// role: 'ai' | 'user';
// content: string;
// footer?: any;
// }
interface ResultItem {
key: number;
role: 'ai' | 'user';
content: string;
footer?: any;
}
interface Props {
itemMessage?: ResultItem;
item?: SpiderItem;
title: string;
runSpider?: (
@ -59,6 +65,7 @@ defineOptions({
});
const props = withDefaults(defineProps<Props>(), {
itemMessage: () => null,
item: () => {
return null;
},
@ -71,21 +78,54 @@ const props = withDefaults(defineProps<Props>(), {
}),
});
// const resultItems = ref<ResultItem[]>([]);
// const styles = computed(() => {
// return {
// 'placeholder': {
// 'padding-top': '32px',
// 'text-align': 'left',
// 'flex': 1,
// },
// } as const
// })
// const [PreviewDrawer, previewDrawerApi] = useVbenDrawer({
// //
// connectedComponent: PptPreview,
// // placement: 'left',
// });
// function openPreviewDrawer(
// placement: DrawerPlacement = 'right',
// filename?: string,
// ) {
// const fileData = filename.value;
// previewDrawerApi.setState({ placement }).setData(fileData).open();
// 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 items = computed<BubbleListProps['items']>(() => {
// if (resultItems.value.length === 0) {
// return [{ content: placeholderNode, variant: 'borderless' }]
// }
// return resultItems
// })
const [PreviewDrawer, previewDrawerApi] = useVbenDrawer({
//
connectedComponent: SpiderPreview,
// placement: 'left',
});
function openPreviewDrawer(
placement: DrawerPlacement = 'right',
filename?: string,
) {
const fileData = filename.value;
previewDrawerApi.setState({ placement }).setData(fileData).open();
}
const selectedDateRange = ref<[Dayjs, Dayjs]>([
dayjs('2025-05-05'),
@ -93,18 +133,18 @@ const selectedDateRange = ref<[Dayjs, Dayjs]>([
]);
// .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;
// }
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 startFetching = async () => {
//
@ -140,65 +180,65 @@ const startFetching = async () => {
//
fetchResult.value = res.data.outputs.result;
message.success('抓取成功');
const { result } = res.data.outputs;
const filename = ref('');
if (result && isDocxURL(result)) {
filename.value = extractDocxFilename(result);
}
// url http://47.112.173.8:6802/static/66f3cfd95e364a239d8036390db658ae.docx
fetchResult.value = `/static/${filename.value}`;
resultItems.value.push({
key: resultItems.value.length + 1,
role: 'ai',
content: '文档已生成',
footer: h(Flex, null, [
h(
Button,
{
size: 'nomarl',
type: 'primary',
onClick: () => {
openPreviewDrawer('right', filename);
},
},
'文档预览',
),
h(
Button,
{
size: 'nomarl',
type: 'primary',
style: {
marginLeft: '10px',
},
onClick: () => {
// <a>
const link = document.createElement('a');
link.href = result; //
link.download = filename; //
document.body.append(link); // <a>
link.click(); //
link.remove(); // <a>
},
},
'文档下载',
),
]),
});
fetchStatus.value = 'completed';
} else {
fetchResult.value = '';
message.error('抓取无结果');
}
// const { result } = res.data.outputs;
//
// const filename = ref('');
//
// if (result && isDocxURL(result)) {
// filename.value = extractDocxFilename(result);
// }
//
// // url http://47.112.173.8:6802/static/66f3cfd95e364a239d8036390db658ae.docx
// fetchResult.value = `/static/${filename.value}`;
// resultItems.value.push({
// key: resultItems.value.length + 1,
// role: 'ai',
// content: '',
// footer: h(Flex, null, [
// h(
// Button,
// {
// size: 'nomarl',
// type: 'primary',
// onClick: () => {
// openPreviewDrawer('right', filename);
// },
// },
// '',
// ),
// h(
// Button,
// {
// size: 'nomarl',
// type: 'primary',
// style: {
// marginLeft: '10px',
// },
// onClick: () => {
// // <a>
// const link = document.createElement('a');
// link.href = result; //
// link.download = filename; //
// document.body.append(link); // <a>
// link.click(); //
// link.remove(); // <a>
// },
// },
// '',
// ),
// ]),
// });
} catch (error) {
console.error(error);
}
//
isFetching.value = false;
fetchStatus.value = 'completed';
};
//
@ -220,58 +260,130 @@ const downloadFile = () => {
};
//
// const roles: BubbleListProps['roles'] = {
// user: {
// placement: 'end',
// typing: false,
// avatar: { icon: h(UserOutlined), style: { background: '#87d068' } },
// },
// ai: {
// placement: 'start',
// typing: false,
// style: {
// maxWidth: 600,
// marginInlineEnd: 44,
// },
// styles: {
// footer: {
// width: '100%',
// },
// },
// avatar: { icon: h(UserOutlined), style: { background: '#fde3cf' } },
// },
// file: {
// placement: 'start',
// avatar: { icon: h(UserOutlined), style: { visibility: 'hidden' } },
// variant: 'borderless',
// messageRender: (items: any) =>
// h(
// Flex,
// { vertical: true, gap: 'middle' },
// items.map((item: any) =>
// h(Attachments.FileCard, { key: item.uid, item }),
// ),
// ),
// },
// };
const roles: BubbleListProps['roles'] = {
user: {
placement: 'end',
typing: false,
avatar: { icon: h(UserOutlined), style: { background: '#87d068' } },
},
ai: {
placement: 'start',
typing: false,
style: {
maxWidth: 600,
marginInlineEnd: 44,
},
styles: {
footer: {
width: '100%',
},
},
avatar: { icon: h(UserOutlined), style: { background: '#fde3cf' } },
},
file: {
placement: 'start',
avatar: { icon: h(UserOutlined), style: { visibility: 'hidden' } },
variant: 'borderless',
messageRender: (items: any) =>
h(
Flex,
{ vertical: true, gap: 'middle' },
items.map((item: any) =>
h(Attachments.FileCard, { key: item.uid, item }),
),
),
},
};
const isFetching = ref(false);
const fetchResult = ref('');
const fetchStatus = ref('');
// itemMessage resultItems
watch(
() => props.itemMessage,
(newVal) => {
resultItems.value = [];
if (newVal && newVal.length > 0) {
newVal.forEach((msg) => {
// resultItems.value.push({
// key: resultItems.value.length + 1,
// role: msg.role, // 'user' or 'ai'
// content: msg.content,
// footer: msg.footer,
// });
if (msg.role === 'user') {
resultItems.value.push({
key: resultItems.value.length + 1,
role: msg.role, // 'user' or 'ai'
content: msg.content,
footer: msg.footer,
});
} else {
const filename = ref('');
if (msg.content && isDocxURL(msg.content)) {
filename.value = extractDocxFilename(msg.content);
}
resultItems.value.push({
key: resultItems.value.length + 1,
role: msg.role, // 'user' or 'ai'
content: '文档已生成',
footer: h(Flex, null, [
h(
Button,
{
size: 'nomarl',
type: 'primary',
onClick: () => {
openPreviewDrawer('right', filename);
},
},
'文档预览',
),
h(
Button,
{
size: 'nomarl',
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>
},
},
'文档下载',
),
]),
});
}
});
}
},
{ deep: true },
);
</script>
<template>
<!-- style="flex-direction: column"-->
<div class="flex h-full">
<div class="flex h-full" style="width: 70%">
<PreviewDrawer />
<!-- <PreviewDrawer />-->
<!-- <div style="flex:1; padding: 20px; overflow-y: auto">-->
<!-- <BubbleList-->
<!-- variant="shadow"-->
<!-- :roles="roles"-->
<!-- :typing="true"-->
<!-- :items="resultItems"-->
<!-- />-->
<!-- </div>-->
<Bubble.List
variant="shadow"
:typing="true"
:items="resultItems"
:roles="roles"
style="flex: 1"
/>
<Card class="w-full self-end" v-loading="isFetching">
<CardHeader class="py-4">
<CardTitle class="text-lg">{{ title }}</CardTitle>

View File

@ -1,38 +1,205 @@
<script setup lang="ts">
import type { WordTempItem } from './typing';
import type { ConversationsProps } from 'ant-design-x-vue';
import { Card, CardContent, CardHeader, CardTitle } from '@vben-core/shadcn-ui';
import type { WordHistoryItem } from './typing';
import { computed, h, ref } from 'vue';
import { DeleteOutlined } from '@ant-design/icons-vue';
// import { Card, CardContent, CardHeader, CardTitle } from '@vben-core/shadcn-ui';
import { Menu } from 'ant-design-vue';
import { Conversations } from 'ant-design-x-vue';
interface Props {
items?: WordHistoryItem[];
title: string;
loading: boolean;
}
defineOptions({
name: 'WordListView',
});
defineEmits(['click']);
const props = withDefaults(defineProps<Props>(), {
items: () => [],
});
const emit = defineEmits(['click', 'clickMode', 'delete']);
const defaultConversationsItems = computed(() => {
return props.items.map((item) => {
return {
key: item.id,
label: item.name,
};
});
});
const conversationsItems = ref(defaultConversationsItems);
const activeKey = ref(defaultConversationsItems.value[0]?.key);
const items: WordTempItem[] = [
const itemsData = ref([
{
id: 'baca08c1-e92b-4dc9-a445-3584803f54d4',
name: '职业创新申报书',
key: 'baca08c1-e92b-4dc9-a445-3584803f54d4',
label: '海南职创申报书生成',
title: '海南职创申报书生成',
},
];
]);
// const items2: ItemType[] = reactive([
// getItem(
// '',
// 'grp',
// null,
// [getItem('', 'baca08c1-e92b-4dc9-a445-3584803f54d4')],
// 'group',
// ),
// ]);
const handleMenuClick = (item: { key: string }) => {
const selectedItem = itemsData.value.find((i) => i.key === item.key);
if (selectedItem) {
// title -> name, key -> id
const transformedItem = {
name: selectedItem.title,
id: selectedItem.key,
};
emit('clickMode', transformedItem); //
}
};
const onConversationClick: ConversationsProps['onActiveChange'] = (key) => {
activeKey.value = key;
emit('click', key);
};
const menuConfig: ConversationsProps['menu'] = (conversation) => ({
items: [
{
label: '删除此对话',
key: conversation.key,
icon: h(DeleteOutlined),
danger: true,
},
],
onClick: (menuInfo) => {
emit('delete', menuInfo.key);
},
});
const selectedKeys = ref([]);
const openKeys = ref([]);
</script>
<template>
<Card style="height: 50vh; overflow-y: auto; border-radius: 0">
<CardHeader class="py-4">
<CardTitle class="text-lg">选择模板</CardTitle>
</CardHeader>
<CardContent class="flex flex-wrap p-5 pt-0">
<ul class="divide-border w-full divide-y" role="list">
<li
v-for="(item, index) in items"
:key="index"
@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="addBtn">模板</div>
<Menu
v-model:open-keys="openKeys"
v-model:selected-keys="selectedKeys"
mode="vertical"
class="mode"
:items="items"
@click="handleMenuClick"
/>
<!-- 🌟 添加会话 -->
<!-- <Button type="link" class="addBtn">会话</Button>-->
<div class="addBtn">会话</div>
<!-- 🌟 添加会话 -->
<!-- 🌟 会话管理 -->
<Conversations
:items="conversationsItems"
:menu="menuConfig"
class="conversations"
:active-key="activeKey"
@active-change="onConversationClick"
/>
</div>
</template>
<style scoped lang="less">
.layout {
width: 100%;
min-width: 1400px;
height: 100%;
display: flex;
background: hsl(216deg 21.74% 95.49%);
font-family: AlibabaPuHuiTi, v-deep(var(--ant-font-family)), sans-serif;
border-radius: v-deep(var(--ant-border-radius));
}
.menu {
background: #ffffff;
width: 200px;
height: 100%;
display: flex;
flex-direction: column;
}
.mode {
overflow-y: auto;
}
.conversations {
padding: 0 12px;
overflow-y: auto;
}
.chat {
height: 100%;
width: 100%;
max-width: 1000px;
max-height: 900px;
margin: 0 auto;
box-sizing: border-box;
display: flex;
flex-direction: column;
padding: 12px;
gap: 16px;
}
//.messages {
// flex: 1;
//}
.placeholder {
padding-top: 32px;
text-align: left;
flex: 1;
}
.sender {
background: #ffffff;
width: 100%;
box-shadow: v-deep(var(--ant-box-shadow));
}
.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: v-deep(var(--ant-color-text));
font-size: 16px;
}
.addBtn {
float: left;
//border: 1px solid #1677ff34;
color: #666666;
width: calc(100% - 24px);
margin: 12px 12px 24px 12px;
}
</style>

View File

@ -1,19 +1,33 @@
<script setup lang="ts">
import type { BubbleListProps } from 'ant-design-x-vue';
import type {
AttachmentsProps,
BubbleListProps,
PromptsProps,
} from 'ant-design-x-vue';
import type { DrawerPlacement } from '@vben/common-ui';
import type { DrawerPlacement } from '@vben-core/popup-ui';
import type { WordTempItem } from './typing';
import { h, ref } from 'vue';
import { computed, h, ref, watch } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { useVbenDrawer } from '@vben-core/popup-ui';
import { Card } from '@vben-core/shadcn-ui';
import { UserOutlined } from '@ant-design/icons-vue';
import { Button, Flex, message, Typography } from 'ant-design-vue';
import { Attachments, BubbleList, Sender } from 'ant-design-x-vue';
import {
CloudUploadOutlined,
PaperClipOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
// import type { VNode } from 'vue';
import { Badge, Button, Flex, message, Typography } from 'ant-design-vue';
import {
Attachments,
Bubble,
Prompts,
Sender,
useXAgent,
useXChat,
} from 'ant-design-x-vue';
import markdownit from 'markdown-it';
import WordPreview from './word-preview.vue';
@ -44,18 +58,18 @@ interface WorkflowResult {
interface Props {
item?: WordTempItem;
paramsData?: {};
runChatflow?: (
params: WorkflowParams,
context: WorkflowContext,
) => Promise<WorkflowResult>;
}
defineOptions({
name: 'SpiderWorkView',
});
defineOptions({ name: 'PlaygroundIndependentSetup' });
const props = withDefaults(defineProps<Props>(), {
item: () => null,
paramsData: () => null,
runChatflow: () => async () => ({
data: {
outputs: {
@ -65,6 +79,15 @@ const props = withDefaults(defineProps<Props>(), {
}),
});
// const { token } = theme.useToken();
interface ResultItem {
key: number;
role: 'ai' | 'user';
content: string;
footer?: any;
}
// markdown
const md = markdownit({ html: true, breaks: true });
@ -83,7 +106,102 @@ const renderMarkdown: BubbleListProps['roles'][string]['messageRender'] = (
]);
};
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 = [
{
key: '0',
label: 'What is Ant Design X?',
},
];
// {
// key: '1',
// description: '',
// icon: h(ReadOutlined, { style: { color: '#FF4D4F' } }),
// },
const senderPromptsItems = computed(() => {
if (typeof props.paramsData !== 'object' || props.paramsData === null)
return [];
return Object.keys(props.paramsData).map((key, index) => ({
key: index,
description: props.paramsData[key],
}));
});
const roles: BubbleListProps['roles'] = {
user: {
placement: 'end',
typing: false,
avatar: { icon: h(UserOutlined), style: { background: '#87d068' } },
},
ai: {
placement: 'start',
typing: false,
style: {
maxWidth: 600,
marginInlineEnd: 44,
},
styles: {
footer: {
width: '100%',
},
},
avatar: { icon: h(UserOutlined), style: { background: '#fde3cf' } },
messageRender: (content) =>
h(Typography, [
h('div', {
innerHTML: md.render(content),
style: {
// padding: '8px',
lineHeight: '1.6',
whiteSpace: 'break-spaces',
},
}),
]),
},
file: {
placement: 'start',
avatar: { icon: h(UserOutlined), style: { visibility: 'hidden' } },
variant: 'borderless',
messageRender: (items: any) =>
h(
Flex,
{ vertical: true, gap: 'middle' },
items.map((item: any) =>
h(Attachments.FileCard, { key: item.uid, item }),
),
),
},
};
// ==================== 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);
const projectName = ref<string>('');
const inputStatus = ref<string>('');
const fetchStatus = ref('');
const resultItems = ref<ResultItem[]>([]);
const conversationId = ref('');
const fetchResult = ref('');
// ==================== Runtime ====================
const [agent] = useXAgent({
request: async ({ message }, { onSuccess }) => {
agentRequestLoading.value = true;
await sleep();
agentRequestLoading.value = false;
onSuccess(`Mock success return. You said: ${message}`);
},
});
const [PreviewDrawer, previewDrawerApi] = useVbenDrawer({
//
@ -97,7 +215,28 @@ function openPreviewDrawer(
const fileData = filename.value;
previewDrawerApi.setState({ placement }).setData(fileData).open();
}
const inputStatus = ref<string>('');
const { setMessages } = useXChat({
agent: agent.value,
});
watch(
activeKey,
() => {
if (activeKey.value !== undefined) {
setMessages([]);
}
},
{ immediate: true },
);
// ==================== Event ====================
// function onSubmit(nextContent: string) {
// if (!nextContent) return;
// onRequest(nextContent);
// content.value = '';
// }
const startFetching = async () => {
//
if (!projectName.value.trim()) {
@ -106,12 +245,12 @@ const startFetching = async () => {
return;
}
isFetching.value = true;
agentRequestLoading.value = true;
fetchStatus.value = 'fetching';
resultItems.value.push({
key: resultItems.value.length + 1,
role: 'user',
content: value.value,
content: content.value,
});
try {
@ -126,10 +265,10 @@ const startFetching = async () => {
inputs: {
projectName: projectName.value,
},
content: value.value || '',
content: content.value || '',
},
);
value.value = '';
content.value = '';
const { answer } = res;
conversationId.value = res.conversationId;
@ -203,79 +342,68 @@ const startFetching = async () => {
}
//
isFetching.value = false;
agentRequestLoading.value = false;
fetchStatus.value = 'completed';
};
const roles: BubbleListProps['roles'] = {
user: {
placement: 'end',
typing: false,
avatar: { icon: h(UserOutlined), style: { background: '#87d068' } },
},
ai: {
placement: 'start',
typing: false,
style: {
maxWidth: 600,
marginInlineEnd: 44,
},
styles: {
footer: {
width: '100%',
},
},
avatar: { icon: h(UserOutlined), style: { background: '#fde3cf' } },
messageRender: (content) =>
h(Typography, [
h('div', {
innerHTML: md.render(content),
style: {
// padding: '8px',
lineHeight: '1.6',
whiteSpace: 'break-spaces',
},
}),
]),
},
file: {
placement: 'start',
avatar: { icon: h(UserOutlined), style: { visibility: 'hidden' } },
variant: 'borderless',
messageRender: (items: any) =>
h(
Flex,
{ vertical: true, gap: 'middle' },
items.map((item: any) =>
h(Attachments.FileCard, { key: item.uid, item }),
),
),
},
const onPromptsItemClick: PromptsProps['onItemClick'] = (info) => {
content.value = info.data.description;
};
const isFetching = ref(false);
const fetchResult = ref('');
const fetchStatus = ref('');
const projectName = ref<string>('');
const value = ref('');
const handleFileChange: AttachmentsProps['onChange'] = (info) =>
(attachedFiles.value = info.fileList);
interface ResultItem {
key: number;
role: 'ai' | 'user';
content: string;
footer?: any;
}
// ==================== Nodes ====================
// const placeholderNode = computed(() =>
// h(
// Space,
// { direction: 'vertical', size: 16, style: 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 insertTextToInput = (text: string) => {
value.value = text + value.value;
};
const resultItems = ref<ResultItem[]>([]);
// 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,
// }));
// });
</script>
<template>
<div style="display: flex; flex-direction: column" class="h-full">
<div class="layout">
<PreviewDrawer />
<div class="chat">
<a-space class="w-full" direction="vertical" style="padding-top: 20px">
<a-input
v-model:value="projectName"
@ -284,43 +412,165 @@ const resultItems = ref<ResultItem[]>([]);
:status="inputStatus"
/>
</a-space>
<div style="flex: 1; padding: 20px; overflow-y: auto">
<BubbleList
:roles="roles"
:typing="true"
<!-- 🌟 消息列表 -->
<Bubble.List
:items="resultItems"
:roles="roles"
style="flex: 1"
:message-render="renderMarkdown"
/>
</div>
<Flex wrap="wrap" :gap="12" class="w-full" style="align-items: flex-end">
<Button @click="insertTextToInput('生成项目摘要')">生成项目摘要</Button>
<Button @click="insertTextToInput('生成目前存在问题')">
生成目前存在问题
</Button>
<Button @click="insertTextToInput('生成项目采用的技术原理')">
生成项目采用的技术原理
</Button>
<Button @click="insertTextToInput('生成能解决的主要问题')">
生成能解决的主要问题
</Button>
<Button @click="insertTextToInput('生成技术关键点和创新点')">
生成技术关键点和创新点
</Button>
<Button @click="insertTextToInput('生成应用前景')">生成应用前景</Button>
<Button @click="insertTextToInput('生成项目实施及效益预测')">
生成项目实施及效益预测
</Button>
<Button @click="insertTextToInput('生成预期成果')">生成预期成果</Button>
<Button @click="insertTextToInput('生成导出doc')">生成导出doc</Button>
<Card class="w-full">
<Sender
v-model:value="value"
:loading="isFetching"
:disabled="isFetching"
:auto-size="{ minRows: 1, maxRows: 12 }"
@submit="startFetching"
<!-- 🌟 提示词 -->
<Prompts
:items="senderPromptsItems"
@item-click="onPromptsItemClick"
:wrap="true"
/>
</Card>
<!-- 🌟 输入框 -->
<Sender
:value="content"
class="sender"
:loading="agentRequestLoading"
@submit="startFetching"
@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>
</div>
</template>
<style scoped lang="less">
.layout {
width: 100%;
min-width: 1400px;
height: 100%;
display: flex;
background: hsl(216deg 21.74% 95.49%);
font-family: AlibabaPuHuiTi, v-deep(var(--ant-font-family)), sans-serif;
border-radius: v-deep(var(--ant-border-radius));
}
.menu {
background: #ffffff;
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: 1000px;
max-height: 900px;
margin: 0 auto;
box-sizing: border-box;
display: flex;
flex-direction: column;
padding: 12px;
gap: 16px;
}
//.messages {
// flex: 1;
//}
.placeholder {
padding-top: 32px;
text-align: left;
flex: 1;
}
.sender {
background: #ffffff;
width: 100%;
box-shadow: v-deep(var(--ant-box-shadow));
}
.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: v-deep(var(--ant-color-text));
font-size: 16px;
}
.addBtn {
background: #1677ff0f;
border: 1px solid #1677ff34;
width: calc(100% - 24px);
margin: 12px 12px 24px 12px;
}
</style>

View File

@ -18,4 +18,5 @@ export const EosRole = createIconifyIcon('eos-icons:role-binding-outlined');
export const IconSystem = createIconifyIcon('icon-park-twotone:system');
export const MaterPerson = createIconifyIcon('material-symbols:person');
export const IconLog = createIconifyIcon('icon-park-outline:log');
export const HugeAi = createIconifyIcon('hugeicons:ai-scan');
// export const MdiUser = createIconifyIcon('mdi:user');