@opentiny/tiny-robot-cli 0.5.2-alpha.11 → 0.5.2-alpha.12

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 CHANGED
@@ -15,14 +15,14 @@ npx @opentiny/tiny-robot-cli add chat
15
15
  - `-t, --template <name>`: template name, currently supports `basic`
16
16
  - `-h, --help`: show help
17
17
 
18
- `add chat` copies the complete Chat example from the `chat-add` template, including its runtime, UI configuration, composables, components, and styles. It updates these dependencies to `0.5.2-alpha.10`:
18
+ `add chat` adds a self-contained floating Chat component, including its runtime, UI configuration, composables, component-scoped styles, and supporting components. It updates these OpenTiny dependencies to `0.5.2-alpha.10`:
19
19
 
20
20
  - `@opentiny/tiny-robot`
21
21
  - `@opentiny/tiny-robot-chat`
22
22
  - `@opentiny/tiny-robot-kit`
23
23
  - `@opentiny/tiny-robot-svgs`
24
24
 
25
- Configure the generated `.env` file with the provider API URL and API key before starting the project.
25
+ It also adds `@vueuse/core@13.9.0`. The command does not modify the application entry file or create `src/index.css`. Configure a private environment file from the generated `.env.example` before starting the project; the generated example only includes the Qwen and DeepSeek API key variables. Their API URL variables remain optional overrides for proxies, private gateways, or other regions. The CLI never creates or modifies `.env`.
26
26
 
27
27
  ## Template Documentation
28
28
 
@@ -1,4 +1,4 @@
1
- import { checkbox, select } from '@inquirer/prompts'
1
+ import { confirm, select } from '@inquirer/prompts'
2
2
  import { Argument } from 'commander'
3
3
  import fs from 'node:fs'
4
4
  import path from 'node:path'
@@ -21,23 +21,13 @@ import {
21
21
  } from '../utils.js'
22
22
 
23
23
  const TARGET_VERSION = '0.5.2-alpha.10'
24
+ const VUEUSE_VERSION = '13.9.0'
24
25
  const DEPENDENCIES = {
25
26
  '@opentiny/tiny-robot': TARGET_VERSION,
26
27
  '@opentiny/tiny-robot-chat': TARGET_VERSION,
27
28
  '@opentiny/tiny-robot-kit': TARGET_VERSION,
28
29
  '@opentiny/tiny-robot-svgs': TARGET_VERSION,
29
- }
30
- const STYLE_IMPORTS = [
31
- "import '@opentiny/tiny-robot/dist/style.css'",
32
- "import '@opentiny/tiny-robot-chat/dist/style.css'",
33
- ]
34
-
35
- function logUnavailable(label) {
36
- logSkip(`${label} could not be applied`)
37
- }
38
-
39
- function logSkippedSelection(label) {
40
- logSkip(`${label} change was not selected`)
30
+ '@vueuse/core': VUEUSE_VERSION,
41
31
  }
42
32
 
43
33
  async function resolveTargetPackage(cwd) {
@@ -51,7 +41,6 @@ async function resolveTargetPackage(cwd) {
51
41
  }
52
42
 
53
43
  const workspacePatterns = findWorkspacePackages(workspaceRoot)
54
-
55
44
  const packageDirs = listPackages(workspaceRoot, workspacePatterns)
56
45
 
57
46
  invariant(packageDirs.length > 0, 'no packages found in workspace.')
@@ -72,7 +61,6 @@ async function resolveTargetPackage(cwd) {
72
61
  }
73
62
 
74
63
  console.error('Error: no package.json found.')
75
-
76
64
  process.exit(1)
77
65
  }
78
66
 
@@ -88,62 +76,8 @@ function writePackageJson(pkgPath, pkg) {
88
76
  fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`)
89
77
  }
90
78
 
91
- function findMainEntry(targetDir) {
92
- for (const file of ['src/main.ts', 'src/main.js']) {
93
- const fullPath = path.join(targetDir, file)
94
-
95
- if (fs.existsSync(fullPath)) {
96
- return fullPath
97
- }
98
- }
99
-
100
- return null
101
- }
102
-
103
- function ensureStyleImport(mainFile) {
104
- const content = fs.readFileSync(mainFile, 'utf-8')
105
- const lines = content.split('\n')
106
- let inserted = false
107
-
108
- for (const styleImport of STYLE_IMPORTS) {
109
- if (lines.some((line) => line.trim() === styleImport)) {
110
- continue
111
- }
112
-
113
- let lastImportIndex = -1
114
-
115
- for (let i = 0; i < lines.length; i++) {
116
- if (/^\s*import\s/.test(lines[i])) {
117
- lastImportIndex = i
118
- continue
119
- }
120
-
121
- if (lastImportIndex !== -1) {
122
- break
123
- }
124
- }
125
-
126
- if (lastImportIndex === -1) {
127
- lines.unshift(styleImport)
128
- } else {
129
- lines.splice(lastImportIndex + 1, 0, styleImport)
130
- }
131
-
132
- inserted = true
133
- }
134
-
135
- if (inserted) {
136
- fs.writeFileSync(mainFile, lines.join('\n'))
137
- }
138
-
139
- return {
140
- type: inserted ? 'inserted' : 'skipped',
141
- }
142
- }
143
-
144
79
  function insertDependencyOrdered(dependencies, name, version) {
145
80
  const next = {}
146
-
147
81
  let inserted = false
148
82
 
149
83
  for (const [key, value] of Object.entries(dependencies)) {
@@ -167,7 +101,6 @@ function ensureDependency(pkg, name, targetVersion) {
167
101
 
168
102
  const currentVersion = pkg.dependencies[name]
169
103
 
170
- // 1. 不存在 => 新增(使用 ordered insert)
171
104
  if (!currentVersion) {
172
105
  pkg.dependencies = insertDependencyOrdered(pkg.dependencies, name, targetVersion)
173
106
 
@@ -177,22 +110,19 @@ function ensureDependency(pkg, name, targetVersion) {
177
110
  }
178
111
  }
179
112
 
180
- // 2. 已存在 => 只更新版本,不调整顺序
181
- const current = semver.valid(currentVersion)
182
-
183
- if (current !== targetVersion) {
184
- pkg.dependencies[name] = targetVersion
185
-
113
+ if (
114
+ currentVersion.startsWith('workspace:') ||
115
+ semver.satisfies(targetVersion, currentVersion, { includePrerelease: true })
116
+ ) {
186
117
  return {
187
- type: 'updated',
188
- from: currentVersion,
189
- to: targetVersion,
118
+ type: 'skipped',
190
119
  }
191
120
  }
192
121
 
193
- // 3. 完全一致 => 跳过
194
122
  return {
195
- type: 'skipped',
123
+ type: 'conflict',
124
+ from: currentVersion,
125
+ to: targetVersion,
196
126
  }
197
127
  }
198
128
 
@@ -202,278 +132,168 @@ function printDependencyResult(result, name) {
202
132
  logSuccess(`Added ${name}@${result.to}`)
203
133
  break
204
134
 
205
- case 'updated':
206
- logSuccess(`Updated ${name} from ${result.from} to ${result.to}`)
207
- break
208
-
209
135
  case 'skipped':
210
136
  logSkip(`${name} already satisfies required version`)
211
137
  break
212
138
  }
213
139
  }
214
140
 
215
- function getChatFeatureFiles(targetDir) {
216
- const mainEntry = findMainEntry(targetDir)
141
+ function getGeneratedFiles(targetDir) {
217
142
  const templateSource = getTemplateFile('src')
143
+ const supportTarget = path.join(targetDir, 'src/tiny-robot-chat')
218
144
 
219
145
  return [
220
146
  {
221
147
  label: 'TinyRobotChat.vue',
148
+ source: path.join(templateSource, 'TinyRobotChat.vue'),
222
149
  target: path.join(targetDir, 'src/TinyRobotChat.vue'),
223
- template: getTemplateFile('src', 'TinyRobotChat.vue'),
224
- action(fileExists) {
225
- return fileExists ? 'overwrite' : 'create'
226
- },
227
- },
228
- {
229
- label: 'main entry style imports',
230
- target: mainEntry,
231
- action() {
232
- return 'modify'
233
- },
150
+ type: 'file',
234
151
  },
235
152
  {
236
153
  label: 'Chat UI components',
237
- target: path.join(targetDir, 'src/components'),
238
- template: path.join(templateSource, 'components'),
239
- action() {
240
- return 'merge'
241
- },
154
+ source: path.join(templateSource, 'tiny-robot-chat/components'),
155
+ target: path.join(supportTarget, 'components'),
156
+ type: 'directory',
242
157
  },
243
158
  {
244
159
  label: 'Chat composables',
245
- target: path.join(targetDir, 'src/composables'),
246
- template: path.join(templateSource, 'composables'),
247
- action() {
248
- return 'merge'
249
- },
160
+ source: path.join(templateSource, 'tiny-robot-chat/composables'),
161
+ target: path.join(supportTarget, 'composables'),
162
+ type: 'directory',
250
163
  },
251
164
  {
252
165
  label: 'Chat configuration',
253
- target: path.join(targetDir, 'src/config'),
254
- template: path.join(templateSource, 'config'),
255
- action() {
256
- return 'merge'
257
- },
258
- },
259
- {
260
- label: 'Chat app styles',
261
- target: path.join(targetDir, 'src/index.css'),
262
- template: path.join(templateSource, 'index.css'),
263
- action(fileExists) {
264
- return fileExists ? 'overwrite' : 'create'
265
- },
266
- },
267
- {
268
- label: '.env',
269
- target: path.join(targetDir, '.env'),
270
- template: getTemplateFile('.env.example'),
271
- action(fileExists) {
272
- return fileExists ? 'modify' : 'create'
273
- },
274
- },
275
-
276
- {
277
- label: 'package.json',
278
- target: path.join(targetDir, 'package.json'),
279
- action() {
280
- return 'modify'
281
- },
166
+ source: path.join(templateSource, 'tiny-robot-chat/config'),
167
+ target: path.join(supportTarget, 'config'),
168
+ type: 'directory',
282
169
  },
283
170
  ]
284
171
  }
285
172
 
286
- async function selectFileChanges(files) {
287
- return checkbox({
288
- message: 'Select which file changes to apply (all selected by default):',
289
- choices: files.map((file) => {
290
- const exists = file.target ? fs.existsSync(file.target) : false
291
-
292
- const disabled = !file.target
293
-
294
- const descriptions = {
295
- 'TinyRobotChat.vue': 'integrate TinyRobot chat component',
296
- 'main entry style imports': 'import TinyRobot styles',
297
- 'Chat UI components': 'copy Chat UI components',
298
- 'Chat composables': 'copy Chat composables',
299
- 'Chat configuration': 'copy Chat runtime and UI configuration',
300
- 'Chat app styles': 'copy Chat application styles',
301
- '.env': 'add environment variables',
302
- 'package.json': 'add TinyRobot dependencies',
303
- }
304
-
305
- const description = descriptions[file.label]
173
+ function findCopyConflicts(source, target) {
174
+ if (!fs.existsSync(target)) {
175
+ return []
176
+ }
306
177
 
307
- return {
308
- name: disabled
309
- ? `${file.action(false)} ${file.label} — ${description} (main.ts/js not found)`
310
- : `${file.action(exists)} ${file.label} — ${description}`,
178
+ const sourceStat = fs.statSync(source)
179
+ const targetStat = fs.statSync(target)
311
180
 
312
- value: file.label,
181
+ if (!sourceStat.isDirectory()) {
182
+ return [target]
183
+ }
313
184
 
314
- checked: !disabled,
185
+ if (!targetStat.isDirectory()) {
186
+ return [target]
187
+ }
315
188
 
316
- disabled,
317
- }
318
- }),
189
+ return fs.readdirSync(source, { withFileTypes: true }).flatMap((entry) => {
190
+ return findCopyConflicts(path.join(source, entry.name), path.join(target, entry.name))
319
191
  })
320
192
  }
321
193
 
322
- function isSelected(selectedFiles, label) {
323
- return selectedFiles.includes(label)
324
- }
194
+ export function applyChatFeature(targetDir) {
195
+ const pkgPath = path.join(targetDir, 'package.json')
325
196
 
326
- async function addFeature(targetDir, type) {
327
- invariant(type === 'chat', `unsupported feature: ${type}`)
197
+ invariant(fs.existsSync(pkgPath), 'package.json not found.')
328
198
 
329
- const files = getChatFeatureFiles(targetDir)
199
+ const generatedFiles = getGeneratedFiles(targetDir)
200
+ const conflicts = generatedFiles.flatMap((file) => findCopyConflicts(file.source, file.target))
330
201
 
331
- const selectedFiles = await selectFileChanges(files)
202
+ if (conflicts.length > 0) {
203
+ const conflictPaths = conflicts.map((file) => path.relative(targetDir, file)).join(', ')
332
204
 
333
- if (selectedFiles.length === 0) {
334
- logSkip('No changes selected.')
335
- return
205
+ throw new Error(`Chat feature files already exist: ${conflictPaths}`)
336
206
  }
337
207
 
338
- const componentFile = files.find((f) => {
339
- return f.label === 'TinyRobotChat.vue'
340
- })
341
-
342
- const envFile = files.find((f) => {
343
- return f.label === '.env'
344
- })
345
-
346
- invariant(componentFile, 'TinyRobotChat.vue config missing.')
347
-
348
- invariant(envFile, '.env config missing.')
349
-
350
- console.log('\nChange Results\n')
351
-
352
- let needsManualStyleImport = false
353
- let envChanged = false
354
- let dependencyChanged = false
208
+ const pkg = readPackageJson(pkgPath)
209
+ const dependencyResults = []
355
210
 
356
- if (isSelected(selectedFiles, 'TinyRobotChat.vue')) {
357
- copyFile(componentFile.template, componentFile.target)
358
- logSuccess(`Copied ${componentFile.label}`)
359
- } else {
360
- logSkippedSelection(componentFile.label)
211
+ for (const [name, version] of Object.entries(DEPENDENCIES)) {
212
+ dependencyResults.push({ name, result: ensureDependency(pkg, name, version) })
361
213
  }
362
214
 
363
- for (const file of files.filter((item) => item.label.startsWith('Chat '))) {
364
- if (!isSelected(selectedFiles, file.label)) {
365
- logSkippedSelection(file.label)
366
- continue
367
- }
215
+ const dependencyConflicts = dependencyResults.filter(({ result }) => result.type === 'conflict')
368
216
 
369
- if (file.target.endsWith('.css')) {
370
- copyFile(file.template, file.target)
371
- } else {
372
- copyDirectory(file.template, file.target)
373
- }
217
+ if (dependencyConflicts.length > 0) {
218
+ const conflictDetails = dependencyConflicts
219
+ .map(({ name, result }) => `${name}: found ${result.from}, required ${result.to}`)
220
+ .join('; ')
374
221
 
375
- logSuccess(`Copied ${file.label}`)
222
+ throw new Error(`Dependency version conflict: ${conflictDetails}`)
376
223
  }
377
224
 
378
- const mainFile = files.find((f) => {
379
- return f.label === 'main entry style imports'
380
- })
381
-
382
- if (!mainFile?.target) {
383
- needsManualStyleImport = true
384
-
385
- logUnavailable('main entry style imports (main.ts/js not found)')
386
- } else {
387
- if (isSelected(selectedFiles, mainFile.label)) {
388
- const result = ensureStyleImport(mainFile.target)
389
-
390
- switch (result.type) {
391
- case 'inserted':
392
- logSuccess('Inserted TinyRobot style import')
393
- break
394
-
395
- case 'skipped':
396
- logSkip('TinyRobot style import already exists')
397
- break
398
- }
225
+ for (const file of generatedFiles) {
226
+ if (file.type === 'file') {
227
+ copyFile(file.source, file.target)
399
228
  } else {
400
- needsManualStyleImport = true
401
-
402
- logSkippedSelection(mainFile.label)
229
+ copyDirectory(file.source, file.target)
403
230
  }
404
231
  }
405
232
 
406
- if (isSelected(selectedFiles, '.env')) {
407
- const envResult = mergeEnvFile(envFile.template, envFile.target)
233
+ const envResult = mergeEnvFile(getTemplateFile('.env.example'), path.join(targetDir, '.env.example'))
408
234
 
409
- switch (envResult.type) {
410
- case 'created':
411
- envChanged = true
235
+ if (dependencyResults.some(({ result }) => result.type !== 'skipped')) {
236
+ writePackageJson(pkgPath, pkg)
237
+ }
412
238
 
413
- logSuccess('Created .env')
414
- break
239
+ return {
240
+ generatedFiles,
241
+ envResult,
242
+ dependencyResults,
243
+ }
244
+ }
415
245
 
416
- case 'merged':
417
- envChanged = true
246
+ async function addFeature(targetDir, type) {
247
+ invariant(type === 'chat', `unsupported feature: ${type}`)
418
248
 
419
- logSuccess(`Added ${envResult.added} env variables`)
420
- break
249
+ const shouldApply = await confirm({
250
+ message: `Add the complete TinyRobot chat feature to ${targetDir}?`,
251
+ default: true,
252
+ })
421
253
 
422
- case 'skipped':
423
- logSkip('.env already contains required variables')
424
- break
425
- }
426
- } else {
427
- logSkippedSelection('.env')
254
+ if (!shouldApply) {
255
+ logSkip('No changes applied.')
256
+ return
428
257
  }
429
258
 
430
- const pkgPath = path.join(targetDir, 'package.json')
259
+ const result = applyChatFeature(targetDir)
431
260
 
432
- invariant(fs.existsSync(pkgPath), 'package.json not found.')
261
+ console.log('\nChange Results\n')
433
262
 
434
- const pkg = readPackageJson(pkgPath)
263
+ for (const file of result.generatedFiles) {
264
+ logSuccess(`Copied ${file.label}`)
265
+ }
435
266
 
436
- if (isSelected(selectedFiles, 'package.json')) {
437
- for (const [name, version] of Object.entries(DEPENDENCIES)) {
438
- const result = ensureDependency(pkg, name, version)
267
+ switch (result.envResult.type) {
268
+ case 'created':
269
+ logSuccess('Created .env.example')
270
+ break
439
271
 
440
- if (result.type !== 'skipped') {
441
- dependencyChanged = true
442
- }
272
+ case 'merged':
273
+ logSuccess(`Added ${result.envResult.added} variables to .env.example`)
274
+ break
443
275
 
444
- printDependencyResult(result, name)
445
- }
276
+ case 'skipped':
277
+ logSkip('.env.example already contains required variables')
278
+ break
279
+ }
446
280
 
447
- writePackageJson(pkgPath, pkg)
448
- } else {
449
- logSkippedSelection('package.json')
281
+ for (const { name, result: dependencyResult } of result.dependencyResults) {
282
+ printDependencyResult(dependencyResult, name)
450
283
  }
451
284
 
452
285
  console.log(`\nSuccessfully added "${type}" feature to ${targetDir}`)
453
286
 
454
287
  printNextSteps({
455
- needsManualStyleImport,
456
- envChanged,
457
- dependencyChanged,
288
+ envChanged: result.envResult.type !== 'skipped',
289
+ dependencyChanged: result.dependencyResults.some(({ result: dependencyResult }) => {
290
+ return dependencyResult.type !== 'skipped'
291
+ }),
458
292
  })
459
293
  }
460
294
 
461
- function printNextSteps({ needsManualStyleImport, envChanged, dependencyChanged }) {
462
- const steps = []
463
-
464
- if (needsManualStyleImport) {
465
- steps.push(
466
- [
467
- 'Import TinyRobot styles in your application entry file.',
468
- '',
469
- 'Example:',
470
- '',
471
- ...STYLE_IMPORTS.map((styleImport) => ` ${styleImport}`),
472
- ].join('\n'),
473
- )
474
- }
475
-
476
- steps.push(
295
+ function printNextSteps({ envChanged, dependencyChanged }) {
296
+ const steps = [
477
297
  [
478
298
  'Render <TinyRobotChat /> near your main application component.',
479
299
  '',
@@ -488,12 +308,12 @@ function printNextSteps({ needsManualStyleImport, envChanged, dependencyChanged
488
308
  ' <TinyRobotChat />',
489
309
  ' </template>',
490
310
  ].join('\n'),
491
- )
311
+ ]
492
312
 
493
313
  if (envChanged) {
494
314
  steps.push(
495
315
  [
496
- 'Configure your AI provider API key in the .env file.',
316
+ 'Copy the required variables from .env.example to your private environment file and configure a provider key.',
497
317
  '',
498
318
  'Example:',
499
319
  '',
@@ -506,10 +326,6 @@ function printNextSteps({ needsManualStyleImport, envChanged, dependencyChanged
506
326
  steps.push(['Install or update project dependencies.', '', 'Example:', '', ' pnpm install'].join('\n'))
507
327
  }
508
328
 
509
- if (steps.length === 0) {
510
- return
511
- }
512
-
513
329
  console.log('\nNext Steps\n')
514
330
 
515
331
  for (const [index, step] of steps.entries()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentiny/tiny-robot-cli",
3
- "version": "0.5.2-alpha.11",
3
+ "version": "0.5.2-alpha.12",
4
4
  "description": "CLI to scaffold TinyRobot product projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,7 +19,8 @@
19
19
  "README.md"
20
20
  ],
21
21
  "scripts": {
22
- "start": "node ./bin/cli.js"
22
+ "start": "node ./bin/cli.js",
23
+ "test": "node --test ./test/*.test.js"
23
24
  },
24
25
  "license": "MIT",
25
26
  "engines": {
@@ -32,5 +33,5 @@
32
33
  "semver": "^7.8.1",
33
34
  "yaml": "^2.9.0"
34
35
  },
35
- "gitHead": "09317e7ea1f51d2b8b6a97ebb7b2ceee9be4a963"
36
+ "gitHead": "dc033027731d2d5b863529cc65b5314a1daa33ff"
36
37
  }
@@ -1,5 +1,2 @@
1
- VITE_DEEPSEEK_API_KEY=
2
- VITE_DEEPSEEK_API_URL=
3
- VITE_QWEN_API_URL=
4
1
  VITE_ALIYUN_DASHSCOPE_KEY=
5
- VITE_AMAP_MCP_URL=
2
+ VITE_DEEPSEEK_API_KEY=
@@ -1,6 +1,6 @@
1
1
  # chat-add
2
2
 
3
- `chat-add` 是一个独立的 Chat 扩展模板,提供通用工作助手场景的最小工程结构。
3
+ `chat-add` 是一个可嵌入现有 Vue 应用的 Chat 扩展模板,提供通用工作助手场景的最小工程结构。
4
4
 
5
5
  ## 启动
6
6
 
@@ -17,19 +17,21 @@ pnpm --filter chat-add build
17
17
 
18
18
  ## 接入真实模型
19
19
 
20
- 在 `packages/chat-add/.env.local` 中配置 `VITE_QWEN_API_URL`、`VITE_ALIYUN_DASHSCOPE_KEY`、`VITE_DEEPSEEK_API_URL` 和 `VITE_DEEPSEEK_API_KEY`,重启 Vite 后即可使用模型选择器中的真实模型。配置读取逻辑位于 `src/config/chat-runtime.ts`。
20
+ 将生成的 `.env.example` 复制为 `.env.local`,按需配置 `VITE_ALIYUN_DASHSCOPE_KEY` 和 `VITE_DEEPSEEK_API_KEY`,重启 Vite 后即可使用模型选择器中的真实模型。
21
+
22
+ Qwen 和 DeepSeek 默认使用 `@opentiny/tiny-robot-chat` 内置的官方接口地址。仅在使用代理、私有网关或其他地域接口时,才需要在 `.env.local` 中额外配置 `VITE_QWEN_API_URL` 或 `VITE_DEEPSEEK_API_URL`。配置读取逻辑位于 `src/tiny-robot-chat/config/chat-runtime.ts`。
21
23
 
22
24
  ## 目录职责
23
25
 
24
- - `src/config/chat-ui.ts`: Chat UI、提示词、模板和菜单配置。
25
- - `src/config/chat-runtime.ts`: MCP 示例和模型配置。
26
- - `src/components`: 窗口头部和输入区工具。
27
- - `src/composables/useWindow.ts`: floating、fullscreen 窗口状态。
28
- - `src/index.css`: 模板公共样式和 Surface 样式变量。
26
+ - `src/TinyRobotChat.vue`: 悬浮入口、Chat 主体以及组件作用域样式。
27
+ - `src/tiny-robot-chat/config/chat-ui.ts`: Chat UI、提示词和模板配置。
28
+ - `src/tiny-robot-chat/config/chat-runtime.ts`: 模型配置和留给后续集成的空 MCP Server 列表。
29
+ - `src/tiny-robot-chat/components`: 窗口头部和输入区工具。
30
+ - `src/tiny-robot-chat/composables/useWindow.ts`: floating、fullscreen 窗口状态。
29
31
 
30
32
  ## 替换数据
31
33
 
32
- 直接修改 `src/config/chat-ui.ts` 和 `src/config/chat-runtime.ts` 中导出的数据;组件通过 props 使用这些配置。
34
+ 直接修改 `src/tiny-robot-chat/config/chat-ui.ts` 和 `src/tiny-robot-chat/config/chat-runtime.ts` 中导出的数据;组件通过 props 使用这些配置。
33
35
 
34
36
  ## 切换窗口模式
35
37
 
@@ -14,7 +14,7 @@
14
14
  "@opentiny/tiny-robot-chat": "0.5.2-alpha.10",
15
15
  "@opentiny/tiny-robot-kit": "0.5.2-alpha.10",
16
16
  "@opentiny/tiny-robot-svgs": "0.5.2-alpha.10",
17
- "@vueuse/core": "13.1.0",
17
+ "@vueuse/core": "13.9.0",
18
18
  "vue": "^3.5.40"
19
19
  },
20
20
  "devDependencies": {
@@ -1,14 +1,16 @@
1
1
  <script setup lang="ts">
2
2
  import type { HistoryMenuItem, PromptProps, TemplateItem } from '@opentiny/tiny-robot'
3
3
  import { TrSender, TrThemeProvider as TrTheme } from '@opentiny/tiny-robot'
4
+ import '@opentiny/tiny-robot/dist/style.css'
4
5
  import { TrChat, useChatHistoryItems, useLocalChatRuntime, type ChatHistoryItem } from '@opentiny/tiny-robot-chat'
6
+ import '@opentiny/tiny-robot-chat/dist/style.css'
5
7
  import { IconAi } from '@opentiny/tiny-robot-svgs'
6
8
  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'
9
+ import ComposerTools from './tiny-robot-chat/components/ComposerTools.vue'
10
+ import WindowHeader from './tiny-robot-chat/components/WindowHeader.vue'
11
+ import { createChatUi, templateCategories } from './tiny-robot-chat/config/chat-ui'
12
+ import { mcpServers, modelProviders } from './tiny-robot-chat/config/chat-runtime'
13
+ import { useWindow } from './tiny-robot-chat/composables/useWindow'
12
14
 
13
15
  const runtime = useLocalChatRuntime({ modelProviders, mcpServers })
14
16
  const window = reactive(useWindow())
@@ -45,52 +47,130 @@ function handleHistoryAction(action: HistoryMenuItem, item: ChatHistoryItem): vo
45
47
 
46
48
  <template>
47
49
  <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>
50
+ <button
51
+ v-if="!window.show"
52
+ class="chat-add-launcher"
53
+ type="button"
54
+ title="打开 TinyRobot"
55
+ aria-label="打开 TinyRobot"
56
+ @click="window.open"
57
+ >
58
+ <IconAi style="font-size: 32px" />
59
+ </button>
60
+ <TrChat
61
+ v-if="window.show"
62
+ ref="chatRef"
63
+ :class="['chat-add-window', `chat-add-window--${window.displayMode}`]"
64
+ :style="window.layoutStyle"
65
+ :runtime="runtime"
66
+ :ui="chatUi"
67
+ v-model:floating-state="window.floatingState"
68
+ @prompt-click="handlePromptClick"
69
+ >
70
+ <template #layout-header>
71
+ <WindowHeader
72
+ :display-mode="window.displayMode"
73
+ :show-history="showHistory"
74
+ :history-data="historyData"
75
+ :active-conversation-id="activeConversationId"
76
+ @new-session="handleNewSession"
77
+ @select-history="handleHistorySelect"
78
+ @rename-history="handleHistoryRename"
79
+ @history-action="handleHistoryAction"
80
+ @update:show-history="showHistory = $event"
81
+ @change-mode="window.setDisplayMode"
82
+ @close="window.close"
83
+ />
84
+ </template>
85
+ <template #composer-before>
86
+ <ComposerTools v-model:current-template="currentTemplate" :template-categories="templateCategories" />
87
+ </template>
88
+ </TrChat>
95
89
  </TrTheme>
96
90
  </template>
91
+
92
+ <style scoped>
93
+ .chat-add-launcher {
94
+ position: fixed;
95
+ right: 24px;
96
+ bottom: 24px;
97
+ z-index: var(--tr-z-index-popover, 1000);
98
+ display: inline-flex;
99
+ align-items: center;
100
+ justify-content: center;
101
+ width: 56px;
102
+ height: 56px;
103
+ padding: 0;
104
+ border: 0;
105
+ border-radius: 50%;
106
+ background: var(--tr-container-bg-default);
107
+ color: var(--tr-color-primary);
108
+ box-shadow: 0 8px 24px rgb(0 0 0 / 20%);
109
+ cursor: pointer;
110
+ }
111
+ .chat-add-launcher:hover {
112
+ background: var(--tr-color-primary-light);
113
+ }
114
+ .chat-add-launcher:focus-visible {
115
+ outline: 2px solid var(--tr-color-primary);
116
+ outline-offset: 3px;
117
+ }
118
+ @media (max-width: 519px) {
119
+ .chat-add-launcher {
120
+ right: 16px;
121
+ bottom: 16px;
122
+ }
123
+ }
124
+ </style>
125
+
126
+ <style>
127
+ .chat-add-window {
128
+ --chat-prompt-min-width: 280px;
129
+ --tr-layout-bg-default: #f5f5f7;
130
+ --tr-chat-ui-header-bg: #f5f5f7;
131
+ --tr-chat-ui-main-bg: #f5f5f7;
132
+ --tr-chat-ui-footer-bg: #f5f5f7;
133
+ min-width: 0;
134
+ }
135
+ .chat-add-window .tr-welcome {
136
+ box-sizing: border-box;
137
+ width: 100%;
138
+ padding: 24px 24px 0;
139
+ }
140
+ .chat-add-window .tr-welcome__title-wrapper {
141
+ display: flex;
142
+ align-items: center;
143
+ justify-content: center;
144
+ }
145
+ .chat-add-window .tr-prompts {
146
+ --tr-prompts-gap: 16px;
147
+ box-sizing: border-box;
148
+ width: 100%;
149
+ padding: 16px 24px;
150
+ }
151
+ .chat-add-window .tr-prompts .tr-prompts__list-container.wrap {
152
+ display: grid;
153
+ grid-template-columns: repeat(2, minmax(var(--chat-prompt-min-width), 1fr));
154
+ }
155
+ .chat-add-window .tr-prompts__list-container > .prompt-item {
156
+ box-sizing: border-box;
157
+ width: 100%;
158
+ min-width: 0;
159
+ }
160
+ @container (max-width: 623px) {
161
+ .chat-add-window .tr-prompts .tr-prompts__list-container.wrap {
162
+ grid-template-columns: minmax(0, 1fr);
163
+ }
164
+ }
165
+ @container (max-width: 519px) {
166
+ .chat-add-window .tr-welcome {
167
+ padding-inline: 12px;
168
+ }
169
+ .chat-add-window .tr-prompts {
170
+ padding-inline: 12px;
171
+ }
172
+ .chat-add-window .tr-bubble__avatar {
173
+ display: none;
174
+ }
175
+ }
176
+ </style>
@@ -1,7 +1,4 @@
1
1
  import { createApp } from 'vue'
2
- import '@opentiny/tiny-robot/dist/style.css'
3
- import '@opentiny/tiny-robot-chat/dist/style.css'
4
- import './index.css'
5
2
  import TinyRobotChat from './TinyRobotChat.vue'
6
3
 
7
4
  createApp(TinyRobotChat).mount('#app')
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ import type { TemplateItem } from '@opentiny/tiny-robot'
3
+ import { TrDropdownMenu, TrSuggestionPillButton, TrSuggestionPills } from '@opentiny/tiny-robot'
4
+ import type { TemplateCategory, TemplateSuggestionItem } from '../config/chat-ui'
5
+ import { computed } from 'vue'
6
+
7
+ const props = defineProps<{
8
+ templateCategories: readonly TemplateCategory[]
9
+ }>()
10
+
11
+ const currentTemplate = defineModel<TemplateItem[]>('currentTemplate', { required: true })
12
+ const templateMenus = computed(() =>
13
+ props.templateCategories.map((category) => ({
14
+ ...category,
15
+ items: category.items.map((item) => ({ id: item.id, text: item.text, template: item })),
16
+ })),
17
+ )
18
+ const overlayTarget = '.chat-add-window'
19
+
20
+ function fillTemplate(template: TemplateSuggestionItem): void {
21
+ currentTemplate.value = template.template.map((item) => ({ ...item }))
22
+ }
23
+
24
+ function handleTemplateSelect(item: unknown): void {
25
+ fillTemplate((item as { template: TemplateSuggestionItem }).template)
26
+ }
27
+ </script>
28
+
29
+ <template>
30
+ <div class="chat-add-tools">
31
+ <TrSuggestionPills class="chat-add-tools__pills">
32
+ <TrDropdownMenu
33
+ v-for="category in templateMenus"
34
+ :key="category.id"
35
+ :append-to="overlayTarget"
36
+ :items="category.items as never"
37
+ trigger="click"
38
+ @item-click="handleTemplateSelect"
39
+ >
40
+ <template #trigger
41
+ ><TrSuggestionPillButton>{{ category.title }}</TrSuggestionPillButton></template
42
+ >
43
+ </TrDropdownMenu>
44
+ </TrSuggestionPills>
45
+ </div>
46
+ </template>
47
+
48
+ <style scoped>
49
+ .chat-add-tools {
50
+ display: flex;
51
+ box-sizing: border-box;
52
+ width: 100%;
53
+ min-width: 0;
54
+ flex-shrink: 0;
55
+ align-items: center;
56
+ gap: 8px;
57
+ padding: 8px 12px;
58
+ }
59
+ .chat-add-tools__pills {
60
+ min-width: 0;
61
+ flex: 1;
62
+ }
63
+ .chat-add-tools__pills :deep(.tr-suggestion-pills__container) {
64
+ min-width: 0;
65
+ mask: linear-gradient(to right, rgb(0 0 0 / 100%) 80%, rgb(0 0 0 / 0%) 100%);
66
+ }
67
+ @container (max-width: 519px) {
68
+ .chat-add-tools {
69
+ padding: 8px;
70
+ }
71
+ }
72
+ </style>
@@ -4,24 +4,34 @@ import { computed, shallowRef, watch } from 'vue'
4
4
 
5
5
  export type DisplayMode = 'floating' | 'fullscreen'
6
6
 
7
+ const FLOATING_GAP = 24
8
+ const FLOATING_MIN_WIDTH = 360
9
+ const FLOATING_MIN_HEIGHT = 480
10
+
7
11
  export function useWindow() {
8
12
  const show = shallowRef(false)
9
13
  const displayMode = shallowRef<DisplayMode>('floating')
14
+ const { width, height } = useWindowSize()
10
15
  const floatingState = shallowRef<LayoutFloatingState>({
11
- placement: 'center',
12
- offsetX: 24,
13
- offsetY: 24,
14
- width: 640,
15
- height: 760,
16
+ placement: 'top-right',
17
+ offsetX: FLOATING_GAP,
18
+ offsetY: FLOATING_GAP,
19
+ width: 500,
20
+ height: Math.max(1, height.value - FLOATING_GAP * 2),
16
21
  })
17
22
  const restoreFloatingState = shallowRef<LayoutFloatingState | null>(null)
18
- const { width, height } = useWindowSize()
19
23
 
20
24
  const floatingOptions = computed<LayoutFloatingOptions>(() => ({
21
25
  draggable: displayMode.value === 'floating',
22
26
  resizable: displayMode.value === 'floating',
23
- minWidth: displayMode.value === 'floating' ? 360 : undefined,
24
- minHeight: displayMode.value === 'floating' ? 480 : undefined,
27
+ minWidth:
28
+ displayMode.value === 'floating'
29
+ ? Math.min(FLOATING_MIN_WIDTH, Math.max(1, width.value - FLOATING_GAP * 2))
30
+ : undefined,
31
+ minHeight:
32
+ displayMode.value === 'floating'
33
+ ? Math.min(FLOATING_MIN_HEIGHT, Math.max(1, height.value - FLOATING_GAP * 2))
34
+ : undefined,
25
35
  }))
26
36
 
27
37
  const layoutStyle = computed(() => ({
@@ -0,0 +1,26 @@
1
+ import type { ChatMcpServers, ChatProviderConfig } from '@opentiny/tiny-robot-chat'
2
+
3
+ export const mcpServers: ChatMcpServers = []
4
+
5
+ export const modelProviders: ChatProviderConfig[] = [
6
+ {
7
+ type: 'qwen',
8
+ label: 'DashScope',
9
+ apiUrl: import.meta.env.VITE_QWEN_API_URL?.trim() || undefined,
10
+ apiKey: import.meta.env.VITE_ALIYUN_DASHSCOPE_KEY?.trim(),
11
+ models: [
12
+ { id: 'qwen3.7-flash', label: 'Qwen3.7 Flash', capabilities: { thinking: true, search: true } },
13
+ { id: 'qwen3.7-plus', label: 'Qwen3.7 Plus', capabilities: { thinking: true, search: true } },
14
+ { id: 'qwen3.7-max', label: 'Qwen3.7 Max', capabilities: { thinking: true, search: true } },
15
+ ],
16
+ },
17
+ {
18
+ type: 'deepseek',
19
+ apiUrl: import.meta.env.VITE_DEEPSEEK_API_URL?.trim() || undefined,
20
+ apiKey: import.meta.env.VITE_DEEPSEEK_API_KEY?.trim(),
21
+ models: [
22
+ { id: 'deepseek-v4-flash', label: 'DeepSeek V4 Flash', capabilities: { thinking: true } },
23
+ { id: 'deepseek-v4-pro', label: 'DeepSeek V4 Pro', capabilities: { thinking: true } },
24
+ ],
25
+ },
26
+ ]
@@ -1,7 +1,7 @@
1
1
  import type { LayoutFloatingOptions } from '@opentiny/tiny-robot'
2
2
  import type { ChatSenderOptions, ChatUIOptions } from '@opentiny/tiny-robot-chat'
3
3
  import type { TemplateItem } from '@opentiny/tiny-robot'
4
- import { IconAi, IconEdit, IconHistory, IconNewSession, IconSparkles, IconUser } from '@opentiny/tiny-robot-svgs'
4
+ import { IconAi, IconUser } from '@opentiny/tiny-robot-svgs'
5
5
  import { h, type CSSProperties } from 'vue'
6
6
 
7
7
  export interface ChatUiConfigOptions {
@@ -9,24 +9,6 @@ export interface ChatUiConfigOptions {
9
9
  templateExtensions: NonNullable<ChatSenderOptions['extensions']>
10
10
  }
11
11
 
12
- export interface ChatMenuItem {
13
- id: string
14
- title: string
15
- icon: unknown
16
- action: 'mcp' | 'template' | 'history' | 'new-session' | 'mode'
17
- }
18
-
19
- export const composerMenus: ChatMenuItem[] = [
20
- { id: 'quick-query', title: '常用查询', icon: IconSparkles, action: 'mcp' },
21
- { id: 'templates', title: '模板', icon: IconEdit, action: 'template' },
22
- ]
23
-
24
- export const windowMenus: ChatMenuItem[] = [
25
- { id: 'new-session', title: '新会话', icon: IconNewSession, action: 'new-session' },
26
- { id: 'history', title: '历史会话', icon: IconHistory, action: 'history' },
27
- { id: 'mode', title: '窗口模式', icon: IconSparkles, action: 'mode' },
28
- ]
29
-
30
12
  export interface PromptItemData {
31
13
  label: string
32
14
  description: string
@@ -52,9 +34,9 @@ export const PROMPT_ITEMS_DATA: PromptItemData[] = [
52
34
  emoji: '✨',
53
35
  },
54
36
  {
55
- label: 'MCP 工具调用',
56
- description: '搜索:北京天气(输入「搜索」「MCP」「工具」等关键词可触发模拟 MCP 工具调用)',
57
- emoji: '🔧',
37
+ label: '工作效率场景',
38
+ description: '需要我帮你总结内容、拆解任务,还是整理一份行动清单?',
39
+ emoji: '⚡',
58
40
  },
59
41
  ]
60
42
 
@@ -1,99 +0,0 @@
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>
@@ -1,57 +0,0 @@
1
- import type { ChatMcpServers, ChatProviderConfig } from '@opentiny/tiny-robot-chat'
2
-
3
- export interface McpExample {
4
- id: string
5
- title: string
6
- request: string
7
- }
8
-
9
- export const mcpExamples: McpExample[] = [
10
- { id: 'weather', title: '查询北京天气', request: '查询北京今天的天气,并给出出行建议' },
11
- { id: 'coffee', title: '查询附近咖啡店', request: '查询我附近的咖啡店,并按距离排序' },
12
- { id: 'exchange-rate', title: '获取当前汇率', request: '查询人民币兑美元的当前汇率' },
13
- ]
14
-
15
- export const mcpServers: ChatMcpServers = [
16
- {
17
- id: 'amap-maps',
18
- name: '高德地图',
19
- description: '覆盖地图、导航、地理编码、天气、路径规划、距离测量、关键词搜索和周边搜索等地理信息服务。',
20
- baseUrl: import.meta.env.VITE_AMAP_MCP_URL?.trim() || 'https://dashscope.aliyuncs.com/api/v1/mcps/amap-maps/mcp',
21
- icon: 'https://img.alicdn.com/imgextra/i4/O1CN01iPPabT1EGRN6uatHP_!!6000000000324-0-tps-512-512.jpg',
22
- headers: import.meta.env.VITE_ALIYUN_DASHSCOPE_KEY?.trim()
23
- ? { Authorization: `Bearer ${import.meta.env.VITE_ALIYUN_DASHSCOPE_KEY.trim()}` }
24
- : undefined,
25
- },
26
- {
27
- id: 'model-context-protocol-mcp',
28
- name: 'Model Context Protocol MCP',
29
- description: 'MCP Server',
30
- baseUrl: '/modelcontextprotocol-mcp',
31
- icon: '/modelcontextprotocol.png',
32
- installed: true,
33
- },
34
- ]
35
-
36
- export const modelProviders: ChatProviderConfig[] = [
37
- {
38
- type: 'qwen',
39
- label: 'DashScope',
40
- apiUrl: import.meta.env.VITE_QWEN_API_URL?.trim() || undefined,
41
- apiKey: import.meta.env.VITE_ALIYUN_DASHSCOPE_KEY?.trim(),
42
- models: [
43
- { id: 'qwen3.7-flash', label: 'Qwen3.7 Flash', capabilities: { thinking: true, search: true } },
44
- { id: 'qwen3.7-plus', label: 'Qwen3.7 Plus', capabilities: { thinking: true, search: true } },
45
- { id: 'qwen3.7-max', label: 'Qwen3.7 Max', capabilities: { thinking: true, search: true } },
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
- { id: 'deepseek-v4-flash', label: 'DeepSeek V4 Flash', capabilities: { thinking: true } },
54
- { id: 'deepseek-v4-pro', label: 'DeepSeek V4 Pro', capabilities: { thinking: true } },
55
- ],
56
- },
57
- ]
@@ -1,116 +0,0 @@
1
- :root {
2
- font-family:
3
- Inter,
4
- -apple-system,
5
- BlinkMacSystemFont,
6
- 'Segoe UI',
7
- sans-serif;
8
- color: #1f2329;
9
- }
10
- * {
11
- box-sizing: border-box;
12
- }
13
- html,
14
- body,
15
- #app {
16
- width: 100%;
17
- height: 100%;
18
- min-height: 0;
19
- margin: 0;
20
- }
21
- body {
22
- overflow: hidden;
23
- }
24
- .chat-add-app {
25
- position: relative;
26
- container-type: inline-size;
27
- display: flex;
28
- width: 100%;
29
- height: 100%;
30
- min-width: 0;
31
- min-height: 0;
32
- }
33
- .chat-add-launcher {
34
- position: fixed;
35
- right: 24px;
36
- bottom: 24px;
37
- z-index: var(--tr-z-index-popover, 1000);
38
- display: inline-flex;
39
- align-items: center;
40
- justify-content: center;
41
- width: 56px;
42
- height: 56px;
43
- padding: 0;
44
- border: 0;
45
- border-radius: 50%;
46
- background: var(--tr-color-primary);
47
- color: #fff;
48
- box-shadow: 0 8px 24px rgb(0 0 0 / 20%);
49
- cursor: pointer;
50
- }
51
- .chat-add-launcher:hover {
52
- background: var(--tr-color-primary-hover);
53
- }
54
- .chat-add-launcher:focus-visible {
55
- outline: 2px solid var(--tr-color-primary);
56
- outline-offset: 3px;
57
- }
58
- .chat-add-window {
59
- container-type: inline-size;
60
- --tr-layout-bg-default: #f5f5f7;
61
- --tr-chat-ui-header-bg: #f5f5f7;
62
- --tr-chat-ui-main-bg: #f5f5f7;
63
- --tr-chat-ui-footer-bg: #f5f5f7;
64
- --tr-suggestion-popover-width: min(440px, calc(100cqw - 16px));
65
- width: 100%;
66
- min-width: 0;
67
- min-height: 0;
68
- }
69
- .chat-add-window .tr-welcome {
70
- box-sizing: border-box;
71
- width: 100%;
72
- padding: 24px 24px 0;
73
- }
74
- .chat-add-window .tr-welcome__title-wrapper {
75
- display: flex;
76
- align-items: center;
77
- justify-content: center;
78
- }
79
- .chat-add-window .tr-prompts {
80
- --tr-prompts-gap: 16px;
81
- box-sizing: border-box;
82
- width: 100%;
83
- padding: 16px 24px;
84
- }
85
- .chat-add-window .prompt-item {
86
- flex: 0 0 calc((100% - 16px) / 2);
87
- box-sizing: border-box;
88
- min-width: 0;
89
- }
90
- .chat-add-window--floating {
91
- border-radius: var(--tr-layout-floating-radius);
92
- box-shadow: var(--tr-layout-floating-shadow);
93
- overflow: hidden;
94
- }
95
- .chat-add-window--fullscreen {
96
- border-radius: 0;
97
- box-shadow: none;
98
- }
99
- @container (max-width: 519px) {
100
- .chat-add-launcher {
101
- right: 16px;
102
- bottom: 16px;
103
- }
104
- .chat-add-window .prompt-item {
105
- flex-basis: 100%;
106
- }
107
- .chat-add-window .tr-welcome {
108
- padding-inline: 12px;
109
- }
110
- .chat-add-window .tr-prompts {
111
- padding-inline: 12px;
112
- }
113
- .chat-add-window .tr-bubble__avatar {
114
- display: none;
115
- }
116
- }