@opentiny/tiny-robot-cli 0.5.0 → 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.
@@ -1,35 +1,176 @@
1
1
  <script setup lang="ts">
2
- import type { ChatMcpServerConfig, ChatModelOption } from '@opentiny/tiny-robot/experimental'
3
- import { TrThemeProvider } from '@opentiny/tiny-robot'
4
- import { TrChat } from '@opentiny/tiny-robot/experimental'
5
- import { ref } from 'vue'
6
-
7
- const show = ref(false)
8
- const fullscreen = ref(false)
9
-
10
- const chatModelOptions: ChatModelOption[] = [
11
- {
12
- id: 'deepseek-chat',
13
- provider: 'deepseek',
14
- name: 'DeepSeek V4 Flash',
15
- model: 'deepseek-v4-flash',
16
- apiUrl: 'https://api.deepseek.com/chat/completions',
17
- apiKey: import.meta.env.VITE_DEEPSEEK_API_KEY || '',
18
- },
19
- ]
20
-
21
- const chatMcpServers: Record<string, ChatMcpServerConfig> = {}
2
+ import type { HistoryMenuItem, PromptProps, TemplateItem } from '@opentiny/tiny-robot'
3
+ import { TrSender, TrThemeProvider as TrTheme } from '@opentiny/tiny-robot'
4
+ import '@opentiny/tiny-robot/dist/style.css'
5
+ import { TrChat, useChatHistoryItems, useLocalChatRuntime, type ChatHistoryItem } from '@opentiny/tiny-robot-chat'
6
+ import '@opentiny/tiny-robot-chat/dist/style.css'
7
+ import { IconAi } from '@opentiny/tiny-robot-svgs'
8
+ import { computed, reactive, ref } from 'vue'
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'
14
+
15
+ const runtime = useLocalChatRuntime({ modelProviders, mcpServers })
16
+ const window = reactive(useWindow())
17
+ const chatRef = ref<{ send: (payload: { text: string }) => Promise<boolean> } | null>(null)
18
+ const currentTemplate = ref<TemplateItem[]>([])
19
+ const showHistory = ref(false)
20
+ const historyData = useChatHistoryItems({ conversations: () => runtime.conversations.value, defaultTitle: '' })
21
+ const activeConversationId = computed(() => runtime.activeConversation.value?.id)
22
+ const templateExtensions = [TrSender.template(currentTemplate as never, { appendTo: '.chat-add-window' })]
23
+ const chatUi = computed(() => createChatUi({ floatingOptions: window.floatingOptions, templateExtensions }))
24
+
25
+ function handlePromptClick({ item }: { item: PromptProps }): void {
26
+ if (item.description) void chatRef.value?.send({ text: item.description })
27
+ }
28
+
29
+ function handleNewSession(): void {
30
+ void runtime.actions.createConversation()
31
+ showHistory.value = false
32
+ }
33
+
34
+ function handleHistorySelect(item: ChatHistoryItem): void {
35
+ void runtime.actions.switchConversation(item.raw.id)
36
+ showHistory.value = false
37
+ }
38
+
39
+ function handleHistoryRename(title: string, item: ChatHistoryItem): void {
40
+ void runtime.actions.renameConversation(item.raw.id, title)
41
+ }
42
+
43
+ function handleHistoryAction(action: HistoryMenuItem, item: ChatHistoryItem): void {
44
+ if (action.id === 'delete') void runtime.actions.deleteConversation(item.raw.id)
45
+ }
22
46
  </script>
23
47
 
24
48
  <template>
25
- <TrThemeProvider>
49
+ <TrTheme>
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>
26
60
  <TrChat
27
- v-model:show="show"
28
- v-model:fullscreen="fullscreen"
29
- title="TrChat Preview"
30
- system-prompt="You are a helpful assistant."
31
- :model-options="chatModelOptions"
32
- :mcp-servers="chatMcpServers"
33
- />
34
- </TrThemeProvider>
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>
89
+ </TrTheme>
35
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>
@@ -0,0 +1,4 @@
1
+ import { createApp } from 'vue'
2
+ import TinyRobotChat from './TinyRobotChat.vue'
3
+
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>
@@ -0,0 +1,192 @@
1
+ <script setup lang="ts">
2
+ import type { HistoryMenuItem } from '@opentiny/tiny-robot'
3
+ import { TrHistory, TrIconButton } from '@opentiny/tiny-robot'
4
+ import { IconClose, IconEnterFullScreen, IconHistory, IconNewSession } from '@opentiny/tiny-robot-svgs'
5
+ import { h } from 'vue'
6
+ import type { ChatHistoryItem } from '@opentiny/tiny-robot-chat'
7
+ import type { DisplayMode } from '../composables/useWindow'
8
+
9
+ const props = defineProps<{
10
+ displayMode: DisplayMode
11
+ showHistory: boolean
12
+ historyData: readonly ChatHistoryItem[]
13
+ activeConversationId?: string
14
+ }>()
15
+
16
+ const emit = defineEmits<{
17
+ newSession: []
18
+ selectHistory: [item: ChatHistoryItem]
19
+ renameHistory: [title: string, item: ChatHistoryItem]
20
+ historyAction: [action: HistoryMenuItem, item: ChatHistoryItem]
21
+ 'update:showHistory': [value: boolean]
22
+ changeMode: [mode: DisplayMode]
23
+ close: []
24
+ }>()
25
+
26
+ const IconFloatWindow = h(
27
+ 'svg',
28
+ {
29
+ xmlns: 'http://www.w3.org/2000/svg',
30
+ width: 16,
31
+ height: 16,
32
+ fill: 'none',
33
+ viewBox: '0 0 16 16',
34
+ 'aria-hidden': 'true',
35
+ },
36
+ [
37
+ h('path', {
38
+ fill: '#191919',
39
+ 'fill-rule': 'evenodd',
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
+ }),
42
+ ],
43
+ )
44
+ </script>
45
+
46
+ <template>
47
+ <div class="chat-add-header">
48
+ <h1 class="chat-add-header__title">OpenTiny NEXT</h1>
49
+ <div class="chat-add-header__actions">
50
+ <TrIconButton :icon="IconNewSession" size="28" svg-size="20" title="新会话" @click="emit('newSession')" />
51
+ <span class="chat-add-header__history-trigger">
52
+ <TrIconButton
53
+ :icon="IconHistory"
54
+ size="28"
55
+ svg-size="20"
56
+ title="历史会话"
57
+ @click="emit('update:showHistory', !props.showHistory)"
58
+ />
59
+ <div v-if="props.showHistory" class="chat-add-header__history-panel">
60
+ <div class="chat-add-header__history-head">
61
+ <h2>历史对话</h2>
62
+ <TrIconButton
63
+ :icon="IconClose"
64
+ size="28"
65
+ svg-size="20"
66
+ title="关闭历史对话"
67
+ @click="emit('update:showHistory', false)"
68
+ />
69
+ </div>
70
+ <!-- @vue-generic {ChatHistoryItem} -->
71
+ <TrHistory
72
+ class="chat-add-header__history"
73
+ :selected="props.activeConversationId"
74
+ :search-bar="true"
75
+ :data="props.historyData as ChatHistoryItem[]"
76
+ @item-title-change="(title, item) => emit('renameHistory', title, item)"
77
+ @item-click="(item) => emit('selectHistory', item)"
78
+ @item-action="(action, item) => emit('historyAction', action, item)"
79
+ />
80
+ </div>
81
+ </span>
82
+ <TrIconButton
83
+ v-if="props.displayMode === 'fullscreen'"
84
+ :icon="IconFloatWindow"
85
+ size="28"
86
+ svg-size="20"
87
+ title="切换为悬浮窗口"
88
+ aria-label="切换为悬浮窗口"
89
+ @click="emit('changeMode', 'floating')"
90
+ />
91
+ <TrIconButton
92
+ v-if="props.displayMode !== 'fullscreen'"
93
+ :icon="IconEnterFullScreen"
94
+ size="28"
95
+ svg-size="20"
96
+ title="切换为全屏模式"
97
+ aria-label="切换为全屏模式"
98
+ @click="emit('changeMode', 'fullscreen')"
99
+ />
100
+ <TrIconButton :icon="IconClose" size="28" svg-size="20" title="关闭" @click="emit('close')" />
101
+ </div>
102
+ </div>
103
+ </template>
104
+
105
+ <style scoped>
106
+ .chat-add-header {
107
+ position: relative;
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: space-between;
111
+ box-sizing: border-box;
112
+ min-width: 0;
113
+ height: 56px;
114
+ padding: 0 16px;
115
+ background: var(--tr-layout-bg-default);
116
+ }
117
+ .chat-add-header__title {
118
+ overflow: hidden;
119
+ margin: 0;
120
+ color: var(--tr-text-primary);
121
+ font-size: 16px;
122
+ font-weight: 600;
123
+ line-height: 24px;
124
+ text-overflow: ellipsis;
125
+ white-space: nowrap;
126
+ }
127
+ .chat-add-header__actions {
128
+ display: flex;
129
+ flex-shrink: 0;
130
+ align-items: center;
131
+ gap: 4px;
132
+ }
133
+ .chat-add-header__history-trigger {
134
+ position: relative;
135
+ display: inline-flex;
136
+ line-height: 0;
137
+ }
138
+ .chat-add-header__history-panel {
139
+ position: absolute;
140
+ top: calc(100% + 8px);
141
+ right: 0;
142
+ z-index: var(--tr-z-index-popover);
143
+ display: flex;
144
+ box-sizing: border-box;
145
+ flex-direction: column;
146
+ gap: 12px;
147
+ width: 320px;
148
+ height: 560px;
149
+ padding: 16px;
150
+ overflow: hidden;
151
+ border: 1px solid var(--tr-border-color-default);
152
+ border-radius: 12px;
153
+ background: var(--tr-container-bg-default);
154
+ box-shadow: var(--tr-shadow-sm);
155
+ }
156
+ .chat-add-header__history-head {
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: space-between;
160
+ flex-shrink: 0;
161
+ }
162
+ .chat-add-header__history-head h2 {
163
+ margin: 0;
164
+ color: var(--tr-text-primary);
165
+ font-size: 16px;
166
+ line-height: 24px;
167
+ }
168
+ .chat-add-header__history {
169
+ min-height: 0;
170
+ overflow-y: auto;
171
+ flex: 1;
172
+ }
173
+ .chat-add-header__history {
174
+ --tr-history-item-selected-bg: var(--tr-history-item-hover-bg);
175
+ --tr-history-item-selected-color: var(--tr-color-primary);
176
+ --tr-history-item-space-y: 4px;
177
+ }
178
+ @media (max-width: 640px) {
179
+ .chat-add-header {
180
+ height: 52px;
181
+ padding: 0 10px;
182
+ }
183
+ .chat-add-header__history-panel {
184
+ position: fixed;
185
+ top: 52px;
186
+ right: 8px;
187
+ left: 8px;
188
+ width: auto;
189
+ height: min(560px, calc(100dvh - 68px));
190
+ }
191
+ }
192
+ </style>
@@ -0,0 +1,93 @@
1
+ import type { LayoutFloatingOptions, LayoutFloatingState } from '@opentiny/tiny-robot'
2
+ import { useEventListener, useWindowSize } from '@vueuse/core'
3
+ import { computed, shallowRef, watch } from 'vue'
4
+
5
+ export type DisplayMode = 'floating' | 'fullscreen'
6
+
7
+ const FLOATING_GAP = 24
8
+ const FLOATING_MIN_WIDTH = 360
9
+ const FLOATING_MIN_HEIGHT = 480
10
+
11
+ export function useWindow() {
12
+ const show = shallowRef(false)
13
+ const displayMode = shallowRef<DisplayMode>('floating')
14
+ const { width, height } = useWindowSize()
15
+ 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),
21
+ })
22
+ const restoreFloatingState = shallowRef<LayoutFloatingState | null>(null)
23
+
24
+ const floatingOptions = computed<LayoutFloatingOptions>(() => ({
25
+ draggable: displayMode.value === 'floating',
26
+ 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,
35
+ }))
36
+
37
+ const layoutStyle = computed(() => ({
38
+ '--tr-layout-main-min-width': '0',
39
+ '--tr-layout-floating-radius': displayMode.value === 'floating' && width.value > 640 ? '12px' : '0px',
40
+ '--tr-layout-floating-shadow':
41
+ displayMode.value === 'floating' && width.value > 640 ? '0 18px 48px rgb(0 0 0 / 18%)' : 'none',
42
+ }))
43
+
44
+ function getFullscreenState(): LayoutFloatingState {
45
+ return { placement: 'top-left', offsetX: 0, offsetY: 0, width: width.value, height: height.value }
46
+ }
47
+
48
+ function setDisplayMode(mode: DisplayMode): void {
49
+ if (mode === displayMode.value) return
50
+ if (displayMode.value === 'floating') restoreFloatingState.value = { ...floatingState.value }
51
+ displayMode.value = mode
52
+ if (mode === 'floating') {
53
+ if (restoreFloatingState.value) floatingState.value = { ...restoreFloatingState.value }
54
+ restoreFloatingState.value = null
55
+ } else {
56
+ floatingState.value = getFullscreenState()
57
+ }
58
+ }
59
+
60
+ function handleEscape(event: KeyboardEvent): void {
61
+ if (event.key === 'Escape' && displayMode.value === 'fullscreen') setDisplayMode('floating')
62
+ }
63
+
64
+ useEventListener('keydown', handleEscape)
65
+ watch(
66
+ [displayMode, width, height],
67
+ () => {
68
+ if (displayMode.value === 'fullscreen') floatingState.value = getFullscreenState()
69
+ },
70
+ { immediate: true },
71
+ )
72
+
73
+ function open(): void {
74
+ show.value = true
75
+ }
76
+
77
+ function close(): void {
78
+ show.value = false
79
+ }
80
+
81
+ return {
82
+ show,
83
+ displayMode,
84
+ floatingState,
85
+ floatingOptions,
86
+ layoutStyle,
87
+ setDisplayMode,
88
+ restoreFloatingState,
89
+ handleEscape,
90
+ open,
91
+ close,
92
+ }
93
+ }
@@ -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
+ ]