diff --git a/docs/.vitepress/themeConfig.ts b/docs/.vitepress/themeConfig.ts index 9a57b75f1..4d79bd341 100644 --- a/docs/.vitepress/themeConfig.ts +++ b/docs/.vitepress/themeConfig.ts @@ -33,9 +33,9 @@ const sharedSidebarItems = [ text: '工具', base: '/tools/', items: [ - { text: 'AI模型交互工具类', link: 'ai-client' }, - { text: '消息数据管理', link: 'message' }, - { text: '会话数据管理', link: 'conversation' }, + { text: 'useMessage 消息数据管理', link: 'message' }, + { text: 'useConversation 会话数据管理', link: 'conversation' }, + { text: 'AIClient 模型交互工具类', link: 'ai-client' }, { text: '工具函数', link: 'utils' }, ], }, diff --git a/docs/demos/examples/Assistant.vue b/docs/demos/examples/Assistant.vue index 42ce80624..25b1a2db3 100644 --- a/docs/demos/examples/Assistant.vue +++ b/docs/demos/examples/Assistant.vue @@ -149,20 +149,30 @@ import { TrWelcome, vDropzone, } from '@opentiny/tiny-robot' -import { ConversationInfo, sseStreamToGenerator, useConversation } from '@opentiny/tiny-robot-kit' +import { ConversationInfo, toolPlugin, useConversation } from '@opentiny/tiny-robot-kit' import { IconAi, IconClose, - IconDislike, IconEdit, IconHistory, - IconLike, IconNewSession, IconSparkles, IconUser, } from '@opentiny/tiny-robot-svgs' import { TinySwitch } from '@opentiny/vue' import { computed, type CSSProperties, h, markRaw, nextTick, onMounted, ref, watch } from 'vue' +import { + DROPDOWN_MENU_ITEMS, + getContainerStyles, + OVERLAY_DESCRIPTION, + OVERLAY_TITLE, + PILL_ITEMS_CONFIG, + PROMPT_ITEMS_DATA, + suggestionPopoverData, + templateSuggestions, +} from './assistantConstants' +import { callMcpTool, MCP_TOOLS } from './mockMcp' +import { assistantResponseProvider } from './responseProvider' const fullscreen = ref(false) const show = ref(true) @@ -171,267 +181,14 @@ const aiAvatar = h(IconAi, { style: { fontSize: '32px' } }) const userAvatar = h(IconUser, { style: { fontSize: '32px' } }) const welcomeIcon = h(IconAi, { style: { fontSize: '48px' } }) -const promptItems: PromptProps[] = [ - { - label: '日常助理场景', - description: '今天需要我帮你安排日程,规划旅行,还是起草一封邮件?', - icon: h('span', { style: { fontSize: '18px' } as CSSProperties }, '🧠'), - badge: 'NEW', - }, - { - label: '学习/知识型场景', - description: '有什么想了解的吗?可以是“Vue3 和 React 的区别”!', - icon: h('span', { style: { fontSize: '18px' } as CSSProperties }, '🤔'), - }, - { - label: '创意生成场景', - description: '想写段文案、起个名字,还是来点灵感?', - icon: h('span', { style: { fontSize: '18px' } as CSSProperties }, '✨'), - }, -] - -// 指令模板测试数据 -const templateSuggestions = [ - { - id: 'write', - text: '帮我写作', - template: [ - { type: 'text', content: '帮我撰写' }, - { type: 'template', content: '文章类型' }, - { type: 'text', content: '字的' }, - { type: 'template', content: '主题' }, - { type: 'text', content: ', 语气类型是' }, - { type: 'template', content: '正式/轻松/专业' }, - { type: 'text', content: ', 具体内容是' }, - { type: 'template', content: '详细描述' }, - ], - }, - { - id: 'translate', - text: '翻译', - template: [ - { type: 'text', content: '请将以下' }, - { type: 'template', content: '中文/英文/法语/德语/日语' }, - { type: 'text', content: '内容翻译成' }, - { type: 'template', content: '目标语言' }, - { type: 'text', content: ':' }, - { type: 'template', content: '需要翻译的内容' }, - ], - }, - { - id: 'summarize', - text: '内容总结', - template: [ - { type: 'text', content: '请对以下内容进行' }, - { type: 'template', content: '简要/详细' }, - { type: 'text', content: '总结,约' }, - { type: 'template', content: '字数' }, - { type: 'text', content: '字:' }, - { type: 'template', content: '需要总结的内容' }, - ], - }, - { - id: 'code-review', - text: '代码审查', - template: [ - { type: 'text', content: '请帮我审查以下' }, - { type: 'template', content: 'JavaScript/TypeScript/Python/Java/C++/Go' }, - { type: 'text', content: '代码,关注' }, - { type: 'template', content: '性能/安全/可读性/最佳实践' }, - { type: 'text', content: '方面:' }, - { type: 'template', content: '代码内容' }, - ], - }, - { - id: 'email-compose', - text: '写邮件', - template: [ - { type: 'text', content: '请帮我起草一封' }, - { type: 'template', content: '正式/非正式' }, - { type: 'text', content: '邮件,发送给' }, - { type: 'template', content: '收件人角色' }, - { type: 'text', content: ',主题是' }, - { type: 'template', content: '邮件主题' }, - { type: 'text', content: ',内容是关于' }, - { type: 'template', content: '邮件内容' }, - ], - }, - { - id: 'data-analysis', - text: '数据分析', - template: [ - { type: 'text', content: '请分析以下' }, - { type: 'template', content: '销售/用户/流量/金融/健康' }, - { type: 'text', content: '数据,关注' }, - { type: 'template', content: '增长率/分布/趋势/异常/关联性' }, - { type: 'text', content: '指标,生成' }, - { type: 'template', content: '柱状图/折线图/饼图/散点图/热力图' }, - { type: 'text', content: '可视化:' }, - { type: 'template', content: '数据内容' }, - ], - }, - { - id: 'product-design', - text: '产品设计', - template: [ - { type: 'text', content: '请设计一个' }, - { type: 'template', content: '移动应用/网站/小程序/桌面软件/智能硬件' }, - { type: 'text', content: '的' }, - { type: 'template', content: '功能名称' }, - { type: 'text', content: '功能,目标用户是' }, - { type: 'template', content: '用户群体' }, - { type: 'text', content: ',核心价值是' }, - { type: 'template', content: '功能价值' }, - ], - }, - { - id: 'meeting-summary', - text: '会议纪要', - template: [ - { type: 'text', content: '请帮我整理一份会议纪要,会议主题是' }, - { type: 'template', content: '会议主题' }, - { type: 'text', content: ',参会人员有' }, - { type: 'template', content: '参会人员' }, - { type: 'text', content: ',会议要点包括' }, - { type: 'template', content: '会议要点' }, - ], - }, - { - id: 'interview-questions', - text: '面试问题', - template: [ - { type: 'text', content: '请为' }, - { type: 'template', content: '岗位名称' }, - { type: 'text', content: '岗位,针对' }, - { type: 'template', content: '技能领域' }, - { type: 'text', content: '方向,设计' }, - { type: 'template', content: '3/5/10' }, - { type: 'text', content: '个' }, - { type: 'template', content: '简单/中等/困难' }, - { type: 'text', content: '面试问题' }, - ], - }, - { - id: 'speech-draft', - text: '演讲稿', - template: [ - { type: 'text', content: '请帮我撰写一篇' }, - { type: 'template', content: '开场/主题/致谢/颁奖/毕业' }, - { type: 'text', content: '演讲稿,主题是' }, - { type: 'template', content: '演讲主题' }, - { type: 'text', content: ',时长约' }, - { type: 'template', content: '5/10/15/30' }, - { type: 'text', content: '分钟,受众是' }, - { type: 'template', content: '目标听众' }, - ], - }, -] - -const dropdownMenuItems = ref([ - { id: '1', text: '去续费' }, - { id: '2', text: '去退订' }, - { id: '3', text: '查账单' }, - { id: '4', text: '导账单' }, - { id: '5', text: '对帐单' }, -]) - -const popoverData = ref([ - { - group: 'basic', - label: '推荐', - icon: IconLike, - items: [ - { id: 'b1', text: '什么是弹性云服务器?' }, - { id: 'b2', text: '如何登录到Windows云服务器?' }, - { id: 'b3', text: '弹性公网IP为什么ping不通?' }, - { id: 'b4', text: '云服务器安全组如何配置?' }, - { id: 'b5', text: '如何查看云服务器密码?' }, - { id: 'b6', text: '什么是弹性云服务器?' }, - { id: 'b7', text: '如何登录到Windows云服务器?' }, - { id: 'b8', text: '弹性公网IP为什么ping不通?' }, - { id: 'b9', text: '云服务器安全组如何配置?' }, - { id: 'b0', text: '如何查看云服务器密码?' }, - ], - }, - { - group: 'purchase', - label: '购买咨询', - icon: IconDislike, - items: [ - { id: 'p1', text: '如何购买弹性云服务器?' }, - { id: 'p2', text: '无法登录弹性云服务器怎么办?' }, - { id: 'p3', text: '云服务器价格怎么计算?' }, - { id: 'p4', text: '如何查看账单详情?' }, - { id: 'p5', text: '如何续费云服务器?' }, - ], - }, - { - group: 'usage', - label: '使用咨询', - icon: IconLike, - items: [ - { id: 'u1', text: '云服务器使用限制与须知' }, - { id: 'u2', text: '使用RDP文件连接Windows实例' }, - { id: 'u3', text: '多用户登录(Windows2016)' }, - { id: 'u4', text: '如何重置云服务器密码?' }, - { id: 'u5', text: '云服务器如何安装软件?' }, - ], - }, - { group: '4', label: '推荐', icon: IconLike, items: [] }, - { group: '5', label: '购买咨询', icon: IconLike, items: [] }, - { group: '6', label: '使用咨询', icon: IconLike, items: [] }, - { group: '7', label: '购买咨询', icon: IconLike, items: [] }, - { group: '8', label: '使用咨询', icon: IconLike, items: [] }, - { group: '9', label: '购买咨询', icon: IconLike, items: [] }, - { group: '10', label: '使用咨询', icon: IconLike, items: [] }, -]) +const promptItems: PromptProps[] = PROMPT_ITEMS_DATA.map((item) => ({ + ...item, + icon: h('span', { style: { fontSize: '18px' } as CSSProperties }, item.emoji), +})) -const handlePopoverItemClick = (item: SuggestionItem) => { - sendMessage(item.text) -} +const dropdownMenuItems = ref(DROPDOWN_MENU_ITEMS) -const pillItems = [ - { - text: '费用成本', - icon: markRaw(IconEdit), - menu: { - items: dropdownMenuItems.value, - onItemClick: (item) => { - sendMessage(item.text) - }, - }, - }, - { - text: '常用指令', - icon: markRaw(IconEdit), - menu: { - items: templateSuggestions.slice(0, 3), - onItemClick: (item) => { - handleFillTemplate((item as unknown as { template: UserItem[] }).template) - }, - }, - }, - { - text: '工作助手', - icon: markRaw(IconEdit), - menu: { - items: templateSuggestions.slice(3, 6), - onItemClick: (item) => { - handleFillTemplate((item as unknown as { template: UserItem[] }).template) - }, - }, - }, - { - text: '内容创作', - icon: markRaw(IconEdit), - menu: { - items: templateSuggestions.slice(6), - onItemClick: (item) => { - handleFillTemplate((item as unknown as { template: UserItem[] }).template) - }, - }, - }, -] +const popoverData = ref(suggestionPopoverData) const { activeConversation, @@ -444,18 +201,16 @@ const { abortActiveRequest, } = useConversation({ useMessageOptions: { - responseProvider: async (requestBody, abortSignal) => { - const response = await fetch('/api/chat/completions', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ...requestBody, stream: true }), - signal: abortSignal, - }) - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - return sseStreamToGenerator(response, { signal: abortSignal }) - }, + responseProvider: assistantResponseProvider, + plugins: [ + toolPlugin({ + getTools: async () => MCP_TOOLS, + callTool: async (toolCall) => { + const args = JSON.parse(toolCall.function?.arguments || '{}') + return callMcpTool(toolCall.function?.name || '', args) + }, + }), + ], }, }) @@ -543,6 +298,34 @@ const handleSendMessage = () => { clearTemplate() } +const handlePopoverItemClick = (item: SuggestionItem) => { + sendMessage(item.text) +} + +const pillItems = computed(() => + PILL_ITEMS_CONFIG.map((config) => { + const base = { text: config.text, icon: markRaw(IconEdit) } + if (config.type === 'dropdown') { + return { + ...base, + menu: { + items: dropdownMenuItems.value, + onItemClick: (item: unknown) => sendMessage((item as { text: string }).text), + }, + } + } + const [start, end] = config.range + const items = end !== undefined ? templateSuggestions.slice(start, end) : templateSuggestions.slice(start) + return { + ...base, + menu: { + items, + onItemClick: (item: unknown) => handleFillTemplate((item as { template: UserItem[] }).template), + }, + } + }), +) + watch( () => inputMessage.value, (value) => { @@ -553,8 +336,8 @@ watch( }, ) -const overlayTitle = '将图片拖到此处完成上传' -const overlayDescription = ['总计最多上传3个图片(每个10MB以内)', '支持图片格式 JPG/JPEG/PNG'] +const overlayTitle = OVERLAY_TITLE +const overlayDescription = OVERLAY_DESCRIPTION const isDragging = ref(false) const targetElement = ref(null) @@ -579,15 +362,7 @@ onMounted(() => { }, 500) }) -const containerStyles = - window.self !== window.top - ? { - height: '100vh', - } - : { - top: '112px', - height: 'calc(100vh - 112px)', - } +const containerStyles = getContainerStyles()