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

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.
@@ -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,83 @@
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
+ export function useWindow() {
8
+ const show = shallowRef(false)
9
+ const displayMode = shallowRef<DisplayMode>('floating')
10
+ const floatingState = shallowRef<LayoutFloatingState>({
11
+ placement: 'center',
12
+ offsetX: 24,
13
+ offsetY: 24,
14
+ width: 640,
15
+ height: 760,
16
+ })
17
+ const restoreFloatingState = shallowRef<LayoutFloatingState | null>(null)
18
+ const { width, height } = useWindowSize()
19
+
20
+ const floatingOptions = computed<LayoutFloatingOptions>(() => ({
21
+ draggable: displayMode.value === 'floating',
22
+ resizable: displayMode.value === 'floating',
23
+ minWidth: displayMode.value === 'floating' ? 360 : undefined,
24
+ minHeight: displayMode.value === 'floating' ? 480 : undefined,
25
+ }))
26
+
27
+ const layoutStyle = computed(() => ({
28
+ '--tr-layout-main-min-width': '0',
29
+ '--tr-layout-floating-radius': displayMode.value === 'floating' && width.value > 640 ? '12px' : '0px',
30
+ '--tr-layout-floating-shadow':
31
+ displayMode.value === 'floating' && width.value > 640 ? '0 18px 48px rgb(0 0 0 / 18%)' : 'none',
32
+ }))
33
+
34
+ function getFullscreenState(): LayoutFloatingState {
35
+ return { placement: 'top-left', offsetX: 0, offsetY: 0, width: width.value, height: height.value }
36
+ }
37
+
38
+ function setDisplayMode(mode: DisplayMode): void {
39
+ if (mode === displayMode.value) return
40
+ if (displayMode.value === 'floating') restoreFloatingState.value = { ...floatingState.value }
41
+ displayMode.value = mode
42
+ if (mode === 'floating') {
43
+ if (restoreFloatingState.value) floatingState.value = { ...restoreFloatingState.value }
44
+ restoreFloatingState.value = null
45
+ } else {
46
+ floatingState.value = getFullscreenState()
47
+ }
48
+ }
49
+
50
+ function handleEscape(event: KeyboardEvent): void {
51
+ if (event.key === 'Escape' && displayMode.value === 'fullscreen') setDisplayMode('floating')
52
+ }
53
+
54
+ useEventListener('keydown', handleEscape)
55
+ watch(
56
+ [displayMode, width, height],
57
+ () => {
58
+ if (displayMode.value === 'fullscreen') floatingState.value = getFullscreenState()
59
+ },
60
+ { immediate: true },
61
+ )
62
+
63
+ function open(): void {
64
+ show.value = true
65
+ }
66
+
67
+ function close(): void {
68
+ show.value = false
69
+ }
70
+
71
+ return {
72
+ show,
73
+ displayMode,
74
+ floatingState,
75
+ floatingOptions,
76
+ layoutStyle,
77
+ setDisplayMode,
78
+ restoreFloatingState,
79
+ handleEscape,
80
+ open,
81
+ close,
82
+ }
83
+ }
@@ -0,0 +1,57 @@
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
+ ]
@@ -0,0 +1,289 @@
1
+ import type { LayoutFloatingOptions } from '@opentiny/tiny-robot'
2
+ import type { ChatSenderOptions, ChatUIOptions } from '@opentiny/tiny-robot-chat'
3
+ import type { TemplateItem } from '@opentiny/tiny-robot'
4
+ import { IconAi, IconEdit, IconHistory, IconNewSession, IconSparkles, IconUser } from '@opentiny/tiny-robot-svgs'
5
+ import { h, type CSSProperties } from 'vue'
6
+
7
+ export interface ChatUiConfigOptions {
8
+ floatingOptions: Readonly<LayoutFloatingOptions>
9
+ templateExtensions: NonNullable<ChatSenderOptions['extensions']>
10
+ }
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
+ export interface PromptItemData {
31
+ label: string
32
+ description: string
33
+ emoji: string
34
+ badge?: string
35
+ }
36
+
37
+ export const PROMPT_ITEMS_DATA: PromptItemData[] = [
38
+ {
39
+ label: '日常助理场景',
40
+ description: '今天需要我帮你安排日程,规划旅行,还是起草一封邮件?',
41
+ emoji: '🧠',
42
+ badge: 'NEW',
43
+ },
44
+ {
45
+ label: '学习/知识型场景',
46
+ description: '有什么想了解的吗?可以是"Vue3 和 React 的区别"!',
47
+ emoji: '🤔',
48
+ },
49
+ {
50
+ label: '创意生成场景',
51
+ description: '想写段文案、起个名字,还是来点灵感?',
52
+ emoji: '✨',
53
+ },
54
+ {
55
+ label: 'MCP 工具调用',
56
+ description: '搜索:北京天气(输入「搜索」「MCP」「工具」等关键词可触发模拟 MCP 工具调用)',
57
+ emoji: '🔧',
58
+ },
59
+ ]
60
+
61
+ export const prompts = PROMPT_ITEMS_DATA
62
+
63
+ export interface TemplateSuggestionItem {
64
+ id: string
65
+ text: string
66
+ template: TemplateItem[]
67
+ }
68
+
69
+ export const templateSuggestions: TemplateSuggestionItem[] = [
70
+ {
71
+ id: 'write',
72
+ text: '帮我写作',
73
+ template: [
74
+ { id: 'write-prefix', type: 'text', content: '请帮我撰写一篇' },
75
+ {
76
+ id: 'write-kind',
77
+ type: 'select',
78
+ content: '',
79
+ placeholder: '文章类型',
80
+ options: [
81
+ { label: '公告', value: '公告' },
82
+ { label: '方案', value: '方案' },
83
+ { label: '邮件', value: '邮件' },
84
+ ],
85
+ },
86
+ { id: 'write-topic-prefix', type: 'text', content: ',主题是' },
87
+ { id: 'write-topic', type: 'block', content: '主题' },
88
+ { id: 'write-tone-prefix', type: 'text', content: ',语气是' },
89
+ {
90
+ id: 'write-tone',
91
+ type: 'select',
92
+ content: '',
93
+ placeholder: '语气类型',
94
+ options: [
95
+ { label: '正式', value: '正式' },
96
+ { label: '轻松', value: '轻松' },
97
+ { label: '专业', value: '专业' },
98
+ ],
99
+ },
100
+ { id: 'write-description-prefix', type: 'text', content: ',具体内容是' },
101
+ { id: 'write-description', type: 'block', content: '详细描述' },
102
+ ],
103
+ },
104
+ {
105
+ id: 'translate',
106
+ text: '翻译',
107
+ template: [
108
+ { id: 'translate-source-prefix', type: 'text', content: '请将以下' },
109
+ {
110
+ id: 'translate-source-language',
111
+ type: 'select',
112
+ content: '',
113
+ placeholder: '源语言',
114
+ options: [
115
+ { label: '中文', value: '中文' },
116
+ { label: '英文', value: '英文' },
117
+ { label: '日语', value: '日语' },
118
+ ],
119
+ },
120
+ { id: 'translate-content-prefix', type: 'text', content: '内容翻译成' },
121
+ {
122
+ id: 'translate-target-language',
123
+ type: 'select',
124
+ content: '',
125
+ placeholder: '目标语言',
126
+ options: [
127
+ { label: '中文', value: '中文' },
128
+ { label: '英文', value: '英文' },
129
+ { label: '日语', value: '日语' },
130
+ ],
131
+ },
132
+ { id: 'translate-content-suffix', type: 'text', content: ',内容是' },
133
+ { id: 'translate-content', type: 'block', content: '需要翻译的内容' },
134
+ ],
135
+ },
136
+ {
137
+ id: 'code-review',
138
+ text: '代码审查',
139
+ template: [
140
+ { id: 'code-review-prefix', type: 'text', content: '请帮我审查以下' },
141
+ {
142
+ id: 'code-review-language',
143
+ type: 'select',
144
+ content: '',
145
+ placeholder: '代码语言',
146
+ options: [
147
+ { label: 'JavaScript', value: 'JavaScript' },
148
+ { label: 'TypeScript', value: 'TypeScript' },
149
+ { label: 'Python', value: 'Python' },
150
+ ],
151
+ },
152
+ { id: 'code-review-focus-prefix', type: 'text', content: '代码,关注' },
153
+ {
154
+ id: 'code-review-focus',
155
+ type: 'select',
156
+ content: '',
157
+ placeholder: '关注方向',
158
+ options: [
159
+ { label: '性能', value: '性能' },
160
+ { label: '安全', value: '安全' },
161
+ { label: '可读性', value: '可读性' },
162
+ ],
163
+ },
164
+ { id: 'code-review-content-prefix', type: 'text', content: '方面,代码如下:' },
165
+ { id: 'code-review-content', type: 'block', content: '代码内容' },
166
+ ],
167
+ },
168
+ {
169
+ id: 'email-compose',
170
+ text: '写邮件',
171
+ template: [
172
+ { id: 'email-compose-prefix', type: 'text', content: '请帮我起草一封' },
173
+ {
174
+ id: 'email-compose-type',
175
+ type: 'select',
176
+ content: '',
177
+ placeholder: '邮件类型',
178
+ options: [
179
+ { label: '正式', value: '正式' },
180
+ { label: '非正式', value: '非正式' },
181
+ ],
182
+ },
183
+ { id: 'email-compose-recipient-prefix', type: 'text', content: '邮件,发送给' },
184
+ { id: 'email-compose-recipient', type: 'block', content: '收件人角色' },
185
+ { id: 'email-compose-subject-prefix', type: 'text', content: ',主题是' },
186
+ { id: 'email-compose-subject', type: 'block', content: '邮件主题' },
187
+ { id: 'email-compose-content-prefix', type: 'text', content: ',内容是关于' },
188
+ { id: 'email-compose-content', type: 'block', content: '邮件内容' },
189
+ ],
190
+ },
191
+ {
192
+ id: 'interview-questions',
193
+ text: '面试问题',
194
+ template: [
195
+ { id: 'interview-questions-prefix', type: 'text', content: '请为' },
196
+ { id: 'interview-questions-role', type: 'block', content: '岗位名称' },
197
+ { id: 'interview-questions-skill-prefix', type: 'text', content: '岗位,针对' },
198
+ { id: 'interview-questions-skill', type: 'block', content: '技能领域' },
199
+ { id: 'interview-questions-count-prefix', type: 'text', content: '方向,设计' },
200
+ {
201
+ id: 'interview-questions-count',
202
+ type: 'select',
203
+ content: '',
204
+ placeholder: '问题数量',
205
+ options: [
206
+ { label: '3', value: '3' },
207
+ { label: '5', value: '5' },
208
+ { label: '10', value: '10' },
209
+ ],
210
+ },
211
+ { id: 'interview-questions-difficulty-prefix', type: 'text', content: '个难度为' },
212
+ {
213
+ id: 'interview-questions-difficulty',
214
+ type: 'select',
215
+ content: '',
216
+ placeholder: '岗位难度',
217
+ options: [
218
+ { label: '简单', value: '简单' },
219
+ { label: '中等', value: '中等' },
220
+ { label: '困难', value: '困难' },
221
+ ],
222
+ },
223
+ { id: 'interview-questions-suffix', type: 'text', content: '的面试问题' },
224
+ ],
225
+ },
226
+ ]
227
+
228
+ export interface TemplateCategory {
229
+ id: string
230
+ title: string
231
+ items: TemplateSuggestionItem[]
232
+ }
233
+
234
+ export const templateCategories: TemplateCategory[] = [
235
+ { id: 'writing', title: '写作表达', items: [templateSuggestions[0]] },
236
+ { id: 'language', title: '语言处理', items: [templateSuggestions[1]] },
237
+ { id: 'engineering', title: '研发协作', items: [templateSuggestions[2]] },
238
+ { id: 'communication', title: '工作沟通', items: [templateSuggestions[3]] },
239
+ { id: 'growth', title: '人才发展', items: [templateSuggestions[4]] },
240
+ ]
241
+
242
+ const promptItems = PROMPT_ITEMS_DATA.map((item) => ({
243
+ ...item,
244
+ icon: h('span', { style: { fontSize: '18px' } as CSSProperties }, item.emoji) as never,
245
+ }))
246
+
247
+ export function createChatUi(options: ChatUiConfigOptions): ChatUIOptions {
248
+ return {
249
+ history: false,
250
+ welcome: {
251
+ title: 'TinyRobot',
252
+ description: '您好,我是TinyRobot,您专属的 AI 智能专家',
253
+ icon: h(IconAi, { style: { fontSize: '48px' } }) as never,
254
+ },
255
+ prompts: { items: promptItems, wrap: true, itemClass: 'prompt-item' },
256
+ bubble: {
257
+ autoScroll: true,
258
+ bubbleList: {
259
+ roleConfigs: {
260
+ assistant: { placement: 'start', avatar: h(IconAi, { style: { fontSize: '32px' } }) as never },
261
+ user: { placement: 'end', avatar: h(IconUser, { style: { fontSize: '32px' } }) as never },
262
+ system: { hidden: true },
263
+ },
264
+ },
265
+ },
266
+ layout: {
267
+ heightMode: 'parent',
268
+ leftAside: false,
269
+ rightAside: {
270
+ mode: 'drawer',
271
+ width: 400,
272
+ defaultOpen: false,
273
+ },
274
+ surface: { mode: 'floating', floatingOptions: options.floatingOptions },
275
+ emptyState: 'center',
276
+ composer: { welcome: 'footer' },
277
+ contentMaxWidth: 1280,
278
+ panelPadding: 0,
279
+ panelGap: 0,
280
+ } as ChatUIOptions['layout'],
281
+ sender: {
282
+ mode: 'multiple',
283
+ clearable: true,
284
+ showWordLimit: true,
285
+ maxLength: 2000,
286
+ extensions: options.templateExtensions,
287
+ },
288
+ }
289
+ }