@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,258 @@
1
+ import './McpServersRecordEditor.scss'
2
+
3
+ import { Button, Input, Select, Switch, Tooltip } from 'antd'
4
+ import { useEffect, useMemo, useState } from 'react'
5
+
6
+ import { FieldRow } from '../ConfigFieldRow'
7
+ import { getTypeIcon } from '../configUtils'
8
+ import type { TranslationFn } from '../configUtils'
9
+ import { KeyValueEditor } from './KeyValueEditor'
10
+ import { StringArrayEditor } from '../ConfigEditors'
11
+
12
+ export const McpServersRecordEditor = ({
13
+ value,
14
+ onChange,
15
+ t,
16
+ keyPlaceholder
17
+ }: {
18
+ value: Record<string, unknown>
19
+ onChange: (nextValue: Record<string, unknown>) => void
20
+ t: TranslationFn
21
+ keyPlaceholder: string
22
+ }) => {
23
+ const [newKey, setNewKey] = useState('')
24
+ const entries = useMemo(() => Object.entries(value), [value])
25
+ const [collapsedKeys, setCollapsedKeys] = useState<Record<string, boolean>>(() => (
26
+ Object.fromEntries(entries.map(([key]) => [key, true]))
27
+ ))
28
+
29
+ useEffect(() => {
30
+ setCollapsedKeys(prev => {
31
+ const next: Record<string, boolean> = {}
32
+ for (const [key] of entries) {
33
+ next[key] = prev[key] ?? true
34
+ }
35
+ return next
36
+ })
37
+ }, [entries])
38
+
39
+ return (
40
+ <div className='config-view__record-list'>
41
+ {entries.map(([key, itemValue]) => {
42
+ const recordValue = (itemValue != null && typeof itemValue === 'object')
43
+ ? itemValue as Record<string, unknown>
44
+ : {}
45
+ const typeValue = typeof recordValue.type === 'string' ? recordValue.type : 'command'
46
+ const envValue = (recordValue.env != null && typeof recordValue.env === 'object')
47
+ ? recordValue.env as Record<string, string>
48
+ : {}
49
+ const headersValue = (recordValue.headers != null && typeof recordValue.headers === 'object')
50
+ ? recordValue.headers as Record<string, string>
51
+ : {}
52
+ const argsValue = Array.isArray(recordValue.args)
53
+ ? recordValue.args.filter(item => typeof item === 'string')
54
+ : []
55
+
56
+ const updateRecord = (nextValue: Record<string, unknown>) => {
57
+ onChange({ ...value, [key]: nextValue })
58
+ }
59
+
60
+ const isCollapsed = collapsedKeys[key] === true
61
+ return (
62
+ <div
63
+ key={key}
64
+ className={`config-view__record-card${isCollapsed ? ' config-view__record-card--collapsed' : ''}`}
65
+ >
66
+ <div className='config-view__record-title'>
67
+ <div className='config-view__record-title-left'>
68
+ <Tooltip title={isCollapsed ? t('config.editor.expand') : t('config.editor.collapse')}>
69
+ <Button
70
+ size='small'
71
+ type='text'
72
+ className='config-view__icon-button config-view__icon-button--compact'
73
+ aria-label={isCollapsed ? t('config.editor.expand') : t('config.editor.collapse')}
74
+ icon={<span className='material-symbols-rounded'>{isCollapsed ? 'chevron_right' : 'expand_more'}</span>}
75
+ onClick={() => {
76
+ setCollapsedKeys(prev => ({ ...prev, [key]: !isCollapsed }))
77
+ }}
78
+ />
79
+ </Tooltip>
80
+ <span>{key}</span>
81
+ </div>
82
+ <Tooltip title={t('config.editor.remove')}>
83
+ <Button
84
+ size='small'
85
+ type='text'
86
+ danger
87
+ className='config-view__icon-button config-view__icon-button--compact'
88
+ aria-label={t('config.editor.remove')}
89
+ icon={<span className='material-symbols-rounded'>delete</span>}
90
+ onClick={() => {
91
+ const updated = { ...value }
92
+ delete updated[key]
93
+ onChange(updated)
94
+ }}
95
+ />
96
+ </Tooltip>
97
+ </div>
98
+ <div className='config-view__record-body'>
99
+ <div className='config-view__record-fields'>
100
+ <FieldRow
101
+ title={t('config.fields.mcpServer.enabled.label')}
102
+ description={t('config.fields.mcpServer.enabled.desc')}
103
+ icon={getTypeIcon('boolean')}
104
+ >
105
+ <Switch
106
+ checked={Boolean(recordValue.enabled)}
107
+ onChange={(next) => {
108
+ updateRecord({ ...recordValue, enabled: next })
109
+ }}
110
+ />
111
+ </FieldRow>
112
+ <FieldRow
113
+ title={t('config.fields.mcpServer.type.label')}
114
+ description={t('config.fields.mcpServer.type.desc')}
115
+ icon={getTypeIcon('string')}
116
+ >
117
+ <Select
118
+ value={typeValue}
119
+ options={[
120
+ { value: 'command', label: t('config.options.mcp.command') },
121
+ { value: 'sse', label: t('config.options.mcp.sse') },
122
+ { value: 'http', label: t('config.options.mcp.http') }
123
+ ]}
124
+ onChange={(next) => {
125
+ const nextRecord = { ...recordValue }
126
+ if (next === 'command') {
127
+ delete nextRecord.type
128
+ if (nextRecord.command == null) nextRecord.command = ''
129
+ if (nextRecord.args == null) nextRecord.args = []
130
+ delete nextRecord.url
131
+ delete nextRecord.headers
132
+ } else {
133
+ nextRecord.type = next
134
+ if (nextRecord.url == null) nextRecord.url = ''
135
+ if (nextRecord.headers == null) nextRecord.headers = {}
136
+ delete nextRecord.command
137
+ delete nextRecord.args
138
+ }
139
+ updateRecord(nextRecord)
140
+ }}
141
+ />
142
+ </FieldRow>
143
+ {typeValue === 'command' && (
144
+ <>
145
+ <FieldRow
146
+ title={t('config.fields.mcpServer.command.label')}
147
+ description={t('config.fields.mcpServer.command.desc')}
148
+ icon={getTypeIcon('string')}
149
+ >
150
+ <Input
151
+ value={typeof recordValue.command === 'string' ? recordValue.command : ''}
152
+ onChange={(event) => {
153
+ updateRecord({ ...recordValue, command: event.target.value })
154
+ }}
155
+ placeholder={t('config.editor.commandPlaceholder')}
156
+ />
157
+ </FieldRow>
158
+ <FieldRow
159
+ title={t('config.fields.mcpServer.args.label')}
160
+ description={t('config.fields.mcpServer.args.desc')}
161
+ icon={getTypeIcon('array')}
162
+ layout='stacked'
163
+ >
164
+ <StringArrayEditor
165
+ value={argsValue}
166
+ onChange={(next) => {
167
+ updateRecord({ ...recordValue, args: next })
168
+ }}
169
+ t={t}
170
+ />
171
+ </FieldRow>
172
+ </>
173
+ )}
174
+ {typeValue !== 'command' && (
175
+ <>
176
+ <FieldRow
177
+ title={t('config.fields.mcpServer.url.label')}
178
+ description={t('config.fields.mcpServer.url.desc')}
179
+ icon={getTypeIcon('string')}
180
+ >
181
+ <Input
182
+ value={typeof recordValue.url === 'string' ? recordValue.url : ''}
183
+ onChange={(event) => {
184
+ updateRecord({ ...recordValue, url: event.target.value })
185
+ }}
186
+ placeholder={t('config.editor.urlPlaceholder')}
187
+ />
188
+ </FieldRow>
189
+ <FieldRow
190
+ title={t('config.fields.mcpServer.headers.label')}
191
+ description={t('config.fields.mcpServer.headers.desc')}
192
+ icon={getTypeIcon('object')}
193
+ layout='stacked'
194
+ >
195
+ <KeyValueEditor
196
+ value={headersValue}
197
+ onChange={(next) => {
198
+ updateRecord({ ...recordValue, headers: next })
199
+ }}
200
+ t={t}
201
+ keyPlaceholder={t('config.editor.newHeaderName')}
202
+ />
203
+ </FieldRow>
204
+ </>
205
+ )}
206
+ <FieldRow
207
+ title={t('config.fields.mcpServer.env.label')}
208
+ description={t('config.fields.mcpServer.env.desc')}
209
+ icon={getTypeIcon('object')}
210
+ layout='stacked'
211
+ >
212
+ <KeyValueEditor
213
+ value={envValue}
214
+ onChange={(next) => {
215
+ updateRecord({ ...recordValue, env: next })
216
+ }}
217
+ t={t}
218
+ keyPlaceholder={t('config.editor.newEnvVarName')}
219
+ />
220
+ </FieldRow>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ )
225
+ })}
226
+ <div className='config-view__record-add'>
227
+ <div className='config-view__record-add-inputs'>
228
+ <Input
229
+ value={newKey}
230
+ placeholder={keyPlaceholder}
231
+ onChange={(event) => setNewKey(event.target.value)}
232
+ />
233
+ <Tooltip title={t('common.confirm')}>
234
+ <Button
235
+ size='small'
236
+ type='primary'
237
+ className='config-view__icon-button'
238
+ aria-label={t('common.confirm')}
239
+ icon={<span className='material-symbols-rounded'>check</span>}
240
+ disabled={newKey.trim() === '' || Object.hasOwn(value, newKey)}
241
+ onClick={() => {
242
+ onChange({
243
+ ...value,
244
+ [newKey]: {
245
+ enabled: true,
246
+ command: '',
247
+ args: []
248
+ }
249
+ })
250
+ setNewKey('')
251
+ }}
252
+ />
253
+ </Tooltip>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ )
258
+ }
@@ -0,0 +1 @@
1
+ @use './RecordEditors.scss';
@@ -0,0 +1,233 @@
1
+ import './ModelServicesRecordEditor.scss'
2
+
3
+ import { Button, Input, Tooltip } from 'antd'
4
+ import { useEffect, useMemo, useState } from 'react'
5
+
6
+ import { ComplexTextEditor, StringArrayEditor } from '../ConfigEditors'
7
+ import { FieldRow } from '../ConfigFieldRow'
8
+ import { getTypeIcon } from '../configUtils'
9
+ import type { TranslationFn } from '../configUtils'
10
+
11
+ export const ModelServicesRecordEditor = ({
12
+ value,
13
+ onChange,
14
+ t,
15
+ keyPlaceholder
16
+ }: {
17
+ value: Record<string, unknown>
18
+ onChange: (nextValue: Record<string, unknown>) => void
19
+ t: TranslationFn
20
+ keyPlaceholder: string
21
+ }) => {
22
+ const [newKey, setNewKey] = useState('')
23
+ const entries = useMemo(() => Object.entries(value), [value])
24
+ const [collapsedKeys, setCollapsedKeys] = useState<Record<string, boolean>>(() => (
25
+ Object.fromEntries(entries.map(([key]) => [key, true]))
26
+ ))
27
+
28
+ useEffect(() => {
29
+ setCollapsedKeys(prev => {
30
+ const next: Record<string, boolean> = {}
31
+ for (const [key] of entries) {
32
+ next[key] = prev[key] ?? true
33
+ }
34
+ return next
35
+ })
36
+ }, [entries])
37
+
38
+ return (
39
+ <div className='config-view__record-list'>
40
+ {entries.map(([key, itemValue]) => {
41
+ const recordValue = (itemValue != null && typeof itemValue === 'object')
42
+ ? itemValue as Record<string, unknown>
43
+ : {}
44
+ const titleValue = typeof recordValue.title === 'string' ? recordValue.title : ''
45
+ const descriptionValue = typeof recordValue.description === 'string' ? recordValue.description : ''
46
+ const displayName = titleValue.trim() !== '' ? titleValue : key
47
+ const models = Array.isArray(recordValue.models)
48
+ ? recordValue.models.filter(item => typeof item === 'string')
49
+ : []
50
+
51
+ const isCollapsed = collapsedKeys[key] === true
52
+ return (
53
+ <div
54
+ key={key}
55
+ className={`config-view__record-card${isCollapsed ? ' config-view__record-card--collapsed' : ''}`}
56
+ >
57
+ <div className='config-view__record-title'>
58
+ <div className='config-view__record-title-left'>
59
+ <Tooltip title={isCollapsed ? t('config.editor.expand') : t('config.editor.collapse')}>
60
+ <Button
61
+ size='small'
62
+ type='text'
63
+ className='config-view__icon-button config-view__icon-button--compact'
64
+ aria-label={isCollapsed ? t('config.editor.expand') : t('config.editor.collapse')}
65
+ icon={<span className='material-symbols-rounded'>{isCollapsed ? 'chevron_right' : 'expand_more'}</span>}
66
+ onClick={() => {
67
+ setCollapsedKeys(prev => ({ ...prev, [key]: !isCollapsed }))
68
+ }}
69
+ />
70
+ </Tooltip>
71
+ <div className='config-view__record-heading'>
72
+ <div>{displayName}</div>
73
+ {displayName !== key && (
74
+ <div className='config-view__record-subtitle'>{key}</div>
75
+ )}
76
+ {descriptionValue !== '' && (
77
+ <div className='config-view__record-desc'>{descriptionValue}</div>
78
+ )}
79
+ </div>
80
+ </div>
81
+ <Tooltip title={t('config.editor.remove')}>
82
+ <Button
83
+ size='small'
84
+ type='text'
85
+ danger
86
+ className='config-view__icon-button config-view__icon-button--compact'
87
+ aria-label={t('config.editor.remove')}
88
+ icon={<span className='material-symbols-rounded'>delete</span>}
89
+ onClick={() => {
90
+ const updated = { ...value }
91
+ delete updated[key]
92
+ onChange(updated)
93
+ }}
94
+ />
95
+ </Tooltip>
96
+ </div>
97
+ <div className='config-view__record-body'>
98
+ <div className='config-view__record-fields'>
99
+ <FieldRow
100
+ title={t('config.fields.modelServices.item.title.label')}
101
+ description={t('config.fields.modelServices.item.title.desc')}
102
+ icon={getTypeIcon('string')}
103
+ >
104
+ <Input
105
+ value={titleValue}
106
+ onChange={(event) => {
107
+ onChange({ ...value, [key]: { ...recordValue, title: event.target.value } })
108
+ }}
109
+ placeholder={t('config.editor.titlePlaceholder')}
110
+ />
111
+ </FieldRow>
112
+ <FieldRow
113
+ title={t('config.fields.modelServices.item.description.label')}
114
+ description={t('config.fields.modelServices.item.description.desc')}
115
+ icon={getTypeIcon('string')}
116
+ layout='stacked'
117
+ >
118
+ <Input.TextArea
119
+ value={descriptionValue}
120
+ onChange={(event) => {
121
+ onChange({ ...value, [key]: { ...recordValue, description: event.target.value } })
122
+ }}
123
+ autoSize={{ minRows: 2 }}
124
+ placeholder={t('config.editor.descriptionPlaceholder')}
125
+ />
126
+ </FieldRow>
127
+ <FieldRow
128
+ title={t('config.fields.modelServices.item.apiBaseUrl.label')}
129
+ description={t('config.fields.modelServices.item.apiBaseUrl.desc')}
130
+ icon={getTypeIcon('string')}
131
+ >
132
+ <Input
133
+ value={typeof recordValue.apiBaseUrl === 'string' ? recordValue.apiBaseUrl : ''}
134
+ onChange={(event) => {
135
+ onChange({ ...value, [key]: { ...recordValue, apiBaseUrl: event.target.value } })
136
+ }}
137
+ />
138
+ </FieldRow>
139
+ <FieldRow
140
+ title={t('config.fields.modelServices.item.apiKey.label')}
141
+ description={t('config.fields.modelServices.item.apiKey.desc')}
142
+ icon={getTypeIcon('string')}
143
+ >
144
+ <Input.Password
145
+ value={typeof recordValue.apiKey === 'string' ? recordValue.apiKey : ''}
146
+ onChange={(event) => {
147
+ onChange({ ...value, [key]: { ...recordValue, apiKey: event.target.value } })
148
+ }}
149
+ placeholder={t('config.editor.secretPlaceholder')}
150
+ />
151
+ </FieldRow>
152
+ <FieldRow
153
+ title={t('config.fields.modelServices.item.models.label')}
154
+ description={t('config.fields.modelServices.item.models.desc')}
155
+ icon={getTypeIcon('array')}
156
+ layout='stacked'
157
+ >
158
+ <StringArrayEditor
159
+ value={models}
160
+ onChange={(next) => {
161
+ onChange({ ...value, [key]: { ...recordValue, models: next } })
162
+ }}
163
+ t={t}
164
+ />
165
+ </FieldRow>
166
+ <FieldRow
167
+ title={t('config.fields.modelServices.item.modelsAlias.label')}
168
+ description={t('config.fields.modelServices.item.modelsAlias.desc')}
169
+ icon={getTypeIcon('object')}
170
+ layout='stacked'
171
+ >
172
+ <ComplexTextEditor
173
+ value={recordValue.modelsAlias ?? {}}
174
+ onChange={(next) => {
175
+ onChange({ ...value, [key]: { ...recordValue, modelsAlias: next } })
176
+ }}
177
+ />
178
+ </FieldRow>
179
+ <FieldRow
180
+ title={t('config.fields.modelServices.item.extra.label')}
181
+ description={t('config.fields.modelServices.item.extra.desc')}
182
+ icon={getTypeIcon('object')}
183
+ layout='stacked'
184
+ >
185
+ <ComplexTextEditor
186
+ value={recordValue.extra ?? {}}
187
+ onChange={(next) => {
188
+ onChange({ ...value, [key]: { ...recordValue, extra: next } })
189
+ }}
190
+ />
191
+ </FieldRow>
192
+ </div>
193
+ </div>
194
+ </div>
195
+ )
196
+ })}
197
+ <div className='config-view__record-add'>
198
+ <div className='config-view__record-add-inputs'>
199
+ <Input
200
+ value={newKey}
201
+ placeholder={keyPlaceholder}
202
+ onChange={(event) => setNewKey(event.target.value)}
203
+ />
204
+ <Tooltip title={t('common.confirm')}>
205
+ <Button
206
+ size='small'
207
+ type='primary'
208
+ className='config-view__icon-button'
209
+ aria-label={t('common.confirm')}
210
+ icon={<span className='material-symbols-rounded'>check</span>}
211
+ disabled={newKey.trim() === '' || Object.hasOwn(value, newKey)}
212
+ onClick={() => {
213
+ onChange({
214
+ ...value,
215
+ [newKey]: {
216
+ title: '',
217
+ description: '',
218
+ apiBaseUrl: '',
219
+ apiKey: '',
220
+ models: [],
221
+ modelsAlias: {},
222
+ extra: {}
223
+ }
224
+ })
225
+ setNewKey('')
226
+ }}
227
+ />
228
+ </Tooltip>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ )
233
+ }
@@ -0,0 +1,117 @@
1
+ .config-view__record-list {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 10px;
5
+ }
6
+
7
+ .config-view__record-card {
8
+ border-radius: 12px;
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: 10px;
12
+ }
13
+
14
+ .config-view__record-title {
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: space-between;
18
+ gap: 8px;
19
+ font-size: 13px;
20
+ font-weight: 600;
21
+ color: var(--text-color);
22
+ }
23
+
24
+ .config-view__record-title-left {
25
+ display: inline-flex;
26
+ align-items: center;
27
+ gap: 6px;
28
+ }
29
+
30
+ .config-view__record-actions {
31
+ display: inline-flex;
32
+ align-items: center;
33
+ gap: 6px;
34
+ }
35
+
36
+ .config-view__record-body {
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 10px;
40
+ }
41
+
42
+ .config-view__record-card--collapsed .config-view__record-body {
43
+ display: none;
44
+ }
45
+
46
+ .config-view__record-heading {
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: 2px;
50
+ }
51
+
52
+ .config-view__record-subtitle {
53
+ font-size: 12px;
54
+ color: var(--sub-text-color);
55
+ }
56
+
57
+ .config-view__record-desc {
58
+ font-size: 12px;
59
+ color: var(--sub-text-color);
60
+ }
61
+
62
+ .config-view__record-fields {
63
+ display: flex;
64
+ flex-direction: column;
65
+ border-radius: 10px;
66
+ }
67
+
68
+ .config-view__record-row {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 8px;
72
+ }
73
+
74
+ .config-view__record-key {
75
+ font-size: 12px;
76
+ color: var(--sub-text-color);
77
+ min-width: 140px;
78
+ }
79
+
80
+ .config-view__record-add {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 8px;
84
+ }
85
+
86
+ .config-view__record-add-inputs {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 8px;
90
+ flex: 1;
91
+ }
92
+
93
+ .config-view__record-add-inputs .ant-input,
94
+ .config-view__record-add-inputs .ant-input-affix-wrapper {
95
+ flex: 1;
96
+ }
97
+
98
+ .config-view__icon-button {
99
+ width: 32px;
100
+ height: 32px;
101
+ min-width: 32px;
102
+ padding: 0;
103
+ display: inline-flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ }
107
+
108
+ .config-view__icon-button--compact {
109
+ width: 28px;
110
+ height: 28px;
111
+ min-width: 28px;
112
+ }
113
+
114
+ .config-view__icon-button--full {
115
+ width: 100%;
116
+ min-width: 100%;
117
+ }
@@ -0,0 +1 @@
1
+ @use './RecordEditors.scss';