@opentiny/tiny-robot-cli 0.5.2-alpha.3 → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -1
- package/bin/commands/add.js +563 -269
- package/bin/commands/create.js +6 -3
- package/bin/runtime-version.js +37 -0
- package/bin/utils.js +147 -45
- package/package.json +5 -3
- package/templates/basic/.env.example +8 -0
- package/templates/basic/README.md +53 -22
- package/templates/basic/index.html +2 -2
- package/templates/basic/package.json +12 -16
- package/templates/basic/public/favicon.svg +1 -0
- package/templates/basic/public/icons.svg +24 -0
- package/templates/basic/src/App.vue +111 -119
- package/templates/basic/src/main.ts +2 -2
- package/templates/basic/tsconfig.app.json +2 -5
- package/templates/basic/tsconfig.node.json +3 -6
- package/templates/basic/vite.config.ts +6 -1
- package/templates/chat/.env.example +9 -0
- package/templates/chat/README.md +46 -0
- package/templates/chat/index.html +12 -0
- package/templates/chat/package.json +28 -0
- package/templates/chat/src/TinyRobotChat.vue +88 -27
- package/templates/chat/src/components/ComposerTools.vue +99 -0
- package/templates/chat/src/components/WindowHeader.vue +192 -0
- package/templates/chat/src/composables/useWindow.ts +83 -0
- package/templates/chat/src/config/chat-runtime.ts +82 -0
- package/templates/chat/src/config/chat-ui.ts +289 -0
- package/templates/chat/src/index.css +109 -0
- package/templates/chat/src/main.ts +7 -0
- package/templates/chat/tsconfig.app.json +13 -0
- package/templates/chat/tsconfig.json +7 -0
- package/templates/chat/tsconfig.node.json +19 -0
- package/templates/chat/vite.config.ts +16 -0
- package/templates/basic/public/modelcontextprotocol.png +0 -0
- package/templates/basic/src/components/ChatList.vue +0 -82
- package/templates/basic/src/components/ChatSender.vue +0 -125
- package/templates/basic/src/components/ConversationHistory.vue +0 -136
- package/templates/basic/src/components/HistoryDrawerButton.vue +0 -43
- package/templates/basic/src/components/McpServerPickerButton.vue +0 -278
- package/templates/basic/src/components/ThemeToggleButton.vue +0 -44
- package/templates/basic/src/components/icons/IconDeepThink.vue +0 -29
- package/templates/basic/src/components/icons/IconModelAliyunBailian.vue +0 -51
- package/templates/basic/src/components/icons/IconModelDeepseek.vue +0 -29
- package/templates/basic/src/components/icons/IconMoon.vue +0 -29
- package/templates/basic/src/components/icons/IconPlugin.vue +0 -29
- package/templates/basic/src/components/icons/IconSun.vue +0 -35
- package/templates/basic/src/components/icons/IconWebSearch.vue +0 -36
- package/templates/basic/src/components/icons/index.ts +0 -7
- package/templates/basic/src/composables/useChat.ts +0 -129
- package/templates/basic/src/composables/useMcp.ts +0 -170
- package/templates/basic/src/composables/useModel.ts +0 -82
- package/templates/basic/src/mcpServers.ts +0 -40
- package/templates/basic/src/models.ts +0 -81
|
@@ -1,130 +1,122 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<TrTheme>
|
|
3
|
-
<main class="app chat-card">
|
|
4
|
-
<ConversationHistory />
|
|
5
|
-
|
|
6
|
-
<section class="chat-panel">
|
|
7
|
-
<div class="chat-panel-content">
|
|
8
|
-
<p v-if="!hasApiConfig" class="config-warning">
|
|
9
|
-
缺少 API 配置,请在 <code>.env</code> 中设置当前模型服务商对应的 Key。
|
|
10
|
-
</p>
|
|
11
|
-
<header class="chat-header">
|
|
12
|
-
<HistoryDrawerButton @click="historyDrawerOpen = true" />
|
|
13
|
-
<h3 v-if="currentConversationTitle">{{ currentConversationTitle }}</h3>
|
|
14
|
-
<ThemeToggleButton v-if="isWelcomePage" />
|
|
15
|
-
</header>
|
|
16
|
-
|
|
17
|
-
<ChatList />
|
|
18
|
-
|
|
19
|
-
<ChatSender />
|
|
20
|
-
</div>
|
|
21
|
-
</section>
|
|
22
|
-
</main>
|
|
23
|
-
</TrTheme>
|
|
24
|
-
</template>
|
|
25
|
-
|
|
26
1
|
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
TrChat,
|
|
4
|
+
useLocalChatRuntime,
|
|
5
|
+
type ChatMcpServers,
|
|
6
|
+
type ChatProviderConfig,
|
|
7
|
+
type ChatUIOptions,
|
|
8
|
+
} from '@opentiny/tiny-robot-chat'
|
|
27
9
|
import { TrThemeProvider as TrTheme } from '@opentiny/tiny-robot'
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
import
|
|
35
|
-
import
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
10
|
+
import { IconBailian, IconDeepseek } from '@opentiny/tiny-robot-svgs'
|
|
11
|
+
|
|
12
|
+
const modelProviders: ChatProviderConfig[] = [
|
|
13
|
+
{
|
|
14
|
+
type: 'qwen',
|
|
15
|
+
label: 'DashScope',
|
|
16
|
+
apiUrl: import.meta.env.VITE_QWEN_API_URL?.trim() || undefined,
|
|
17
|
+
apiKey: import.meta.env.VITE_ALIYUN_DASHSCOPE_KEY?.trim(),
|
|
18
|
+
models: [
|
|
19
|
+
{
|
|
20
|
+
id: 'qwen3.7-flash',
|
|
21
|
+
label: 'Qwen3.7 Flash',
|
|
22
|
+
icon: IconBailian,
|
|
23
|
+
capabilities: {
|
|
24
|
+
thinking: true,
|
|
25
|
+
search: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'qwen3.7-plus',
|
|
30
|
+
label: 'Qwen3.7 Plus',
|
|
31
|
+
icon: IconBailian,
|
|
32
|
+
capabilities: {
|
|
33
|
+
thinking: true,
|
|
34
|
+
search: true,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'qwen3.7-max',
|
|
39
|
+
label: 'Qwen3.7 Max',
|
|
40
|
+
icon: IconBailian,
|
|
41
|
+
capabilities: {
|
|
42
|
+
thinking: true,
|
|
43
|
+
search: true,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'deepseek',
|
|
50
|
+
apiUrl: import.meta.env.VITE_DEEPSEEK_API_URL?.trim() || undefined,
|
|
51
|
+
apiKey: import.meta.env.VITE_DEEPSEEK_API_KEY?.trim(),
|
|
52
|
+
models: [
|
|
53
|
+
{
|
|
54
|
+
id: 'deepseek-v4-flash',
|
|
55
|
+
label: 'DeepSeek V4 Flash',
|
|
56
|
+
icon: IconDeepseek,
|
|
57
|
+
capabilities: {
|
|
58
|
+
thinking: true,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'deepseek-v4-pro',
|
|
63
|
+
label: 'DeepSeek V4 Pro',
|
|
64
|
+
icon: IconDeepseek,
|
|
65
|
+
capabilities: {
|
|
66
|
+
thinking: true,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
const amapMcpApiKey = import.meta.env.VITE_ALIYUN_DASHSCOPE_KEY?.trim()
|
|
74
|
+
|
|
75
|
+
const mcpServers = [
|
|
76
|
+
{
|
|
77
|
+
id: 'amap-maps',
|
|
78
|
+
name: '高德地图',
|
|
79
|
+
description: '覆盖地图、导航、地理编码、天气、路径规划、距离测量、关键词搜索和周边搜索等地理信息服务。',
|
|
80
|
+
baseUrl: import.meta.env.VITE_AMAP_MCP_URL?.trim() || 'https://dashscope.aliyuncs.com/api/v1/mcps/amap-maps/mcp',
|
|
81
|
+
icon: 'https://img.alicdn.com/imgextra/i4/O1CN01iPPabT1EGRN6uatHP_!!6000000000324-0-tps-512-512.jpg',
|
|
82
|
+
headers: amapMcpApiKey ? { Authorization: `Bearer ${amapMcpApiKey}` } : undefined,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'model-context-protocol-mcp',
|
|
86
|
+
name: 'Model Context Protocol MCP',
|
|
87
|
+
description: 'MCP Server',
|
|
88
|
+
baseUrl: '/modelcontextprotocol-mcp',
|
|
89
|
+
installed: true,
|
|
90
|
+
},
|
|
91
|
+
] satisfies ChatMcpServers
|
|
92
|
+
|
|
93
|
+
const runtime = useLocalChatRuntime({
|
|
94
|
+
mcpServers,
|
|
95
|
+
modelProviders,
|
|
44
96
|
})
|
|
45
97
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
98
|
+
const chatUi = {
|
|
99
|
+
layout: {
|
|
100
|
+
rightAside: {
|
|
101
|
+
mode: 'dock',
|
|
102
|
+
width: 400,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
} satisfies ChatUIOptions
|
|
49
106
|
</script>
|
|
50
107
|
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.chat-card {
|
|
61
|
-
width: 100%;
|
|
62
|
-
height: 100vh;
|
|
63
|
-
display: flex;
|
|
64
|
-
gap: 0;
|
|
65
|
-
overflow: hidden;
|
|
66
|
-
align-items: stretch;
|
|
67
|
-
flex-direction: row;
|
|
68
|
-
padding: 0;
|
|
69
|
-
margin: 0;
|
|
70
|
-
border-radius: 0;
|
|
71
|
-
background: var(--tr-container-bg-default);
|
|
72
|
-
box-shadow: none;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
.chat-panel {
|
|
76
|
-
flex: 1;
|
|
77
|
-
min-width: 0;
|
|
78
|
-
display: flex;
|
|
79
|
-
padding: 12px;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.chat-panel-content {
|
|
83
|
-
width: 100%;
|
|
84
|
-
max-width: 980px;
|
|
85
|
-
margin: 0 auto;
|
|
86
|
-
display: flex;
|
|
87
|
-
flex-direction: column;
|
|
88
|
-
gap: 12px;
|
|
89
|
-
min-height: 0;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.chat-header h3 {
|
|
93
|
-
margin: 0;
|
|
94
|
-
flex: 1;
|
|
95
|
-
min-width: 0;
|
|
96
|
-
white-space: nowrap;
|
|
97
|
-
overflow: hidden;
|
|
98
|
-
text-overflow: ellipsis;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.chat-header {
|
|
102
|
-
display: flex;
|
|
103
|
-
align-items: center;
|
|
104
|
-
gap: 8px;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.config-warning {
|
|
108
|
-
margin: 0;
|
|
109
|
-
padding: 10px 12px;
|
|
110
|
-
border-radius: 8px;
|
|
111
|
-
background: var(--tr-color-warning-light);
|
|
112
|
-
color: var(--tr-color-warning);
|
|
113
|
-
font-size: 14px;
|
|
114
|
-
}
|
|
108
|
+
<template>
|
|
109
|
+
<TrTheme>
|
|
110
|
+
<div class="chat-basic">
|
|
111
|
+
<TrChat :runtime="runtime" :ui="chatUi" />
|
|
112
|
+
</div>
|
|
113
|
+
</TrTheme>
|
|
114
|
+
</template>
|
|
115
115
|
|
|
116
|
+
<style scoped>
|
|
116
117
|
@media (max-width: 959px) {
|
|
117
|
-
.chat-
|
|
118
|
-
|
|
119
|
-
justify-content: center;
|
|
120
|
-
min-height: 32px;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
.chat-header h3 {
|
|
124
|
-
width: 100%;
|
|
125
|
-
text-align: center;
|
|
126
|
-
padding: 0 48px;
|
|
127
|
-
flex: none;
|
|
118
|
+
.chat-basic :deep(.tr-bubble__avatar) {
|
|
119
|
+
display: none;
|
|
128
120
|
}
|
|
129
121
|
}
|
|
130
122
|
</style>
|
|
@@ -3,14 +3,11 @@
|
|
|
3
3
|
"compilerOptions": {
|
|
4
4
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
5
5
|
"types": ["vite/client"],
|
|
6
|
-
|
|
7
|
-
/* Linting */
|
|
8
|
-
"strict": true,
|
|
6
|
+
"allowArbitraryExtensions": true,
|
|
9
7
|
"noUnusedLocals": true,
|
|
10
8
|
"noUnusedParameters": true,
|
|
11
9
|
"erasableSyntaxOnly": true,
|
|
12
|
-
"noFallthroughCasesInSwitch": true
|
|
13
|
-
"noUncheckedSideEffectImports": true
|
|
10
|
+
"noFallthroughCasesInSwitch": true
|
|
14
11
|
},
|
|
15
12
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
|
16
13
|
}
|
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
-
"target": "
|
|
4
|
+
"target": "es2023",
|
|
5
5
|
"lib": ["ES2023"],
|
|
6
|
-
"module": "ESNext",
|
|
7
6
|
"types": ["node"],
|
|
8
7
|
"skipLibCheck": true,
|
|
9
8
|
|
|
10
9
|
/* Bundler mode */
|
|
11
|
-
"
|
|
10
|
+
"module": "nodenext",
|
|
12
11
|
"allowImportingTsExtensions": true,
|
|
13
12
|
"verbatimModuleSyntax": true,
|
|
14
13
|
"moduleDetection": "force",
|
|
15
14
|
"noEmit": true,
|
|
16
15
|
|
|
17
16
|
/* Linting */
|
|
18
|
-
"strict": true,
|
|
19
17
|
"noUnusedLocals": true,
|
|
20
18
|
"noUnusedParameters": true,
|
|
21
19
|
"erasableSyntaxOnly": true,
|
|
22
|
-
"noFallthroughCasesInSwitch": true
|
|
23
|
-
"noUncheckedSideEffectImports": true
|
|
20
|
+
"noFallthroughCasesInSwitch": true
|
|
24
21
|
},
|
|
25
22
|
"include": ["vite.config.ts"]
|
|
26
23
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { defineConfig } from 'vite'
|
|
2
2
|
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const workspaceRoot = resolve(import.meta.dirname, '..')
|
|
3
6
|
|
|
4
|
-
// https://vite.dev/config/
|
|
5
7
|
export default defineConfig({
|
|
6
8
|
plugins: [vue()],
|
|
7
9
|
server: {
|
|
10
|
+
fs: {
|
|
11
|
+
allow: [workspaceRoot],
|
|
12
|
+
},
|
|
8
13
|
proxy: {
|
|
9
14
|
'/modelcontextprotocol-mcp': {
|
|
10
15
|
target: 'https://modelcontextprotocol.io/mcp',
|
|
@@ -1 +1,10 @@
|
|
|
1
|
+
# Optional custom model endpoints. Leave blank to use official Provider defaults.
|
|
2
|
+
VITE_QWEN_API_URL=
|
|
3
|
+
VITE_DEEPSEEK_API_URL=
|
|
4
|
+
|
|
5
|
+
# Optional custom MCP endpoint. Leave blank to use the official endpoint.
|
|
6
|
+
VITE_AMAP_MCP_URL=
|
|
7
|
+
|
|
8
|
+
# Optional local direct-connection credentials.
|
|
9
|
+
VITE_ALIYUN_DASHSCOPE_KEY=
|
|
1
10
|
VITE_DEEPSEEK_API_KEY=
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# TinyRobot Chat Add
|
|
2
|
+
|
|
3
|
+
这是一个独立的 Chat 扩展模板,提供通用工作助手场景的最小工程结构。
|
|
4
|
+
|
|
5
|
+
## 启动
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
pnpm dev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 构建
|
|
12
|
+
|
|
13
|
+
```powershell
|
|
14
|
+
pnpm type-check
|
|
15
|
+
pnpm build
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 接入真实模型
|
|
19
|
+
|
|
20
|
+
复制项目根目录的 `.env.example` 为 `.env.local`,配置 `VITE_QWEN_API_URL`、`VITE_ALIYUN_DASHSCOPE_KEY`、`VITE_DEEPSEEK_API_URL` 和 `VITE_DEEPSEEK_API_KEY`,重启 Vite 后即可使用模型选择器中的真实模型。模板只提供 `.env.example`,不会自动创建 `.env`。配置读取逻辑位于 `src/config/chat-runtime.ts`;使用 `add chat` 注入后,对应文件位于 `src/tiny-robot-chat/config/chat-runtime.ts`。
|
|
21
|
+
|
|
22
|
+
## 模型配置
|
|
23
|
+
|
|
24
|
+
模型配置中的 `icon` 用于模型选择器触发器;在窄容器中,带图标的模型会只显示图标。Qwen 模型支持深度思考和联网搜索,DeepSeek 模型支持深度思考。
|
|
25
|
+
|
|
26
|
+
DeepSeek 模型默认继承 `low`、`high`、`max` 三档思考强度,默认值为 `high`。开启 `深度思考` 后,模型选择器才会显示思考等级。业务侧可以在模型对象中通过 `efforts` 和 `defaultEffort` 覆盖选项;只有必须始终思考的模型才配置 `thinkingRequired: true`。
|
|
27
|
+
|
|
28
|
+
## MCP 代理
|
|
29
|
+
|
|
30
|
+
独立模板的 `vite.config.ts` 已配置 `/modelcontextprotocol-mcp` 代理。使用 `add chat` 时,CLI 不会复制或修改业务项目的 `vite.config.*`,需要手动把该代理配置添加到业务项目的 `server.proxy` 下,并重启 Vite。
|
|
31
|
+
|
|
32
|
+
## 目录职责
|
|
33
|
+
|
|
34
|
+
- `src/config/chat-ui.ts`: Chat UI、提示词、模板和菜单配置。
|
|
35
|
+
- `src/config/chat-runtime.ts`: MCP 示例和模型配置。
|
|
36
|
+
- `src/components`: 窗口头部和输入区工具。
|
|
37
|
+
- `src/composables/useWindow.ts`: floating、fullscreen 窗口状态。
|
|
38
|
+
- `src/index.css`: 仅作用于 Chat 容器的组件样式和 Surface 样式变量,不重置宿主页面的全局样式。
|
|
39
|
+
|
|
40
|
+
## 替换数据
|
|
41
|
+
|
|
42
|
+
直接修改 `src/config/chat-ui.ts` 和 `src/config/chat-runtime.ts` 中导出的数据;组件通过 props 使用这些配置。
|
|
43
|
+
|
|
44
|
+
## 切换窗口模式
|
|
45
|
+
|
|
46
|
+
默认显示 AI 头像按钮,点击后展开 Chat。窗口头部提供 floating、fullscreen 两种模式。floating 支持拖拽、缩放和位置恢复;fullscreen 铺满对应区域,可使用 Escape 返回悬浮窗口。点击关闭后返回 AI 头像按钮。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Chat Add Template</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.ts"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vue-tsc -b && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"type-check": "vue-tsc -b"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@opentiny/tiny-robot": "__TINY_ROBOT_VERSION__",
|
|
14
|
+
"@opentiny/tiny-robot-chat": "__TINY_ROBOT_VERSION__",
|
|
15
|
+
"@opentiny/tiny-robot-kit": "__TINY_ROBOT_VERSION__",
|
|
16
|
+
"@opentiny/tiny-robot-svgs": "__TINY_ROBOT_VERSION__",
|
|
17
|
+
"@vueuse/core": "13.9.0",
|
|
18
|
+
"vue": "^3.5.40"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^24.13.3",
|
|
22
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
23
|
+
"@vue/tsconfig": "^0.9.1",
|
|
24
|
+
"typescript": "~6.0.2",
|
|
25
|
+
"vite": "^8.2.0",
|
|
26
|
+
"vue-tsc": "^3.3.8"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,35 +1,96 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
3
|
-
import { TrThemeProvider } from '@opentiny/tiny-robot'
|
|
4
|
-
import { TrChat } from '@opentiny/tiny-robot
|
|
5
|
-
import {
|
|
2
|
+
import type { HistoryMenuItem, PromptProps, TemplateItem } from '@opentiny/tiny-robot'
|
|
3
|
+
import { TrSender, TrThemeProvider as TrTheme } from '@opentiny/tiny-robot'
|
|
4
|
+
import { TrChat, useChatHistoryItems, useLocalChatRuntime, type ChatHistoryItem } from '@opentiny/tiny-robot-chat'
|
|
5
|
+
import { IconAi } from '@opentiny/tiny-robot-svgs'
|
|
6
|
+
import { computed, reactive, ref } from 'vue'
|
|
7
|
+
import ComposerTools from './components/ComposerTools.vue'
|
|
8
|
+
import WindowHeader from './components/WindowHeader.vue'
|
|
9
|
+
import { composerMenus, createChatUi, templateCategories } from './config/chat-ui'
|
|
10
|
+
import { mcpExamples, mcpServers, modelProviders } from './config/chat-runtime'
|
|
11
|
+
import { useWindow } from './composables/useWindow'
|
|
6
12
|
|
|
7
|
-
const
|
|
8
|
-
const
|
|
13
|
+
const runtime = useLocalChatRuntime({ modelProviders, mcpServers })
|
|
14
|
+
const window = reactive(useWindow())
|
|
15
|
+
const chatRef = ref<{ send: (payload: { text: string }) => Promise<boolean> } | null>(null)
|
|
16
|
+
const currentTemplate = ref<TemplateItem[]>([])
|
|
17
|
+
const showHistory = ref(false)
|
|
18
|
+
const historyData = useChatHistoryItems({ conversations: () => runtime.conversations.value, defaultTitle: '' })
|
|
19
|
+
const activeConversationId = computed(() => runtime.activeConversation.value?.id)
|
|
20
|
+
const templateExtensions = [TrSender.template(currentTemplate as never, { appendTo: '.chat-add-window' })]
|
|
21
|
+
const chatUi = computed(() => createChatUi({ floatingOptions: window.floatingOptions, templateExtensions }))
|
|
9
22
|
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
|
|
13
|
-
provider: 'deepseek',
|
|
14
|
-
name: 'DeepSeek V4 Flash',
|
|
15
|
-
model: 'deepseek-v4-flash',
|
|
16
|
-
apiUrl: 'https://api.deepseek.com/chat/completions',
|
|
17
|
-
apiKey: import.meta.env.VITE_DEEPSEEK_API_KEY || '',
|
|
18
|
-
},
|
|
19
|
-
]
|
|
23
|
+
function handlePromptClick({ item }: { item: PromptProps }): void {
|
|
24
|
+
if (item.description) void chatRef.value?.send({ text: item.description })
|
|
25
|
+
}
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
function handleNewSession(): void {
|
|
28
|
+
void runtime.actions.createConversation()
|
|
29
|
+
showHistory.value = false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function handleHistorySelect(item: ChatHistoryItem): void {
|
|
33
|
+
void runtime.actions.switchConversation(item.raw.id)
|
|
34
|
+
showHistory.value = false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function handleHistoryRename(title: string, item: ChatHistoryItem): void {
|
|
38
|
+
void runtime.actions.renameConversation(item.raw.id, title)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handleHistoryAction(action: HistoryMenuItem, item: ChatHistoryItem): void {
|
|
42
|
+
if (action.id === 'delete') void runtime.actions.deleteConversation(item.raw.id)
|
|
43
|
+
}
|
|
22
44
|
</script>
|
|
23
45
|
|
|
24
46
|
<template>
|
|
25
|
-
<
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
<TrTheme>
|
|
48
|
+
<main class="chat-add-app">
|
|
49
|
+
<button
|
|
50
|
+
v-if="!window.show"
|
|
51
|
+
class="chat-add-launcher"
|
|
52
|
+
type="button"
|
|
53
|
+
title="打开 TinyRobot"
|
|
54
|
+
aria-label="打开 TinyRobot"
|
|
55
|
+
@click="window.open"
|
|
56
|
+
>
|
|
57
|
+
<IconAi style="font-size: 32px" />
|
|
58
|
+
</button>
|
|
59
|
+
<TrChat
|
|
60
|
+
v-if="window.show"
|
|
61
|
+
ref="chatRef"
|
|
62
|
+
:class="['chat-add-window', `chat-add-window--${window.displayMode}`]"
|
|
63
|
+
:style="window.layoutStyle"
|
|
64
|
+
:runtime="runtime"
|
|
65
|
+
:ui="chatUi"
|
|
66
|
+
v-model:floating-state="window.floatingState"
|
|
67
|
+
@prompt-click="handlePromptClick"
|
|
68
|
+
>
|
|
69
|
+
<template #layout-header>
|
|
70
|
+
<WindowHeader
|
|
71
|
+
:display-mode="window.displayMode"
|
|
72
|
+
:show-history="showHistory"
|
|
73
|
+
:history-data="historyData"
|
|
74
|
+
:active-conversation-id="activeConversationId"
|
|
75
|
+
@new-session="handleNewSession"
|
|
76
|
+
@select-history="handleHistorySelect"
|
|
77
|
+
@rename-history="handleHistoryRename"
|
|
78
|
+
@history-action="handleHistoryAction"
|
|
79
|
+
@update:show-history="showHistory = $event"
|
|
80
|
+
@change-mode="window.setDisplayMode"
|
|
81
|
+
@close="window.close"
|
|
82
|
+
/>
|
|
83
|
+
</template>
|
|
84
|
+
<template #composer-before="{ submit }">
|
|
85
|
+
<ComposerTools
|
|
86
|
+
v-model:current-template="currentTemplate"
|
|
87
|
+
:template-categories="templateCategories"
|
|
88
|
+
:mcp-examples="mcpExamples"
|
|
89
|
+
:menus="composerMenus"
|
|
90
|
+
:submit="submit"
|
|
91
|
+
/>
|
|
92
|
+
</template>
|
|
93
|
+
</TrChat>
|
|
94
|
+
</main>
|
|
95
|
+
</TrTheme>
|
|
35
96
|
</template>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SuggestionData, SuggestionItem, TemplateItem } from '@opentiny/tiny-robot'
|
|
3
|
+
import { TrDropdownMenu, TrSuggestionPillButton, TrSuggestionPills, TrSuggestionPopover } from '@opentiny/tiny-robot'
|
|
4
|
+
import type { ChatSenderSlotProps } from '@opentiny/tiny-robot-chat'
|
|
5
|
+
import { IconSparkles } from '@opentiny/tiny-robot-svgs'
|
|
6
|
+
import type { ChatMenuItem, TemplateCategory, TemplateSuggestionItem } from '../config/chat-ui'
|
|
7
|
+
import type { McpExample } from '../config/chat-runtime'
|
|
8
|
+
import { computed } from 'vue'
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
templateCategories: readonly TemplateCategory[]
|
|
12
|
+
mcpExamples: readonly McpExample[]
|
|
13
|
+
menus: readonly ChatMenuItem[]
|
|
14
|
+
submit: ChatSenderSlotProps['submit']
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const currentTemplate = defineModel<TemplateItem[]>('currentTemplate', { required: true })
|
|
18
|
+
const suggestionData: SuggestionData = props.mcpExamples.map((item) => ({ id: item.id, text: item.request }))
|
|
19
|
+
const mcpMenuItems = props.mcpExamples.map((item) => ({ id: item.id, text: item.request }))
|
|
20
|
+
const mcpMenu = computed(() => props.menus.find((menu) => menu.action === 'mcp'))
|
|
21
|
+
const templateMenus = computed(() =>
|
|
22
|
+
props.templateCategories.map((category) => ({
|
|
23
|
+
...category,
|
|
24
|
+
items: category.items.map((item) => ({ id: item.id, text: item.text, template: item })),
|
|
25
|
+
})),
|
|
26
|
+
)
|
|
27
|
+
const overlayTarget = '.chat-add-window'
|
|
28
|
+
|
|
29
|
+
function fillTemplate(template: TemplateSuggestionItem): void {
|
|
30
|
+
currentTemplate.value = template.template.map((item) => ({ ...item }))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function submitSuggestion(item: SuggestionItem): void {
|
|
34
|
+
props.submit({ text: item.text })
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<div class="chat-add-tools">
|
|
40
|
+
<TrSuggestionPopover :append-to="overlayTarget" :data="suggestionData" @item-click="submitSuggestion">
|
|
41
|
+
<template #trigger>
|
|
42
|
+
<TrSuggestionPillButton title="常用查询"
|
|
43
|
+
><IconSparkles style="font-size: 16px; color: #1476ff"
|
|
44
|
+
/></TrSuggestionPillButton>
|
|
45
|
+
</template>
|
|
46
|
+
</TrSuggestionPopover>
|
|
47
|
+
<TrSuggestionPills class="chat-add-tools__pills">
|
|
48
|
+
<TrDropdownMenu
|
|
49
|
+
v-if="mcpMenu"
|
|
50
|
+
:append-to="overlayTarget"
|
|
51
|
+
:items="mcpMenuItems"
|
|
52
|
+
trigger="click"
|
|
53
|
+
@item-click="(item) => submitSuggestion(item as SuggestionItem)"
|
|
54
|
+
>
|
|
55
|
+
<template #trigger
|
|
56
|
+
><TrSuggestionPillButton>{{ mcpMenu.title }}</TrSuggestionPillButton></template
|
|
57
|
+
>
|
|
58
|
+
</TrDropdownMenu>
|
|
59
|
+
<TrDropdownMenu
|
|
60
|
+
v-for="category in templateMenus"
|
|
61
|
+
:key="category.id"
|
|
62
|
+
:append-to="overlayTarget"
|
|
63
|
+
:items="category.items as never"
|
|
64
|
+
trigger="click"
|
|
65
|
+
@item-click="(item) => fillTemplate((item as unknown as { template: TemplateSuggestionItem }).template)"
|
|
66
|
+
>
|
|
67
|
+
<template #trigger
|
|
68
|
+
><TrSuggestionPillButton>{{ category.title }}</TrSuggestionPillButton></template
|
|
69
|
+
>
|
|
70
|
+
</TrDropdownMenu>
|
|
71
|
+
</TrSuggestionPills>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<style scoped>
|
|
76
|
+
.chat-add-tools {
|
|
77
|
+
display: flex;
|
|
78
|
+
box-sizing: border-box;
|
|
79
|
+
width: 100%;
|
|
80
|
+
min-width: 0;
|
|
81
|
+
flex-shrink: 0;
|
|
82
|
+
align-items: center;
|
|
83
|
+
gap: 8px;
|
|
84
|
+
padding: 8px 12px;
|
|
85
|
+
}
|
|
86
|
+
.chat-add-tools__pills {
|
|
87
|
+
min-width: 0;
|
|
88
|
+
flex: 1;
|
|
89
|
+
}
|
|
90
|
+
.chat-add-tools__pills :deep(.tr-suggestion-pills__container) {
|
|
91
|
+
min-width: 0;
|
|
92
|
+
mask: linear-gradient(to right, rgb(0 0 0 / 100%) 80%, rgb(0 0 0 / 0%) 100%);
|
|
93
|
+
}
|
|
94
|
+
@container (max-width: 519px) {
|
|
95
|
+
.chat-add-tools {
|
|
96
|
+
padding: 8px;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
</style>
|