@vibe-forge/client 0.2.0-alpha.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.
Files changed (184) hide show
  1. package/LICENSE +21 -0
  2. package/cli.cjs +6 -0
  3. package/index.html +27 -0
  4. package/package.json +42 -0
  5. package/src/App.tsx +174 -0
  6. package/src/api.ts +241 -0
  7. package/src/components/ArchiveView.scss +168 -0
  8. package/src/components/ArchiveView.tsx +299 -0
  9. package/src/components/AutomationView/AutomationView.scss +26 -0
  10. package/src/components/AutomationView/RuleFormPanel.scss +129 -0
  11. package/src/components/AutomationView/RuleFormPanel.tsx +257 -0
  12. package/src/components/AutomationView/RuleSidebar.scss +219 -0
  13. package/src/components/AutomationView/RuleSidebar.tsx +258 -0
  14. package/src/components/AutomationView/RunHistoryPanel.scss +286 -0
  15. package/src/components/AutomationView/RunHistoryPanel.tsx +320 -0
  16. package/src/components/AutomationView/TaskList.scss +128 -0
  17. package/src/components/AutomationView/TaskList.tsx +79 -0
  18. package/src/components/AutomationView/TriggerList.scss +153 -0
  19. package/src/components/AutomationView/TriggerList.tsx +217 -0
  20. package/src/components/AutomationView/index.tsx +228 -0
  21. package/src/components/AutomationView/types.ts +21 -0
  22. package/src/components/Chat.scss +89 -0
  23. package/src/components/Chat.tsx +92 -0
  24. package/src/components/ConfigView.scss +185 -0
  25. package/src/components/ConfigView.tsx +258 -0
  26. package/src/components/NavRail.scss +71 -0
  27. package/src/components/NavRail.tsx +188 -0
  28. package/src/components/Sidebar.scss +112 -0
  29. package/src/components/Sidebar.tsx +291 -0
  30. package/src/components/chat/ChatHeader.scss +401 -0
  31. package/src/components/chat/ChatHeader.tsx +342 -0
  32. package/src/components/chat/ChatHistoryView.tsx +122 -0
  33. package/src/components/chat/ChatSettingsView.tsx +22 -0
  34. package/src/components/chat/ChatTimelineView.scss +53 -0
  35. package/src/components/chat/ChatTimelineView.tsx +158 -0
  36. package/src/components/chat/CodeBlock.scss +87 -0
  37. package/src/components/chat/CodeBlock.tsx +179 -0
  38. package/src/components/chat/CompletionMenu.scss +70 -0
  39. package/src/components/chat/CompletionMenu.tsx +58 -0
  40. package/src/components/chat/CurrentTodoList.scss +217 -0
  41. package/src/components/chat/CurrentTodoList.tsx +103 -0
  42. package/src/components/chat/MarkdownContent.tsx +43 -0
  43. package/src/components/chat/MessageFooter.tsx +48 -0
  44. package/src/components/chat/MessageItem.scss +251 -0
  45. package/src/components/chat/MessageItem.tsx +78 -0
  46. package/src/components/chat/NewSessionGuide.scss +186 -0
  47. package/src/components/chat/NewSessionGuide.tsx +167 -0
  48. package/src/components/chat/Sender.scss +367 -0
  49. package/src/components/chat/Sender.tsx +541 -0
  50. package/src/components/chat/SessionTimelinePanel/EventList.scss +58 -0
  51. package/src/components/chat/SessionTimelinePanel/EventList.tsx +212 -0
  52. package/src/components/chat/SessionTimelinePanel/gantt.ts +177 -0
  53. package/src/components/chat/SessionTimelinePanel/git-graph.ts +518 -0
  54. package/src/components/chat/SessionTimelinePanel/index.scss +28 -0
  55. package/src/components/chat/SessionTimelinePanel/index.tsx +121 -0
  56. package/src/components/chat/SessionTimelinePanel/mermaid.ts +4 -0
  57. package/src/components/chat/SessionTimelinePanel/types.ts +64 -0
  58. package/src/components/chat/SessionTimelinePanel/utils.ts +20 -0
  59. package/src/components/chat/ThinkingStatus.scss +70 -0
  60. package/src/components/chat/ThinkingStatus.tsx +13 -0
  61. package/src/components/chat/ToolCallBox.scss +137 -0
  62. package/src/components/chat/ToolCallBox.tsx +55 -0
  63. package/src/components/chat/ToolGroup.scss +154 -0
  64. package/src/components/chat/ToolGroup.tsx +102 -0
  65. package/src/components/chat/ToolRenderer.tsx +45 -0
  66. package/src/components/chat/messageUtils.ts +171 -0
  67. package/src/components/chat/safeSerialize.ts +84 -0
  68. package/src/components/chat/tools/DefaultTool.tsx +63 -0
  69. package/src/components/chat/tools/adapter-claude/BashTool.scss +71 -0
  70. package/src/components/chat/tools/adapter-claude/BashTool.tsx +82 -0
  71. package/src/components/chat/tools/adapter-claude/GlobTool.scss +88 -0
  72. package/src/components/chat/tools/adapter-claude/GlobTool.tsx +85 -0
  73. package/src/components/chat/tools/adapter-claude/GrepTool.scss +96 -0
  74. package/src/components/chat/tools/adapter-claude/GrepTool.tsx +114 -0
  75. package/src/components/chat/tools/adapter-claude/LSTool.scss +85 -0
  76. package/src/components/chat/tools/adapter-claude/LSTool.tsx +94 -0
  77. package/src/components/chat/tools/adapter-claude/ReadTool.scss +57 -0
  78. package/src/components/chat/tools/adapter-claude/ReadTool.tsx +87 -0
  79. package/src/components/chat/tools/adapter-claude/TodoTool.scss +78 -0
  80. package/src/components/chat/tools/adapter-claude/TodoTool.tsx +60 -0
  81. package/src/components/chat/tools/adapter-claude/WriteTool.scss +92 -0
  82. package/src/components/chat/tools/adapter-claude/WriteTool.tsx +86 -0
  83. package/src/components/chat/tools/adapter-claude/components/FileList.scss +65 -0
  84. package/src/components/chat/tools/adapter-claude/components/FileList.tsx +185 -0
  85. package/src/components/chat/tools/adapter-claude/index.ts +28 -0
  86. package/src/components/chat/tools/defineToolRender.ts +28 -0
  87. package/src/components/chat/tools/task/GetTaskInfoTool.scss +50 -0
  88. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +88 -0
  89. package/src/components/chat/tools/task/ListTasksTool.scss +56 -0
  90. package/src/components/chat/tools/task/ListTasksTool.tsx +83 -0
  91. package/src/components/chat/tools/task/StartTasksTool.scss +56 -0
  92. package/src/components/chat/tools/task/StartTasksTool.tsx +96 -0
  93. package/src/components/chat/tools/task/components/TaskToolCard.scss +127 -0
  94. package/src/components/chat/tools/task/components/TaskToolCard.tsx +177 -0
  95. package/src/components/chat/tools/task/index.ts +15 -0
  96. package/src/components/chat/useChatModels.tsx +206 -0
  97. package/src/components/chat/useChatSession.ts +370 -0
  98. package/src/components/config/ConfigAboutSection.scss +111 -0
  99. package/src/components/config/ConfigAboutSection.tsx +86 -0
  100. package/src/components/config/ConfigDisplayValue.scss +22 -0
  101. package/src/components/config/ConfigDisplayValue.tsx +62 -0
  102. package/src/components/config/ConfigEditors.scss +65 -0
  103. package/src/components/config/ConfigEditors.tsx +98 -0
  104. package/src/components/config/ConfigFieldRow.scss +97 -0
  105. package/src/components/config/ConfigFieldRow.tsx +36 -0
  106. package/src/components/config/ConfigSectionForm.scss +94 -0
  107. package/src/components/config/ConfigSectionForm.tsx +436 -0
  108. package/src/components/config/ConfigSectionPanel.tsx +67 -0
  109. package/src/components/config/ConfigShortcutInput.scss +11 -0
  110. package/src/components/config/ConfigShortcutInput.tsx +52 -0
  111. package/src/components/config/ConfigSourceSwitch.tsx +57 -0
  112. package/src/components/config/configSchema.ts +319 -0
  113. package/src/components/config/configUtils.ts +83 -0
  114. package/src/components/config/index.tsx +5 -0
  115. package/src/components/config/recordEditors/BooleanRecordEditor.scss +1 -0
  116. package/src/components/config/recordEditors/BooleanRecordEditor.tsx +75 -0
  117. package/src/components/config/recordEditors/KeyValueEditor.scss +1 -0
  118. package/src/components/config/recordEditors/KeyValueEditor.tsx +97 -0
  119. package/src/components/config/recordEditors/McpServersRecordEditor.scss +1 -0
  120. package/src/components/config/recordEditors/McpServersRecordEditor.tsx +258 -0
  121. package/src/components/config/recordEditors/ModelServicesRecordEditor.scss +1 -0
  122. package/src/components/config/recordEditors/ModelServicesRecordEditor.tsx +233 -0
  123. package/src/components/config/recordEditors/RecordEditors.scss +117 -0
  124. package/src/components/config/recordEditors/RecordJsonEditor.scss +1 -0
  125. package/src/components/config/recordEditors/RecordJsonEditor.tsx +113 -0
  126. package/src/components/config/recordEditors/index.tsx +5 -0
  127. package/src/components/knowledge-base/KnowledgeBaseView.scss +19 -0
  128. package/src/components/knowledge-base/KnowledgeBaseView.tsx +186 -0
  129. package/src/components/knowledge-base/components/ActionButton.scss +5 -0
  130. package/src/components/knowledge-base/components/ActionButton.tsx +9 -0
  131. package/src/components/knowledge-base/components/EmptyState.scss +19 -0
  132. package/src/components/knowledge-base/components/EmptyState.tsx +42 -0
  133. package/src/components/knowledge-base/components/EntitiesTab.scss +5 -0
  134. package/src/components/knowledge-base/components/EntitiesTab.tsx +80 -0
  135. package/src/components/knowledge-base/components/EntityItem.scss +82 -0
  136. package/src/components/knowledge-base/components/EntityItem.tsx +79 -0
  137. package/src/components/knowledge-base/components/EntityList.scss +5 -0
  138. package/src/components/knowledge-base/components/EntityList.tsx +70 -0
  139. package/src/components/knowledge-base/components/FilterBar.scss +21 -0
  140. package/src/components/knowledge-base/components/FilterBar.tsx +51 -0
  141. package/src/components/knowledge-base/components/FlowsTab.scss +5 -0
  142. package/src/components/knowledge-base/components/FlowsTab.tsx +80 -0
  143. package/src/components/knowledge-base/components/KnowledgeBaseHeader.scss +27 -0
  144. package/src/components/knowledge-base/components/KnowledgeBaseHeader.tsx +29 -0
  145. package/src/components/knowledge-base/components/KnowledgeList.scss +19 -0
  146. package/src/components/knowledge-base/components/KnowledgeList.tsx +19 -0
  147. package/src/components/knowledge-base/components/LoadingState.scss +5 -0
  148. package/src/components/knowledge-base/components/LoadingState.tsx +11 -0
  149. package/src/components/knowledge-base/components/MetaList.scss +19 -0
  150. package/src/components/knowledge-base/components/MetaList.tsx +18 -0
  151. package/src/components/knowledge-base/components/RulesTab.scss +5 -0
  152. package/src/components/knowledge-base/components/RulesTab.tsx +49 -0
  153. package/src/components/knowledge-base/components/SectionHeader.scss +22 -0
  154. package/src/components/knowledge-base/components/SectionHeader.tsx +21 -0
  155. package/src/components/knowledge-base/components/SkillsTab.scss +5 -0
  156. package/src/components/knowledge-base/components/SkillsTab.tsx +49 -0
  157. package/src/components/knowledge-base/components/SpecItem.scss +138 -0
  158. package/src/components/knowledge-base/components/SpecItem.tsx +131 -0
  159. package/src/components/knowledge-base/components/SpecList.scss +5 -0
  160. package/src/components/knowledge-base/components/SpecList.tsx +70 -0
  161. package/src/components/knowledge-base/components/TabContent.scss +8 -0
  162. package/src/components/knowledge-base/components/TabContent.tsx +17 -0
  163. package/src/components/knowledge-base/components/TabLabel.scss +10 -0
  164. package/src/components/knowledge-base/components/TabLabel.tsx +15 -0
  165. package/src/components/knowledge-base/index.tsx +1 -0
  166. package/src/components/sidebar/SessionItem.scss +256 -0
  167. package/src/components/sidebar/SessionItem.tsx +265 -0
  168. package/src/components/sidebar/SessionList.scss +92 -0
  169. package/src/components/sidebar/SessionList.tsx +166 -0
  170. package/src/components/sidebar/SidebarHeader.scss +79 -0
  171. package/src/components/sidebar/SidebarHeader.tsx +128 -0
  172. package/src/connectionManager.ts +172 -0
  173. package/src/hooks/useGlobalShortcut.ts +26 -0
  174. package/src/hooks/useQueryParams.ts +54 -0
  175. package/src/i18n.ts +22 -0
  176. package/src/main.tsx +41 -0
  177. package/src/resources/locales/en.json +765 -0
  178. package/src/resources/locales/zh.json +766 -0
  179. package/src/store/index.ts +23 -0
  180. package/src/styles/global.scss +100 -0
  181. package/src/utils/shortcutUtils.ts +88 -0
  182. package/src/vite-env.d.ts +12 -0
  183. package/src/ws.ts +33 -0
  184. package/vite.config.ts +26 -0
@@ -0,0 +1,128 @@
1
+ import './SidebarHeader.scss'
2
+
3
+ import { Button, Checkbox, Input, Popconfirm, Tooltip } from 'antd'
4
+ import { useAtom } from 'jotai'
5
+ import React from 'react'
6
+ import { useTranslation } from 'react-i18next'
7
+
8
+ import { isSidebarCollapsedAtom } from '#~/store/index.js'
9
+
10
+ interface SidebarHeaderProps {
11
+ searchQuery: string
12
+ onSearchChange: (query: string) => void
13
+ isBatchMode: boolean
14
+ onToggleBatchMode: () => void
15
+ selectedCount: number
16
+ totalCount: number
17
+ onSelectAll: (selected: boolean) => void
18
+ onBatchArchive: () => void
19
+ isCreatingSession: boolean
20
+ onCreateSession: () => void
21
+ }
22
+
23
+ export function SidebarHeader({
24
+ searchQuery,
25
+ onSearchChange,
26
+ isBatchMode,
27
+ onToggleBatchMode,
28
+ selectedCount,
29
+ totalCount,
30
+ onSelectAll,
31
+ onBatchArchive,
32
+ isCreatingSession,
33
+ onCreateSession
34
+ }: SidebarHeaderProps) {
35
+ const { t } = useTranslation()
36
+ const [isSidebarCollapsed, setIsSidebarCollapsed] = useAtom(isSidebarCollapsedAtom)
37
+ const isAllSelected = totalCount > 0 && selectedCount === totalCount
38
+
39
+ return (
40
+ <div className='sidebar-header'>
41
+ <div className='header-bottom'>
42
+ {isBatchMode && (
43
+ <div className='batch-select-wrapper'>
44
+ <Tooltip title={isAllSelected ? t('common.deselectAll') : t('common.selectAll')}>
45
+ <Checkbox
46
+ checked={isAllSelected}
47
+ indeterminate={selectedCount > 0 && selectedCount < totalCount}
48
+ onChange={(e) => onSelectAll(e.target.checked)}
49
+ />
50
+ </Tooltip>
51
+ </div>
52
+ )}
53
+ <Input
54
+ className='search-input'
55
+ placeholder={t('common.search')}
56
+ value={searchQuery}
57
+ onChange={(e) => onSearchChange(e.target.value)}
58
+ prefix={
59
+ <span className='material-symbols-rounded search-icon'>
60
+ search
61
+ </span>
62
+ }
63
+ allowClear
64
+ />
65
+ <div className='batch-actions'>
66
+ <Tooltip title={isBatchMode ? t('common.cancelBatch') : t('common.batchMode')}>
67
+ <Button
68
+ className={`action-btn ${isBatchMode ? 'active' : ''}`}
69
+ type={isBatchMode ? 'primary' : 'text'}
70
+ onClick={onToggleBatchMode}
71
+ >
72
+ <span className='material-symbols-rounded'>
73
+ {isBatchMode ? 'close' : 'checklist'}
74
+ </span>
75
+ </Button>
76
+ </Tooltip>
77
+ {isBatchMode && (
78
+ <Popconfirm
79
+ title={t('common.archiveConfirm', { count: selectedCount })}
80
+ onConfirm={onBatchArchive}
81
+ okText={t('common.confirm')}
82
+ cancelText={t('common.cancel')}
83
+ okButtonProps={{ danger: false }}
84
+ disabled={selectedCount === 0}
85
+ >
86
+ <Tooltip title={t('common.batchArchive')}>
87
+ <Button
88
+ className='action-btn'
89
+ type='text'
90
+ disabled={selectedCount === 0}
91
+ >
92
+ <span className='material-symbols-rounded'>
93
+ archive
94
+ </span>
95
+ </Button>
96
+ </Tooltip>
97
+ </Popconfirm>
98
+ )}
99
+ </div>
100
+ <Tooltip title={isSidebarCollapsed ? t('common.expand') : t('common.collapse')}>
101
+ <Button
102
+ className='sidebar-collapse-btn'
103
+ type='text'
104
+ onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
105
+ >
106
+ <span className='material-symbols-rounded'>
107
+ {isSidebarCollapsed ? 'dock_to_right' : 'left_panel_close'}
108
+ </span>
109
+ </Button>
110
+ </Tooltip>
111
+ {isSidebarCollapsed && (
112
+ <Tooltip title={isCreatingSession ? t('common.alreadyInNewChat') : t('common.newChat')} placement='right'>
113
+ <Button
114
+ className={`sidebar-new-chat-btn ${isCreatingSession ? 'active' : ''}`}
115
+ type='text'
116
+ disabled={!!isCreatingSession}
117
+ onClick={onCreateSession}
118
+ >
119
+ <span className={`material-symbols-rounded ${isCreatingSession ? 'filled' : ''}`}>
120
+ {isCreatingSession ? 'chat_bubble' : 'send'}
121
+ </span>
122
+ </Button>
123
+ </Tooltip>
124
+ )}
125
+ </div>
126
+ </div>
127
+ )
128
+ }
@@ -0,0 +1,172 @@
1
+ import type { WSEvent } from '@vibe-forge/core'
2
+ import { createSocket } from './ws'
3
+ import type { WSHandlers } from './ws'
4
+
5
+ class ConnectionManager {
6
+ private sockets = new Map<string, WebSocket>()
7
+ private subscribers = new Map<string, Set<WSHandlers>>()
8
+ private disconnectTimers = new Map<string, NodeJS.Timeout>()
9
+ private connectionParams = new Map<string, string>()
10
+
11
+ // Keep connection alive for 60 seconds after last subscriber leaves
12
+ // This allows for quick navigation between sessions without reconnecting
13
+ // and basic background task updates if the user quickly checks back
14
+ private readonly DISCONNECT_DELAY = 60000
15
+
16
+ /**
17
+ * Connect to a session or reuse existing connection
18
+ * @param sessionId The session ID to connect to
19
+ * @param handlers Event handlers for this subscription
20
+ * @returns A cleanup function to unsubscribe
21
+ */
22
+ connect(sessionId: string, handlers: WSHandlers, params?: Record<string, string>): () => void {
23
+ // 1. Cancel any pending disconnect timer
24
+ if (this.disconnectTimers.has(sessionId)) {
25
+ clearTimeout(this.disconnectTimers.get(sessionId)!)
26
+ this.disconnectTimers.delete(sessionId)
27
+ }
28
+
29
+ // 2. Add subscriber
30
+ if (!this.subscribers.has(sessionId)) {
31
+ this.subscribers.set(sessionId, new Set())
32
+ }
33
+ this.subscribers.get(sessionId)!.add(handlers)
34
+
35
+ // 3. Ensure WebSocket is open
36
+ const nextParams = this.normalizeParams({ sessionId, ...(params ?? {}) })
37
+ const paramsKey = this.buildParamsKey(nextParams)
38
+ const prevParamsKey = this.connectionParams.get(sessionId)
39
+ let ws = this.sockets.get(sessionId)
40
+
41
+ if (prevParamsKey != null && prevParamsKey !== paramsKey && ws) {
42
+ ws.close()
43
+ this.sockets.delete(sessionId)
44
+ ws = undefined
45
+ }
46
+
47
+ // If socket doesn't exist or is closed/closing, create a new one
48
+ if (!ws || ws.readyState === WebSocket.CLOSED || ws.readyState === WebSocket.CLOSING) {
49
+ this.createConnection(sessionId, nextParams)
50
+ } else if (ws.readyState === WebSocket.OPEN) {
51
+ // If already open, trigger onOpen immediately for this new subscriber
52
+ handlers.onOpen?.()
53
+ }
54
+
55
+ this.connectionParams.set(sessionId, paramsKey)
56
+ // 4. Return unsubscribe function
57
+ return () => this.disconnect(sessionId, handlers)
58
+ }
59
+
60
+ private createConnection(sessionId: string, params: Record<string, string>) {
61
+ const ws = createSocket({
62
+ onOpen: () => {
63
+ this.broadcast(sessionId, 'onOpen')
64
+ },
65
+ onMessage: (data: WSEvent) => {
66
+ this.broadcast(sessionId, 'onMessage', data)
67
+ },
68
+ onError: (err: Event) => {
69
+ this.broadcast(sessionId, 'onError', err)
70
+ },
71
+ onClose: () => {
72
+ this.broadcast(sessionId, 'onClose')
73
+ // Clean up if closed from server side
74
+ if (this.sockets.get(sessionId) === ws) {
75
+ this.sockets.delete(sessionId)
76
+ }
77
+ }
78
+ }, params)
79
+
80
+ this.sockets.set(sessionId, ws)
81
+ }
82
+
83
+ private disconnect(sessionId: string, handlers: WSHandlers) {
84
+ const subs = this.subscribers.get(sessionId)
85
+ if (subs) {
86
+ subs.delete(handlers)
87
+
88
+ // If no more subscribers, schedule disconnect
89
+ if (subs.size === 0) {
90
+ this.disconnectTimers.set(
91
+ sessionId,
92
+ setTimeout(() => {
93
+ const ws = this.sockets.get(sessionId)
94
+ if (ws) {
95
+ ws.close()
96
+ this.sockets.delete(sessionId)
97
+ }
98
+ this.disconnectTimers.delete(sessionId)
99
+ this.subscribers.delete(sessionId)
100
+ this.connectionParams.delete(sessionId)
101
+ }, this.DISCONNECT_DELAY)
102
+ )
103
+ }
104
+ }
105
+ }
106
+
107
+ private broadcast(sessionId: string, method: keyof WSHandlers, data?: any) {
108
+ const subs = this.subscribers.get(sessionId)
109
+ if (subs) {
110
+ // Create a copy to avoid issues if handlers unsubscribe during execution
111
+ const handlers = Array.from(subs)
112
+ handlers.forEach(h => {
113
+ if (h[method]) {
114
+ try {
115
+ // @ts-ignore - Dynamic dispatch
116
+ h[method](data)
117
+ } catch (e) {
118
+ console.error(`Error in ${method} handler for session ${sessionId}:`, e)
119
+ }
120
+ }
121
+ })
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Manually close a connection if needed
127
+ */
128
+ close(sessionId: string) {
129
+ const ws = this.sockets.get(sessionId)
130
+ if (ws) {
131
+ ws.close()
132
+ this.sockets.delete(sessionId)
133
+ }
134
+ if (this.disconnectTimers.has(sessionId)) {
135
+ clearTimeout(this.disconnectTimers.get(sessionId)!)
136
+ this.disconnectTimers.delete(sessionId)
137
+ }
138
+ this.subscribers.delete(sessionId)
139
+ this.connectionParams.delete(sessionId)
140
+ }
141
+
142
+ /**
143
+ * Send a message to the session
144
+ */
145
+ send(sessionId: string, data: any) {
146
+ const ws = this.sockets.get(sessionId)
147
+ if (ws && ws.readyState === WebSocket.OPEN) {
148
+ ws.send(typeof data === 'string' ? data : JSON.stringify(data))
149
+ } else {
150
+ console.warn(`Cannot send message: Session ${sessionId} not connected or ready`)
151
+ }
152
+ }
153
+
154
+ private normalizeParams(params: Record<string, string>) {
155
+ return Object.entries(params).reduce<Record<string, string>>((acc, [key, value]) => {
156
+ if (value == null) return acc
157
+ const trimmed = String(value).trim()
158
+ if (trimmed === '') return acc
159
+ acc[key] = trimmed
160
+ return acc
161
+ }, {})
162
+ }
163
+
164
+ private buildParamsKey(params: Record<string, string>) {
165
+ return Object.entries(params)
166
+ .sort(([a], [b]) => a.localeCompare(b))
167
+ .map(([key, value]) => `${key}=${value}`)
168
+ .join('&')
169
+ }
170
+ }
171
+
172
+ export const connectionManager = new ConnectionManager()
@@ -0,0 +1,26 @@
1
+ import { useEffect } from 'react'
2
+
3
+ import { isShortcutMatch } from '../utils/shortcutUtils'
4
+
5
+ export const useGlobalShortcut = ({
6
+ shortcut,
7
+ enabled = true,
8
+ isMac,
9
+ onTrigger
10
+ }: {
11
+ shortcut?: string
12
+ enabled?: boolean
13
+ isMac: boolean
14
+ onTrigger: (event: KeyboardEvent) => void
15
+ }) => {
16
+ useEffect(() => {
17
+ if (!enabled) return
18
+ const handleKeyDown = (event: KeyboardEvent) => {
19
+ if (isShortcutMatch(event, shortcut, isMac)) {
20
+ onTrigger(event)
21
+ }
22
+ }
23
+ window.addEventListener('keydown', handleKeyDown)
24
+ return () => window.removeEventListener('keydown', handleKeyDown)
25
+ }, [enabled, isMac, onTrigger, shortcut])
26
+ }
@@ -0,0 +1,54 @@
1
+ import { useCallback, useMemo } from 'react'
2
+ import { useSearchParams } from 'react-router-dom'
3
+
4
+ type QueryParamConfig<T extends Record<string, string>> = {
5
+ keys: Array<keyof T & string>
6
+ defaults?: Partial<T>
7
+ omit?: Partial<Record<keyof T & string, (value: string) => boolean>>
8
+ }
9
+
10
+ export const useQueryParams = <T extends Record<string, string>>({
11
+ keys,
12
+ defaults,
13
+ omit
14
+ }: QueryParamConfig<T>) => {
15
+ const [searchParams, setSearchParams] = useSearchParams()
16
+ const keySet = useMemo(() => new Set(keys), [keys])
17
+
18
+ const values = useMemo(() => {
19
+ return keys.reduce((acc, key) => {
20
+ const raw = searchParams.get(key)
21
+ const fallback = defaults?.[key] ?? ''
22
+ acc[key] = (raw ?? fallback) as T[keyof T]
23
+ return acc
24
+ }, {} as T)
25
+ }, [defaults, keys, searchParams])
26
+
27
+ const update = useCallback((patch: Partial<T>) => {
28
+ const nextParams = new URLSearchParams()
29
+ const merged = keys.reduce((acc, key) => {
30
+ const raw = searchParams.get(key)
31
+ const fallback = defaults?.[key] ?? ''
32
+ acc[key] = (raw ?? fallback) as T[keyof T]
33
+ return acc
34
+ }, {} as T)
35
+
36
+ Object.assign(merged, patch)
37
+
38
+ keys.forEach((key) => {
39
+ const value = merged[key] ?? ''
40
+ const shouldOmit = value === '' || (omit?.[key] ? omit[key]!(value) : false)
41
+ if (!shouldOmit) nextParams.set(key, value)
42
+ })
43
+
44
+ for (const [key, value] of searchParams.entries()) {
45
+ if (!keySet.has(key)) nextParams.append(key, value)
46
+ }
47
+
48
+ if (nextParams.toString() !== searchParams.toString()) {
49
+ setSearchParams(nextParams, { replace: true })
50
+ }
51
+ }, [defaults, keySet, keys, omit, searchParams, setSearchParams])
52
+
53
+ return { values, update, searchParams }
54
+ }
package/src/i18n.ts ADDED
@@ -0,0 +1,22 @@
1
+ import i18n from 'i18next'
2
+ import LanguageDetector from 'i18next-browser-languagedetector'
3
+ import { initReactI18next } from 'react-i18next'
4
+
5
+ import en from './resources/locales/en.json'
6
+ import zh from './resources/locales/zh.json'
7
+
8
+ void i18n
9
+ .use(LanguageDetector)
10
+ .use(initReactI18next)
11
+ .init({
12
+ resources: {
13
+ zh: { translation: zh },
14
+ en: { translation: en }
15
+ },
16
+ fallbackLng: 'zh',
17
+ interpolation: {
18
+ escapeValue: false
19
+ }
20
+ })
21
+
22
+ export default i18n
package/src/main.tsx ADDED
@@ -0,0 +1,41 @@
1
+ import './styles/global.scss'
2
+ import './i18n'
3
+
4
+ import { App as AntdApp, ConfigProvider } from 'antd'
5
+ import zhCN from 'antd/locale/zh_CN'
6
+ import React from 'react'
7
+ import { createRoot } from 'react-dom/client'
8
+ import { BrowserRouter } from 'react-router-dom'
9
+ import { SWRConfig } from 'swr'
10
+
11
+ import App from './App'
12
+
13
+ const root = createRoot(document.getElementById('root')!)
14
+ root.render(
15
+ <React.StrictMode>
16
+ <ConfigProvider locale={zhCN} theme={{ token: { colorPrimary: '#000000' } }}>
17
+ <AntdApp>
18
+ <SWRConfig
19
+ value={{
20
+ fetcher: async (path: string) => {
21
+ const serverHost = (import.meta.env.__VF_PROJECT_AI_SERVER_HOST__ != null &&
22
+ import.meta.env.__VF_PROJECT_AI_SERVER_HOST__ !== '')
23
+ ? import.meta.env.__VF_PROJECT_AI_SERVER_HOST__
24
+ : window.location.hostname
25
+ const serverPort = (import.meta.env.__VF_PROJECT_AI_SERVER_PORT__ != null &&
26
+ import.meta.env.__VF_PROJECT_AI_SERVER_PORT__ !== '')
27
+ ? import.meta.env.__VF_PROJECT_AI_SERVER_PORT__
28
+ : '8787'
29
+ const baseUrl = `http://${serverHost}:${serverPort}`
30
+ return fetch(`${baseUrl}${path}`).then(async (r) => r.json() as Promise<unknown>)
31
+ }
32
+ }}
33
+ >
34
+ <BrowserRouter>
35
+ <App />
36
+ </BrowserRouter>
37
+ </SWRConfig>
38
+ </AntdApp>
39
+ </ConfigProvider>
40
+ </React.StrictMode>
41
+ )