@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
package/bin/commands/create.js
CHANGED
|
@@ -2,6 +2,7 @@ import { input, select } from '@inquirer/prompts'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
|
|
5
|
+
import { resolveRuntimeVersion } from '../runtime-version.js'
|
|
5
6
|
import {
|
|
6
7
|
DEFAULT_PROJECT_NAME,
|
|
7
8
|
DEFAULT_TEMPLATE,
|
|
@@ -119,12 +120,13 @@ function printCreateSuccess(projectName) {
|
|
|
119
120
|
console.log()
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
async function createProject(initialProjectName, initialTemplateName, skipPrompt) {
|
|
123
|
+
async function createProject(initialProjectName, initialTemplateName, runtimeVersion, skipPrompt) {
|
|
124
|
+
const runtime = resolveRuntimeVersion(runtimeVersion)
|
|
123
125
|
const options = await resolveCreateOptions(initialProjectName, initialTemplateName, skipPrompt)
|
|
124
126
|
|
|
125
127
|
const { templateDir, targetDir } = validateCreateOptions(options)
|
|
126
128
|
|
|
127
|
-
scaffoldProject(templateDir, targetDir, options.projectName)
|
|
129
|
+
scaffoldProject(templateDir, targetDir, options.projectName, runtime.specifier)
|
|
128
130
|
|
|
129
131
|
printCreateSuccess(options.projectName)
|
|
130
132
|
}
|
|
@@ -134,10 +136,11 @@ export function registerCreateCommand(program) {
|
|
|
134
136
|
.command('create [project-name]')
|
|
135
137
|
.description('Create a TinyRobot project from template')
|
|
136
138
|
.option('-t, --template <name>', 'template name')
|
|
139
|
+
.option('--runtime-version <version>', 'override the TinyRobot runtime version')
|
|
137
140
|
.action((projectName, options) => {
|
|
138
141
|
const skipPrompt = !process.stdout.isTTY
|
|
139
142
|
|
|
140
|
-
createProject(projectName ?? '', options.template ?? '', skipPrompt).catch((error) => {
|
|
143
|
+
createProject(projectName ?? '', options.template ?? '', options.runtimeVersion, skipPrompt).catch((error) => {
|
|
141
144
|
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
142
145
|
console.error('\nOperation cancelled.')
|
|
143
146
|
process.exit(1)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import semver from 'semver'
|
|
4
|
+
|
|
5
|
+
const CLI_PACKAGE_FILE = fileURLToPath(new URL('../package.json', import.meta.url))
|
|
6
|
+
|
|
7
|
+
export function readCliVersion() {
|
|
8
|
+
const packageJson = JSON.parse(fs.readFileSync(CLI_PACKAGE_FILE, 'utf8'))
|
|
9
|
+
return packageJson.version
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatRuntimeSpecifier(version) {
|
|
13
|
+
const parsedVersion = typeof version === 'string' ? semver.parse(version) : null
|
|
14
|
+
const canonicalVersion = parsedVersion
|
|
15
|
+
? `${parsedVersion.version}${parsedVersion.build.length > 0 ? `+${parsedVersion.build.join('.')}` : ''}`
|
|
16
|
+
: null
|
|
17
|
+
if (!parsedVersion || canonicalVersion !== version) {
|
|
18
|
+
throw new Error(`Runtime version must be a valid semantic version: ${String(version)}`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return parsedVersion.prerelease.length > 0 ? version : `^${version}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveRuntimeVersion(override) {
|
|
25
|
+
const version = override ?? readCliVersion()
|
|
26
|
+
return { version, specifier: formatRuntimeSpecifier(version) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createRuntimeDependencies(specifier) {
|
|
30
|
+
return {
|
|
31
|
+
'@opentiny/tiny-robot': specifier,
|
|
32
|
+
'@opentiny/tiny-robot-chat': specifier,
|
|
33
|
+
'@opentiny/tiny-robot-kit': specifier,
|
|
34
|
+
'@opentiny/tiny-robot-svgs': specifier,
|
|
35
|
+
'@vueuse/core': '13.9.0',
|
|
36
|
+
}
|
|
37
|
+
}
|
package/bin/utils.js
CHANGED
|
@@ -5,6 +5,7 @@ import pc from 'picocolors'
|
|
|
5
5
|
import yaml from 'yaml'
|
|
6
6
|
|
|
7
7
|
const TEMPLATE_PLACEHOLDER = '__PROJECT_NAME__'
|
|
8
|
+
const RUNTIME_VERSION_PLACEHOLDER = '__TINY_ROBOT_VERSION__'
|
|
8
9
|
|
|
9
10
|
export const BUILTIN_TEMPLATES = ['basic']
|
|
10
11
|
export const DEFAULT_TEMPLATE = 'basic'
|
|
@@ -36,10 +37,7 @@ export function logError(message) {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export function invariant(condition, message) {
|
|
39
|
-
if (!condition)
|
|
40
|
-
console.error(`Error: ${message}`)
|
|
41
|
-
process.exit(1)
|
|
42
|
-
}
|
|
40
|
+
if (!condition) throw new Error(message)
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
export function exists(file) {
|
|
@@ -88,6 +86,10 @@ export function validateProjectName(name) {
|
|
|
88
86
|
}
|
|
89
87
|
|
|
90
88
|
export function copyTemplate(sourceDir, targetDir) {
|
|
89
|
+
copyDirectory(sourceDir, targetDir)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function copyDirectory(sourceDir, targetDir) {
|
|
91
93
|
fs.cpSync(sourceDir, targetDir, {
|
|
92
94
|
recursive: true,
|
|
93
95
|
filter: (source) => {
|
|
@@ -110,7 +112,7 @@ function renameSpecialFiles(targetDir) {
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
function replaceTemplateVariables(targetDir, variables) {
|
|
113
|
-
const replaceFiles = ['package.json', 'README.md']
|
|
115
|
+
const replaceFiles = ['package.json', 'README.md', 'index.html']
|
|
114
116
|
|
|
115
117
|
for (const relativePath of replaceFiles) {
|
|
116
118
|
const file = path.join(targetDir, relativePath)
|
|
@@ -129,13 +131,14 @@ function replaceTemplateVariables(targetDir, variables) {
|
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
export function scaffoldProject(templateDir, targetDir, projectName) {
|
|
134
|
+
export function scaffoldProject(templateDir, targetDir, projectName, runtimeVersion) {
|
|
133
135
|
copyTemplate(templateDir, targetDir)
|
|
134
136
|
|
|
135
137
|
renameSpecialFiles(targetDir)
|
|
136
138
|
|
|
137
139
|
replaceTemplateVariables(targetDir, {
|
|
138
140
|
[TEMPLATE_PLACEHOLDER]: projectName,
|
|
141
|
+
[RUNTIME_VERSION_PLACEHOLDER]: runtimeVersion,
|
|
139
142
|
})
|
|
140
143
|
}
|
|
141
144
|
|
|
@@ -166,20 +169,24 @@ export function findProjectRoot(cwd) {
|
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
export function findSubPackageRoot(cwd, workspaceRoot) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return false
|
|
172
|
-
}
|
|
172
|
+
let dir = path.resolve(cwd)
|
|
173
|
+
const root = path.resolve(workspaceRoot)
|
|
173
174
|
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
for (;;) {
|
|
176
|
+
if (dir === root) return null
|
|
177
|
+
if (isPackageDir(dir)) return dir
|
|
178
|
+
|
|
179
|
+
const parent = path.dirname(dir)
|
|
180
|
+
if (parent === dir) return null
|
|
181
|
+
dir = parent
|
|
182
|
+
}
|
|
176
183
|
}
|
|
177
184
|
|
|
178
185
|
export function findWorkspacePackages(workspaceRoot) {
|
|
179
186
|
const workspaceFile = resolveWorkspaceFile(workspaceRoot)
|
|
180
187
|
|
|
181
188
|
if (!workspaceFile) {
|
|
182
|
-
return
|
|
189
|
+
return undefined
|
|
183
190
|
}
|
|
184
191
|
|
|
185
192
|
try {
|
|
@@ -187,56 +194,139 @@ export function findWorkspacePackages(workspaceRoot) {
|
|
|
187
194
|
|
|
188
195
|
const config = yaml.parse(content)
|
|
189
196
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
198
|
+
throw new Error('workspace configuration must be a YAML mapping')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!Object.prototype.hasOwnProperty.call(config, 'packages')) {
|
|
202
|
+
return undefined
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!Array.isArray(config.packages)) {
|
|
206
|
+
throw new Error('packages must be an array')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (config.packages.some((pattern) => typeof pattern !== 'string' || pattern.trim().length === 0)) {
|
|
210
|
+
throw new Error('packages entries must be non-empty strings')
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return config.packages.map((pattern) => pattern.trim())
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const detail = error instanceof Error ? `: ${error.message}` : ''
|
|
216
|
+
throw new Error(`Failed to read ${path.basename(workspaceFile)}${detail}`)
|
|
197
217
|
}
|
|
198
218
|
}
|
|
199
219
|
|
|
220
|
+
const WORKSPACE_SCAN_IGNORES = new Set(['.git', 'node_modules'])
|
|
221
|
+
|
|
200
222
|
function normalizeWorkspacePattern(pattern) {
|
|
201
|
-
|
|
223
|
+
let normalized = pattern.replaceAll('\\', '/').trim()
|
|
224
|
+
if (normalized.startsWith('./')) normalized = normalized.slice(2)
|
|
225
|
+
return normalized.replace(/\/$/, '')
|
|
202
226
|
}
|
|
203
227
|
|
|
204
|
-
|
|
205
|
-
|
|
228
|
+
function globSource(pattern) {
|
|
229
|
+
let source = ''
|
|
206
230
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
packageDirs.push(dir)
|
|
210
|
-
}
|
|
211
|
-
}
|
|
231
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
232
|
+
const char = pattern[index]
|
|
212
233
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
234
|
+
if (char === '*') {
|
|
235
|
+
if (pattern[index + 1] === '*') {
|
|
236
|
+
while (pattern[index + 1] === '*') index += 1
|
|
237
|
+
if (pattern[index + 1] === '/') {
|
|
238
|
+
source += '(?:.*/)?'
|
|
239
|
+
index += 1
|
|
240
|
+
} else {
|
|
241
|
+
source += '.*'
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
source += '[^/]*'
|
|
245
|
+
}
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
217
248
|
|
|
218
|
-
if (
|
|
249
|
+
if (char === '?') {
|
|
250
|
+
source += '[^/]'
|
|
219
251
|
continue
|
|
220
252
|
}
|
|
221
253
|
|
|
222
|
-
if (
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
254
|
+
if (char === '{') {
|
|
255
|
+
const close = pattern.indexOf('}', index + 1)
|
|
256
|
+
if (close !== -1) {
|
|
257
|
+
const alternatives = pattern
|
|
258
|
+
.slice(index + 1, close)
|
|
259
|
+
.split(',')
|
|
260
|
+
.map((item) => globSource(item))
|
|
261
|
+
source += `(?:${alternatives.join('|')})`
|
|
262
|
+
index = close
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
}
|
|
226
266
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
267
|
+
if (char === '[') {
|
|
268
|
+
const close = pattern.indexOf(']', index + 1)
|
|
269
|
+
if (close !== -1) {
|
|
270
|
+
source += pattern.slice(index, close + 1)
|
|
271
|
+
index = close
|
|
272
|
+
continue
|
|
231
273
|
}
|
|
232
|
-
} else {
|
|
233
|
-
addPackage(fullPath)
|
|
234
274
|
}
|
|
275
|
+
|
|
276
|
+
source += char.replace(/[\\.^$+()|]/g, '\\$&')
|
|
235
277
|
}
|
|
236
278
|
|
|
279
|
+
return source
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function matchesWorkspacePattern(relativePath, pattern) {
|
|
283
|
+
const normalized = normalizeWorkspacePattern(pattern)
|
|
284
|
+
if (normalized === '.') return relativePath === ''
|
|
285
|
+
return new RegExp(`^${globSource(normalized)}$`).test(relativePath)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function collectPackageDirs(workspaceRoot) {
|
|
289
|
+
const packageDirs = []
|
|
290
|
+
|
|
291
|
+
const visit = (dir) => {
|
|
292
|
+
if (isPackageDir(dir)) packageDirs.push(dir)
|
|
293
|
+
|
|
294
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
295
|
+
if (!entry.isDirectory() || WORKSPACE_SCAN_IGNORES.has(entry.name)) continue
|
|
296
|
+
visit(path.join(dir, entry.name))
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
visit(workspaceRoot)
|
|
237
301
|
return packageDirs
|
|
238
302
|
}
|
|
239
303
|
|
|
304
|
+
export function listPackages(workspaceRoot, patterns = undefined) {
|
|
305
|
+
const configuredPatterns = patterns ?? ['**']
|
|
306
|
+
const includePatterns = configuredPatterns.filter((pattern) => !pattern.startsWith('!'))
|
|
307
|
+
const excludePatterns = configuredPatterns
|
|
308
|
+
.filter((pattern) => pattern.startsWith('!'))
|
|
309
|
+
.map((pattern) => pattern.slice(1))
|
|
310
|
+
|
|
311
|
+
if (includePatterns.length === 0) return []
|
|
312
|
+
|
|
313
|
+
const root = path.resolve(workspaceRoot)
|
|
314
|
+
const packageDirs = collectPackageDirs(root)
|
|
315
|
+
const matched = packageDirs.filter((dir) => {
|
|
316
|
+
const relativePath = path.relative(root, dir).replaceAll('\\', '/')
|
|
317
|
+
return (
|
|
318
|
+
includePatterns.some((pattern) => matchesWorkspacePattern(relativePath, pattern)) &&
|
|
319
|
+
!excludePatterns.some((pattern) => matchesWorkspacePattern(relativePath, pattern))
|
|
320
|
+
)
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
return [...new Set(matched)].sort((left, right) => {
|
|
324
|
+
const leftRelative = path.relative(root, left)
|
|
325
|
+
const rightRelative = path.relative(root, right)
|
|
326
|
+
return leftRelative.localeCompare(rightRelative)
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
|
|
240
330
|
function ensureDir(file) {
|
|
241
331
|
fs.mkdirSync(path.dirname(file), {
|
|
242
332
|
recursive: true,
|
|
@@ -286,6 +376,19 @@ export function mergeEnvFile(templateFile, targetFile) {
|
|
|
286
376
|
|
|
287
377
|
const targetContent = fs.readFileSync(targetFile, 'utf-8')
|
|
288
378
|
|
|
379
|
+
const result = mergeEnvContent(templateContent, targetContent)
|
|
380
|
+
|
|
381
|
+
if (result.type === 'merged') {
|
|
382
|
+
fs.writeFileSync(targetFile, result.content)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
type: result.type,
|
|
387
|
+
...(result.added === undefined ? {} : { added: result.added }),
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function mergeEnvContent(templateContent, targetContent) {
|
|
289
392
|
const templateEnv = parseEnv(templateContent)
|
|
290
393
|
|
|
291
394
|
const targetEnv = parseEnv(targetContent)
|
|
@@ -308,10 +411,9 @@ export function mergeEnvFile(templateFile, targetFile) {
|
|
|
308
411
|
|
|
309
412
|
const nextContent = targetTrimmed ? `${targetTrimmed}\n${appendLines.join('\n')}\n` : `${appendLines.join('\n')}\n`
|
|
310
413
|
|
|
311
|
-
fs.writeFileSync(targetFile, nextContent)
|
|
312
|
-
|
|
313
414
|
return {
|
|
314
415
|
type: 'merged',
|
|
315
416
|
added: appendLines.length,
|
|
417
|
+
content: nextContent,
|
|
316
418
|
}
|
|
317
419
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opentiny/tiny-robot-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-beta.0",
|
|
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": {
|
|
@@ -27,10 +28,11 @@
|
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
30
|
"@inquirer/prompts": "^8.3.2",
|
|
31
|
+
"@vue/compiler-sfc": "^3.5.40",
|
|
30
32
|
"commander": "^14.0.3",
|
|
31
33
|
"picocolors": "^1.1.1",
|
|
32
34
|
"semver": "^7.8.1",
|
|
33
35
|
"yaml": "^2.9.0"
|
|
34
36
|
},
|
|
35
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "30a225596e22a9f9f51d70333901c0ebcf0fccd1"
|
|
36
38
|
}
|
|
@@ -1,2 +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.
|
|
1
9
|
VITE_ALIYUN_DASHSCOPE_KEY=
|
|
2
10
|
VITE_DEEPSEEK_API_KEY=
|
|
@@ -1,39 +1,71 @@
|
|
|
1
|
-
#
|
|
1
|
+
# **PROJECT_NAME**
|
|
2
2
|
|
|
3
|
-
TinyRobot
|
|
3
|
+
This project is generated from the TinyRobot Chat Basic template. It covers conversations, streaming responses, cancellation, Markdown rendering, responsive history, themes, model capabilities, and MCP Server/Tool management.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## MCP support
|
|
6
6
|
|
|
7
|
-
|
|
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
|
|
7
|
+
The demo declares MCP Servers through `mcpServers`:
|
|
15
8
|
|
|
16
|
-
|
|
9
|
+
```ts
|
|
10
|
+
const mcpServers = [
|
|
11
|
+
{ id: 'amap-maps', name: '高德地图', baseUrl: 'https://...' },
|
|
12
|
+
{
|
|
13
|
+
id: 'model-context-protocol-mcp',
|
|
14
|
+
name: 'Model Context Protocol MCP',
|
|
15
|
+
baseUrl: '/modelcontextprotocol-mcp',
|
|
16
|
+
installed: true,
|
|
17
|
+
},
|
|
18
|
+
]
|
|
19
|
+
```
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
`installed: true` means initially installed only. The Server remains disabled and does not connect or discover Tools until enabled.
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
| Server | Transport | Authentication |
|
|
24
|
+
| -------------------------- | --------------- | ---------------- |
|
|
25
|
+
| 高德地图 | Streamable HTTP | Optional API Key |
|
|
26
|
+
| Model Context Protocol MCP | Streamable HTTP | None |
|
|
27
|
+
|
|
28
|
+
Tools are discovered dynamically with `client.listTools()`. Tool choices are stored in the next user message's `runConfig` metadata snapshot, so changing a Tool affects subsequent turns without modifying a request already in progress.
|
|
29
|
+
|
|
30
|
+
Server installation, enabling, Tool loading, retry, de-duplication, and Tool calls are handled by the default adapter in `@opentiny/tiny-robot-chat`. If a Server cannot load its Tools, it remains installed but is automatically disabled. Other enabled Servers and normal chat remain available.
|
|
31
|
+
|
|
32
|
+
## Model connection modes
|
|
33
|
+
|
|
34
|
+
- API Key only: connects directly to the official Qwen or DeepSeek endpoint.
|
|
35
|
+
- `apiUrl` + API Key: connects to a custom service with frontend authentication.
|
|
36
|
+
- `apiUrl` only: connects to a backend proxy without frontend authentication.
|
|
37
|
+
|
|
38
|
+
The Chat package does not prompt for or validate missing API Keys. The upstream service or backend proxy decides whether authentication is required.
|
|
23
39
|
|
|
24
|
-
|
|
40
|
+
MCP follows the same three modes: the official endpoint with an optional API Key, a custom `baseUrl` with optional headers, or a proxy `baseUrl` without headers.
|
|
41
|
+
|
|
42
|
+
The basic template already includes the Model Context MCP proxy in `vite.config.ts`. If you use `add chat` in an existing project, the CLI does not modify that project's Vite configuration; add the same `/modelcontextprotocol-mcp` proxy under `server.proxy` manually and restart Vite.
|
|
43
|
+
|
|
44
|
+
## Model capabilities
|
|
45
|
+
|
|
46
|
+
Qwen models support deep thinking and web search. DeepSeek models support deep thinking. Unsupported capabilities remain visible as disabled buttons so the available model capabilities are clear.
|
|
47
|
+
|
|
48
|
+
DeepSeek models inherit the provider's thinking effort options: `low`, `high`, and `max`, with `high` as the default. The effort selector appears only after `深度思考` is enabled. To customize a model, set `efforts` and `defaultEffort` on that model in `src/App.vue`; use `thinkingRequired: true` only for models that cannot disable thinking.
|
|
49
|
+
|
|
50
|
+
## Environment variables
|
|
51
|
+
|
|
52
|
+
Copy `.env.example` to `.env.local`. Leave endpoint variables blank for official Provider/MCP defaults, or set them for custom services and backend proxies:
|
|
25
53
|
|
|
26
54
|
```env
|
|
27
|
-
|
|
28
|
-
|
|
55
|
+
VITE_QWEN_API_URL=
|
|
56
|
+
VITE_DEEPSEEK_API_URL=
|
|
57
|
+
VITE_AMAP_MCP_URL=
|
|
58
|
+
VITE_ALIYUN_DASHSCOPE_KEY=
|
|
59
|
+
VITE_DEEPSEEK_API_KEY=
|
|
29
60
|
```
|
|
30
61
|
|
|
31
|
-
`
|
|
62
|
+
For a proxy setup, set for example `VITE_QWEN_API_URL=/api/chat/qwen`, `VITE_DEEPSEEK_API_URL=/api/chat/deepseek`, and `VITE_AMAP_MCP_URL=/api/mcp/amap-maps`, then leave the API Key variables blank.
|
|
63
|
+
|
|
64
|
+
Variables prefixed with `VITE_` are exposed to browser code. Direct browser connections are intended only for servers without secrets and with CORS enabled. Production applications should keep credentials behind a backend or BFF proxy.
|
|
32
65
|
|
|
33
66
|
## Development
|
|
34
67
|
|
|
35
68
|
```bash
|
|
36
|
-
pnpm install
|
|
37
69
|
pnpm dev
|
|
38
70
|
```
|
|
39
71
|
|
|
@@ -41,5 +73,4 @@ pnpm dev
|
|
|
41
73
|
|
|
42
74
|
```bash
|
|
43
75
|
pnpm build
|
|
44
|
-
pnpm preview
|
|
45
76
|
```
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<link rel="icon" type="image/
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<title>
|
|
7
|
+
<title>__PROJECT_NAME__</title>
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<div id="app"></div>
|
|
@@ -1,29 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "__PROJECT_NAME__",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite",
|
|
8
8
|
"build": "vue-tsc -b && vite build",
|
|
9
|
-
"preview": "vite preview"
|
|
10
|
-
"type-check": "vue-tsc --noEmit"
|
|
9
|
+
"preview": "vite preview"
|
|
11
10
|
},
|
|
12
11
|
"dependencies": {
|
|
13
|
-
"@
|
|
14
|
-
"@opentiny/tiny-robot": "
|
|
15
|
-
"@opentiny/tiny-robot-
|
|
16
|
-
"
|
|
17
|
-
"dompurify": "^3.3.1",
|
|
18
|
-
"markdown-it": "^14.1.0",
|
|
19
|
-
"vue": "^3.5.30"
|
|
12
|
+
"@opentiny/tiny-robot-chat": "__TINY_ROBOT_VERSION__",
|
|
13
|
+
"@opentiny/tiny-robot": "__TINY_ROBOT_VERSION__",
|
|
14
|
+
"@opentiny/tiny-robot-svgs": "__TINY_ROBOT_VERSION__",
|
|
15
|
+
"vue": "^3.5.40"
|
|
20
16
|
},
|
|
21
17
|
"devDependencies": {
|
|
22
|
-
"@types/node": "^24.
|
|
23
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
24
|
-
"@vue/tsconfig": "^0.9.
|
|
25
|
-
"typescript": "~
|
|
26
|
-
"vite": "^8.0
|
|
27
|
-
"vue-tsc": "^3.
|
|
18
|
+
"@types/node": "^24.13.3",
|
|
19
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
20
|
+
"@vue/tsconfig": "^0.9.1",
|
|
21
|
+
"typescript": "~6.0.2",
|
|
22
|
+
"vite": "^8.2.0",
|
|
23
|
+
"vue-tsc": "^3.3.8"
|
|
28
24
|
}
|
|
29
25
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
|
3
|
+
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
|
4
|
+
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
|
5
|
+
</symbol>
|
|
6
|
+
<symbol id="discord-icon" viewBox="0 0 20 19">
|
|
7
|
+
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
|
8
|
+
</symbol>
|
|
9
|
+
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
|
10
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
|
11
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
|
12
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
|
13
|
+
</symbol>
|
|
14
|
+
<symbol id="github-icon" viewBox="0 0 19 19">
|
|
15
|
+
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
|
16
|
+
</symbol>
|
|
17
|
+
<symbol id="social-icon" viewBox="0 0 20 20">
|
|
18
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
|
19
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
|
20
|
+
</symbol>
|
|
21
|
+
<symbol id="x-icon" viewBox="0 0 19 19">
|
|
22
|
+
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
|
23
|
+
</symbol>
|
|
24
|
+
</svg>
|