@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
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import './Sender.scss'
|
|
2
2
|
|
|
3
|
-
import { App, Button, Input, Select, Tooltip } from 'antd'
|
|
3
|
+
import { App, Button, Cascader, Input, Select, Tooltip } from 'antd'
|
|
4
4
|
import type { TextAreaRef } from 'antd/es/input/TextArea'
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { useRef, useState } from 'react'
|
|
6
6
|
import { useTranslation } from 'react-i18next'
|
|
7
7
|
import useSWR from 'swr'
|
|
8
8
|
|
|
9
|
+
import type { PermissionMode } from '#~/hooks/chat/use-chat-permission-mode'
|
|
9
10
|
import type { AskUserQuestionParams, ChatMessageContent, SessionInfo, SessionStatus } from '@vibe-forge/core'
|
|
11
|
+
import { isShortcutMatch } from '../../../utils/shortcutUtils'
|
|
10
12
|
import type { CompletionItem } from './CompletionMenu'
|
|
11
|
-
import type { PermissionMode } from '#~/hooks/chat/use-chat-permission-mode'
|
|
12
13
|
import { CompletionMenu } from './CompletionMenu'
|
|
13
14
|
import { ThinkingStatus } from './ThinkingStatus'
|
|
14
|
-
import { isShortcutMatch } from '../../../utils/shortcutUtils'
|
|
15
15
|
|
|
16
16
|
const { TextArea } = Input
|
|
17
17
|
|
|
@@ -19,6 +19,7 @@ interface ModelSelectOption {
|
|
|
19
19
|
value: string
|
|
20
20
|
label: React.ReactNode
|
|
21
21
|
searchText: string
|
|
22
|
+
displayLabel: string
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
interface ModelSelectGroup {
|
|
@@ -34,13 +35,37 @@ interface PendingImage {
|
|
|
34
35
|
mimeType?: string
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
interface SenderToolGroup {
|
|
39
|
+
key: 'chrome-devtools' | 'system'
|
|
40
|
+
label: string
|
|
41
|
+
tools: string[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface SenderToolOption {
|
|
45
|
+
value: string
|
|
46
|
+
label: React.ReactNode
|
|
47
|
+
children?: SenderToolOption[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const formatToolLabel = (tool: string) => {
|
|
51
|
+
const parts = tool.split('__')
|
|
52
|
+
return parts[parts.length - 1] || tool
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const getToolGroupIcon = (groupKey: SenderToolGroup['key']) => {
|
|
56
|
+
return groupKey === 'chrome-devtools' ? 'web_traffic' : 'memory'
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
export function Sender({
|
|
38
60
|
onSend,
|
|
39
61
|
onSendContent,
|
|
62
|
+
adapterLocked = false,
|
|
40
63
|
sessionStatus,
|
|
41
64
|
onInterrupt,
|
|
42
65
|
onClear,
|
|
43
66
|
sessionInfo,
|
|
67
|
+
connectionError,
|
|
68
|
+
onRetryConnection,
|
|
44
69
|
interactionRequest,
|
|
45
70
|
onInteractionResponse,
|
|
46
71
|
placeholder,
|
|
@@ -57,10 +82,13 @@ export function Sender({
|
|
|
57
82
|
}: {
|
|
58
83
|
onSend: (text: string) => void
|
|
59
84
|
onSendContent: (content: ChatMessageContent[]) => void
|
|
85
|
+
adapterLocked?: boolean
|
|
60
86
|
sessionStatus?: SessionStatus
|
|
61
87
|
onInterrupt: () => void
|
|
62
88
|
onClear?: () => void
|
|
63
89
|
sessionInfo?: SessionInfo | null
|
|
90
|
+
connectionError?: string | null
|
|
91
|
+
onRetryConnection?: () => void
|
|
64
92
|
interactionRequest?: { id: string; payload: AskUserQuestionParams } | null
|
|
65
93
|
onInteractionResponse?: (id: string, data: string | string[]) => void
|
|
66
94
|
placeholder?: string
|
|
@@ -85,7 +113,6 @@ export function Sender({
|
|
|
85
113
|
|
|
86
114
|
const [showToolsList, setShowToolsList] = useState(false)
|
|
87
115
|
const textareaRef = useRef<TextAreaRef>(null)
|
|
88
|
-
const toolsRef = useRef<HTMLDivElement>(null)
|
|
89
116
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
90
117
|
const isMac = navigator.platform.includes('Mac')
|
|
91
118
|
const [pendingImages, setPendingImages] = useState<PendingImage[]>([])
|
|
@@ -107,16 +134,39 @@ export function Sender({
|
|
|
107
134
|
: 'mod+enter'
|
|
108
135
|
|
|
109
136
|
const isThinking = sessionStatus === 'running'
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
const groupedTools: SenderToolGroup[] = sessionInfo != null && sessionInfo.type === 'init'
|
|
138
|
+
? [
|
|
139
|
+
{
|
|
140
|
+
key: 'chrome-devtools',
|
|
141
|
+
label: t('chat.toolGroupChromeDevtools'),
|
|
142
|
+
tools: sessionInfo.tools.filter(tool => tool.startsWith('mcp__ChromeDevtools__'))
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: 'system',
|
|
146
|
+
label: t('chat.toolGroupSystem'),
|
|
147
|
+
tools: sessionInfo.tools.filter(tool => !tool.startsWith('mcp__ChromeDevtools__'))
|
|
148
|
+
}
|
|
149
|
+
].filter(group => group.tools.length > 0)
|
|
150
|
+
: []
|
|
151
|
+
const toolCascaderOptions: SenderToolOption[] = groupedTools.map(group => ({
|
|
152
|
+
value: group.key,
|
|
153
|
+
label: (
|
|
154
|
+
<span className='sender-tool-group-option'>
|
|
155
|
+
<span className='sender-tool-group-option__icon material-symbols-rounded'>{getToolGroupIcon(group.key)}</span>
|
|
156
|
+
<span className='sender-tool-group-option__text'>{group.label}</span>
|
|
157
|
+
<span className='sender-tool-group-option__count'>{group.tools.length}</span>
|
|
158
|
+
</span>
|
|
159
|
+
),
|
|
160
|
+
children: group.tools.map(tool => ({
|
|
161
|
+
value: tool,
|
|
162
|
+
label: (
|
|
163
|
+
<span className='sender-tool-option'>
|
|
164
|
+
<span className='sender-tool-option__dot' />
|
|
165
|
+
<span className='sender-tool-option__text'>{formatToolLabel(tool)}</span>
|
|
166
|
+
</span>
|
|
167
|
+
)
|
|
168
|
+
}))
|
|
169
|
+
}))
|
|
120
170
|
|
|
121
171
|
const [historyIndex, setHistoryIndex] = useState(-1)
|
|
122
172
|
const [draft, setDraft] = useState('')
|
|
@@ -354,7 +404,9 @@ export function Sender({
|
|
|
354
404
|
handleSend()
|
|
355
405
|
return
|
|
356
406
|
}
|
|
357
|
-
if (
|
|
407
|
+
if (
|
|
408
|
+
clearInputShortcut != null && clearInputShortcut.trim() !== '' && isShortcutMatch(e, clearInputShortcut, isMac)
|
|
409
|
+
) {
|
|
358
410
|
e.preventDefault()
|
|
359
411
|
clearInputValue()
|
|
360
412
|
return
|
|
@@ -544,6 +596,20 @@ export function Sender({
|
|
|
544
596
|
</div>
|
|
545
597
|
)}
|
|
546
598
|
<div className='chat-input-container'>
|
|
599
|
+
{connectionError && connectionError.trim() !== '' && (
|
|
600
|
+
<div className='connection-error-banner'>
|
|
601
|
+
<div className='connection-error-content'>
|
|
602
|
+
<span className='material-symbols-rounded'>error</span>
|
|
603
|
+
<div className='connection-error-copy'>
|
|
604
|
+
<div className='connection-error-title'>{t('chat.connectionErrorTitle')}</div>
|
|
605
|
+
<div className='connection-error-message'>{connectionError}</div>
|
|
606
|
+
</div>
|
|
607
|
+
</div>
|
|
608
|
+
<Button size='small' onClick={onRetryConnection}>
|
|
609
|
+
{t('chat.retryConnection')}
|
|
610
|
+
</Button>
|
|
611
|
+
</div>
|
|
612
|
+
)}
|
|
547
613
|
{modelUnavailable && (
|
|
548
614
|
<div className='model-unavailable'>
|
|
549
615
|
{t('chat.modelConfigRequired')}
|
|
@@ -622,31 +688,25 @@ export function Sender({
|
|
|
622
688
|
</Tooltip>
|
|
623
689
|
|
|
624
690
|
{sessionInfo != null && sessionInfo.type === 'init' && (
|
|
625
|
-
<div className='session-info-toolbar'
|
|
626
|
-
<
|
|
627
|
-
|
|
628
|
-
|
|
691
|
+
<div className='session-info-toolbar'>
|
|
692
|
+
<Cascader
|
|
693
|
+
open={showToolsList}
|
|
694
|
+
options={toolCascaderOptions}
|
|
695
|
+
expandTrigger='hover'
|
|
696
|
+
placement='topLeft'
|
|
697
|
+
allowClear={false}
|
|
698
|
+
popupClassName='sender-tools-cascader-popup'
|
|
699
|
+
onOpenChange={setShowToolsList}
|
|
700
|
+
onChange={() => setShowToolsList(false)}
|
|
629
701
|
>
|
|
630
|
-
<
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
<div className='tools-list-popup'>
|
|
637
|
-
<div className='popup-header'>{t('chat.availableTools')}</div>
|
|
638
|
-
<div className='popup-content'>
|
|
639
|
-
<div className='tools-list'>
|
|
640
|
-
{sessionInfo.tools.map(tool => (
|
|
641
|
-
<div key={tool} className='tool-item'>
|
|
642
|
-
<span className='material-symbols-rounded'>check_circle</span>
|
|
643
|
-
<span className='tool-name'>{tool}</span>
|
|
644
|
-
</div>
|
|
645
|
-
))}
|
|
646
|
-
</div>
|
|
647
|
-
</div>
|
|
702
|
+
<div className={`info-item ${showToolsList ? 'active' : ''}`}>
|
|
703
|
+
<span className='info-item-leading'>
|
|
704
|
+
<span className='material-symbols-rounded'>build</span>
|
|
705
|
+
</span>
|
|
706
|
+
<span className='info-text'>{t('chat.toolsCount', { count: sessionInfo.tools.length })}</span>
|
|
707
|
+
<span className='material-symbols-rounded arrow-icon'>keyboard_arrow_up</span>
|
|
648
708
|
</div>
|
|
649
|
-
|
|
709
|
+
</Cascader>
|
|
650
710
|
</div>
|
|
651
711
|
)}
|
|
652
712
|
</div>
|
|
@@ -660,7 +720,7 @@ export function Sender({
|
|
|
660
720
|
options={adapterOptions}
|
|
661
721
|
showSearch={false}
|
|
662
722
|
allowClear={false}
|
|
663
|
-
disabled={modelUnavailable || isThinking}
|
|
723
|
+
disabled={adapterLocked || modelUnavailable || isThinking}
|
|
664
724
|
onChange={(value) => onAdapterChange?.(value)}
|
|
665
725
|
placeholder={t('chat.adapterSelectPlaceholder', { defaultValue: 'Adapter' })}
|
|
666
726
|
optionLabelProp='label'
|
|
@@ -678,7 +738,7 @@ export function Sender({
|
|
|
678
738
|
disabled={modelUnavailable || isThinking}
|
|
679
739
|
onChange={(value) => onModelChange?.(value)}
|
|
680
740
|
placeholder={modelUnavailable ? t('chat.modelUnavailable') : t('chat.modelSelectPlaceholder')}
|
|
681
|
-
optionLabelProp='
|
|
741
|
+
optionLabelProp='displayLabel'
|
|
682
742
|
filterOption={(input, option) => {
|
|
683
743
|
const searchText = String((option as ModelSelectOption | undefined)?.searchText ?? '')
|
|
684
744
|
return searchText.toLowerCase().includes(input.toLowerCase())
|
|
@@ -701,7 +761,9 @@ export function Sender({
|
|
|
701
761
|
/>
|
|
702
762
|
|
|
703
763
|
<div
|
|
704
|
-
className={`chat-send-btn ${input.trim() !== '' && !modelUnavailable ? 'active' : ''} ${
|
|
764
|
+
className={`chat-send-btn ${input.trim() !== '' && !modelUnavailable ? 'active' : ''} ${
|
|
765
|
+
isThinking ? 'thinking' : ''
|
|
766
|
+
} ${modelUnavailable ? 'disabled' : ''}`}
|
|
705
767
|
onClick={modelUnavailable ? undefined : (isThinking ? onInterrupt : handleSend)}
|
|
706
768
|
>
|
|
707
769
|
<span className='material-symbols-rounded'>
|
|
@@ -3,7 +3,7 @@ import './ToolGroup.scss'
|
|
|
3
3
|
import type { ChatMessage, ChatMessageContent } from '@vibe-forge/core'
|
|
4
4
|
import React, { useState } from 'react'
|
|
5
5
|
import { useTranslation } from 'react-i18next'
|
|
6
|
-
import { MessageFooter } from '../../
|
|
6
|
+
import { MessageFooter } from '../../messages/MessageFooter'
|
|
7
7
|
import { ToolRenderer } from './ToolRenderer'
|
|
8
8
|
|
|
9
9
|
type ToolGroupProps = {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.app-shell {
|
|
2
|
+
height: 100vh;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: row;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.app-shell__content {
|
|
9
|
+
flex: 1;
|
|
10
|
+
position: relative;
|
|
11
|
+
overflow: hidden;
|
|
12
|
+
background-color: #fff;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.app-shell--dark {
|
|
16
|
+
.app-shell__content {
|
|
17
|
+
background-color: #141414;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import './AppShell.scss'
|
|
2
|
+
|
|
3
|
+
import { Layout } from 'antd'
|
|
4
|
+
import type { PropsWithChildren } from 'react'
|
|
5
|
+
|
|
6
|
+
import type { Session } from '@vibe-forge/core'
|
|
7
|
+
|
|
8
|
+
import { NavRail } from '#~/components/NavRail'
|
|
9
|
+
import { Sidebar } from '#~/components/Sidebar'
|
|
10
|
+
|
|
11
|
+
type AppShellProps = PropsWithChildren<{
|
|
12
|
+
activeSessionId?: string
|
|
13
|
+
isDarkMode: boolean
|
|
14
|
+
onDeletedSession: (deletedId: string, nextId?: string) => void
|
|
15
|
+
onSelectSession: (session: Session, isNew?: boolean) => void
|
|
16
|
+
showSidebar: boolean
|
|
17
|
+
sidebarWidth: number
|
|
18
|
+
}>
|
|
19
|
+
|
|
20
|
+
export function AppShell({
|
|
21
|
+
activeSessionId,
|
|
22
|
+
children,
|
|
23
|
+
isDarkMode,
|
|
24
|
+
onDeletedSession,
|
|
25
|
+
onSelectSession,
|
|
26
|
+
showSidebar,
|
|
27
|
+
sidebarWidth
|
|
28
|
+
}: AppShellProps) {
|
|
29
|
+
return (
|
|
30
|
+
<Layout className={`app-shell ${isDarkMode ? 'app-shell--dark' : ''}`}>
|
|
31
|
+
<NavRail />
|
|
32
|
+
{showSidebar && (
|
|
33
|
+
<Sidebar
|
|
34
|
+
width={sidebarWidth}
|
|
35
|
+
activeId={activeSessionId}
|
|
36
|
+
onSelectSession={onSelectSession}
|
|
37
|
+
onDeletedSession={onDeletedSession}
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
40
|
+
<Layout.Content className='app-shell__content'>
|
|
41
|
+
{children}
|
|
42
|
+
</Layout.Content>
|
|
43
|
+
</Layout>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { ModelServiceConfig } from '@vibe-forge/core'
|
|
2
|
+
|
|
3
|
+
export interface ServiceModelEntry {
|
|
4
|
+
serviceKey: string
|
|
5
|
+
model: string
|
|
6
|
+
selectorValue: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const normalizeNonEmptyString = (value: string | undefined) => {
|
|
10
|
+
const normalized = typeof value === 'string' ? value.trim() : ''
|
|
11
|
+
return normalized === '' ? undefined : normalized
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const buildServiceModelSelector = (serviceKey: string, modelName: string) => `${serviceKey},${modelName}`
|
|
15
|
+
|
|
16
|
+
export const parseServiceModelSelector = (value: string | undefined) => {
|
|
17
|
+
const normalizedValue = normalizeNonEmptyString(value)
|
|
18
|
+
if (!normalizedValue || !normalizedValue.includes(',')) return undefined
|
|
19
|
+
|
|
20
|
+
const [serviceKey, modelName] = normalizedValue.split(/,(.+)/)
|
|
21
|
+
const normalizedServiceKey = normalizeNonEmptyString(serviceKey)
|
|
22
|
+
const normalizedModelName = normalizeNonEmptyString(modelName)
|
|
23
|
+
if (!normalizedServiceKey || !normalizedModelName) return undefined
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
serviceKey: normalizedServiceKey,
|
|
27
|
+
modelName: normalizedModelName,
|
|
28
|
+
selectorValue: buildServiceModelSelector(normalizedServiceKey, normalizedModelName)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const listServiceModels = (modelServices: Record<string, ModelServiceConfig>) => {
|
|
33
|
+
const list: ServiceModelEntry[] = []
|
|
34
|
+
|
|
35
|
+
for (const [serviceKey, serviceValue] of Object.entries(modelServices)) {
|
|
36
|
+
const normalizedServiceKey = normalizeNonEmptyString(serviceKey)
|
|
37
|
+
if (!normalizedServiceKey) continue
|
|
38
|
+
|
|
39
|
+
const service = (serviceValue != null && typeof serviceValue === 'object')
|
|
40
|
+
? serviceValue
|
|
41
|
+
: undefined
|
|
42
|
+
const models = Array.isArray(service?.models) ? service.models : []
|
|
43
|
+
|
|
44
|
+
for (const model of models) {
|
|
45
|
+
const normalizedModel = normalizeNonEmptyString(model)
|
|
46
|
+
if (!normalizedModel) continue
|
|
47
|
+
|
|
48
|
+
list.push({
|
|
49
|
+
serviceKey: normalizedServiceKey,
|
|
50
|
+
model: normalizedModel,
|
|
51
|
+
selectorValue: buildServiceModelSelector(normalizedServiceKey, normalizedModel)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return list
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const findExactServiceModel = (serviceModels: ServiceModelEntry[], serviceKey: string, modelName: string) => (
|
|
60
|
+
serviceModels.find(entry => entry.serviceKey === serviceKey && entry.model === modelName)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
export const resolveServiceModelSelector = (params: {
|
|
64
|
+
value?: string
|
|
65
|
+
serviceModels: ServiceModelEntry[]
|
|
66
|
+
preferredServiceKey?: string
|
|
67
|
+
}) => {
|
|
68
|
+
const normalizedValue = normalizeNonEmptyString(params.value)
|
|
69
|
+
if (!normalizedValue) return undefined
|
|
70
|
+
|
|
71
|
+
const parsed = parseServiceModelSelector(normalizedValue)
|
|
72
|
+
if (parsed) {
|
|
73
|
+
const exactMatch = findExactServiceModel(params.serviceModels, parsed.serviceKey, parsed.modelName)
|
|
74
|
+
if (exactMatch) return exactMatch.selectorValue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const modelName = parsed?.modelName ?? normalizedValue
|
|
78
|
+
const candidates = params.serviceModels.filter(entry => entry.model === modelName)
|
|
79
|
+
if (candidates.length === 0) return undefined
|
|
80
|
+
|
|
81
|
+
const preferredKeys = [parsed?.serviceKey, normalizeNonEmptyString(params.preferredServiceKey)]
|
|
82
|
+
.filter((value, index, array): value is string => Boolean(value) && array.indexOf(value) === index)
|
|
83
|
+
|
|
84
|
+
for (const preferredKey of preferredKeys) {
|
|
85
|
+
const candidate = candidates.find(entry => entry.serviceKey === preferredKey)
|
|
86
|
+
if (candidate) return candidate.selectorValue
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return candidates[0]?.selectorValue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const resolveChatModelSelection = (params: {
|
|
93
|
+
value?: string
|
|
94
|
+
builtinModels?: Iterable<string>
|
|
95
|
+
serviceModels: ServiceModelEntry[]
|
|
96
|
+
defaultModelService?: string
|
|
97
|
+
}) => {
|
|
98
|
+
const normalizedValue = normalizeNonEmptyString(params.value)
|
|
99
|
+
if (!normalizedValue) return undefined
|
|
100
|
+
|
|
101
|
+
const builtinModelSet = new Set(
|
|
102
|
+
Array.from(params.builtinModels ?? [])
|
|
103
|
+
.map(item => normalizeNonEmptyString(item))
|
|
104
|
+
.filter((item): item is string => Boolean(item))
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if (builtinModelSet.has(normalizedValue)) return normalizedValue
|
|
108
|
+
|
|
109
|
+
return resolveServiceModelSelector({
|
|
110
|
+
value: normalizedValue,
|
|
111
|
+
serviceModels: params.serviceModels,
|
|
112
|
+
preferredServiceKey: params.defaultModelService
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const resolveDefaultChatModelSelection = (params: {
|
|
117
|
+
defaultModel?: string
|
|
118
|
+
defaultModelService?: string
|
|
119
|
+
builtinModels?: Iterable<string>
|
|
120
|
+
serviceModels: ServiceModelEntry[]
|
|
121
|
+
}) => {
|
|
122
|
+
const builtinModels = Array.from(params.builtinModels ?? [])
|
|
123
|
+
.map(item => normalizeNonEmptyString(item))
|
|
124
|
+
.filter((item): item is string => Boolean(item))
|
|
125
|
+
const builtinModelSet = new Set(builtinModels)
|
|
126
|
+
const normalizedDefaultModel = normalizeNonEmptyString(params.defaultModel)
|
|
127
|
+
|
|
128
|
+
if (normalizedDefaultModel) {
|
|
129
|
+
const parsed = parseServiceModelSelector(normalizedDefaultModel)
|
|
130
|
+
const resolvedServiceModel = resolveServiceModelSelector({
|
|
131
|
+
value: normalizedDefaultModel,
|
|
132
|
+
serviceModels: params.serviceModels,
|
|
133
|
+
preferredServiceKey: parsed?.serviceKey ?? params.defaultModelService
|
|
134
|
+
})
|
|
135
|
+
if (resolvedServiceModel) return resolvedServiceModel
|
|
136
|
+
|
|
137
|
+
if (builtinModelSet.has(normalizedDefaultModel)) return normalizedDefaultModel
|
|
138
|
+
if (parsed?.modelName && builtinModelSet.has(parsed.modelName)) return parsed.modelName
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const normalizedDefaultModelService = normalizeNonEmptyString(params.defaultModelService)
|
|
142
|
+
if (normalizedDefaultModelService) {
|
|
143
|
+
const defaultServiceModel = params.serviceModels.find(entry => entry.serviceKey === normalizedDefaultModelService)
|
|
144
|
+
if (defaultServiceModel) return defaultServiceModel.selectorValue
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (builtinModels.length > 0) return builtinModels[0]
|
|
148
|
+
|
|
149
|
+
return params.serviceModels[0]?.selectorValue
|
|
150
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ReactNode, createElement } from 'react'
|
|
2
2
|
import { useEffect, useMemo, useState } from 'react'
|
|
3
3
|
import useSWR from 'swr'
|
|
4
4
|
|
|
5
5
|
import { getConfig } from '#~/api.js'
|
|
6
|
-
import type { ConfigResponse } from '@vibe-forge/core'
|
|
7
6
|
import { getAdapterDisplay } from '#~/resources/adapters.js'
|
|
7
|
+
import type { ConfigResponse } from '@vibe-forge/core'
|
|
8
8
|
|
|
9
9
|
const ADAPTER_STORAGE_KEY = 'vf_chat_adapter'
|
|
10
10
|
|
|
@@ -26,6 +26,22 @@ export function useChatAdapter() {
|
|
|
26
26
|
|
|
27
27
|
const defaultAdapter = configRes?.sources?.merged?.general?.defaultAdapter
|
|
28
28
|
|
|
29
|
+
const resolveAdapter = (value?: string) => {
|
|
30
|
+
const normalizedValue = typeof value === 'string' ? value.trim() : ''
|
|
31
|
+
const keys = Object.keys(mergedAdapters)
|
|
32
|
+
if (keys.length === 0) return undefined
|
|
33
|
+
if (normalizedValue !== '' && keys.includes(normalizedValue)) return normalizedValue
|
|
34
|
+
if (defaultAdapter && keys.includes(defaultAdapter as string)) return defaultAdapter as string
|
|
35
|
+
return keys[0]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const updateSelectedAdapter = (value?: string) => {
|
|
39
|
+
setSelectedAdapter((prev) => {
|
|
40
|
+
const nextValue = resolveAdapter(value)
|
|
41
|
+
return nextValue === prev ? prev : nextValue
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
const adapterOptions = useMemo<Array<{ value: string; label: ReactNode }>>(() => {
|
|
30
46
|
const keys = Object.keys(mergedAdapters)
|
|
31
47
|
return keys.map((key) => {
|
|
@@ -55,11 +71,7 @@ export function useChatAdapter() {
|
|
|
55
71
|
setSelectedAdapter(undefined)
|
|
56
72
|
return
|
|
57
73
|
}
|
|
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
|
-
})
|
|
74
|
+
setSelectedAdapter((prev) => resolveAdapter(prev))
|
|
63
75
|
}, [defaultAdapter, mergedAdapters])
|
|
64
76
|
|
|
65
77
|
// Persist to localStorage
|
|
@@ -75,7 +87,7 @@ export function useChatAdapter() {
|
|
|
75
87
|
|
|
76
88
|
return {
|
|
77
89
|
selectedAdapter,
|
|
78
|
-
setSelectedAdapter,
|
|
90
|
+
setSelectedAdapter: updateSelectedAdapter,
|
|
79
91
|
adapterOptions
|
|
80
92
|
}
|
|
81
93
|
}
|