@vibe-forge/client 0.3.0 → 0.5.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/cli.cjs +2 -1
- package/dist/assets/{arc-CwMXUVsq.js → arc-C4ymrcSQ.js} +1 -1
- package/dist/assets/{blockDiagram-c4efeb88-CGxJV7KJ.js → blockDiagram-c4efeb88-CeB7-kgP.js} +1 -1
- package/dist/assets/{c4Diagram-c83219d4-BKhin7cY.js → c4Diagram-c83219d4-C935Im8S.js} +1 -1
- package/dist/assets/channel-84s1ACzD.js +1 -0
- package/dist/assets/{classDiagram-beda092f-BASmn22R.js → classDiagram-beda092f-B9IV13KI.js} +1 -1
- package/dist/assets/{classDiagram-v2-2358418a-BUk9rNBX.js → classDiagram-v2-2358418a-CXF_K4fE.js} +1 -1
- package/dist/assets/clone-B2E8tddE.js +1 -0
- package/dist/assets/{createText-1719965b-2XqnWjQY.js → createText-1719965b-DwX8iC5F.js} +1 -1
- package/dist/assets/devicon-BWlTeAUU.woff +0 -0
- package/dist/assets/devicon-CirD-cQx.ttf +0 -0
- package/dist/assets/devicon-Dg8iWy0i.svg +1211 -0
- package/dist/assets/devicon-TqfHp33-.eot +0 -0
- package/dist/assets/{edges-96097737-B7e32Jeg.js → edges-96097737-9P1uH1RE.js} +1 -1
- package/dist/assets/{erDiagram-0228fc6a-CCR2or72.js → erDiagram-0228fc6a-ixeGTFvg.js} +1 -1
- package/dist/assets/{flowDb-c6c81e3f-B72HWT9x.js → flowDb-c6c81e3f-G1gSTTBI.js} +1 -1
- package/dist/assets/{flowDiagram-50d868cf-WOi0KARY.js → flowDiagram-50d868cf-CzrG99nD.js} +1 -1
- package/dist/assets/flowDiagram-v2-4f6560a1-CJfJYbME.js +1 -0
- package/dist/assets/{flowchart-elk-definition-6af322e1-i_Yd0LCE.js → flowchart-elk-definition-6af322e1-sFCoysWa.js} +1 -1
- package/dist/assets/{ganttDiagram-a2739b55-CFH9zF14.js → ganttDiagram-a2739b55-Ccsk_Lru.js} +1 -1
- package/dist/assets/{gitGraphDiagram-82fe8481-DglKfMze.js → gitGraphDiagram-82fe8481-CwathJ6H.js} +1 -1
- package/dist/assets/{graph-BKbBNGPf.js → graph-DRCU-8Rz.js} +1 -1
- package/dist/assets/{index-5325376f-BK7F9nSl.js → index-5325376f-Bq-fg2i_.js} +1 -1
- package/dist/assets/index-CHMuZ5-1.css +1 -0
- package/dist/assets/index-cGZvDhhU.js +542 -0
- package/dist/assets/{infoDiagram-8eee0895-BLFL77_D.js → infoDiagram-8eee0895-JBcUkJ6T.js} +1 -1
- package/dist/assets/{journeyDiagram-c64418c1-CS9XctDL.js → journeyDiagram-c64418c1-DsdQU-R8.js} +1 -1
- package/dist/assets/{layout-By3JZZGt.js → layout-s0slG1OL.js} +1 -1
- package/dist/assets/{line-9GUsXbwv.js → line-CymFqgW6.js} +1 -1
- package/dist/assets/{linear-DzGV4E9N.js → linear-lDQVZ6aQ.js} +1 -1
- package/dist/assets/{mermaid.core-CG3Ib42Q.js → mermaid.core-Cmlqga_E.js} +6 -6
- package/dist/assets/{mindmap-definition-8da855dc-WQ3LPKJU.js → mindmap-definition-8da855dc-CqqTDJn_.js} +1 -1
- package/dist/assets/{pieDiagram-a8764435-DHVIUZiN.js → pieDiagram-a8764435-BL2Ajx7Z.js} +1 -1
- package/dist/assets/{quadrantDiagram-1e28029f-C3G9Ye8-.js → quadrantDiagram-1e28029f-ClL_3ASt.js} +1 -1
- package/dist/assets/{requirementDiagram-08caed73-C9ES1D5G.js → requirementDiagram-08caed73-CB1RgE3K.js} +1 -1
- package/dist/assets/{sankeyDiagram-a04cb91d-B4BKXclQ.js → sankeyDiagram-a04cb91d-tgleEYiD.js} +1 -1
- package/dist/assets/{sequenceDiagram-c5b8d532-DrgEb25G.js → sequenceDiagram-c5b8d532-DlatQT5R.js} +1 -1
- package/dist/assets/{stateDiagram-1ecb1508-CF1XWARJ.js → stateDiagram-1ecb1508-B--MLqRs.js} +1 -1
- package/dist/assets/{stateDiagram-v2-c2b004d7-IO3i3yXv.js → stateDiagram-v2-c2b004d7-CRMZ6Dpx.js} +1 -1
- package/dist/assets/{styles-b4e223ce-DACN9aSc.js → styles-b4e223ce-CPiYHfUz.js} +1 -1
- package/dist/assets/{styles-ca3715f6-bekm2WLP.js → styles-ca3715f6-B9UKPAzX.js} +1 -1
- package/dist/assets/{styles-d45a18b0-OzTDVBb8.js → styles-d45a18b0-BC1Ak1So.js} +1 -1
- package/dist/assets/{svgDrawCommon-b86b1483-BWroJerr.js → svgDrawCommon-b86b1483-DV8R0g-n.js} +1 -1
- package/dist/assets/{timeline-definition-faaaa080-CCfRNigO.js → timeline-definition-faaaa080-CiqGS5DC.js} +1 -1
- package/dist/assets/{xychartDiagram-f5964ef8-C3cbfVqN.js → xychartDiagram-f5964ef8-h6VSD3GE.js} +1 -1
- package/dist/index.html +2 -7
- package/index.html +0 -5
- package/package.json +12 -6
- package/src/App.tsx +2 -0
- package/src/api/README.md +26 -0
- package/src/api/automation.ts +88 -0
- package/src/api/base.ts +54 -0
- package/src/api/benchmark.ts +45 -0
- package/src/api/config.ts +24 -0
- package/src/api/knowledge.ts +72 -0
- package/src/api/projects.ts +15 -0
- package/src/api/sessions.ts +84 -0
- package/src/api/types.ts +20 -0
- package/src/api.ts +44 -269
- package/src/components/AutomationView/AutomationView.scss +5 -1
- package/src/components/AutomationView/RuleFormPanel.tsx +3 -2
- package/src/components/AutomationView/TaskList.scss +4 -6
- package/src/components/AutomationView/TaskList.tsx +2 -1
- package/src/components/AutomationView/TriggerList.scss +4 -1
- package/src/components/BenchmarkView/BenchmarkCasePanel.scss +267 -0
- package/src/components/BenchmarkView/BenchmarkCasePanel.tsx +309 -0
- package/src/components/BenchmarkView/BenchmarkSidebar.scss +182 -0
- package/src/components/BenchmarkView/BenchmarkSidebar.tsx +262 -0
- package/src/components/BenchmarkView/BenchmarkView.scss +78 -0
- package/src/components/BenchmarkView/index.tsx +197 -0
- package/src/components/BenchmarkView/types.ts +10 -0
- package/src/components/BenchmarkView/utils.ts +21 -0
- package/src/components/Chat.tsx +43 -29
- package/src/components/{chat/CodeBlock.tsx → CodeBlock.tsx} +3 -1
- package/src/components/ConfigView.tsx +32 -25
- package/src/components/{chat/MarkdownContent.tsx → MarkdownContent.tsx} +1 -1
- package/src/components/NavRail.tsx +7 -0
- package/src/components/chat/ChatHeader.scss +37 -19
- package/src/components/chat/ChatHeader.tsx +6 -9
- package/src/components/chat/ChatHistoryView.tsx +99 -45
- package/src/components/chat/CurrentTodoList.tsx +10 -9
- package/src/components/chat/{MessageItem.scss → Messages/MessageItem.scss} +14 -0
- package/src/components/chat/{MessageItem.tsx → Messages/MessageItem.tsx} +30 -8
- package/src/components/chat/{messageUtils.ts → Messages/message-utils.ts} +1 -1
- package/src/components/chat/{Sender.scss → Sender/Sender.scss} +146 -3
- package/src/components/chat/{Sender.tsx → Sender/Sender.tsx} +183 -5
- package/src/components/chat/tools/DefaultTool.tsx +184 -21
- package/src/components/chat/tools/adapter-claude/BashTool.scss +67 -51
- package/src/components/chat/tools/adapter-claude/BashTool.tsx +83 -49
- package/src/components/chat/tools/adapter-claude/GlobTool.scss +0 -79
- package/src/components/chat/tools/adapter-claude/GlobTool.tsx +16 -36
- package/src/components/chat/tools/adapter-claude/GrepTool.scss +0 -87
- package/src/components/chat/tools/adapter-claude/GrepTool.tsx +22 -41
- package/src/components/chat/tools/adapter-claude/LSTool.scss +0 -79
- package/src/components/chat/tools/adapter-claude/LSTool.tsx +15 -15
- package/src/components/chat/tools/adapter-claude/ReadTool.scss +0 -55
- package/src/components/chat/tools/adapter-claude/ReadTool.tsx +20 -42
- package/src/components/chat/tools/adapter-claude/TodoTool.scss +8 -23
- package/src/components/chat/tools/adapter-claude/TodoTool.tsx +24 -11
- package/src/components/chat/tools/adapter-claude/WriteTool.scss +21 -69
- package/src/components/chat/tools/adapter-claude/WriteTool.tsx +22 -58
- package/src/components/chat/tools/adapter-claude/index.ts +4 -10
- package/src/components/chat/tools/adapter-claude/utils.ts +54 -0
- package/src/components/chat/tools/core/ToolCallBox.scss +356 -0
- package/src/components/chat/{ToolGroup.tsx → tools/core/ToolGroup.tsx} +26 -7
- package/src/components/chat/{ToolRenderer.tsx → tools/core/ToolRenderer.tsx} +6 -4
- package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.scss +11 -0
- package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +75 -0
- package/src/components/chat/tools/plugin-chrome-devtools/index.ts +45 -0
- package/src/components/chat/tools/task/GetTaskInfoTool.scss +2 -27
- package/src/components/chat/tools/task/GetTaskInfoTool.tsx +48 -38
- package/src/components/chat/tools/task/ListTasksTool.scss +3 -28
- package/src/components/chat/tools/task/ListTasksTool.tsx +11 -8
- package/src/components/chat/tools/task/StartTasksTool.scss +3 -28
- package/src/components/chat/tools/task/StartTasksTool.tsx +14 -17
- package/src/components/chat/tools/task/components/TaskRow.scss +105 -0
- package/src/components/chat/tools/task/components/TaskRow.tsx +163 -0
- package/src/components/chat/tools/task/components/TaskToolCard.scss +15 -15
- package/src/components/chat/tools/task/components/TaskToolCard.tsx +8 -6
- package/src/components/config/ConfigSectionForm.tsx +12 -1
- package/src/components/config/ConfigSourceSwitch.tsx +12 -34
- package/src/components/config/channelDefinitions.ts +6 -0
- package/src/components/config/configSchema.ts +10 -1
- package/src/components/config/recordEditors/ChannelRecordEditor.scss +1 -0
- package/src/components/config/recordEditors/ChannelRecordEditor.tsx +397 -0
- package/src/components/config/recordEditors/index.tsx +1 -0
- package/src/components/knowledge-base/components/RuleItem.tsx +1 -1
- package/src/components/knowledge-base/components/SpecItem.tsx +1 -1
- package/src/components/sidebar/SessionItem.scss +17 -0
- package/src/components/sidebar/SessionItem.tsx +21 -13
- package/src/hooks/chat/use-chat-adapter.ts +81 -0
- package/src/hooks/chat/use-chat-interaction.ts +26 -0
- package/src/{components/chat/useChatModels.tsx → hooks/chat/use-chat-models.tsx} +117 -22
- package/src/hooks/chat/use-chat-permission-mode.ts +47 -0
- package/src/hooks/chat/use-chat-scroll.ts +51 -0
- package/src/hooks/chat/use-chat-session-actions.ts +153 -0
- package/src/hooks/chat/use-chat-session-messages.ts +262 -0
- package/src/hooks/chat/use-chat-session.ts +63 -0
- package/src/hooks/chat/use-chat-view.ts +39 -0
- package/src/main.tsx +10 -13
- package/src/resources/adapters.ts +20 -0
- package/src/resources/locales/en.json +66 -0
- package/src/resources/locales/zh.json +66 -0
- package/src/runtime-config.ts +52 -0
- package/src/vite-env.d.ts +11 -0
- package/src/ws.ts +5 -3
- package/vite.config.ts +12 -4
- package/dist/assets/channel-jbCEHqbG.js +0 -1
- package/dist/assets/clone-CCRKqS4L.js +0 -1
- package/dist/assets/flowDiagram-v2-4f6560a1-Baslbgn4.js +0 -1
- package/dist/assets/index-B0qfCb1G.css +0 -1
- package/dist/assets/index-CNo75dYr.js +0 -497
- package/src/components/chat/ToolCallBox.scss +0 -137
- package/src/components/chat/useChatSession.ts +0 -370
- /package/src/components/{chat/CodeBlock.scss → CodeBlock.scss} +0 -0
- /package/src/components/chat/{MessageFooter.tsx → Messages/MessageFooter.tsx} +0 -0
- /package/src/components/chat/{CompletionMenu.scss → Sender/CompletionMenu.scss} +0 -0
- /package/src/components/chat/{CompletionMenu.tsx → Sender/CompletionMenu.tsx} +0 -0
- /package/src/components/chat/{ThinkingStatus.scss → Sender/ThinkingStatus.scss} +0 -0
- /package/src/components/chat/{ThinkingStatus.tsx → Sender/ThinkingStatus.tsx} +0 -0
- /package/src/components/chat/{ToolCallBox.tsx → tools/core/ToolCallBox.tsx} +0 -0
- /package/src/components/chat/{ToolGroup.scss → tools/core/ToolGroup.scss} +0 -0
- /package/src/{components/chat/safeSerialize.ts → utils/safe-serialize.ts} +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createElement, type ReactNode } from 'react'
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
3
|
+
import useSWR from 'swr'
|
|
4
|
+
|
|
5
|
+
import { getConfig } from '#~/api.js'
|
|
6
|
+
import type { ConfigResponse } from '@vibe-forge/core'
|
|
7
|
+
import { getAdapterDisplay } from '#~/resources/adapters.js'
|
|
8
|
+
|
|
9
|
+
const ADAPTER_STORAGE_KEY = 'vf_chat_adapter'
|
|
10
|
+
|
|
11
|
+
export function useChatAdapter() {
|
|
12
|
+
const [selectedAdapter, setSelectedAdapter] = useState<string | undefined>(() => {
|
|
13
|
+
try {
|
|
14
|
+
const raw = localStorage.getItem(ADAPTER_STORAGE_KEY)
|
|
15
|
+
return raw == null || raw.trim() === '' ? undefined : raw
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const { data: configRes } = useSWR<ConfigResponse>('/api/config', getConfig)
|
|
22
|
+
|
|
23
|
+
const mergedAdapters = useMemo(() => {
|
|
24
|
+
return configRes?.sources?.merged?.adapters ?? {}
|
|
25
|
+
}, [configRes?.sources?.merged?.adapters])
|
|
26
|
+
|
|
27
|
+
const defaultAdapter = configRes?.sources?.merged?.general?.defaultAdapter
|
|
28
|
+
|
|
29
|
+
const adapterOptions = useMemo<Array<{ value: string; label: ReactNode }>>(() => {
|
|
30
|
+
const keys = Object.keys(mergedAdapters)
|
|
31
|
+
return keys.map((key) => {
|
|
32
|
+
const display = getAdapterDisplay(key)
|
|
33
|
+
return {
|
|
34
|
+
value: key,
|
|
35
|
+
label: createElement('span', { className: 'adapter-option' }, [
|
|
36
|
+
display.icon != null
|
|
37
|
+
? createElement('img', {
|
|
38
|
+
key: 'icon',
|
|
39
|
+
className: 'adapter-option__icon',
|
|
40
|
+
src: display.icon,
|
|
41
|
+
alt: '',
|
|
42
|
+
'aria-hidden': true
|
|
43
|
+
})
|
|
44
|
+
: null,
|
|
45
|
+
createElement('span', { key: 'text', className: 'adapter-option__text' }, display.title)
|
|
46
|
+
])
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}, [mergedAdapters])
|
|
50
|
+
|
|
51
|
+
// Auto-select: use stored value if valid, else config default, else first available
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const keys = Object.keys(mergedAdapters)
|
|
54
|
+
if (keys.length === 0) {
|
|
55
|
+
setSelectedAdapter(undefined)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
setSelectedAdapter((prev) => {
|
|
59
|
+
if (prev != null && keys.includes(prev)) return prev
|
|
60
|
+
if (defaultAdapter && keys.includes(defaultAdapter as string)) return defaultAdapter as string
|
|
61
|
+
return keys[0]
|
|
62
|
+
})
|
|
63
|
+
}, [defaultAdapter, mergedAdapters])
|
|
64
|
+
|
|
65
|
+
// Persist to localStorage
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
try {
|
|
68
|
+
if (selectedAdapter == null || selectedAdapter.trim() === '') {
|
|
69
|
+
localStorage.removeItem(ADAPTER_STORAGE_KEY)
|
|
70
|
+
} else {
|
|
71
|
+
localStorage.setItem(ADAPTER_STORAGE_KEY, selectedAdapter)
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
}, [selectedAdapter])
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
selectedAdapter,
|
|
78
|
+
setSelectedAdapter,
|
|
79
|
+
adapterOptions
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { AskUserQuestionParams } from '@vibe-forge/core'
|
|
4
|
+
import { connectionManager } from '#~/connectionManager.js'
|
|
5
|
+
|
|
6
|
+
export function useChatInteraction({ sessionId }: { sessionId?: string }) {
|
|
7
|
+
const [interactionRequest, setInteractionRequest] = useState<{ id: string; payload: AskUserQuestionParams } | null>(
|
|
8
|
+
null
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const handleInteractionResponse = useCallback((id: string, data: string | string[]) => {
|
|
12
|
+
if (!sessionId) return
|
|
13
|
+
connectionManager.send(sessionId, {
|
|
14
|
+
type: 'interaction_response',
|
|
15
|
+
id,
|
|
16
|
+
data
|
|
17
|
+
})
|
|
18
|
+
setInteractionRequest(null)
|
|
19
|
+
}, [sessionId])
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
interactionRequest,
|
|
23
|
+
setInteractionRequest,
|
|
24
|
+
handleInteractionResponse
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useState } from 'react'
|
|
1
|
+
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 { ConfigResponse, ModelServiceConfig, RecommendedModelConfig } from '@vibe-forge/core'
|
|
6
|
-
import { getConfig } from '
|
|
5
|
+
import type { AdapterBuiltinModel, ConfigResponse, ModelServiceConfig, RecommendedModelConfig } from '@vibe-forge/core'
|
|
6
|
+
import { getConfig } from '#~/api.js'
|
|
7
7
|
|
|
8
|
-
interface ModelSelectOption {
|
|
8
|
+
export interface ModelSelectOption {
|
|
9
9
|
value: string
|
|
10
10
|
label: React.ReactNode
|
|
11
11
|
searchText: string
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
interface ModelSelectGroup {
|
|
14
|
+
export interface ModelSelectGroup {
|
|
15
15
|
label: React.ReactNode
|
|
16
16
|
options: ModelSelectOption[]
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function useChatModels(
|
|
19
|
+
export function useChatModels({
|
|
20
|
+
selectedAdapter
|
|
21
|
+
}: {
|
|
22
|
+
selectedAdapter?: string
|
|
23
|
+
} = {}) {
|
|
20
24
|
const { t } = useTranslation()
|
|
21
25
|
const [selectedModel, setSelectedModel] = useState<string | undefined>(() => {
|
|
22
26
|
try {
|
|
@@ -41,12 +45,38 @@ export function useChatModels() {
|
|
|
41
45
|
))
|
|
42
46
|
}, [configRes?.sources?.merged?.general?.recommendedModels])
|
|
43
47
|
|
|
48
|
+
const adapterBuiltinModels = useMemo(() => {
|
|
49
|
+
const raw = configRes?.sources?.merged?.adapterBuiltinModels
|
|
50
|
+
return (raw ?? {}) as Record<string, AdapterBuiltinModel[]>
|
|
51
|
+
}, [configRes?.sources?.merged?.adapterBuiltinModels])
|
|
52
|
+
|
|
53
|
+
const activeBuiltinModels = useMemo(() => {
|
|
54
|
+
if (selectedAdapter && adapterBuiltinModels[selectedAdapter]) {
|
|
55
|
+
return { [selectedAdapter]: adapterBuiltinModels[selectedAdapter] }
|
|
56
|
+
}
|
|
57
|
+
return adapterBuiltinModels
|
|
58
|
+
}, [adapterBuiltinModels, selectedAdapter])
|
|
59
|
+
|
|
60
|
+
const activeBuiltinModelValues = useMemo(() => (
|
|
61
|
+
Object.values(activeBuiltinModels).flat().map(model => model.value)
|
|
62
|
+
), [activeBuiltinModels])
|
|
63
|
+
|
|
64
|
+
const builtinModelSet = useMemo(() => {
|
|
65
|
+
const set = new Set<string>()
|
|
66
|
+
for (const models of Object.values(activeBuiltinModels)) {
|
|
67
|
+
for (const m of models) set.add(m.value)
|
|
68
|
+
}
|
|
69
|
+
return set
|
|
70
|
+
}, [activeBuiltinModels])
|
|
71
|
+
|
|
44
72
|
const modelServiceEntries = useMemo(() => Object.entries(mergedModelServices), [mergedModelServices])
|
|
45
73
|
|
|
46
74
|
const availableModels = useMemo(() => {
|
|
47
75
|
const list: Array<{ model: string; serviceKey: string; serviceTitle: string }> = []
|
|
48
76
|
for (const [serviceKey, serviceValue] of modelServiceEntries) {
|
|
49
|
-
const service = (serviceValue != null && typeof serviceValue === 'object')
|
|
77
|
+
const service = (serviceValue != null && typeof serviceValue === 'object')
|
|
78
|
+
? serviceValue as ModelServiceConfig
|
|
79
|
+
: undefined
|
|
50
80
|
const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
|
|
51
81
|
const models = Array.isArray(service?.models) ? service?.models.filter(item => typeof item === 'string') : []
|
|
52
82
|
for (const model of models) {
|
|
@@ -59,19 +89,55 @@ export function useChatModels() {
|
|
|
59
89
|
const availableModelValues = useMemo(() => availableModels.map(item => item.model), [availableModels])
|
|
60
90
|
const availableModelKey = useMemo(() => availableModelValues.join('|'), [availableModelValues])
|
|
61
91
|
const availableModelSet = useMemo(() => new Set(availableModelValues), [availableModelKey])
|
|
62
|
-
const hasAvailableModels = availableModelValues.length > 0
|
|
92
|
+
const hasAvailableModels = availableModelValues.length > 0 || builtinModelSet.size > 0
|
|
93
|
+
const modelToService = useMemo(() => {
|
|
94
|
+
const map = new Map<string, { key: string; title: string }>()
|
|
95
|
+
for (const entry of availableModels) {
|
|
96
|
+
if (!map.has(entry.model)) {
|
|
97
|
+
map.set(entry.model, { key: entry.serviceKey, title: entry.serviceTitle })
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return map
|
|
101
|
+
}, [availableModels])
|
|
63
102
|
const defaultModelService = configRes?.sources?.merged?.general?.defaultModelService
|
|
64
103
|
const defaultModel = configRes?.sources?.merged?.general?.defaultModel
|
|
104
|
+
const formatModelWithService = useCallback((model: string | undefined) => {
|
|
105
|
+
const normalizedModel = typeof model === 'string' ? model.trim() : ''
|
|
106
|
+
if (normalizedModel === '') return undefined
|
|
107
|
+
// Builtin adapter models pass through as-is (no service prefix)
|
|
108
|
+
if (builtinModelSet.has(normalizedModel)) return normalizedModel
|
|
109
|
+
if (normalizedModel.includes(',')) return normalizedModel
|
|
110
|
+
const resolvedService = modelToService.get(normalizedModel)?.key ?? defaultModelService
|
|
111
|
+
return resolvedService ? `${resolvedService},${normalizedModel}` : normalizedModel
|
|
112
|
+
}, [builtinModelSet, defaultModelService, modelToService])
|
|
65
113
|
const resolvedDefaultModel = useMemo(() => {
|
|
66
|
-
if (
|
|
114
|
+
if (defaultModel && builtinModelSet.has(defaultModel)) return defaultModel
|
|
115
|
+
if (activeBuiltinModelValues.length > 0) {
|
|
116
|
+
return activeBuiltinModelValues[0]
|
|
117
|
+
}
|
|
67
118
|
if (defaultModel && availableModelSet.has(defaultModel)) return defaultModel
|
|
68
119
|
if (defaultModelService && mergedModelServices[defaultModelService]) {
|
|
69
120
|
const service = mergedModelServices[defaultModelService]
|
|
70
121
|
const models = Array.isArray(service?.models) ? service?.models.filter(item => typeof item === 'string') : []
|
|
71
122
|
if (models.length > 0) return models[0]
|
|
72
123
|
}
|
|
73
|
-
return availableModelValues[0]
|
|
74
|
-
|
|
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
|
|
128
|
+
}, [
|
|
129
|
+
activeBuiltinModels,
|
|
130
|
+
activeBuiltinModelValues,
|
|
131
|
+
availableModelSet,
|
|
132
|
+
availableModelValues,
|
|
133
|
+
builtinModelSet,
|
|
134
|
+
defaultModel,
|
|
135
|
+
defaultModelService,
|
|
136
|
+
mergedModelServices
|
|
137
|
+
])
|
|
138
|
+
const selectedModelWithService = useMemo(() => (
|
|
139
|
+
formatModelWithService(selectedModel)
|
|
140
|
+
), [formatModelWithService, selectedModel])
|
|
75
141
|
|
|
76
142
|
useEffect(() => {
|
|
77
143
|
if (!hasAvailableModels) {
|
|
@@ -79,10 +145,13 @@ export function useChatModels() {
|
|
|
79
145
|
return
|
|
80
146
|
}
|
|
81
147
|
setSelectedModel((prev) => {
|
|
82
|
-
if (prev != null
|
|
148
|
+
if (prev != null) {
|
|
149
|
+
const isValid = availableModelSet.has(prev) || builtinModelSet.has(prev)
|
|
150
|
+
if (isValid) return prev
|
|
151
|
+
}
|
|
83
152
|
return resolvedDefaultModel
|
|
84
153
|
})
|
|
85
|
-
}, [availableModelSet, hasAvailableModels, resolvedDefaultModel])
|
|
154
|
+
}, [availableModelSet, builtinModelSet, hasAvailableModels, resolvedDefaultModel, selectedAdapter])
|
|
86
155
|
|
|
87
156
|
useEffect(() => {
|
|
88
157
|
try {
|
|
@@ -126,13 +195,6 @@ export function useChatModels() {
|
|
|
126
195
|
}
|
|
127
196
|
}
|
|
128
197
|
|
|
129
|
-
const modelToService = new Map<string, { key: string; title: string }>()
|
|
130
|
-
for (const entry of availableModels) {
|
|
131
|
-
if (!modelToService.has(entry.model)) {
|
|
132
|
-
modelToService.set(entry.model, { key: entry.serviceKey, title: entry.serviceTitle })
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
198
|
const resolveFirstAlias = (modelsAlias: Record<string, string[]> | undefined, model: string) => {
|
|
137
199
|
if (!modelsAlias) return undefined
|
|
138
200
|
for (const [alias, aliasModels] of Object.entries(modelsAlias)) {
|
|
@@ -144,7 +206,9 @@ export function useChatModels() {
|
|
|
144
206
|
|
|
145
207
|
const serviceGroups = modelServiceEntries
|
|
146
208
|
.map(([serviceKey, serviceValue]) => {
|
|
147
|
-
const service = (serviceValue != null && typeof serviceValue === 'object')
|
|
209
|
+
const service = (serviceValue != null && typeof serviceValue === 'object')
|
|
210
|
+
? serviceValue as ModelServiceConfig
|
|
211
|
+
: undefined
|
|
148
212
|
const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
|
|
149
213
|
const groupTitle = serviceTitle?.trim() !== '' ? serviceTitle : serviceKey
|
|
150
214
|
const serviceDescription = service?.description
|
|
@@ -212,11 +276,42 @@ export function useChatModels() {
|
|
|
212
276
|
options: recommendedOptions
|
|
213
277
|
})
|
|
214
278
|
}
|
|
279
|
+
|
|
280
|
+
// Adapter builtin model groups (filtered to active adapter)
|
|
281
|
+
for (const [adapterKey, models] of Object.entries(activeBuiltinModels)) {
|
|
282
|
+
if (!Array.isArray(models) || models.length === 0) continue
|
|
283
|
+
const adapterTitle = t('chat.modelGroupBuiltin', {
|
|
284
|
+
adapter: adapterKey,
|
|
285
|
+
defaultValue: `${adapterKey} (Default)`
|
|
286
|
+
})
|
|
287
|
+
groups.push({
|
|
288
|
+
label: (
|
|
289
|
+
<div className='model-group-label'>
|
|
290
|
+
<div className='model-group-title'>{adapterTitle}</div>
|
|
291
|
+
</div>
|
|
292
|
+
),
|
|
293
|
+
options: models.map(m => buildOption({
|
|
294
|
+
value: m.value,
|
|
295
|
+
title: m.title,
|
|
296
|
+
description: m.description
|
|
297
|
+
}))
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
215
301
|
return [...groups, ...serviceGroups]
|
|
216
|
-
}, [
|
|
302
|
+
}, [
|
|
303
|
+
activeBuiltinModels,
|
|
304
|
+
availableModelSet,
|
|
305
|
+
modelToService,
|
|
306
|
+
mergedModelServices,
|
|
307
|
+
modelServiceEntries,
|
|
308
|
+
recommendedModels,
|
|
309
|
+
t
|
|
310
|
+
])
|
|
217
311
|
|
|
218
312
|
return {
|
|
219
313
|
selectedModel,
|
|
314
|
+
selectedModelWithService,
|
|
220
315
|
setSelectedModel,
|
|
221
316
|
modelOptions,
|
|
222
317
|
hasAvailableModels
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
export type PermissionMode = 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions'
|
|
5
|
+
|
|
6
|
+
const PERMISSION_MODE_STORAGE_KEY = 'vf_chat_permission_mode'
|
|
7
|
+
|
|
8
|
+
const isPermissionMode = (value: string): value is PermissionMode => {
|
|
9
|
+
return value === 'default'
|
|
10
|
+
|| value === 'acceptEdits'
|
|
11
|
+
|| value === 'plan'
|
|
12
|
+
|| value === 'dontAsk'
|
|
13
|
+
|| value === 'bypassPermissions'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useChatPermissionMode() {
|
|
17
|
+
const [permissionMode, setPermissionMode] = useState<PermissionMode>('default')
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
try {
|
|
21
|
+
const raw = localStorage.getItem(PERMISSION_MODE_STORAGE_KEY)
|
|
22
|
+
if (raw && isPermissionMode(raw)) {
|
|
23
|
+
setPermissionMode(raw)
|
|
24
|
+
}
|
|
25
|
+
} catch {}
|
|
26
|
+
}, [])
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
try {
|
|
30
|
+
localStorage.setItem(PERMISSION_MODE_STORAGE_KEY, permissionMode)
|
|
31
|
+
} catch {}
|
|
32
|
+
}, [permissionMode])
|
|
33
|
+
|
|
34
|
+
const permissionModeOptions = useMemo<Array<{ value: PermissionMode; label: ReactNode }>>(() => ([
|
|
35
|
+
{ value: 'default', label: '默认' },
|
|
36
|
+
{ value: 'acceptEdits', label: '接受编辑' },
|
|
37
|
+
{ value: 'plan', label: '计划' },
|
|
38
|
+
{ value: 'dontAsk', label: '不询问' },
|
|
39
|
+
{ value: 'bypassPermissions', label: '跳过权限' }
|
|
40
|
+
]), [])
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
permissionMode,
|
|
44
|
+
setPermissionMode,
|
|
45
|
+
permissionModeOptions
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
const SCROLL_THRESHOLD = 80
|
|
4
|
+
|
|
5
|
+
export function useChatScroll({ messagesLength }: { messagesLength: number }) {
|
|
6
|
+
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
7
|
+
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
|
8
|
+
const messagesContentRef = useRef<HTMLDivElement>(null)
|
|
9
|
+
const [showScrollBottom, setShowScrollBottom] = useState(false)
|
|
10
|
+
|
|
11
|
+
const updateScrollState = useCallback(() => {
|
|
12
|
+
const container = messagesContainerRef.current
|
|
13
|
+
if (!container) return
|
|
14
|
+
const distanceToBottom = container.scrollHeight - (container.scrollTop + container.clientHeight)
|
|
15
|
+
setShowScrollBottom(distanceToBottom > SCROLL_THRESHOLD)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
if (messagesContainerRef.current) {
|
|
21
|
+
messagesContainerRef.current.scrollTo({
|
|
22
|
+
top: messagesContainerRef.current.scrollHeight,
|
|
23
|
+
behavior
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
}, 50)
|
|
27
|
+
}, [])
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const container = messagesContainerRef.current
|
|
31
|
+
if (!container) return
|
|
32
|
+
updateScrollState()
|
|
33
|
+
const handleScroll = () => updateScrollState()
|
|
34
|
+
container.addEventListener('scroll', handleScroll, { passive: true })
|
|
35
|
+
return () => {
|
|
36
|
+
container.removeEventListener('scroll', handleScroll)
|
|
37
|
+
}
|
|
38
|
+
}, [updateScrollState])
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
updateScrollState()
|
|
42
|
+
}, [updateScrollState, messagesLength])
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
messagesEndRef,
|
|
46
|
+
messagesContainerRef,
|
|
47
|
+
messagesContentRef,
|
|
48
|
+
showScrollBottom,
|
|
49
|
+
scrollToBottom
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { App } from 'antd'
|
|
2
|
+
import { useCallback, useState } from 'react'
|
|
3
|
+
import { useTranslation } from 'react-i18next'
|
|
4
|
+
import { useNavigate } from 'react-router-dom'
|
|
5
|
+
import { useSWRConfig } from 'swr'
|
|
6
|
+
|
|
7
|
+
import { createSession } from '#~/api.js'
|
|
8
|
+
import { connectionManager } from '#~/connectionManager.js'
|
|
9
|
+
import type { ChatMessageContent, Session } from '@vibe-forge/core'
|
|
10
|
+
import type { PermissionMode } from './use-chat-permission-mode'
|
|
11
|
+
|
|
12
|
+
export function useChatSessionActions({
|
|
13
|
+
session,
|
|
14
|
+
modelForQuery,
|
|
15
|
+
hasAvailableModels,
|
|
16
|
+
permissionMode,
|
|
17
|
+
adapter,
|
|
18
|
+
onClearMessages
|
|
19
|
+
}: {
|
|
20
|
+
session?: Session
|
|
21
|
+
modelForQuery?: string
|
|
22
|
+
hasAvailableModels: boolean
|
|
23
|
+
permissionMode: PermissionMode
|
|
24
|
+
adapter?: string
|
|
25
|
+
onClearMessages: () => void
|
|
26
|
+
}) {
|
|
27
|
+
const { message } = App.useApp()
|
|
28
|
+
const { t } = useTranslation()
|
|
29
|
+
const navigate = useNavigate()
|
|
30
|
+
const { mutate } = useSWRConfig()
|
|
31
|
+
const [isCreating, setIsCreating] = useState(false)
|
|
32
|
+
const isThinking = isCreating || session?.status === 'running'
|
|
33
|
+
|
|
34
|
+
const send = useCallback(async (text: string) => {
|
|
35
|
+
if (text.trim() === '' || isThinking) return
|
|
36
|
+
if (!hasAvailableModels) {
|
|
37
|
+
void message.warning(t('chat.modelConfigRequired'))
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!session?.id) {
|
|
42
|
+
setIsCreating(true)
|
|
43
|
+
try {
|
|
44
|
+
const { session: newSession } = await createSession(undefined, text.trim(), undefined, modelForQuery, {
|
|
45
|
+
permissionMode,
|
|
46
|
+
adapter
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
await mutate('/api/sessions', (prev: { sessions: Session[] } | undefined) => {
|
|
50
|
+
if (!prev?.sessions) return { sessions: [newSession] }
|
|
51
|
+
return {
|
|
52
|
+
...prev,
|
|
53
|
+
sessions: [newSession, ...prev.sessions]
|
|
54
|
+
}
|
|
55
|
+
}, false)
|
|
56
|
+
|
|
57
|
+
void navigate(`/session/${newSession.id}`)
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error(err)
|
|
60
|
+
setIsCreating(false)
|
|
61
|
+
void message.error('Failed to create session')
|
|
62
|
+
}
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
connectionManager.send(session.id, {
|
|
67
|
+
type: 'user_message',
|
|
68
|
+
text: text.trim()
|
|
69
|
+
})
|
|
70
|
+
}, [
|
|
71
|
+
adapter,
|
|
72
|
+
hasAvailableModels,
|
|
73
|
+
isThinking,
|
|
74
|
+
message,
|
|
75
|
+
mutate,
|
|
76
|
+
navigate,
|
|
77
|
+
permissionMode,
|
|
78
|
+
modelForQuery,
|
|
79
|
+
session?.id,
|
|
80
|
+
t
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
const sendContent = useCallback(async (content: ChatMessageContent[]) => {
|
|
84
|
+
if (content.length === 0 || isThinking) return
|
|
85
|
+
if (!hasAvailableModels) {
|
|
86
|
+
void message.warning(t('chat.modelConfigRequired'))
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!session?.id) {
|
|
91
|
+
setIsCreating(true)
|
|
92
|
+
try {
|
|
93
|
+
const { session: newSession } = await createSession(undefined, undefined, content, modelForQuery, {
|
|
94
|
+
permissionMode,
|
|
95
|
+
adapter
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
await mutate('/api/sessions', (prev: { sessions: Session[] } | undefined) => {
|
|
99
|
+
if (!prev?.sessions) return { sessions: [newSession] }
|
|
100
|
+
return {
|
|
101
|
+
...prev,
|
|
102
|
+
sessions: [newSession, ...prev.sessions]
|
|
103
|
+
}
|
|
104
|
+
}, false)
|
|
105
|
+
|
|
106
|
+
void navigate(`/session/${newSession.id}`)
|
|
107
|
+
setIsCreating(false)
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(err)
|
|
110
|
+
setIsCreating(false)
|
|
111
|
+
void message.error('Failed to create session')
|
|
112
|
+
}
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
connectionManager.send(session.id, {
|
|
117
|
+
type: 'user_message',
|
|
118
|
+
content
|
|
119
|
+
})
|
|
120
|
+
}, [
|
|
121
|
+
adapter,
|
|
122
|
+
hasAvailableModels,
|
|
123
|
+
isThinking,
|
|
124
|
+
message,
|
|
125
|
+
mutate,
|
|
126
|
+
navigate,
|
|
127
|
+
permissionMode,
|
|
128
|
+
modelForQuery,
|
|
129
|
+
session?.id,
|
|
130
|
+
t
|
|
131
|
+
])
|
|
132
|
+
|
|
133
|
+
const interrupt = useCallback(() => {
|
|
134
|
+
if (!session?.id || isThinking === false) return
|
|
135
|
+
connectionManager.send(session.id, {
|
|
136
|
+
type: 'interrupt'
|
|
137
|
+
})
|
|
138
|
+
}, [isThinking, session?.id])
|
|
139
|
+
|
|
140
|
+
const clearMessages = useCallback(() => {
|
|
141
|
+
onClearMessages()
|
|
142
|
+
void message.success('Messages cleared')
|
|
143
|
+
}, [message, onClearMessages])
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
isCreating,
|
|
147
|
+
isThinking,
|
|
148
|
+
send,
|
|
149
|
+
sendContent,
|
|
150
|
+
interrupt,
|
|
151
|
+
clearMessages
|
|
152
|
+
}
|
|
153
|
+
}
|