@vibe-forge/client 0.5.0 → 0.6.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.
- package/AGENTS.md +37 -0
- package/dist/assets/{arc-C4ymrcSQ.js → arc-CMAHd5G3.js} +1 -1
- package/dist/assets/{blockDiagram-c4efeb88-CeB7-kgP.js → blockDiagram-c4efeb88-DKww-VCP.js} +1 -1
- package/dist/assets/{c4Diagram-c83219d4-C935Im8S.js → c4Diagram-c83219d4-DKrjVHyY.js} +1 -1
- package/dist/assets/channel-Bi4g8rj9.js +1 -0
- package/dist/assets/{classDiagram-beda092f-B9IV13KI.js → classDiagram-beda092f-BXx5rdo3.js} +1 -1
- package/dist/assets/{classDiagram-v2-2358418a-CXF_K4fE.js → classDiagram-v2-2358418a-CnR3WLsr.js} +1 -1
- package/dist/assets/clone-DPrpP2ky.js +1 -0
- package/dist/assets/{createText-1719965b-DwX8iC5F.js → createText-1719965b-CmOsl1W7.js} +1 -1
- package/dist/assets/{edges-96097737-9P1uH1RE.js → edges-96097737-CQeQgpjD.js} +1 -1
- package/dist/assets/{erDiagram-0228fc6a-ixeGTFvg.js → erDiagram-0228fc6a-ZUNB-ucF.js} +1 -1
- package/dist/assets/{flowDb-c6c81e3f-G1gSTTBI.js → flowDb-c6c81e3f-DuuKeSLX.js} +1 -1
- package/dist/assets/{flowDiagram-50d868cf-CzrG99nD.js → flowDiagram-50d868cf-Bc6n85yR.js} +1 -1
- package/dist/assets/flowDiagram-v2-4f6560a1-BZqaeqoh.js +1 -0
- package/dist/assets/{flowchart-elk-definition-6af322e1-sFCoysWa.js → flowchart-elk-definition-6af322e1-cAG5afW9.js} +1 -1
- package/dist/assets/{ganttDiagram-a2739b55-Ccsk_Lru.js → ganttDiagram-a2739b55-Dp6xhY5I.js} +1 -1
- package/dist/assets/{gitGraphDiagram-82fe8481-CwathJ6H.js → gitGraphDiagram-82fe8481-MlIIRBdG.js} +1 -1
- package/dist/assets/{graph-DRCU-8Rz.js → graph-D7Es8jZ-.js} +1 -1
- package/dist/assets/{index-5325376f-Bq-fg2i_.js → index-5325376f-DC18ottv.js} +1 -1
- package/dist/assets/index-D37AbgPQ.js +545 -0
- package/dist/assets/{index-CHMuZ5-1.css → index-fcJ9v94I.css} +1 -1
- package/dist/assets/{infoDiagram-8eee0895-JBcUkJ6T.js → infoDiagram-8eee0895-CXk21kFp.js} +1 -1
- package/dist/assets/{journeyDiagram-c64418c1-DsdQU-R8.js → journeyDiagram-c64418c1-899BKBHL.js} +1 -1
- package/dist/assets/{layout-s0slG1OL.js → layout-DLaxdy48.js} +1 -1
- package/dist/assets/{line-CymFqgW6.js → line-_lw5YbRM.js} +1 -1
- package/dist/assets/{linear-lDQVZ6aQ.js → linear-D5iu84ui.js} +1 -1
- package/dist/assets/{mermaid.core-Cmlqga_E.js → mermaid.core-C6sW3GFM.js} +4 -4
- package/dist/assets/{mindmap-definition-8da855dc-CqqTDJn_.js → mindmap-definition-8da855dc-BS9Xy9KN.js} +1 -1
- package/dist/assets/{pieDiagram-a8764435-BL2Ajx7Z.js → pieDiagram-a8764435-DZt9cEgs.js} +1 -1
- package/dist/assets/{quadrantDiagram-1e28029f-ClL_3ASt.js → quadrantDiagram-1e28029f-BTIeHOgn.js} +1 -1
- package/dist/assets/{requirementDiagram-08caed73-CB1RgE3K.js → requirementDiagram-08caed73-BHJAKD2g.js} +1 -1
- package/dist/assets/{sankeyDiagram-a04cb91d-tgleEYiD.js → sankeyDiagram-a04cb91d-DnAkVOK8.js} +1 -1
- package/dist/assets/{sequenceDiagram-c5b8d532-DlatQT5R.js → sequenceDiagram-c5b8d532-92tE3oFv.js} +1 -1
- package/dist/assets/{stateDiagram-1ecb1508-B--MLqRs.js → stateDiagram-1ecb1508-DG0ObiMg.js} +1 -1
- package/dist/assets/{stateDiagram-v2-c2b004d7-CRMZ6Dpx.js → stateDiagram-v2-c2b004d7-BKoJx2ci.js} +1 -1
- package/dist/assets/{styles-b4e223ce-CPiYHfUz.js → styles-b4e223ce-Ba6G4ri9.js} +1 -1
- package/dist/assets/{styles-ca3715f6-B9UKPAzX.js → styles-ca3715f6-Bn6RIIVW.js} +1 -1
- package/dist/assets/{styles-d45a18b0-BC1Ak1So.js → styles-d45a18b0-_dELBUI6.js} +1 -1
- package/dist/assets/{svgDrawCommon-b86b1483-DV8R0g-n.js → svgDrawCommon-b86b1483-CRK-ZoIs.js} +1 -1
- package/dist/assets/{timeline-definition-faaaa080-CiqGS5DC.js → timeline-definition-faaaa080-DvQ_RA_i.js} +1 -1
- package/dist/assets/{xychartDiagram-f5964ef8-h6VSD3GE.js → xychartDiagram-f5964ef8-CJxeDLfg.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +10 -8
- package/src/App.tsx +20 -168
- package/src/api/base.ts +116 -7
- package/src/api.ts +3 -1
- package/src/components/ArchiveView.tsx +5 -5
- package/src/components/ConfigView.tsx +3 -3
- package/src/components/{AutomationView → automation-view}/index.tsx +10 -9
- package/src/components/{BenchmarkView → benchmark-view}/index.tsx +5 -4
- package/src/components/chat/ChatHeader.tsx +6 -6
- package/src/components/chat/ChatHistoryView.tsx +64 -16
- package/src/components/chat/ChatTimelineView.tsx +3 -3
- package/src/components/chat/CurrentTodoList.scss +56 -27
- package/src/components/chat/{Sender → sender}/Sender.scss +201 -71
- package/src/components/chat/{Sender → sender}/Sender.tsx +104 -42
- package/src/components/chat/tools/core/ToolGroup.tsx +1 -1
- package/src/components/config/ConfigSectionForm.tsx +1 -1
- package/src/components/layout/AppShell.scss +19 -0
- package/src/components/layout/AppShell.tsx +45 -0
- package/src/hooks/chat/model-selector.ts +150 -0
- package/src/hooks/chat/use-chat-adapter.ts +20 -8
- package/src/hooks/chat/use-chat-models.tsx +79 -74
- package/src/hooks/chat/use-chat-permission-mode.ts +14 -10
- package/src/hooks/chat/use-chat-session-actions.ts +13 -10
- package/src/hooks/chat/use-chat-session-messages.ts +46 -6
- package/src/hooks/chat/use-chat-session.ts +42 -1
- package/src/hooks/use-app-preferences.ts +41 -0
- package/src/hooks/use-session-subscription.ts +101 -0
- package/src/hooks/use-sidebar-navigation.ts +35 -0
- package/src/resources/locales/en.json +6 -0
- package/src/resources/locales/zh.json +6 -0
- package/src/routes/AppRoutes.tsx +22 -0
- package/src/routes/ArchiveRoute.tsx +5 -0
- package/src/routes/AutomationRoute.tsx +5 -0
- package/src/routes/BenchmarkRoute.tsx +5 -0
- package/src/{components/Chat.scss → routes/ChatRoute.scss} +35 -0
- package/src/{components/Chat.tsx → routes/ChatRoute.tsx} +54 -28
- package/src/routes/ConfigRoute.tsx +5 -0
- package/src/routes/KnowledgeRoute.tsx +5 -0
- package/dist/assets/channel-84s1ACzD.js +0 -1
- package/dist/assets/clone-B2E8tddE.js +0 -1
- package/dist/assets/flowDiagram-v2-4f6560a1-CJfJYbME.js +0 -1
- package/dist/assets/index-cGZvDhhU.js +0 -542
- /package/src/components/{AutomationView → automation-view}/RuleFormPanel.scss +0 -0
- /package/src/components/{AutomationView → automation-view}/RuleFormPanel.tsx +0 -0
- /package/src/components/{AutomationView → automation-view}/RuleSidebar.scss +0 -0
- /package/src/components/{AutomationView → automation-view}/RuleSidebar.tsx +0 -0
- /package/src/components/{AutomationView → automation-view}/RunHistoryPanel.scss +0 -0
- /package/src/components/{AutomationView → automation-view}/RunHistoryPanel.tsx +0 -0
- /package/src/components/{AutomationView → automation-view}/TaskList.scss +0 -0
- /package/src/components/{AutomationView → automation-view}/TaskList.tsx +0 -0
- /package/src/components/{AutomationView → automation-view}/TriggerList.scss +0 -0
- /package/src/components/{AutomationView → automation-view}/TriggerList.tsx +0 -0
- /package/src/components/{AutomationView/AutomationView.scss → automation-view/index.scss} +0 -0
- /package/src/components/{AutomationView → automation-view}/types.ts +0 -0
- /package/src/components/{BenchmarkView → benchmark-view}/BenchmarkCasePanel.scss +0 -0
- /package/src/components/{BenchmarkView → benchmark-view}/BenchmarkCasePanel.tsx +0 -0
- /package/src/components/{BenchmarkView → benchmark-view}/BenchmarkSidebar.scss +0 -0
- /package/src/components/{BenchmarkView → benchmark-view}/BenchmarkSidebar.tsx +0 -0
- /package/src/components/{BenchmarkView → benchmark-view}/BenchmarkView.scss +0 -0
- /package/src/components/{BenchmarkView → benchmark-view}/types.ts +0 -0
- /package/src/components/{BenchmarkView → benchmark-view}/utils.ts +0 -0
- /package/src/components/chat/{Messages → messages}/MessageFooter.tsx +0 -0
- /package/src/components/chat/{Messages → messages}/MessageItem.scss +0 -0
- /package/src/components/chat/{Messages → messages}/MessageItem.tsx +0 -0
- /package/src/components/chat/{Messages → messages}/message-utils.ts +0 -0
- /package/src/components/chat/{Sender → sender}/CompletionMenu.scss +0 -0
- /package/src/components/chat/{Sender → sender}/CompletionMenu.tsx +0 -0
- /package/src/components/chat/{Sender → sender}/ThinkingStatus.scss +0 -0
- /package/src/components/chat/{Sender → sender}/ThinkingStatus.tsx +0 -0
- /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/EventList.scss +0 -0
- /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/EventList.tsx +0 -0
- /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/gantt.ts +0 -0
- /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/git-graph.ts +0 -0
- /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/index.scss +0 -0
- /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/index.tsx +0 -0
- /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/mermaid.ts +0 -0
- /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/types.ts +0 -0
- /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/utils.ts +0 -0
- /package/src/components/config/{recordEditors → record-editors}/BooleanRecordEditor.scss +0 -0
- /package/src/components/config/{recordEditors → record-editors}/BooleanRecordEditor.tsx +0 -0
- /package/src/components/config/{recordEditors → record-editors}/ChannelRecordEditor.scss +0 -0
- /package/src/components/config/{recordEditors → record-editors}/ChannelRecordEditor.tsx +0 -0
- /package/src/components/config/{recordEditors → record-editors}/KeyValueEditor.scss +0 -0
- /package/src/components/config/{recordEditors → record-editors}/KeyValueEditor.tsx +0 -0
- /package/src/components/config/{recordEditors → record-editors}/McpServersRecordEditor.scss +0 -0
- /package/src/components/config/{recordEditors → record-editors}/McpServersRecordEditor.tsx +0 -0
- /package/src/components/config/{recordEditors → record-editors}/ModelServicesRecordEditor.scss +0 -0
- /package/src/components/config/{recordEditors → record-editors}/ModelServicesRecordEditor.tsx +0 -0
- /package/src/components/config/{recordEditors → record-editors}/RecordEditors.scss +0 -0
- /package/src/components/config/{recordEditors → record-editors}/RecordJsonEditor.scss +0 -0
- /package/src/components/config/{recordEditors → record-editors}/RecordJsonEditor.tsx +0 -0
- /package/src/components/config/{recordEditors → record-editors}/index.tsx +0 -0
|
@@ -2,13 +2,21 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
|
|
2
2
|
import { useTranslation } from 'react-i18next'
|
|
3
3
|
import useSWR from 'swr'
|
|
4
4
|
|
|
5
|
-
import type { AdapterBuiltinModel, ConfigResponse, ModelServiceConfig, RecommendedModelConfig } from '@vibe-forge/core'
|
|
6
5
|
import { getConfig } from '#~/api.js'
|
|
6
|
+
import type { AdapterBuiltinModel, ConfigResponse, ModelServiceConfig, RecommendedModelConfig } from '@vibe-forge/core'
|
|
7
|
+
import {
|
|
8
|
+
buildServiceModelSelector,
|
|
9
|
+
listServiceModels,
|
|
10
|
+
resolveChatModelSelection,
|
|
11
|
+
resolveDefaultChatModelSelection,
|
|
12
|
+
resolveServiceModelSelector
|
|
13
|
+
} from './model-selector'
|
|
7
14
|
|
|
8
15
|
export interface ModelSelectOption {
|
|
9
16
|
value: string
|
|
10
17
|
label: React.ReactNode
|
|
11
18
|
searchText: string
|
|
19
|
+
displayLabel: string
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
export interface ModelSelectGroup {
|
|
@@ -71,87 +79,69 @@ export function useChatModels({
|
|
|
71
79
|
|
|
72
80
|
const modelServiceEntries = useMemo(() => Object.entries(mergedModelServices), [mergedModelServices])
|
|
73
81
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
for (const [serviceKey, serviceValue] of modelServiceEntries) {
|
|
77
|
-
const service = (serviceValue != null && typeof serviceValue === 'object')
|
|
78
|
-
? serviceValue as ModelServiceConfig
|
|
79
|
-
: undefined
|
|
80
|
-
const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
|
|
81
|
-
const models = Array.isArray(service?.models) ? service?.models.filter(item => typeof item === 'string') : []
|
|
82
|
-
for (const model of models) {
|
|
83
|
-
list.push({ model, serviceKey, serviceTitle })
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return list
|
|
87
|
-
}, [modelServiceEntries])
|
|
88
|
-
|
|
89
|
-
const availableModelValues = useMemo(() => availableModels.map(item => item.model), [availableModels])
|
|
90
|
-
const availableModelKey = useMemo(() => availableModelValues.join('|'), [availableModelValues])
|
|
91
|
-
const availableModelSet = useMemo(() => new Set(availableModelValues), [availableModelKey])
|
|
92
|
-
const hasAvailableModels = availableModelValues.length > 0 || builtinModelSet.size > 0
|
|
82
|
+
const availableServiceModels = useMemo(() => listServiceModels(mergedModelServices), [mergedModelServices])
|
|
83
|
+
const hasAvailableModels = availableServiceModels.length > 0 || builtinModelSet.size > 0
|
|
93
84
|
const modelToService = useMemo(() => {
|
|
94
85
|
const map = new Map<string, { key: string; title: string }>()
|
|
95
|
-
for (const entry of
|
|
86
|
+
for (const entry of availableServiceModels) {
|
|
87
|
+
const serviceValue = mergedModelServices[entry.serviceKey]
|
|
88
|
+
const serviceTitle = serviceValue?.title?.trim() !== '' ? serviceValue?.title ?? '' : entry.serviceKey
|
|
96
89
|
if (!map.has(entry.model)) {
|
|
97
|
-
map.set(entry.model, { key: entry.serviceKey, title:
|
|
90
|
+
map.set(entry.model, { key: entry.serviceKey, title: serviceTitle })
|
|
98
91
|
}
|
|
99
92
|
}
|
|
100
93
|
return map
|
|
101
|
-
}, [
|
|
94
|
+
}, [availableServiceModels, mergedModelServices])
|
|
102
95
|
const defaultModelService = configRes?.sources?.merged?.general?.defaultModelService
|
|
103
96
|
const defaultModel = configRes?.sources?.merged?.general?.defaultModel
|
|
104
97
|
const formatModelWithService = useCallback((model: string | undefined) => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}, [builtinModelSet, defaultModelService, modelToService])
|
|
98
|
+
return resolveChatModelSelection({
|
|
99
|
+
value: model,
|
|
100
|
+
builtinModels: activeBuiltinModelValues,
|
|
101
|
+
serviceModels: availableServiceModels,
|
|
102
|
+
defaultModelService
|
|
103
|
+
})
|
|
104
|
+
}, [activeBuiltinModelValues, availableServiceModels, defaultModelService])
|
|
113
105
|
const resolvedDefaultModel = useMemo(() => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const service = mergedModelServices[defaultModelService]
|
|
121
|
-
const models = Array.isArray(service?.models) ? service?.models.filter(item => typeof item === 'string') : []
|
|
122
|
-
if (models.length > 0) return models[0]
|
|
123
|
-
}
|
|
124
|
-
if (availableModelValues.length > 0) return availableModelValues[0]
|
|
125
|
-
// Fall back to first builtin model from the active adapter
|
|
126
|
-
const firstBuiltin = Object.values(activeBuiltinModels).flat()[0]
|
|
127
|
-
return firstBuiltin?.value
|
|
106
|
+
return resolveDefaultChatModelSelection({
|
|
107
|
+
defaultModel,
|
|
108
|
+
defaultModelService,
|
|
109
|
+
builtinModels: activeBuiltinModelValues,
|
|
110
|
+
serviceModels: availableServiceModels
|
|
111
|
+
})
|
|
128
112
|
}, [
|
|
129
|
-
activeBuiltinModels,
|
|
130
113
|
activeBuiltinModelValues,
|
|
131
|
-
|
|
132
|
-
availableModelValues,
|
|
133
|
-
builtinModelSet,
|
|
114
|
+
availableServiceModels,
|
|
134
115
|
defaultModel,
|
|
135
|
-
defaultModelService
|
|
136
|
-
mergedModelServices
|
|
116
|
+
defaultModelService
|
|
137
117
|
])
|
|
138
118
|
const selectedModelWithService = useMemo(() => (
|
|
139
|
-
formatModelWithService(selectedModel)
|
|
140
|
-
), [formatModelWithService, selectedModel])
|
|
119
|
+
formatModelWithService(selectedModel) ?? resolvedDefaultModel
|
|
120
|
+
), [formatModelWithService, resolvedDefaultModel, selectedModel])
|
|
121
|
+
|
|
122
|
+
const resolveSelectableModel = useCallback((value?: string) => {
|
|
123
|
+
return resolveChatModelSelection({
|
|
124
|
+
value,
|
|
125
|
+
builtinModels: activeBuiltinModelValues,
|
|
126
|
+
serviceModels: availableServiceModels,
|
|
127
|
+
defaultModelService
|
|
128
|
+
}) ?? resolvedDefaultModel
|
|
129
|
+
}, [activeBuiltinModelValues, availableServiceModels, defaultModelService, resolvedDefaultModel])
|
|
130
|
+
|
|
131
|
+
const updateSelectedModel = useCallback((value?: string) => {
|
|
132
|
+
setSelectedModel((prev) => {
|
|
133
|
+
const nextValue = resolveSelectableModel(value)
|
|
134
|
+
return nextValue === prev ? prev : nextValue
|
|
135
|
+
})
|
|
136
|
+
}, [resolveSelectableModel])
|
|
141
137
|
|
|
142
138
|
useEffect(() => {
|
|
143
139
|
if (!hasAvailableModels) {
|
|
144
140
|
setSelectedModel(undefined)
|
|
145
141
|
return
|
|
146
142
|
}
|
|
147
|
-
setSelectedModel((prev) =>
|
|
148
|
-
|
|
149
|
-
const isValid = availableModelSet.has(prev) || builtinModelSet.has(prev)
|
|
150
|
-
if (isValid) return prev
|
|
151
|
-
}
|
|
152
|
-
return resolvedDefaultModel
|
|
153
|
-
})
|
|
154
|
-
}, [availableModelSet, builtinModelSet, hasAvailableModels, resolvedDefaultModel, selectedAdapter])
|
|
143
|
+
setSelectedModel((prev) => resolveSelectableModel(prev))
|
|
144
|
+
}, [hasAvailableModels, resolveSelectableModel, selectedAdapter])
|
|
155
145
|
|
|
156
146
|
useEffect(() => {
|
|
157
147
|
try {
|
|
@@ -191,7 +181,8 @@ export function useChatModels({
|
|
|
191
181
|
return {
|
|
192
182
|
value: params.value,
|
|
193
183
|
label,
|
|
194
|
-
searchText
|
|
184
|
+
searchText,
|
|
185
|
+
displayLabel: params.title
|
|
195
186
|
}
|
|
196
187
|
}
|
|
197
188
|
|
|
@@ -212,14 +203,16 @@ export function useChatModels({
|
|
|
212
203
|
const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
|
|
213
204
|
const groupTitle = serviceTitle?.trim() !== '' ? serviceTitle : serviceKey
|
|
214
205
|
const serviceDescription = service?.description
|
|
215
|
-
const models = Array.isArray(service?.models)
|
|
206
|
+
const models = Array.isArray(service?.models)
|
|
207
|
+
? service.models.filter((item): item is string => typeof item === 'string')
|
|
208
|
+
: []
|
|
216
209
|
if (models.length === 0) return null
|
|
217
|
-
const options = models.map((model) => {
|
|
210
|
+
const options = models.map((model: string) => {
|
|
218
211
|
const alias = resolveFirstAlias(service?.modelsAlias as Record<string, string[]> | undefined, model)
|
|
219
212
|
const title = alias ?? model
|
|
220
213
|
const description = alias ? model : serviceTitle
|
|
221
214
|
return buildOption({
|
|
222
|
-
value: model,
|
|
215
|
+
value: buildServiceModelSelector(serviceKey, model),
|
|
223
216
|
title,
|
|
224
217
|
description,
|
|
225
218
|
serviceKey,
|
|
@@ -241,7 +234,11 @@ export function useChatModels({
|
|
|
241
234
|
const recommendedOptions = recommendedModels
|
|
242
235
|
.filter((item) => {
|
|
243
236
|
if (item.placement && item.placement !== 'modelSelector') return false
|
|
244
|
-
return
|
|
237
|
+
return resolveServiceModelSelector({
|
|
238
|
+
value: item.service ? buildServiceModelSelector(item.service, item.model) : item.model,
|
|
239
|
+
serviceModels: availableServiceModels,
|
|
240
|
+
preferredServiceKey: item.service ?? defaultModelService
|
|
241
|
+
}) != null
|
|
245
242
|
})
|
|
246
243
|
.map((item) => {
|
|
247
244
|
const serviceInfo = item.service ? mergedModelServices[item.service] : undefined
|
|
@@ -255,8 +252,13 @@ export function useChatModels({
|
|
|
255
252
|
const description = item.description?.trim() !== ''
|
|
256
253
|
? item.description
|
|
257
254
|
: serviceTitle
|
|
255
|
+
const value = resolveServiceModelSelector({
|
|
256
|
+
value: item.service ? buildServiceModelSelector(item.service, item.model) : item.model,
|
|
257
|
+
serviceModels: availableServiceModels,
|
|
258
|
+
preferredServiceKey: item.service ?? defaultModelService
|
|
259
|
+
}) ?? item.model
|
|
258
260
|
return buildOption({
|
|
259
|
-
value
|
|
261
|
+
value,
|
|
260
262
|
title,
|
|
261
263
|
description,
|
|
262
264
|
serviceKey: item.service ?? modelToService.get(item.model)?.key,
|
|
@@ -290,18 +292,21 @@ export function useChatModels({
|
|
|
290
292
|
<div className='model-group-title'>{adapterTitle}</div>
|
|
291
293
|
</div>
|
|
292
294
|
),
|
|
293
|
-
options: models.map(m =>
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
options: models.map(m =>
|
|
296
|
+
buildOption({
|
|
297
|
+
value: m.value,
|
|
298
|
+
title: m.title,
|
|
299
|
+
description: m.description
|
|
300
|
+
})
|
|
301
|
+
)
|
|
298
302
|
})
|
|
299
303
|
}
|
|
300
304
|
|
|
301
305
|
return [...groups, ...serviceGroups]
|
|
302
306
|
}, [
|
|
303
307
|
activeBuiltinModels,
|
|
304
|
-
|
|
308
|
+
availableServiceModels,
|
|
309
|
+
defaultModelService,
|
|
305
310
|
modelToService,
|
|
306
311
|
mergedModelServices,
|
|
307
312
|
modelServiceEntries,
|
|
@@ -312,7 +317,7 @@ export function useChatModels({
|
|
|
312
317
|
return {
|
|
313
318
|
selectedModel,
|
|
314
319
|
selectedModelWithService,
|
|
315
|
-
setSelectedModel,
|
|
320
|
+
setSelectedModel: updateSelectedModel,
|
|
316
321
|
modelOptions,
|
|
317
322
|
hasAvailableModels
|
|
318
323
|
}
|
|
@@ -5,22 +5,26 @@ export type PermissionMode = 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'b
|
|
|
5
5
|
|
|
6
6
|
const PERMISSION_MODE_STORAGE_KEY = 'vf_chat_permission_mode'
|
|
7
7
|
|
|
8
|
-
const isPermissionMode = (value: string): value is PermissionMode => {
|
|
9
|
-
return value === 'default'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
export const isPermissionMode = (value: string): value is PermissionMode => {
|
|
9
|
+
return value === 'default' ||
|
|
10
|
+
value === 'acceptEdits' ||
|
|
11
|
+
value === 'plan' ||
|
|
12
|
+
value === 'dontAsk' ||
|
|
13
|
+
value === 'bypassPermissions'
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function useChatPermissionMode() {
|
|
17
17
|
const [permissionMode, setPermissionMode] = useState<PermissionMode>('default')
|
|
18
18
|
|
|
19
|
+
const updatePermissionMode = (value?: string) => {
|
|
20
|
+
setPermissionMode(isPermissionMode(value ?? '') ? value : 'default')
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
useEffect(() => {
|
|
20
24
|
try {
|
|
21
25
|
const raw = localStorage.getItem(PERMISSION_MODE_STORAGE_KEY)
|
|
22
26
|
if (raw && isPermissionMode(raw)) {
|
|
23
|
-
|
|
27
|
+
updatePermissionMode(raw)
|
|
24
28
|
}
|
|
25
29
|
} catch {}
|
|
26
30
|
}, [])
|
|
@@ -31,17 +35,17 @@ export function useChatPermissionMode() {
|
|
|
31
35
|
} catch {}
|
|
32
36
|
}, [permissionMode])
|
|
33
37
|
|
|
34
|
-
const permissionModeOptions = useMemo<Array<{ value: PermissionMode; label: ReactNode }>>(() =>
|
|
38
|
+
const permissionModeOptions = useMemo<Array<{ value: PermissionMode; label: ReactNode }>>(() => [
|
|
35
39
|
{ value: 'default', label: '默认' },
|
|
36
40
|
{ value: 'acceptEdits', label: '接受编辑' },
|
|
37
41
|
{ value: 'plan', label: '计划' },
|
|
38
42
|
{ value: 'dontAsk', label: '不询问' },
|
|
39
43
|
{ value: 'bypassPermissions', label: '跳过权限' }
|
|
40
|
-
]
|
|
44
|
+
], [])
|
|
41
45
|
|
|
42
46
|
return {
|
|
43
47
|
permissionMode,
|
|
44
|
-
setPermissionMode,
|
|
48
|
+
setPermissionMode: updatePermissionMode,
|
|
45
49
|
permissionModeOptions
|
|
46
50
|
}
|
|
47
51
|
}
|
|
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
|
|
|
4
4
|
import { useNavigate } from 'react-router-dom'
|
|
5
5
|
import { useSWRConfig } from 'swr'
|
|
6
6
|
|
|
7
|
-
import { createSession } from '#~/api.js'
|
|
7
|
+
import { createSession, getApiErrorMessage } from '#~/api.js'
|
|
8
8
|
import { connectionManager } from '#~/connectionManager.js'
|
|
9
9
|
import type { ChatMessageContent, Session } from '@vibe-forge/core'
|
|
10
10
|
import type { PermissionMode } from './use-chat-permission-mode'
|
|
@@ -32,10 +32,10 @@ export function useChatSessionActions({
|
|
|
32
32
|
const isThinking = isCreating || session?.status === 'running'
|
|
33
33
|
|
|
34
34
|
const send = useCallback(async (text: string) => {
|
|
35
|
-
if (text.trim() === '' || isThinking) return
|
|
35
|
+
if (text.trim() === '' || isThinking) return false
|
|
36
36
|
if (!hasAvailableModels) {
|
|
37
37
|
void message.warning(t('chat.modelConfigRequired'))
|
|
38
|
-
return
|
|
38
|
+
return false
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
if (!session?.id) {
|
|
@@ -55,18 +55,20 @@ export function useChatSessionActions({
|
|
|
55
55
|
}, false)
|
|
56
56
|
|
|
57
57
|
void navigate(`/session/${newSession.id}`)
|
|
58
|
+
return true
|
|
58
59
|
} catch (err) {
|
|
59
60
|
console.error(err)
|
|
60
61
|
setIsCreating(false)
|
|
61
|
-
void message.error('Failed to create session')
|
|
62
|
+
void message.error(getApiErrorMessage(err, 'Failed to create session'))
|
|
63
|
+
return false
|
|
62
64
|
}
|
|
63
|
-
return
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
connectionManager.send(session.id, {
|
|
67
68
|
type: 'user_message',
|
|
68
69
|
text: text.trim()
|
|
69
70
|
})
|
|
71
|
+
return true
|
|
70
72
|
}, [
|
|
71
73
|
adapter,
|
|
72
74
|
hasAvailableModels,
|
|
@@ -81,10 +83,10 @@ export function useChatSessionActions({
|
|
|
81
83
|
])
|
|
82
84
|
|
|
83
85
|
const sendContent = useCallback(async (content: ChatMessageContent[]) => {
|
|
84
|
-
if (content.length === 0 || isThinking) return
|
|
86
|
+
if (content.length === 0 || isThinking) return false
|
|
85
87
|
if (!hasAvailableModels) {
|
|
86
88
|
void message.warning(t('chat.modelConfigRequired'))
|
|
87
|
-
return
|
|
89
|
+
return false
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
if (!session?.id) {
|
|
@@ -104,19 +106,20 @@ export function useChatSessionActions({
|
|
|
104
106
|
}, false)
|
|
105
107
|
|
|
106
108
|
void navigate(`/session/${newSession.id}`)
|
|
107
|
-
|
|
109
|
+
return true
|
|
108
110
|
} catch (err) {
|
|
109
111
|
console.error(err)
|
|
110
112
|
setIsCreating(false)
|
|
111
|
-
void message.error('Failed to create session')
|
|
113
|
+
void message.error(getApiErrorMessage(err, 'Failed to create session'))
|
|
114
|
+
return false
|
|
112
115
|
}
|
|
113
|
-
return
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
connectionManager.send(session.id, {
|
|
117
119
|
type: 'user_message',
|
|
118
120
|
content
|
|
119
121
|
})
|
|
122
|
+
return true
|
|
120
123
|
}, [
|
|
121
124
|
adapter,
|
|
122
125
|
hasAvailableModels,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { App } from 'antd'
|
|
2
|
-
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
3
|
+
import { useTranslation } from 'react-i18next'
|
|
3
4
|
import { useSWRConfig } from 'swr'
|
|
4
5
|
|
|
5
6
|
import { getSessionMessages } from '#~/api.js'
|
|
@@ -47,20 +48,32 @@ export function useChatSessionMessages({
|
|
|
47
48
|
adapter?: string
|
|
48
49
|
setInteractionRequest: (value: { id: string; payload: AskUserQuestionParams } | null) => void
|
|
49
50
|
}) {
|
|
50
|
-
const {
|
|
51
|
+
const { t } = useTranslation()
|
|
51
52
|
const { mutate } = useSWRConfig()
|
|
52
53
|
const [messages, setMessages] = useState<ChatMessage[]>([])
|
|
53
54
|
const [sessionInfo, setSessionInfo] = useState<SessionInfo | null>(null)
|
|
54
55
|
const [isReady, setIsReady] = useState(false)
|
|
56
|
+
const [connectionError, setConnectionError] = useState<string | null>(null)
|
|
57
|
+
const [retryCount, setRetryCount] = useState(0)
|
|
55
58
|
const isInitialLoadRef = useRef<boolean>(true)
|
|
56
59
|
const lastConnectedModelRef = useRef<string | undefined>(undefined)
|
|
57
60
|
const lastConnectedPermissionModeRef = useRef<string | undefined>(undefined)
|
|
58
61
|
const lastConnectedAdapterRef = useRef<string | undefined>(undefined)
|
|
62
|
+
const expectedCloseRef = useRef(false)
|
|
63
|
+
|
|
64
|
+
const retryConnection = useCallback(() => {
|
|
65
|
+
if (session?.id == null || session.id === '') return
|
|
66
|
+
expectedCloseRef.current = true
|
|
67
|
+
setConnectionError(null)
|
|
68
|
+
connectionManager.close(session.id)
|
|
69
|
+
setRetryCount((count) => count + 1)
|
|
70
|
+
}, [session?.id])
|
|
59
71
|
|
|
60
72
|
useEffect(() => {
|
|
61
73
|
setMessages([])
|
|
62
74
|
setSessionInfo(null)
|
|
63
75
|
setIsReady(false)
|
|
76
|
+
setConnectionError(null)
|
|
64
77
|
setInteractionRequest(null)
|
|
65
78
|
isInitialLoadRef.current = true
|
|
66
79
|
|
|
@@ -68,6 +81,7 @@ export function useChatSessionMessages({
|
|
|
68
81
|
setIsReady(true)
|
|
69
82
|
lastConnectedModelRef.current = undefined
|
|
70
83
|
lastConnectedPermissionModeRef.current = undefined
|
|
84
|
+
lastConnectedAdapterRef.current = undefined
|
|
71
85
|
return
|
|
72
86
|
}
|
|
73
87
|
|
|
@@ -150,6 +164,8 @@ export function useChatSessionMessages({
|
|
|
150
164
|
normalizedAdapter !== lastConnectedAdapterRef.current &&
|
|
151
165
|
session?.status !== 'running'
|
|
152
166
|
if (modelChanged || permissionModeChanged || adapterChanged) {
|
|
167
|
+
expectedCloseRef.current = true
|
|
168
|
+
setConnectionError(null)
|
|
153
169
|
connectionManager.send(session.id, { type: 'terminate_session' })
|
|
154
170
|
connectionManager.close(session.id)
|
|
155
171
|
}
|
|
@@ -173,11 +189,13 @@ export function useChatSessionMessages({
|
|
|
173
189
|
|
|
174
190
|
cleanup = connectionManager.connect(session.id, {
|
|
175
191
|
onOpen() {
|
|
192
|
+
expectedCloseRef.current = false
|
|
193
|
+
setConnectionError(null)
|
|
176
194
|
},
|
|
177
195
|
onMessage(data: WSEvent) {
|
|
178
196
|
if (isDisposed) return
|
|
179
197
|
if (data.type === 'error') {
|
|
180
|
-
|
|
198
|
+
setConnectionError(data.message)
|
|
181
199
|
return
|
|
182
200
|
}
|
|
183
201
|
|
|
@@ -241,22 +259,44 @@ export function useChatSessionMessages({
|
|
|
241
259
|
setInteractionRequest({ id: data.id, payload: data.payload })
|
|
242
260
|
}
|
|
243
261
|
},
|
|
262
|
+
onError() {
|
|
263
|
+
if (isDisposed) return
|
|
264
|
+
setConnectionError(t('chat.connectionError'))
|
|
265
|
+
},
|
|
244
266
|
onClose() {
|
|
267
|
+
if (isDisposed) return
|
|
268
|
+
if (expectedCloseRef.current) {
|
|
269
|
+
expectedCloseRef.current = false
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
setConnectionError((current) => current ?? t('chat.connectionClosed'))
|
|
245
273
|
}
|
|
246
274
|
}, Object.keys(connectionParams).length > 0 ? connectionParams : undefined)
|
|
247
|
-
}, modelChanged ? 200 : 100)
|
|
275
|
+
}, (modelChanged || permissionModeChanged || adapterChanged) ? 200 : 100)
|
|
248
276
|
|
|
249
277
|
return () => {
|
|
250
278
|
isDisposed = true
|
|
251
279
|
clearTimeout(timer)
|
|
252
280
|
cleanup?.()
|
|
253
281
|
}
|
|
254
|
-
}, [
|
|
282
|
+
}, [
|
|
283
|
+
adapter,
|
|
284
|
+
modelForQuery,
|
|
285
|
+
mutate,
|
|
286
|
+
permissionMode,
|
|
287
|
+
retryCount,
|
|
288
|
+
session?.id,
|
|
289
|
+
session?.status,
|
|
290
|
+
setInteractionRequest,
|
|
291
|
+
t
|
|
292
|
+
])
|
|
255
293
|
|
|
256
294
|
return {
|
|
257
295
|
messages,
|
|
258
296
|
setMessages,
|
|
259
297
|
sessionInfo,
|
|
260
|
-
isReady
|
|
298
|
+
isReady,
|
|
299
|
+
connectionError,
|
|
300
|
+
retryConnection
|
|
261
301
|
}
|
|
262
302
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
1
2
|
import { useTranslation } from 'react-i18next'
|
|
2
3
|
|
|
3
4
|
import type { Session } from '@vibe-forge/core'
|
|
@@ -27,20 +28,60 @@ export function useChatSession({
|
|
|
27
28
|
const { interactionRequest, setInteractionRequest, handleInteractionResponse } = useChatInteraction({
|
|
28
29
|
sessionId: session?.id
|
|
29
30
|
})
|
|
30
|
-
const { messages, setMessages, sessionInfo, isReady } = useChatSessionMessages({
|
|
31
|
+
const { messages, setMessages, sessionInfo, isReady, connectionError, retryConnection } = useChatSessionMessages({
|
|
31
32
|
session,
|
|
32
33
|
modelForQuery: selectedModelWithService,
|
|
33
34
|
permissionMode,
|
|
34
35
|
adapter: selectedAdapter,
|
|
35
36
|
setInteractionRequest
|
|
36
37
|
})
|
|
38
|
+
const lastObservedSessionRef = useRef<Pick<Session, 'id' | 'model' | 'permissionMode' | 'adapter'> | null>(null)
|
|
37
39
|
const isThinking = session?.status === 'running'
|
|
38
40
|
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (session?.id == null || session.id === '') {
|
|
43
|
+
lastObservedSessionRef.current = null
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const previous = lastObservedSessionRef.current
|
|
48
|
+
const sessionChanged = previous?.id !== session.id
|
|
49
|
+
|
|
50
|
+
if (sessionChanged || previous?.model !== session.model) {
|
|
51
|
+
setSelectedModel(session.model)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (sessionChanged || previous?.permissionMode !== session.permissionMode) {
|
|
55
|
+
setPermissionMode(session.permissionMode)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (sessionChanged || previous?.adapter !== session.adapter) {
|
|
59
|
+
setSelectedAdapter(session.adapter)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
lastObservedSessionRef.current = {
|
|
63
|
+
id: session.id,
|
|
64
|
+
model: session.model,
|
|
65
|
+
permissionMode: session.permissionMode,
|
|
66
|
+
adapter: session.adapter
|
|
67
|
+
}
|
|
68
|
+
}, [
|
|
69
|
+
session?.adapter,
|
|
70
|
+
session?.id,
|
|
71
|
+
session?.model,
|
|
72
|
+
session?.permissionMode,
|
|
73
|
+
setPermissionMode,
|
|
74
|
+
setSelectedAdapter,
|
|
75
|
+
setSelectedModel
|
|
76
|
+
])
|
|
77
|
+
|
|
39
78
|
return {
|
|
40
79
|
messages,
|
|
41
80
|
sessionInfo,
|
|
42
81
|
interactionRequest,
|
|
43
82
|
isReady,
|
|
83
|
+
connectionError,
|
|
84
|
+
retryConnection,
|
|
44
85
|
isThinking,
|
|
45
86
|
activeView,
|
|
46
87
|
setActiveView,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { theme } from 'antd'
|
|
2
|
+
import { useAtomValue } from 'jotai'
|
|
3
|
+
import { useEffect, useMemo } from 'react'
|
|
4
|
+
import { useTranslation } from 'react-i18next'
|
|
5
|
+
import useSWR from 'swr'
|
|
6
|
+
|
|
7
|
+
import type { ConfigResponse } from '@vibe-forge/core'
|
|
8
|
+
|
|
9
|
+
import { getConfig } from '#~/api'
|
|
10
|
+
import { themeAtom } from '#~/store'
|
|
11
|
+
|
|
12
|
+
export function useAppPreferences() {
|
|
13
|
+
const { i18n } = useTranslation()
|
|
14
|
+
const themeMode = useAtomValue(themeAtom)
|
|
15
|
+
const { data: configRes } = useSWR<ConfigResponse>('/api/config', getConfig)
|
|
16
|
+
const interfaceLanguage = configRes?.sources?.merged?.general?.interfaceLanguage
|
|
17
|
+
const isDarkMode = themeMode === 'dark'
|
|
18
|
+
|| (themeMode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
document.documentElement.classList.toggle('dark', isDarkMode)
|
|
22
|
+
}, [isDarkMode])
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (interfaceLanguage && i18n.language !== interfaceLanguage) {
|
|
26
|
+
void i18n.changeLanguage(interfaceLanguage)
|
|
27
|
+
}
|
|
28
|
+
}, [i18n, interfaceLanguage])
|
|
29
|
+
|
|
30
|
+
const themeConfig = useMemo(() => ({
|
|
31
|
+
algorithm: isDarkMode ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
|
32
|
+
token: {
|
|
33
|
+
colorPrimary: isDarkMode ? '#3b82f6' : '#000000'
|
|
34
|
+
}
|
|
35
|
+
}), [isDarkMode])
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
isDarkMode,
|
|
39
|
+
themeConfig
|
|
40
|
+
}
|
|
41
|
+
}
|