@opentiny/tiny-robot-cli 0.4.2-alpha.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/LICENSE +22 -0
- package/README.md +21 -0
- package/bin/cli.js +187 -0
- package/package.json +37 -0
- package/templates/basic/.env.example +2 -0
- package/templates/basic/README.md +45 -0
- package/templates/basic/index.html +13 -0
- package/templates/basic/package.json +29 -0
- package/templates/basic/public/favicon.ico +0 -0
- package/templates/basic/public/modelcontextprotocol.png +0 -0
- package/templates/basic/src/App.vue +130 -0
- package/templates/basic/src/components/ChatList.vue +82 -0
- package/templates/basic/src/components/ChatSender.vue +125 -0
- package/templates/basic/src/components/ConversationHistory.vue +136 -0
- package/templates/basic/src/components/HistoryDrawerButton.vue +43 -0
- package/templates/basic/src/components/McpServerPickerButton.vue +278 -0
- package/templates/basic/src/components/ThemeToggleButton.vue +44 -0
- package/templates/basic/src/components/icons/IconDeepThink.vue +29 -0
- package/templates/basic/src/components/icons/IconModelAliyunBailian.vue +51 -0
- package/templates/basic/src/components/icons/IconModelDeepseek.vue +29 -0
- package/templates/basic/src/components/icons/IconMoon.vue +29 -0
- package/templates/basic/src/components/icons/IconPlugin.vue +29 -0
- package/templates/basic/src/components/icons/IconSun.vue +35 -0
- package/templates/basic/src/components/icons/IconWebSearch.vue +36 -0
- package/templates/basic/src/components/icons/index.ts +7 -0
- package/templates/basic/src/composables/useChat.ts +129 -0
- package/templates/basic/src/composables/useMcp.ts +170 -0
- package/templates/basic/src/composables/useModel.ts +82 -0
- package/templates/basic/src/main.ts +7 -0
- package/templates/basic/src/mcpServers.ts +40 -0
- package/templates/basic/src/models.ts +81 -0
- package/templates/basic/src/style.css +21 -0
- package/templates/basic/tsconfig.app.json +16 -0
- package/templates/basic/tsconfig.json +7 -0
- package/templates/basic/tsconfig.node.json +26 -0
- package/templates/basic/vite.config.ts +16 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 - present OpenTiny Authors.
|
|
4
|
+
Copyright (c) 2025 - present Huawei Cloud Computing Technologies Co., Ltd.
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @opentiny/tiny-robot-cli
|
|
2
|
+
|
|
3
|
+
A lightweight CLI for scaffolding TinyRobot-based product projects.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @opentiny/tiny-robot-cli create my-app
|
|
9
|
+
pnpm dlx @opentiny/tiny-robot-cli create my-app
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Options
|
|
13
|
+
|
|
14
|
+
- `-t, --template <name>`: template name, currently supports `basic`
|
|
15
|
+
- `-h, --help`: show help
|
|
16
|
+
|
|
17
|
+
## Template Documentation
|
|
18
|
+
|
|
19
|
+
Template-specific features and environment variables are documented in each template directory, for example:
|
|
20
|
+
|
|
21
|
+
- `packages/cli/templates/basic/README.md`
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { input, select } from '@inquirer/prompts'
|
|
8
|
+
import { Command } from 'commander'
|
|
9
|
+
|
|
10
|
+
const TEMPLATE_PLACEHOLDER = '__PROJECT_NAME__'
|
|
11
|
+
const DEFAULT_TEMPLATE = 'basic'
|
|
12
|
+
const DEFAULT_PROJECT_NAME = 'tiny-robot-app'
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
14
|
+
const __dirname = path.dirname(__filename)
|
|
15
|
+
const templatesRoot = path.resolve(__dirname, '../templates')
|
|
16
|
+
|
|
17
|
+
function getAvailableTemplates() {
|
|
18
|
+
if (!fs.existsSync(templatesRoot)) {
|
|
19
|
+
return []
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return fs
|
|
23
|
+
.readdirSync(templatesRoot, { withFileTypes: true })
|
|
24
|
+
.filter((entry) => entry.isDirectory())
|
|
25
|
+
.map((entry) => entry.name)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateProjectName(name) {
|
|
29
|
+
// Keep project naming rules strict for npm package compatibility.
|
|
30
|
+
const npmSafePattern = /^[a-z0-9-]+$/
|
|
31
|
+
return npmSafePattern.test(name)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getTemplateDir(templateName) {
|
|
35
|
+
return path.join(templatesRoot, templateName)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function resolveProjectName(initialProjectName, skipPrompts) {
|
|
39
|
+
if (initialProjectName) {
|
|
40
|
+
return initialProjectName
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (skipPrompts) {
|
|
44
|
+
return DEFAULT_PROJECT_NAME
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return input({
|
|
48
|
+
message: 'Project name:',
|
|
49
|
+
default: DEFAULT_PROJECT_NAME,
|
|
50
|
+
validate: (value) => {
|
|
51
|
+
if (!value) {
|
|
52
|
+
return 'Project name is required.'
|
|
53
|
+
}
|
|
54
|
+
if (!validateProjectName(value)) {
|
|
55
|
+
return 'Project name can only contain lowercase letters, numbers, and dashes.'
|
|
56
|
+
}
|
|
57
|
+
const targetDir = path.resolve(process.cwd(), value)
|
|
58
|
+
if (fs.existsSync(targetDir)) {
|
|
59
|
+
return `Target directory already exists: ${targetDir}`
|
|
60
|
+
}
|
|
61
|
+
return true
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function resolveTemplateName(initialTemplateName, availableTemplates, skipPrompts) {
|
|
67
|
+
if (initialTemplateName) {
|
|
68
|
+
return initialTemplateName
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (skipPrompts) {
|
|
72
|
+
return availableTemplates.includes(DEFAULT_TEMPLATE) ? DEFAULT_TEMPLATE : availableTemplates[0]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return select({
|
|
76
|
+
message: 'Template:',
|
|
77
|
+
default: availableTemplates.includes(DEFAULT_TEMPLATE) ? DEFAULT_TEMPLATE : availableTemplates[0],
|
|
78
|
+
choices: availableTemplates.map((templateName) => ({
|
|
79
|
+
name: templateName,
|
|
80
|
+
value: templateName,
|
|
81
|
+
})),
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function copyTemplate(sourceDir, targetDir) {
|
|
86
|
+
fs.cpSync(sourceDir, targetDir, {
|
|
87
|
+
recursive: true,
|
|
88
|
+
filter: (source) => {
|
|
89
|
+
const name = path.basename(source)
|
|
90
|
+
// Ignore local build artifacts to keep generated projects clean.
|
|
91
|
+
return !['node_modules', '.git', 'dist', '.DS_Store', '.vite'].includes(name)
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renameSpecialFiles(targetDir) {
|
|
97
|
+
const from = path.join(targetDir, '_gitignore')
|
|
98
|
+
const to = path.join(targetDir, '.gitignore')
|
|
99
|
+
|
|
100
|
+
if (fs.existsSync(from)) {
|
|
101
|
+
fs.renameSync(from, to)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function replaceProjectName(targetDir, projectName) {
|
|
106
|
+
const filesToReplace = ['package.json', 'README.md']
|
|
107
|
+
|
|
108
|
+
for (const relativeFile of filesToReplace) {
|
|
109
|
+
const absoluteFile = path.join(targetDir, relativeFile)
|
|
110
|
+
|
|
111
|
+
if (!fs.existsSync(absoluteFile)) {
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const content = fs.readFileSync(absoluteFile, 'utf-8')
|
|
116
|
+
const nextContent = content.replaceAll(TEMPLATE_PLACEHOLDER, projectName)
|
|
117
|
+
fs.writeFileSync(absoluteFile, nextContent, 'utf-8')
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function createProject(initialProjectName, initialTemplateName, skipPrompts) {
|
|
122
|
+
const availableTemplates = getAvailableTemplates()
|
|
123
|
+
if (availableTemplates.length === 0) {
|
|
124
|
+
console.error('Error: no templates found.')
|
|
125
|
+
process.exit(1)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const projectName = await resolveProjectName(initialProjectName, skipPrompts)
|
|
129
|
+
const templateName = await resolveTemplateName(initialTemplateName, availableTemplates, skipPrompts)
|
|
130
|
+
|
|
131
|
+
if (!validateProjectName(projectName)) {
|
|
132
|
+
console.error('Error: project name can only contain lowercase letters, numbers, and dashes.')
|
|
133
|
+
process.exit(1)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const templateDir = getTemplateDir(templateName)
|
|
137
|
+
const targetDir = path.resolve(process.cwd(), projectName)
|
|
138
|
+
|
|
139
|
+
if (!fs.existsSync(templateDir)) {
|
|
140
|
+
console.error(`Error: template "${templateName}" does not exist. Available: ${availableTemplates.join(', ')}`)
|
|
141
|
+
process.exit(1)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (fs.existsSync(targetDir)) {
|
|
145
|
+
console.error(`Error: target directory already exists: ${targetDir}`)
|
|
146
|
+
process.exit(1)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
copyTemplate(templateDir, targetDir)
|
|
150
|
+
renameSpecialFiles(targetDir)
|
|
151
|
+
replaceProjectName(targetDir, projectName)
|
|
152
|
+
|
|
153
|
+
console.log('\nProject created successfully!')
|
|
154
|
+
console.log(`\nNext steps:`)
|
|
155
|
+
console.log(` cd ${projectName}`)
|
|
156
|
+
console.log(' pnpm install')
|
|
157
|
+
console.log(' pnpm dev\n')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function run() {
|
|
161
|
+
const program = new Command()
|
|
162
|
+
program
|
|
163
|
+
.name('tiny-robot-cli')
|
|
164
|
+
.description('CLI to scaffold TinyRobot product projects')
|
|
165
|
+
.showHelpAfterError()
|
|
166
|
+
|
|
167
|
+
program
|
|
168
|
+
.command('create [project-name]')
|
|
169
|
+
.description('Create a TinyRobot project from template')
|
|
170
|
+
.option('-t, --template <name>', 'template name')
|
|
171
|
+
.action((projectName, options) => {
|
|
172
|
+
const skipPrompts = !process.stdout.isTTY
|
|
173
|
+
createProject(projectName ?? '', options.template ?? '', skipPrompts).catch((error) => {
|
|
174
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
|
|
175
|
+
process.exit(1)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
if (process.argv.length <= 2) {
|
|
180
|
+
program.outputHelp()
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
program.parse(process.argv)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
run()
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opentiny/tiny-robot-cli",
|
|
3
|
+
"version": "0.4.2-alpha.0",
|
|
4
|
+
"description": "CLI to scaffold TinyRobot product projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"homepage": "https://docs.opentiny.design/tiny-robot/",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/opentiny/tiny-robot.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/opentiny/tiny-robot/issues"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"tiny-robot-cli": "./bin/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin",
|
|
19
|
+
"templates",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"start": "node ./bin/cli.js"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20.13.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@inquirer/prompts": "^8.3.2",
|
|
34
|
+
"commander": "^14.0.3"
|
|
35
|
+
},
|
|
36
|
+
"gitHead": "9d5703d90bf641988103e5a665b63fcaeb1f9394"
|
|
37
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
TinyRobot AI chat starter built with Vue 3 + Vite.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Vue 3 + Vite + TypeScript project scaffold
|
|
8
|
+
- TinyRobot chat UI with `TrBubbleList`, `TrSender`, and markdown rendering
|
|
9
|
+
- Conversation management via `useConversation`
|
|
10
|
+
- Model switch with thinking/search capability toggles
|
|
11
|
+
- MCP server picker for add/toggle/delete server usage
|
|
12
|
+
- MCP transport support for both `sse` and `streamableHttp`
|
|
13
|
+
- Tool calling pipeline through `toolPlugin` + MCP `listTools` / `callTool`
|
|
14
|
+
- Theme toggle and responsive layout for desktop/mobile
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
1. Copy environment variables:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cp .env.example .env
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
2. Fill your provider keys in `.env`:
|
|
25
|
+
|
|
26
|
+
```env
|
|
27
|
+
VITE_ALIYUN_DASHSCOPE_KEY=your_dashscope_key
|
|
28
|
+
VITE_DEEPSEEK_API_KEY=your_deepseek_key
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`VITE_ALIYUN_DASHSCOPE_KEY` is also used by configured MCP servers that require DashScope authorization.
|
|
32
|
+
|
|
33
|
+
## Development
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpm install
|
|
37
|
+
pnpm dev
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Build
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm build
|
|
44
|
+
pnpm preview
|
|
45
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>TinyRobot AI Chat</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.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 --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@modelcontextprotocol/sdk": "^1.18.1",
|
|
14
|
+
"@opentiny/tiny-robot": "^0.4.1",
|
|
15
|
+
"@opentiny/tiny-robot-kit": "^0.4.1",
|
|
16
|
+
"@opentiny/tiny-robot-svgs": "^0.4.1",
|
|
17
|
+
"dompurify": "^3.3.1",
|
|
18
|
+
"markdown-it": "^14.1.0",
|
|
19
|
+
"vue": "^3.5.30"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^24.12.0",
|
|
23
|
+
"@vitejs/plugin-vue": "^6.0.5",
|
|
24
|
+
"@vue/tsconfig": "^0.9.0",
|
|
25
|
+
"typescript": "~5.9.3",
|
|
26
|
+
"vite": "^8.0.1",
|
|
27
|
+
"vue-tsc": "^3.2.5"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
<script setup lang="ts">
|
|
27
|
+
import { TrThemeProvider as TrTheme } from '@opentiny/tiny-robot'
|
|
28
|
+
import { computed, provide, ref } from 'vue'
|
|
29
|
+
import ChatList from './components/ChatList.vue'
|
|
30
|
+
import ChatSender from './components/ChatSender.vue'
|
|
31
|
+
import ConversationHistory from './components/ConversationHistory.vue'
|
|
32
|
+
import HistoryDrawerButton from './components/HistoryDrawerButton.vue'
|
|
33
|
+
import ThemeToggleButton from './components/ThemeToggleButton.vue'
|
|
34
|
+
import { useChat } from './composables/useChat'
|
|
35
|
+
import { useModel } from './composables/useModel'
|
|
36
|
+
|
|
37
|
+
const { activeConversation, messages } = useChat()
|
|
38
|
+
const { hasApiConfig } = useModel()
|
|
39
|
+
const historyDrawerOpen = ref(false)
|
|
40
|
+
provide('historyDrawerOpen', historyDrawerOpen)
|
|
41
|
+
|
|
42
|
+
const isWelcomePage = computed(() => {
|
|
43
|
+
return messages.value.filter((item) => item.role !== 'system').length === 0
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const currentConversationTitle = computed(() => {
|
|
47
|
+
return activeConversation.value?.title
|
|
48
|
+
})
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<style scoped>
|
|
52
|
+
.app {
|
|
53
|
+
height: 100vh;
|
|
54
|
+
display: flex;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
align-items: stretch;
|
|
57
|
+
padding: 0;
|
|
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
|
+
}
|
|
115
|
+
|
|
116
|
+
@media (max-width: 959px) {
|
|
117
|
+
.chat-header {
|
|
118
|
+
position: relative;
|
|
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;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
</style>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<tr-bubble-provider :fallback-content-renderer="BubbleRenderers.Markdown">
|
|
3
|
+
<tr-welcome
|
|
4
|
+
v-if="visibleMessages.length === 0"
|
|
5
|
+
title="TinyRobot AI 助手"
|
|
6
|
+
description="您好,我是TinyRobot,您专属的 AI 智能专家"
|
|
7
|
+
:icon="welcomeIcon"
|
|
8
|
+
class="chat-list chat-welcome"
|
|
9
|
+
/>
|
|
10
|
+
<tr-bubble-list
|
|
11
|
+
v-else
|
|
12
|
+
:messages="messages"
|
|
13
|
+
:role-configs="roles"
|
|
14
|
+
:auto-scroll="true"
|
|
15
|
+
class="chat-list"
|
|
16
|
+
></tr-bubble-list>
|
|
17
|
+
</tr-bubble-provider>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
import { BubbleRenderers, TrBubbleList, TrBubbleProvider, TrWelcome, type BubbleRoleConfig } from '@opentiny/tiny-robot'
|
|
22
|
+
import { IconAi, IconUser } from '@opentiny/tiny-robot-svgs'
|
|
23
|
+
import { computed, h } from 'vue'
|
|
24
|
+
import { useChat } from '../composables/useChat'
|
|
25
|
+
|
|
26
|
+
const { messages } = useChat()
|
|
27
|
+
|
|
28
|
+
const aiAvatar = h(IconAi, { style: { fontSize: '28px' } })
|
|
29
|
+
const userAvatar = h(IconUser, { style: { fontSize: '28px' } })
|
|
30
|
+
const welcomeIcon = h(IconAi, { style: { fontSize: '40px' } })
|
|
31
|
+
const visibleMessages = computed(() => messages.value.filter((item) => item.role !== 'system'))
|
|
32
|
+
|
|
33
|
+
const roles: Record<string, BubbleRoleConfig> = {
|
|
34
|
+
assistant: {
|
|
35
|
+
placement: 'start',
|
|
36
|
+
avatar: aiAvatar,
|
|
37
|
+
},
|
|
38
|
+
user: {
|
|
39
|
+
placement: 'end',
|
|
40
|
+
avatar: userAvatar,
|
|
41
|
+
},
|
|
42
|
+
system: {
|
|
43
|
+
hidden: true,
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<style scoped>
|
|
49
|
+
.chat-list {
|
|
50
|
+
flex: 1;
|
|
51
|
+
min-height: 0;
|
|
52
|
+
overflow: auto;
|
|
53
|
+
border-radius: 10px;
|
|
54
|
+
padding: 8px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.chat-welcome {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: center;
|
|
61
|
+
margin-bottom: 10%;
|
|
62
|
+
|
|
63
|
+
&.tr-welcome {
|
|
64
|
+
--title-color: var(--tr-text-primary);
|
|
65
|
+
--description-color: var(--tr-text-secondary);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
:deep() {
|
|
70
|
+
[data-box-type='box'][data-role='user'] {
|
|
71
|
+
--tr-bubble-box-bg: var(--tr-color-primary-light);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
[data-box-type='box']:not([data-role='user']) {
|
|
75
|
+
--tr-bubble-box-bg: transparent;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
[data-type='markdown'] p {
|
|
79
|
+
margin: 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
</style>
|