feat(site): add the tiny-robot drawer to the official website. (#3467)
* feat(site): 添加tiny-robot的初始代码 * fix(site): fix * fix(robot): fix * fix(robot): 添加自定义适配 dify * fix(site): fix * fix(site): 在表格中,专门添加智能体文件 * fix(site): ai-agent * fix(site): fix * ci(workflow): update branches for auto deployment * chore: 还原action脚本 * fix(site): fix 检视意见 --------- Co-authored-by: ajaxzheng <894103554@qq.com>
This commit is contained in:
parent
fb9222f35b
commit
cfe8e981e0
|
@ -63,4 +63,4 @@ jobs:
|
|||
uses: actions/deploy-pages@v4
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
<template>
|
||||
<tiny-grid :data="tableData" :edit-config="{ trigger: 'click', mode: 'cell', showStatus: true }">
|
||||
<tiny-grid-column type="index" width="60"></tiny-grid-column>
|
||||
<tiny-grid-column type="selection" width="60"></tiny-grid-column>
|
||||
<tiny-grid-column field="employees" title="员工数"></tiny-grid-column>
|
||||
<tiny-grid-column field="createdDate" title="创建日期"></tiny-grid-column>
|
||||
<tiny-grid-column field="city" title="城市"></tiny-grid-column>
|
||||
<tiny-grid-column
|
||||
field="boole"
|
||||
title="布尔值"
|
||||
align="center"
|
||||
format-text="boole"
|
||||
:editor="checkboxEdit"
|
||||
></tiny-grid-column>
|
||||
</tiny-grid>
|
||||
</template>
|
||||
|
||||
<script setup lang="jsx">
|
||||
import { TinyGrid, TinyGridColumn } from '@opentiny/vue'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const tableData = reactive([
|
||||
{
|
||||
id: '1',
|
||||
name: 'GFD 科技 YX 公司',
|
||||
city: '福州',
|
||||
employees: 800,
|
||||
createdDate: '2014-04-30 00:56:00',
|
||||
boole: false
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'WWW 科技 YX 公司',
|
||||
city: '深圳',
|
||||
employees: 300,
|
||||
createdDate: '2016-07-08 12:36:22',
|
||||
boole: true
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'RFV 有限责任公司',
|
||||
city: '中山',
|
||||
employees: 1300,
|
||||
createdDate: '2014-02-14 14:14:14',
|
||||
boole: false
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'TGB 科技 YX 公司',
|
||||
city: '龙岩',
|
||||
employees: 360,
|
||||
createdDate: '2013-01-13 13:13:13',
|
||||
boole: true
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'YHN 科技 YX 公司',
|
||||
city: '韶关',
|
||||
employees: 810,
|
||||
createdDate: '2012-12-12 12:12:12',
|
||||
boole: true
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'WSX 科技 YX 公司',
|
||||
city: '黄冈',
|
||||
employees: 800,
|
||||
createdDate: '2011-11-11 11:11:11',
|
||||
boole: true
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
name: 'KBG 物业 YX 公司',
|
||||
city: '赤壁',
|
||||
employees: 400,
|
||||
createdDate: '2016-04-30 23:56:00',
|
||||
boole: false
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
name: '深圳市福德宝网络技术 YX 公司',
|
||||
boole: true,
|
||||
city: '厦门',
|
||||
createdDate: '2016-06-03 13:53:25',
|
||||
employees: 540
|
||||
}
|
||||
])
|
||||
|
||||
function checkboxEdit(h, { row }) {
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.boole}
|
||||
onChange={(event) => {
|
||||
row.boole = event.target.checked
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,182 @@
|
|||
<template>
|
||||
<tiny-grid
|
||||
:data="tableData"
|
||||
:edit-config="{ trigger: 'click', mode: 'cell', showStatus: true }"
|
||||
:tiny_mcp_config="{
|
||||
server,
|
||||
business: {
|
||||
id: 'company-information',
|
||||
description: '公司人员信息表'
|
||||
}
|
||||
}"
|
||||
>
|
||||
<tiny-grid-column type="index" width="60"></tiny-grid-column>
|
||||
<tiny-grid-column type="selection" width="60"></tiny-grid-column>
|
||||
<tiny-grid-column field="employees" title="员工数"></tiny-grid-column>
|
||||
<tiny-grid-column field="createdDate" title="创建日期"></tiny-grid-column>
|
||||
<tiny-grid-column field="city" title="城市"></tiny-grid-column>
|
||||
<tiny-grid-column
|
||||
field="boole"
|
||||
title="布尔值"
|
||||
align="center"
|
||||
format-text="boole"
|
||||
:editor="checkboxEdit"
|
||||
></tiny-grid-column>
|
||||
</tiny-grid>
|
||||
</template>
|
||||
|
||||
<script lang="jsx">
|
||||
import { TinyGrid, TinyGridColumn } from '@opentiny/vue'
|
||||
import { createTransportPair, createSseProxy } from '@opentiny/next'
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TinyGrid,
|
||||
TinyGridColumn
|
||||
},
|
||||
data() {
|
||||
const tableData = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'GFD 科技 YX 公司',
|
||||
city: '福州',
|
||||
employees: 800,
|
||||
createdDate: '2014-04-30 00:56:00',
|
||||
boole: false
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'WWW 科技 YX 公司',
|
||||
city: '深圳',
|
||||
employees: 300,
|
||||
createdDate: '2016-07-08 12:36:22',
|
||||
boole: true
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'RFV 有限责任公司',
|
||||
city: '中山',
|
||||
employees: 1300,
|
||||
createdDate: '2014-02-14 14:14:14',
|
||||
boole: false
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'TGB 科技 YX 公司',
|
||||
city: '龙岩',
|
||||
employees: 360,
|
||||
createdDate: '2013-01-13 13:13:13',
|
||||
boole: true
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'YHN 科技 YX 公司',
|
||||
city: '韶关',
|
||||
employees: 810,
|
||||
createdDate: '2012-12-12 12:12:12',
|
||||
boole: true
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'WSX 科技 YX 公司',
|
||||
city: '黄冈',
|
||||
employees: 800,
|
||||
createdDate: '2011-11-11 11:11:11',
|
||||
boole: true
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
name: 'KBG 物业 YX 公司',
|
||||
city: '赤壁',
|
||||
employees: 400,
|
||||
createdDate: '2016-04-30 23:56:00',
|
||||
boole: false
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
name: '深圳市福德宝网络技术 YX 公司',
|
||||
boole: true,
|
||||
city: '厦门',
|
||||
createdDate: '2016-06-03 13:53:25',
|
||||
employees: 540
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
server: new McpServer({ name: 'base-config', version: '1.0.0' }, {}),
|
||||
sessionID: '',
|
||||
op: {
|
||||
editConfig: {
|
||||
trigger: 'click',
|
||||
mode: 'cell',
|
||||
showStatus: true
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
type: 'index',
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
type: 'selection',
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
field: 'employees',
|
||||
title: '员工数'
|
||||
},
|
||||
{
|
||||
field: 'createdDate',
|
||||
title: '创建日期'
|
||||
},
|
||||
{
|
||||
field: 'city',
|
||||
title: '城市'
|
||||
},
|
||||
{
|
||||
field: 'boole',
|
||||
title: '布尔值',
|
||||
align: 'center',
|
||||
formatText: 'boole',
|
||||
editor: this.checkboxEdit
|
||||
}
|
||||
],
|
||||
data: tableData
|
||||
},
|
||||
tableData
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkboxEdit(h, { row }) {
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.boole}
|
||||
onChange={(event) => {
|
||||
row.boole = event.target.checked
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// 1、
|
||||
const [transport, clientTransport] = createTransportPair()
|
||||
|
||||
// 2、
|
||||
const client = new Client({ name: 'ai-agent', version: '1.0.0' }, {})
|
||||
await client.connect(clientTransport)
|
||||
const { sessionId } = await createSseProxy({
|
||||
client,
|
||||
url: 'http://39.108.160.245/sse'
|
||||
})
|
||||
|
||||
this.sessionID = sessionId
|
||||
window.$sessionId = this.sessionID
|
||||
|
||||
// 3、
|
||||
await this.server.connect(transport)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: Grid 表格
|
||||
---
|
||||
|
||||
# Grid 表格
|
||||
|
||||
<div>表格组件,提供了非常强大数据表格功能,在 Grid 可以展示数据列表,可以对数据列表进行选择、编辑等。</div>
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: Grid Table
|
||||
---
|
||||
|
||||
# Grid Table
|
||||
|
||||
<div>Table component, which provides powerful data table functions. In Grid, data lists can be displayed, selected, and edited.</div>
|
|
@ -0,0 +1,23 @@
|
|||
export default {
|
||||
column: '1',
|
||||
owner: '',
|
||||
demos: [
|
||||
{
|
||||
demoId: 'grid-ai-agent',
|
||||
name: { 'zh-CN': '表格智能体', 'en-US': 'grid agent' },
|
||||
desc: {
|
||||
'zh-CN': `表格智能体是表格组件面向 AI 领域的应用,提供了一个基于表格数据的智能体交互界面。用户可以通过自然语言与表格进行交互,智能体会根据表格内容和用户输入执行操作。<br>
|
||||
目前表格智能体支持的操作包括:<br>
|
||||
- 查询表格数据:用户可以输入自然语言查询表格中的数据,智能体会解析查询并返回结果,以及根据结果执行后续的操作。<br>
|
||||
- 滚动到指定行:用户可以通过自然语言指令,将表格滚动到指定位置。<br>
|
||||
- 选中表格数据:用户可以通过自然语言指令,选中表格数据。<br>
|
||||
- 选中全部数据:用户可以通过自然语言指令,选中全部数据。<br>
|
||||
|
||||
通过属性 <code>tiny-mcp-config</code> 可以配置表格的业务意义以及传入<code>server</code>对象,详见示例。`,
|
||||
'en-US': ``
|
||||
},
|
||||
codeFiles: ['ai-agent/basic-usage.vue']
|
||||
}
|
||||
],
|
||||
apis: [{ name: 'grid-ai-agent', 'type': 'component', 'props': [], 'events': [], 'slots': [] }]
|
||||
}
|
|
@ -175,6 +175,7 @@ export const cmpMenus = [
|
|||
'key': 'cmp-table-components',
|
||||
'children': [
|
||||
{ 'nameCn': '基本用法', 'name': '', 'key': 'grid' },
|
||||
{ 'nameCn': 'AI智能体', 'name': '', 'key': 'grid-ai-agent' },
|
||||
{ 'nameCn': '序号列', 'name': '', 'key': 'grid-serial-column' },
|
||||
{ 'nameCn': '选中行', 'name': '', 'key': 'grid-operation-column' },
|
||||
{ 'nameCn': '空数据', 'name': '', 'key': 'grid-empty' },
|
||||
|
|
|
@ -27,6 +27,13 @@
|
|||
"@docsearch/css": "^3.8.0",
|
||||
"@docsearch/js": "^3.8.0",
|
||||
"@docsearch/react": "npm:@docsearch/css",
|
||||
"@modelcontextprotocol/sdk": "1.12.1",
|
||||
"@opentiny/next": "0.1.2",
|
||||
"@opentiny/tiny-robot": "0.2.0-alpha.7",
|
||||
"@opentiny/tiny-robot-kit": "0.2.0-alpha.7",
|
||||
"@opentiny/tiny-robot-svgs": "0.2.0-alpha.7",
|
||||
"@opentiny/tiny-schema-renderer": "1.0.0-beta.5",
|
||||
"@opentiny/tiny-vue-mcp": "0.0.1-beta.1",
|
||||
"@opentiny/utils": "workspace:~",
|
||||
"@opentiny/vue": "workspace:~",
|
||||
"@opentiny/vue-common": "workspace:~",
|
||||
|
@ -100,4 +107,4 @@
|
|||
"vite-svg-loader": "^3.6.0",
|
||||
"vue-tsc": "^1.8.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { createI18n } from 'vue-i18n'
|
||||
import { initI18n } from '@opentiny/vue-locale'
|
||||
import { initI18n, t } from '@opentiny/vue-locale'
|
||||
import { $local } from '../tools'
|
||||
import zh from './zh.json'
|
||||
import en from './en.json'
|
||||
import { zhCN, enUS } from '@opentiny/tiny-vue-mcp'
|
||||
|
||||
const messages = { enUS: en, zhCN: zh }
|
||||
const messages = { enUS: { ...en, ...enUS }, zhCN: { ...zh, ...zhCN } }
|
||||
// $local._lang = $local._lang !== 'zhCN' && $local._lang !== 'enUS' ? 'zhCN' : $local._lang
|
||||
$local._lang = 'zhCN'
|
||||
const customCreateI18n = ({ locale, messages }) =>
|
||||
|
@ -26,3 +27,5 @@ const i18nByKey = i18n.global.t
|
|||
const getWord = (cn, en) => (i18n.global.locale === 'zhCN' ? cn : en)
|
||||
|
||||
export { i18n, i18nByKey, getWord }
|
||||
|
||||
export { t }
|
||||
|
|
|
@ -2,6 +2,9 @@ import { createHead } from '@vueuse/head'
|
|||
import { createApp } from 'vue'
|
||||
import '@unocss/reset/eric-meyer.css'
|
||||
|
||||
// tiny-robot 对话框
|
||||
import '@opentiny/tiny-robot/dist/style.css'
|
||||
|
||||
// markdown文件内代码高亮
|
||||
import 'prismjs/themes/prism.css'
|
||||
import 'uno.css'
|
||||
|
@ -16,7 +19,7 @@ import './assets/custom-markdown.css'
|
|||
import './assets/custom-block.less'
|
||||
import './assets/md-preview.less'
|
||||
|
||||
import { i18n } from './i18n/index'
|
||||
import { i18n, t } from './i18n/index'
|
||||
import { router } from './router'
|
||||
import App from './App.vue'
|
||||
import { appData } from './tools'
|
||||
|
@ -33,6 +36,12 @@ import '@docsearch/css'
|
|||
import { doSearchEverySite } from './tools/docsearch'
|
||||
import '@opentiny/vue-theme/dark-theme-index.css'
|
||||
|
||||
import { registerMcpConfig } from '@opentiny/vue-common'
|
||||
import { createMcpTools, getTinyVueMcpConfig } from '@opentiny/tiny-vue-mcp'
|
||||
|
||||
// 注册TinyVue组件mcp配置
|
||||
registerMcpConfig(getTinyVueMcpConfig({ t }), createMcpTools)
|
||||
|
||||
const envTarget = import.meta.env.VITE_BUILD_TARGET || 'open'
|
||||
|
||||
hljs.registerLanguage('javascript', javascript)
|
||||
|
|
|
@ -9,7 +9,13 @@
|
|||
<slot name="header-right" />
|
||||
</template>
|
||||
</ComponentHeader>
|
||||
<div class="docs-content" id="doc-layout-scroller" ref="scrollRef" @scroll="onDocLayoutScroll">
|
||||
<div
|
||||
class="docs-content"
|
||||
:class="{ 'docs-on-robot-show': show }"
|
||||
id="doc-layout-scroller"
|
||||
ref="scrollRef"
|
||||
@scroll="onDocLayoutScroll"
|
||||
>
|
||||
<div class="ti-rel cmp-container">
|
||||
<div class="flex-horizontal docs-content-main">
|
||||
<div class="docs-tabs-wrap">
|
||||
|
@ -84,6 +90,7 @@
|
|||
</div>
|
||||
<div id="footer"></div>
|
||||
</div>
|
||||
<robotChat v-if="show"></robotChat>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -101,6 +108,8 @@ import ComponentContributor from './components/contributor.vue'
|
|||
import ApiDocs from './components/api-docs.vue'
|
||||
import useTasksFinish from './composition/useTasksFinish'
|
||||
|
||||
import robotChat from './tiny-robot-chat.vue'
|
||||
|
||||
const props = defineProps({ loadData: {}, appMode: {}, demoKey: {} })
|
||||
|
||||
const emit = defineEmits(['single-demo-change', 'load-page'])
|
||||
|
@ -435,9 +444,21 @@ const handleAnchorClick = (e, data) => {
|
|||
}
|
||||
|
||||
defineExpose({ loadPage })
|
||||
|
||||
const show = ref(false)
|
||||
onMounted(() => {
|
||||
// tiny-robot 通过路由参数存在 mcp-robot, 则弹出对话容器
|
||||
const hasRobot = router.currentRoute.value.hash === '#grid-ai-agent'
|
||||
show.value = !!hasRobot
|
||||
|
||||
document.body.classList.toggle('docs-on-robot-show', show.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:global(.docs-on-robot-show .docs-content) {
|
||||
margin-right: 480px;
|
||||
}
|
||||
.docs-content {
|
||||
flex: 1;
|
||||
overflow: hidden auto;
|
||||
|
|
|
@ -256,6 +256,10 @@ export default defineComponent({
|
|||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.docs-on-robot-show .float-settings {
|
||||
right: 680px;
|
||||
}
|
||||
|
||||
.float-settings {
|
||||
position: fixed;
|
||||
right: 200px;
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import type { ChatCompletionRequest } from '@opentiny/tiny-robot-kit'
|
||||
import type { AIModelConfig } from '@opentiny/tiny-robot-kit'
|
||||
import type { ChatCompletionResponse } from '@opentiny/tiny-robot-kit'
|
||||
import type { StreamHandler } from '@opentiny/tiny-robot-kit'
|
||||
import { BaseModelProvider } from '@opentiny/tiny-robot-kit'
|
||||
import { handleSSEStream } from './utils.js'
|
||||
|
||||
/**
|
||||
* 对接AIClient的自定义 Dify 大模型服务
|
||||
*
|
||||
* const client = new AIClient({
|
||||
* provider: 'custom',
|
||||
* providerImplementation: new CustomModelProvider( config )
|
||||
* });
|
||||
*/
|
||||
export class DifyModelProvider extends BaseModelProvider {
|
||||
constructor(config: AIModelConfig) {
|
||||
super(config)
|
||||
}
|
||||
/** 同步请示不需要实现 */
|
||||
chat(request: ChatCompletionRequest): Promise<ChatCompletionResponse> {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
/** 异步流式请求 */
|
||||
async chatStream(request: ChatCompletionRequest, handler: StreamHandler): Promise<void> {
|
||||
const { signal } = request
|
||||
this.validateRequest(request)
|
||||
|
||||
try {
|
||||
// 验证请求的messages属性,必须是数组,且每个消息必须有role\content属性
|
||||
const lastMessage = request.messages[request.messages.length - 1].content
|
||||
|
||||
// 模拟异步流式响应
|
||||
const response = await fetch(`${this.config.apiUrl}/chat-messages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.config.apiKey}`,
|
||||
'Accept': 'text/event-stream'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: lastMessage,
|
||||
user: 'user',
|
||||
response_mode: 'streaming',
|
||||
inputs: {
|
||||
sessionId: window.$sessionId
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await handleSSEStream(response, handler, signal)
|
||||
} catch (error) {
|
||||
if (signal && signal.aborted) {
|
||||
console.warn('Request was aborted:', error)
|
||||
} else {
|
||||
console.error('Error in chatStream:', error)
|
||||
// handler.onError(handleRequestError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,318 @@
|
|||
import type { AIModelConfig } from '@opentiny/tiny-robot-kit'
|
||||
import { AIClient, useConversation } from '@opentiny/tiny-robot-kit'
|
||||
import { IconAi, IconUser } from '@opentiny/tiny-robot-svgs'
|
||||
import { h, nextTick, onMounted, reactive, ref, toRaw, watch } from 'vue'
|
||||
import { DifyModelProvider } from './DifyModelProvider.js'
|
||||
|
||||
const difyConfig: AIModelConfig = {
|
||||
provider: 'custom',
|
||||
apiUrl: 'https://api.dify.ai/v1',
|
||||
apiKey: 'app-H0VJI4LqZ4KskdcA5a07pjXf'
|
||||
}
|
||||
export function useTinyRobot() {
|
||||
const client = new AIClient({
|
||||
providerImplementation: new DifyModelProvider(difyConfig),
|
||||
...difyConfig
|
||||
})
|
||||
|
||||
const fullscreen = ref(false)
|
||||
const show = ref(true)
|
||||
|
||||
const aiAvatar = h(IconAi, { style: { fontSize: '32px' } })
|
||||
const userAvatar = h(IconUser, { style: { fontSize: '32px' } })
|
||||
const welcomeIcon = h(IconAi, { style: { fontSize: '48px' } })
|
||||
|
||||
const promptItems = [
|
||||
{
|
||||
label: '日常助理场景',
|
||||
description: '今天需要我帮你安排日程,规划旅行,还是起草一封邮件?',
|
||||
icon: h('span', { style: { fontSize: '18px' } }, '🧠'),
|
||||
badge: 'NEW'
|
||||
},
|
||||
{
|
||||
label: '学习/知识型场景',
|
||||
description: '有什么想了解的吗?可以是“Vue3 和 React 的区别”!',
|
||||
icon: h('span', { style: { fontSize: '18px' } }, '🤔')
|
||||
},
|
||||
{
|
||||
label: '创意生成场景',
|
||||
description: '想写段文案、起个名字,还是来点灵感?',
|
||||
icon: h('span', { style: { fontSize: '18px' } }, '✨')
|
||||
}
|
||||
]
|
||||
|
||||
// 指令模板测试数据
|
||||
const templateSuggestions = [
|
||||
{
|
||||
id: 'write',
|
||||
text: '帮我写作',
|
||||
value: '帮我写作',
|
||||
description: '智能写作助手',
|
||||
template: '帮我撰写 [文章类型] 字的 [主题], 语气类型是 [正式/轻松/专业], 具体内容是 [详细描述]'
|
||||
},
|
||||
{
|
||||
id: 'translate',
|
||||
text: '翻译',
|
||||
value: '翻译',
|
||||
description: '多语言翻译',
|
||||
template: '请将以下 [中文/英文/法语/德语/日语] 内容翻译成 [目标语言]: [需要翻译的内容]'
|
||||
},
|
||||
{
|
||||
id: 'summarize',
|
||||
text: '内容总结',
|
||||
value: '内容总结',
|
||||
description: '智能总结长文本',
|
||||
template: '请对以下内容进行 [简要/详细] 总结,约 [字数] 字: [需要总结的内容]'
|
||||
},
|
||||
{
|
||||
id: 'code-review',
|
||||
text: '代码审查',
|
||||
value: '代码审查',
|
||||
description: '代码质量审查',
|
||||
template:
|
||||
'请帮我审查以下 [JavaScript/TypeScript/Python/Java/C++/Go] 代码,关注 [性能/安全/可读性/最佳实践] 方面: [代码内容]'
|
||||
},
|
||||
{
|
||||
id: 'email-compose',
|
||||
text: '写邮件',
|
||||
value: '写邮件',
|
||||
description: '邮件草拟',
|
||||
template: '请帮我起草一封 [正式/非正式] 邮件,发送给 [收件人角色],主题是 [邮件主题],内容是关于 [邮件内容]'
|
||||
},
|
||||
{
|
||||
id: 'data-analysis',
|
||||
text: '数据分析',
|
||||
value: '数据分析',
|
||||
description: '数据分析与报告',
|
||||
template:
|
||||
'请分析以下 [销售/用户/流量/金融/健康] 数据,关注 [增长率/分布/趋势/异常/关联性] 指标,生成 [柱状图/折线图/饼图/散点图/热力图] 可视化: [数据内容]'
|
||||
},
|
||||
{
|
||||
id: 'product-design',
|
||||
text: '产品设计',
|
||||
value: '产品设计',
|
||||
description: '产品功能设计',
|
||||
template:
|
||||
'请设计一个 [移动应用/网站/小程序/桌面软件/智能硬件] 的 [功能名称] 功能,目标用户是 [用户群体],核心价值是 [功能价值]'
|
||||
},
|
||||
{
|
||||
id: 'meeting-summary',
|
||||
text: '会议纪要',
|
||||
value: '会议纪要',
|
||||
description: '会议记录整理',
|
||||
template: '请帮我整理一份会议纪要,会议主题是 [会议主题],参会人员有 [参会人员],会议要点包括 [会议要点]'
|
||||
},
|
||||
{
|
||||
id: 'interview-questions',
|
||||
text: '面试问题',
|
||||
value: '面试问题',
|
||||
description: '生成面试问题',
|
||||
template: '请为 [岗位名称] 岗位,针对 [技能领域] 方向,设计 [3/5/10] 个 [简单/中等/困难] 面试问题'
|
||||
},
|
||||
{
|
||||
id: 'speech-draft',
|
||||
text: '演讲稿',
|
||||
value: '演讲稿',
|
||||
description: '演讲稿撰写',
|
||||
template:
|
||||
'请帮我撰写一篇 [开场/主题/致谢/颁奖/毕业] 演讲稿,主题是 [演讲主题],时长约 [5/10/15/30] 分钟,受众是 [目标听众]'
|
||||
}
|
||||
]
|
||||
|
||||
// 模板分类测试数据
|
||||
const templateCategories = [
|
||||
{
|
||||
id: 'common',
|
||||
label: '常用指令',
|
||||
items: templateSuggestions.slice(0, 3)
|
||||
},
|
||||
{
|
||||
id: 'work',
|
||||
label: '工作助手',
|
||||
items: templateSuggestions.slice(3, 6)
|
||||
},
|
||||
{
|
||||
id: 'content',
|
||||
label: '内容创作',
|
||||
items: templateSuggestions.slice(6)
|
||||
}
|
||||
]
|
||||
|
||||
const { messageManager, createConversation } = useConversation({ client })
|
||||
|
||||
const randomId = () => Math.random().toString(36).substring(2, 15)
|
||||
|
||||
const currentMessageId = ref('')
|
||||
|
||||
const { messages, messageState, inputMessage, sendMessage, abortRequest } = messageManager
|
||||
|
||||
const handlePromptItemClick = (ev, item) => {
|
||||
sendMessage(item.description)
|
||||
}
|
||||
|
||||
const roles = {
|
||||
assistant: {
|
||||
placement: 'start',
|
||||
avatar: aiAvatar,
|
||||
maxWidth: '80%'
|
||||
},
|
||||
user: {
|
||||
placement: 'end',
|
||||
avatar: userAvatar,
|
||||
maxWidth: '80%'
|
||||
}
|
||||
}
|
||||
|
||||
const showHistory = ref(false)
|
||||
|
||||
const historyData = reactive([])
|
||||
|
||||
watch(
|
||||
() => messages.value[messages.value.length - 1]?.content,
|
||||
() => {
|
||||
if (!messages.value.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (messages.value.length === 1) {
|
||||
currentMessageId.value = randomId()
|
||||
}
|
||||
|
||||
const allSessions = historyData.flatMap((item) => item.items)
|
||||
const currentSession = allSessions.find((item) => item.id === currentMessageId.value)
|
||||
|
||||
const data = toRaw(messages.value)
|
||||
if (!currentSession) {
|
||||
const today = historyData.find((item) => item.group === '今天')
|
||||
if (today) {
|
||||
today.items.unshift({ title: messages.value[0].content, id: currentMessageId.value, data })
|
||||
} else {
|
||||
historyData.unshift({
|
||||
group: '今天',
|
||||
items: [{ title: messages.value[0].content, id: currentMessageId.value, data }]
|
||||
})
|
||||
}
|
||||
} else {
|
||||
currentSession.data = data
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleHistorySelect = (item) => {
|
||||
currentMessageId.value = item.id
|
||||
messages.value = item.data
|
||||
showHistory.value = false
|
||||
}
|
||||
|
||||
// 指令列表
|
||||
const suggestionItems = templateSuggestions
|
||||
const categories = templateCategories
|
||||
|
||||
const senderRef = ref(null)
|
||||
const currentTemplate = ref('')
|
||||
const suggestionOpen = ref(false)
|
||||
|
||||
// 设置指令
|
||||
const handleFillTemplate = (templateText) => {
|
||||
setTimeout(() => {
|
||||
currentTemplate.value = templateText
|
||||
inputMessage.value = ''
|
||||
|
||||
// 等待DOM更新后激活第一个字段
|
||||
setTimeout(() => {
|
||||
senderRef.value?.activateTemplateFirstField()
|
||||
}, 100)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 清除当前指令
|
||||
const clearTemplate = () => {
|
||||
// 清空指令相关状态
|
||||
currentTemplate.value = ''
|
||||
|
||||
// 确保重新聚焦到输入框
|
||||
nextTick(() => {
|
||||
senderRef.value?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const handleSendMessage = () => {
|
||||
sendMessage(inputMessage.value)
|
||||
|
||||
clearTemplate()
|
||||
}
|
||||
|
||||
const handleMessageKeydown = (event, triggerFn, suggestionKeyDown) => {
|
||||
// 如果指令面板已打开,交给 suggestion 组件处理键盘事件
|
||||
if (suggestionOpen.value) {
|
||||
suggestionKeyDown(event)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果按下斜杠键并且不在指令编辑模式,触发指令面板
|
||||
if (event.key === '/' && !currentTemplate.value) {
|
||||
triggerFn({
|
||||
text: '',
|
||||
position: 0
|
||||
})
|
||||
}
|
||||
|
||||
// ESC 键清除当前指令
|
||||
if (event.key === 'Escape' && currentTemplate.value) {
|
||||
event.preventDefault()
|
||||
clearTemplate()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => inputMessage.value,
|
||||
(value) => {
|
||||
// 如果指令面板已打开,并且指令为空,关闭指令面板
|
||||
if (suggestionOpen.value && value === '') {
|
||||
suggestionOpen.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 页面加载完成后自动聚焦输入框
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
senderRef.value?.focus()
|
||||
}, 500)
|
||||
})
|
||||
|
||||
return {
|
||||
client,
|
||||
fullscreen,
|
||||
show,
|
||||
aiAvatar,
|
||||
userAvatar,
|
||||
welcomeIcon,
|
||||
promptItems,
|
||||
templateSuggestions,
|
||||
templateCategories,
|
||||
messageManager,
|
||||
createConversation,
|
||||
messages,
|
||||
messageState,
|
||||
inputMessage,
|
||||
sendMessage,
|
||||
abortRequest,
|
||||
roles,
|
||||
showHistory,
|
||||
historyData,
|
||||
currentMessageId,
|
||||
handlePromptItemClick,
|
||||
handleHistorySelect,
|
||||
suggestionItems,
|
||||
categories,
|
||||
senderRef,
|
||||
currentTemplate,
|
||||
suggestionOpen,
|
||||
handleFillTemplate,
|
||||
clearTemplate,
|
||||
handleSendMessage,
|
||||
handleMessageKeydown
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* 工具函数模块
|
||||
* 提供一些实用的辅助函数
|
||||
*/
|
||||
|
||||
import type { ChatMessage, ChatCompletionResponse, StreamHandler } from '@opentiny/tiny-robot-kit'
|
||||
|
||||
/**
|
||||
* 处理SSE流式响应
|
||||
* @param response fetch响应对象
|
||||
* @param handler 流处理器
|
||||
*/
|
||||
export async function handleSSEStream(response: Response, handler: StreamHandler, signal?: AbortSignal): Promise<void> {
|
||||
// 获取ReadableStream
|
||||
const reader = response.body?.getReader()
|
||||
if (!reader) {
|
||||
throw new Error('Response body is null')
|
||||
}
|
||||
|
||||
// 处理流式数据
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
if (signal) {
|
||||
signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
reader.cancel().catch((err) => console.error('Error cancelling reader:', err))
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
if (signal?.aborted) {
|
||||
await reader.cancel()
|
||||
break
|
||||
}
|
||||
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
// 解码二进制数据
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
buffer += chunk
|
||||
|
||||
// 处理完整的SSE消息
|
||||
const lines = buffer.split('\n\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
let messageIndex = 0
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim() === '') continue
|
||||
if (line.trim() === 'data: [DONE]') {
|
||||
handler.onDone()
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析SSE消息
|
||||
const dataMatch = line.match(/^data: (.+)$/m)
|
||||
if (!dataMatch) continue
|
||||
|
||||
const data = JSON.parse(dataMatch[1])
|
||||
if (data?.event === 'message' && data?.answer) {
|
||||
handler.onData({
|
||||
id: data.id,
|
||||
created: data.created_at,
|
||||
choices: [
|
||||
{
|
||||
index: messageIndex++,
|
||||
delta: {
|
||||
role: 'assistant',
|
||||
content: data.answer
|
||||
},
|
||||
finish_reason: null
|
||||
}
|
||||
],
|
||||
object: '',
|
||||
model: ''
|
||||
})
|
||||
}
|
||||
if (data?.event === 'message_end') {
|
||||
handler.onData({
|
||||
id: data.id,
|
||||
created: data.created_at,
|
||||
choices: [
|
||||
{
|
||||
index: messageIndex++,
|
||||
delta: {
|
||||
role: 'assistant',
|
||||
content: ''
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}
|
||||
],
|
||||
object: '',
|
||||
model: ''
|
||||
})
|
||||
}
|
||||
// handler.onData(data)
|
||||
} catch (error) {
|
||||
console.error('Error parsing SSE message:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.trim() === 'data: [DONE]' || signal?.aborted) {
|
||||
handler.onDone()
|
||||
}
|
||||
} catch (error) {
|
||||
if (signal?.aborted) return
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化消息
|
||||
* 将各种格式的消息转换为标准的ChatMessage格式
|
||||
* @param messages 消息数组
|
||||
* @returns 标准格式的消息数组
|
||||
*/
|
||||
export function formatMessages(messages: Array<ChatMessage | string>): ChatMessage[] {
|
||||
return messages.map((msg) => {
|
||||
// 如果已经是标准格式,直接返回
|
||||
if (typeof msg === 'object' && 'role' in msg && 'content' in msg) {
|
||||
return {
|
||||
role: msg.role,
|
||||
content: String(msg.content),
|
||||
...(msg.name ? { name: msg.name } : {})
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是字符串,默认为用户消息
|
||||
if (typeof msg === 'string') {
|
||||
return {
|
||||
role: 'user',
|
||||
content: msg
|
||||
}
|
||||
}
|
||||
|
||||
// 其他情况,尝试转换为字符串
|
||||
return {
|
||||
role: 'user',
|
||||
content: String(msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中提取文本内容
|
||||
* @param response 聊天完成响应
|
||||
* @returns 文本内容
|
||||
*/
|
||||
export function extractTextFromResponse(response: ChatCompletionResponse): string {
|
||||
if (!response.choices || !response.choices.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return response.choices[0].message?.content || ''
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
<template>
|
||||
<!-- mcp-robot弹窗 -->
|
||||
<tr-container v-model:show="show" v-model:fullscreen="fullscreen">
|
||||
<template #operations>
|
||||
<tr-icon-button :icon="IconNewSession" size="28" svgSize="20" @click="createConversation()" />
|
||||
<span style="display: inline-flex; line-height: 0; position: relative">
|
||||
<tr-icon-button :icon="IconHistory" size="28" svgSize="20" @click="showHistory = true" />
|
||||
<tr-history
|
||||
v-show="showHistory"
|
||||
class="tr-history-demo"
|
||||
tab-title="历史对话"
|
||||
:selected="currentMessageId"
|
||||
:search-bar="true"
|
||||
:data="historyData"
|
||||
@close="showHistory = false"
|
||||
@item-click="handleHistorySelect"
|
||||
></tr-history>
|
||||
</span>
|
||||
</template>
|
||||
<div v-if="messages.length === 0">
|
||||
<tr-welcome title="盘古助手" description="您好,我是盘古助手,您专属的华为云专家" :icon="welcomeIcon">
|
||||
<template #footer>
|
||||
<div class="welcome-footer">
|
||||
<span>根据相关法律法规要求,您需要先 <a>登录</a>,若没有帐号,您可前往 <a>注册</a></span>
|
||||
</div>
|
||||
</template>
|
||||
</tr-welcome>
|
||||
<tr-prompts
|
||||
:items="promptItems"
|
||||
:wrap="true"
|
||||
item-class="prompt-item"
|
||||
class="tiny-prompts"
|
||||
@item-click="handlePromptItemClick"
|
||||
></tr-prompts>
|
||||
</div>
|
||||
<tr-bubble-list v-else :items="messages" :roles="roles" auto-scroll type="markdown"></tr-bubble-list>
|
||||
|
||||
<template #footer>
|
||||
<div class="chat-input">
|
||||
<tr-suggestion
|
||||
v-model:open="suggestionOpen"
|
||||
:items="suggestionItems"
|
||||
:categories="categories"
|
||||
@fill-template="handleFillTemplate"
|
||||
:maxVisibleItems="5"
|
||||
>
|
||||
<template #trigger="{ onKeyDown, onTrigger }">
|
||||
<tr-sender
|
||||
ref="senderRef"
|
||||
mode="single"
|
||||
v-model="inputMessage"
|
||||
:placeholder="GeneratingStatus.includes(messageState.status) ? '正在思考中...' : '请输入您的问题'"
|
||||
:clearable="true"
|
||||
:loading="GeneratingStatus.includes(messageState.status)"
|
||||
:showWordLimit="true"
|
||||
:maxLength="1000"
|
||||
:template="currentTemplate"
|
||||
@submit="handleSendMessage"
|
||||
@cancel="abortRequest"
|
||||
@keydown="handleMessageKeydown($event, onTrigger, onKeyDown)"
|
||||
@reset-template="clearTemplate"
|
||||
></tr-sender>
|
||||
</template>
|
||||
</tr-suggestion>
|
||||
</div>
|
||||
</template>
|
||||
</tr-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
TrBubbleList,
|
||||
TrContainer,
|
||||
TrHistory,
|
||||
TrIconButton,
|
||||
TrPrompts,
|
||||
TrSender,
|
||||
TrSuggestion,
|
||||
TrWelcome
|
||||
} from '@opentiny/tiny-robot'
|
||||
import { GeneratingStatus } from '@opentiny/tiny-robot-kit'
|
||||
import { IconHistory, IconNewSession } from '@opentiny/tiny-robot-svgs'
|
||||
import { useTinyRobot } from './composition/useTinyRobot'
|
||||
|
||||
const {
|
||||
client,
|
||||
fullscreen,
|
||||
show,
|
||||
aiAvatar,
|
||||
userAvatar,
|
||||
welcomeIcon,
|
||||
promptItems,
|
||||
templateSuggestions,
|
||||
templateCategories,
|
||||
messageManager,
|
||||
createConversation,
|
||||
messages,
|
||||
messageState,
|
||||
inputMessage,
|
||||
sendMessage,
|
||||
abortRequest,
|
||||
roles,
|
||||
showHistory,
|
||||
historyData,
|
||||
currentMessageId,
|
||||
handlePromptItemClick,
|
||||
handleHistorySelect,
|
||||
suggestionItems,
|
||||
categories,
|
||||
senderRef,
|
||||
currentTemplate,
|
||||
suggestionOpen,
|
||||
handleFillTemplate,
|
||||
clearTemplate,
|
||||
handleSendMessage,
|
||||
handleMessageKeydown
|
||||
} = useTinyRobot()
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.chat-input {
|
||||
margin-top: 8px;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.tiny-container {
|
||||
top: 64px;
|
||||
|
||||
container-type: inline-size;
|
||||
|
||||
:deep(.tr-welcome__title-wrapper) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-footer {
|
||||
margin-top: 12px;
|
||||
color: rgb(128, 128, 128);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.tiny-prompts {
|
||||
padding: 16px 24px;
|
||||
|
||||
:deep(.prompt-item) {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
@container (width >=64rem) {
|
||||
width: calc(50% - 8px);
|
||||
}
|
||||
|
||||
.tr-prompt__content-label {
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tr-history-demo {
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
top: 100%;
|
||||
z-index: 100;
|
||||
width: 300px;
|
||||
height: 600px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue