@opentiny/tiny-robot-cli 0.5.2-alpha.13 → 0.5.2-alpha.14

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.
Files changed (48) hide show
  1. package/README.md +32 -5
  2. package/bin/commands/add.js +589 -198
  3. package/bin/utils.js +139 -47
  4. package/package.json +4 -3
  5. package/templates/basic/.env.example +8 -0
  6. package/templates/basic/README.md +53 -22
  7. package/templates/basic/index.html +2 -2
  8. package/templates/basic/package.json +12 -16
  9. package/templates/basic/public/favicon.svg +1 -0
  10. package/templates/basic/public/icons.svg +24 -0
  11. package/templates/basic/src/App.vue +112 -119
  12. package/templates/basic/src/main.ts +2 -2
  13. package/templates/basic/tsconfig.app.json +2 -5
  14. package/templates/basic/tsconfig.node.json +3 -6
  15. package/templates/basic/vite.config.ts +6 -1
  16. package/templates/chat/.env.example +8 -0
  17. package/templates/chat/README.md +21 -13
  18. package/templates/chat/package.json +5 -5
  19. package/templates/chat/src/TinyRobotChat.vue +52 -132
  20. package/templates/chat/src/components/ComposerTools.vue +99 -0
  21. package/templates/chat/src/{tiny-robot-chat/components → components}/WindowHeader.vue +1 -1
  22. package/templates/chat/src/{tiny-robot-chat/composables → composables}/useWindow.ts +8 -18
  23. package/templates/chat/src/config/chat-runtime.ts +82 -0
  24. package/templates/chat/src/{tiny-robot-chat/config → config}/chat-ui.ts +22 -4
  25. package/templates/chat/src/index.css +116 -0
  26. package/templates/chat/src/main.ts +3 -0
  27. package/templates/basic/public/modelcontextprotocol.png +0 -0
  28. package/templates/basic/src/components/ChatList.vue +0 -82
  29. package/templates/basic/src/components/ChatSender.vue +0 -125
  30. package/templates/basic/src/components/ConversationHistory.vue +0 -136
  31. package/templates/basic/src/components/HistoryDrawerButton.vue +0 -43
  32. package/templates/basic/src/components/McpServerPickerButton.vue +0 -278
  33. package/templates/basic/src/components/ThemeToggleButton.vue +0 -44
  34. package/templates/basic/src/components/icons/IconDeepThink.vue +0 -29
  35. package/templates/basic/src/components/icons/IconModelAliyunBailian.vue +0 -51
  36. package/templates/basic/src/components/icons/IconModelDeepseek.vue +0 -29
  37. package/templates/basic/src/components/icons/IconMoon.vue +0 -29
  38. package/templates/basic/src/components/icons/IconPlugin.vue +0 -29
  39. package/templates/basic/src/components/icons/IconSun.vue +0 -35
  40. package/templates/basic/src/components/icons/IconWebSearch.vue +0 -36
  41. package/templates/basic/src/components/icons/index.ts +0 -7
  42. package/templates/basic/src/composables/useChat.ts +0 -129
  43. package/templates/basic/src/composables/useMcp.ts +0 -170
  44. package/templates/basic/src/composables/useModel.ts +0 -82
  45. package/templates/basic/src/mcpServers.ts +0 -40
  46. package/templates/basic/src/models.ts +0 -81
  47. package/templates/chat/src/tiny-robot-chat/components/ComposerTools.vue +0 -72
  48. package/templates/chat/src/tiny-robot-chat/config/chat-runtime.ts +0 -26
@@ -0,0 +1,99 @@
1
+ <script setup lang="ts">
2
+ import type { SuggestionData, SuggestionItem, TemplateItem } from '@opentiny/tiny-robot'
3
+ import { TrDropdownMenu, TrSuggestionPillButton, TrSuggestionPills, TrSuggestionPopover } from '@opentiny/tiny-robot'
4
+ import type { ChatSenderSlotProps } from '@opentiny/tiny-robot-chat'
5
+ import { IconSparkles } from '@opentiny/tiny-robot-svgs'
6
+ import type { ChatMenuItem, TemplateCategory, TemplateSuggestionItem } from '../config/chat-ui'
7
+ import type { McpExample } from '../config/chat-runtime'
8
+ import { computed } from 'vue'
9
+
10
+ const props = defineProps<{
11
+ templateCategories: readonly TemplateCategory[]
12
+ mcpExamples: readonly McpExample[]
13
+ menus: readonly ChatMenuItem[]
14
+ submit: ChatSenderSlotProps['submit']
15
+ }>()
16
+
17
+ const currentTemplate = defineModel<TemplateItem[]>('currentTemplate', { required: true })
18
+ const suggestionData: SuggestionData = props.mcpExamples.map((item) => ({ id: item.id, text: item.request }))
19
+ const mcpMenuItems = props.mcpExamples.map((item) => ({ id: item.id, text: item.request }))
20
+ const mcpMenu = computed(() => props.menus.find((menu) => menu.action === 'mcp'))
21
+ const templateMenus = computed(() =>
22
+ props.templateCategories.map((category) => ({
23
+ ...category,
24
+ items: category.items.map((item) => ({ id: item.id, text: item.text, template: item })),
25
+ })),
26
+ )
27
+ const overlayTarget = '.chat-add-window'
28
+
29
+ function fillTemplate(template: TemplateSuggestionItem): void {
30
+ currentTemplate.value = template.template.map((item) => ({ ...item }))
31
+ }
32
+
33
+ function submitSuggestion(item: SuggestionItem): void {
34
+ props.submit({ text: item.text })
35
+ }
36
+ </script>
37
+
38
+ <template>
39
+ <div class="chat-add-tools">
40
+ <TrSuggestionPopover :append-to="overlayTarget" :data="suggestionData" @item-click="submitSuggestion">
41
+ <template #trigger>
42
+ <TrSuggestionPillButton title="常用查询"
43
+ ><IconSparkles style="font-size: 16px; color: #1476ff"
44
+ /></TrSuggestionPillButton>
45
+ </template>
46
+ </TrSuggestionPopover>
47
+ <TrSuggestionPills class="chat-add-tools__pills">
48
+ <TrDropdownMenu
49
+ v-if="mcpMenu"
50
+ :append-to="overlayTarget"
51
+ :items="mcpMenuItems"
52
+ trigger="click"
53
+ @item-click="(item) => submitSuggestion(item as SuggestionItem)"
54
+ >
55
+ <template #trigger
56
+ ><TrSuggestionPillButton>{{ mcpMenu.title }}</TrSuggestionPillButton></template
57
+ >
58
+ </TrDropdownMenu>
59
+ <TrDropdownMenu
60
+ v-for="category in templateMenus"
61
+ :key="category.id"
62
+ :append-to="overlayTarget"
63
+ :items="category.items as never"
64
+ trigger="click"
65
+ @item-click="(item) => fillTemplate((item as unknown as { template: TemplateSuggestionItem }).template)"
66
+ >
67
+ <template #trigger
68
+ ><TrSuggestionPillButton>{{ category.title }}</TrSuggestionPillButton></template
69
+ >
70
+ </TrDropdownMenu>
71
+ </TrSuggestionPills>
72
+ </div>
73
+ </template>
74
+
75
+ <style scoped>
76
+ .chat-add-tools {
77
+ display: flex;
78
+ box-sizing: border-box;
79
+ width: 100%;
80
+ min-width: 0;
81
+ flex-shrink: 0;
82
+ align-items: center;
83
+ gap: 8px;
84
+ padding: 8px 12px;
85
+ }
86
+ .chat-add-tools__pills {
87
+ min-width: 0;
88
+ flex: 1;
89
+ }
90
+ .chat-add-tools__pills :deep(.tr-suggestion-pills__container) {
91
+ min-width: 0;
92
+ mask: linear-gradient(to right, rgb(0 0 0 / 100%) 80%, rgb(0 0 0 / 0%) 100%);
93
+ }
94
+ @container (max-width: 519px) {
95
+ .chat-add-tools {
96
+ padding: 8px;
97
+ }
98
+ }
99
+ </style>
@@ -35,7 +35,7 @@ const IconFloatWindow = h(
35
35
  },
36
36
  [
37
37
  h('path', {
38
- fill: '#191919',
38
+ fill: 'currentColor',
39
39
  'fill-rule': 'evenodd',
40
40
  d: 'M6.19 14.5H4q-.285 0-.54-.06c-.15-.04-.3-.08-.44-.14s-.27-.13-.4-.22c-.14-.09-.27-.2-.39-.32s-.23-.25-.32-.39c-.09-.13-.16-.26-.22-.4s-.1-.29-.14-.44c-.04-.17-.05-.35-.05-.53V4c0-.19.01-.37.05-.54.04-.15.08-.3.14-.44s.13-.27.22-.4c.09-.14.2-.27.32-.39s.25-.23.39-.32c.13-.09.26-.16.4-.22s.29-.1.44-.14c.17-.04.35-.05.54-.05h8c.18 0 .36.01.53.05.15.04.3.08.44.14s.27.13.4.22c.14.09.27.2.39.32s.23.25.32.39c.09.13.16.26.22.4s.1.29.14.44q.06.255.06.54v2.15c0 .28-.22.5-.5.5s-.5-.22-.5-.5V4c0-.12-.02-.23-.04-.35-.02-.08-.05-.16-.08-.24a2 2 0 0 0-.13-.23 1.5 1.5 0 0 0-.19-.25 1.5 1.5 0 0 0-.25-.19c-.07-.05-.15-.09-.23-.13a2 2 0 0 0-.24-.08c-.12-.02-.23-.03-.34-.03H4c-.12 0-.23.01-.35.03-.08.02-.16.05-.24.08-.08.04-.16.08-.23.13a1.5 1.5 0 0 0-.25.19 1.5 1.5 0 0 0-.19.25c-.05.07-.09.15-.13.23-.03.08-.06.16-.08.24-.02.12-.03.23-.03.35v8c0 .11.01.22.03.34.02.08.05.16.08.24.04.08.08.16.13.23.05.09.12.17.19.25.08.07.16.14.25.19.07.05.15.09.23.13.08.03.16.06.24.08.12.02.23.04.35.04h2.19c.28 0 .5.22.5.5s-.22.5-.5.5m7.12-6.97c-.1-.02-.21-.03-.31-.03H9c-.11 0-.22.01-.32.03-.09.02-.18.05-.27.08-.09.04-.17.08-.24.13-.09.06-.16.12-.24.19-.07.08-.13.15-.19.24-.05.07-.09.15-.13.24-.03.09-.06.18-.08.27-.02.1-.03.21-.03.32v4c0 .1.01.21.03.31.02.09.05.18.08.27.04.09.08.17.13.24.06.09.12.16.19.24.08.07.15.13.24.19.07.05.15.09.24.13.09.03.18.06.27.08.1.02.21.04.32.04h4c.1 0 .21-.02.31-.04.09-.02.18-.05.27-.08.09-.04.17-.08.24-.13.09-.06.16-.12.24-.19.07-.08.13-.15.19-.24.05-.07.09-.15.13-.24.03-.09.06-.18.08-.27.02-.1.04-.21.04-.31V9c0-.11-.02-.22-.04-.32-.02-.09-.05-.18-.08-.27-.04-.09-.08-.17-.13-.24-.06-.09-.12-.16-.19-.24-.08-.07-.15-.13-.24-.19-.07-.05-.15-.09-.24-.13-.09-.03-.18-.06-.27-.08M9 8.5h4c.06 0 .13.01.19.03.06.03.11.06.16.11s.08.1.11.16c.02.06.04.13.04.2v4c0 .06-.02.13-.04.19-.03.06-.06.11-.11.16s-.1.08-.16.11c-.06.02-.13.04-.19.04H9c-.07 0-.14-.02-.2-.04a.6.6 0 0 1-.16-.11.6.6 0 0 1-.11-.16.6.6 0 0 1-.03-.19V9c0-.07.01-.14.03-.2.03-.06.06-.11.11-.16s.1-.08.16-.11c.06-.02.13-.03.2-.03',
41
41
  }),
@@ -4,34 +4,24 @@ 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
-
11
7
  export function useWindow() {
12
8
  const show = shallowRef(false)
13
9
  const displayMode = shallowRef<DisplayMode>('floating')
14
- const { width, height } = useWindowSize()
15
10
  const floatingState = shallowRef<LayoutFloatingState>({
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),
11
+ placement: 'center',
12
+ offsetX: 24,
13
+ offsetY: 24,
14
+ width: 640,
15
+ height: 760,
21
16
  })
22
17
  const restoreFloatingState = shallowRef<LayoutFloatingState | null>(null)
18
+ const { width, height } = useWindowSize()
23
19
 
24
20
  const floatingOptions = computed<LayoutFloatingOptions>(() => ({
25
21
  draggable: displayMode.value === 'floating',
26
22
  resizable: displayMode.value === 'floating',
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,
23
+ minWidth: displayMode.value === 'floating' ? 360 : undefined,
24
+ minHeight: displayMode.value === 'floating' ? 480 : undefined,
35
25
  }))
36
26
 
37
27
  const layoutStyle = computed(() => ({
@@ -0,0 +1,82 @@
1
+ import type { ChatMcpServers, ChatProviderConfig } from '@opentiny/tiny-robot-chat'
2
+ import { IconBailian, IconDeepseek } from '@opentiny/tiny-robot-svgs'
3
+
4
+ export interface McpExample {
5
+ id: string
6
+ title: string
7
+ request: string
8
+ }
9
+
10
+ export const mcpExamples: McpExample[] = [
11
+ { id: 'weather', title: '查询北京天气', request: '查询北京今天的天气,并给出出行建议' },
12
+ { id: 'coffee', title: '查询附近咖啡店', request: '查询我附近的咖啡店,并按距离排序' },
13
+ { id: 'exchange-rate', title: '获取当前汇率', request: '查询人民币兑美元的当前汇率' },
14
+ ]
15
+
16
+ export const mcpServers: ChatMcpServers = [
17
+ {
18
+ id: 'amap-maps',
19
+ name: '高德地图',
20
+ description: '覆盖地图、导航、地理编码、天气、路径规划、距离测量、关键词搜索和周边搜索等地理信息服务。',
21
+ baseUrl: import.meta.env.VITE_AMAP_MCP_URL?.trim() || 'https://dashscope.aliyuncs.com/api/v1/mcps/amap-maps/mcp',
22
+ icon: 'https://img.alicdn.com/imgextra/i4/O1CN01iPPabT1EGRN6uatHP_!!6000000000324-0-tps-512-512.jpg',
23
+ headers: import.meta.env.VITE_ALIYUN_DASHSCOPE_KEY?.trim()
24
+ ? { Authorization: `Bearer ${import.meta.env.VITE_ALIYUN_DASHSCOPE_KEY.trim()}` }
25
+ : undefined,
26
+ },
27
+ {
28
+ id: 'model-context-protocol-mcp',
29
+ name: 'Model Context Protocol MCP',
30
+ description: 'MCP Server',
31
+ baseUrl: '/modelcontextprotocol-mcp',
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
+ {
44
+ id: 'qwen3.7-flash',
45
+ label: 'Qwen3.7 Flash',
46
+ icon: IconBailian,
47
+ capabilities: { thinking: true, search: true },
48
+ },
49
+ {
50
+ id: 'qwen3.7-plus',
51
+ label: 'Qwen3.7 Plus',
52
+ icon: IconBailian,
53
+ capabilities: { thinking: true, search: true },
54
+ },
55
+ {
56
+ id: 'qwen3.7-max',
57
+ label: 'Qwen3.7 Max',
58
+ icon: IconBailian,
59
+ capabilities: { thinking: true, search: true },
60
+ },
61
+ ],
62
+ },
63
+ {
64
+ type: 'deepseek',
65
+ apiUrl: import.meta.env.VITE_DEEPSEEK_API_URL?.trim() || undefined,
66
+ apiKey: import.meta.env.VITE_DEEPSEEK_API_KEY?.trim(),
67
+ models: [
68
+ {
69
+ id: 'deepseek-v4-flash',
70
+ label: 'DeepSeek V4 Flash',
71
+ icon: IconDeepseek,
72
+ capabilities: { thinking: true },
73
+ },
74
+ {
75
+ id: 'deepseek-v4-pro',
76
+ label: 'DeepSeek V4 Pro',
77
+ icon: IconDeepseek,
78
+ capabilities: { thinking: true },
79
+ },
80
+ ],
81
+ },
82
+ ]
@@ -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, IconUser } from '@opentiny/tiny-robot-svgs'
4
+ import { IconAi, IconEdit, IconHistory, IconNewSession, IconSparkles, IconUser } from '@opentiny/tiny-robot-svgs'
5
5
  import { h, type CSSProperties } from 'vue'
6
6
 
7
7
  export interface ChatUiConfigOptions {
@@ -9,6 +9,24 @@ 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
+
12
30
  export interface PromptItemData {
13
31
  label: string
14
32
  description: string
@@ -34,9 +52,9 @@ export const PROMPT_ITEMS_DATA: PromptItemData[] = [
34
52
  emoji: '✨',
35
53
  },
36
54
  {
37
- label: '工作效率场景',
38
- description: '需要我帮你总结内容、拆解任务,还是整理一份行动清单?',
39
- emoji: '⚡',
55
+ label: 'MCP 工具调用',
56
+ description: '搜索:北京天气(输入「搜索」「MCP」「工具」等关键词可触发模拟 MCP 工具调用)',
57
+ emoji: '🔧',
40
58
  },
41
59
  ]
42
60
 
@@ -0,0 +1,116 @@
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: var(--tr-container-bg-default);
61
+ --tr-chat-ui-header-bg: var(--tr-container-bg-default);
62
+ --tr-chat-ui-main-bg: var(--tr-page-bg-default);
63
+ --tr-chat-ui-footer-bg: var(--tr-page-bg-default);
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
+ }
@@ -1,4 +1,7 @@
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'
2
5
  import TinyRobotChat from './TinyRobotChat.vue'
3
6
 
4
7
  createApp(TinyRobotChat).mount('#app')
@@ -1,82 +0,0 @@
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>
@@ -1,125 +0,0 @@
1
- <template>
2
- <tr-sender
3
- v-model="inputMessage"
4
- mode="multiple"
5
- :placeholder="isProcessing ? '思考中...' : '请输入你的问题...'"
6
- :clearable="true"
7
- :loading="isProcessing"
8
- :show-word-limit="true"
9
- :max-length="1000"
10
- @submit="sendMessage"
11
- @cancel="abortActiveRequest"
12
- >
13
- <template #footer>
14
- <div class="model-actions">
15
- <button
16
- class="sender-action-btn sender-capability-btn"
17
- :class="{ 'sender-capability-btn--active': thinkingEnabled }"
18
- type="button"
19
- :disabled="!supportsThinking"
20
- @click="thinkingEnabled = !thinkingEnabled"
21
- >
22
- <IconDeepThink :size="16" class="sender-action-btn__icon" />
23
- 深度思考
24
- </button>
25
- <button
26
- class="sender-action-btn sender-capability-btn"
27
- :class="{ 'sender-capability-btn--active': searchEnabled }"
28
- type="button"
29
- :disabled="!supportsSearch"
30
- @click="searchEnabled = !searchEnabled"
31
- >
32
- <IconWebSearch :size="16" class="sender-action-btn__icon" />
33
- 联网搜索
34
- </button>
35
- <McpServerPickerButton />
36
- <tr-dropdown-menu :items="modelMenuItems" trigger="click" @item-click="handleModelSelect">
37
- <template #trigger>
38
- <button class="sender-action-btn sender-model-btn" type="button">
39
- <component :is="selectedModel?.icon" :size="16" class="sender-action-btn__icon" />
40
- <span>{{ selectedModel?.name || '选择模型' }}</span>
41
- </button>
42
- </template>
43
- </tr-dropdown-menu>
44
- </div>
45
- </template>
46
- </tr-sender>
47
- </template>
48
-
49
- <script setup lang="ts">
50
- import { TrDropdownMenu, TrSender } from '@opentiny/tiny-robot'
51
- import { computed } from 'vue'
52
- import { useChat } from '../composables/useChat'
53
- import { useModel } from '../composables/useModel'
54
- import McpServerPickerButton from './McpServerPickerButton.vue'
55
- import { IconDeepThink, IconWebSearch } from './icons'
56
-
57
- const { inputMessage, isProcessing, sendMessage, abortActiveRequest } = useChat()
58
- const {
59
- modelOptions,
60
- selectedModel,
61
- selectedModelId,
62
- thinkingEnabled,
63
- searchEnabled,
64
- supportsThinking,
65
- supportsSearch,
66
- } = useModel()
67
-
68
- const modelMenuItems = computed(() =>
69
- modelOptions.map((item) => ({
70
- id: item.id,
71
- text: item.name,
72
- })),
73
- )
74
-
75
- function handleModelSelect(item: { id?: string }) {
76
- if (!item.id) {
77
- return
78
- }
79
- selectedModelId.value = item.id
80
- }
81
- </script>
82
-
83
- <style scoped>
84
- .model-actions {
85
- display: flex;
86
- align-items: center;
87
- gap: 8px;
88
- }
89
-
90
- .sender-action-btn {
91
- border: 1px solid var(--tr-border-color-disabled);
92
- border-radius: var(--tr-radius-full);
93
- background: var(--tr-container-bg-default);
94
- color: var(--tr-text-secondary);
95
- font-size: var(--tr-font-size-sm);
96
- height: 32px;
97
- padding: 0 10px;
98
- line-height: 1;
99
- cursor: pointer;
100
- display: inline-flex;
101
- align-items: center;
102
- gap: 6px;
103
- }
104
-
105
- .sender-action-btn__icon {
106
- flex-shrink: 0;
107
- }
108
-
109
- .sender-action-btn:hover:not(:disabled) {
110
- border-color: var(--tr-border-color-hover);
111
- color: var(--tr-text-primary);
112
- background: var(--tr-container-bg-hover);
113
- }
114
-
115
- .sender-action-btn:disabled {
116
- cursor: not-allowed;
117
- opacity: 0.5;
118
- }
119
-
120
- .sender-capability-btn--active {
121
- border-color: var(--tr-border-color-hover);
122
- color: var(--tr-text-primary);
123
- background: var(--tr-container-bg-default-2);
124
- }
125
- </style>