@vibe-forge/client 0.3.0 → 0.4.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 +1 -1
- package/dist/assets/{arc-CwMXUVsq.js → arc-DgIxeTMg.js} +1 -1
- package/dist/assets/{blockDiagram-c4efeb88-CGxJV7KJ.js → blockDiagram-c4efeb88-CEAob3X9.js} +1 -1
- package/dist/assets/{c4Diagram-c83219d4-BKhin7cY.js → c4Diagram-c83219d4-DwIxpDKd.js} +1 -1
- package/dist/assets/channel-DhtnrNJ6.js +1 -0
- package/dist/assets/{classDiagram-beda092f-BASmn22R.js → classDiagram-beda092f-Cz1q8u_0.js} +1 -1
- package/dist/assets/{classDiagram-v2-2358418a-BUk9rNBX.js → classDiagram-v2-2358418a-CImgTuwd.js} +1 -1
- package/dist/assets/clone-7bHB6YkC.js +1 -0
- package/dist/assets/{createText-1719965b-2XqnWjQY.js → createText-1719965b-C1_HJcCc.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-BU8qStzd.js} +1 -1
- package/dist/assets/{erDiagram-0228fc6a-CCR2or72.js → erDiagram-0228fc6a-DNA1Fz2L.js} +1 -1
- package/dist/assets/{flowDb-c6c81e3f-B72HWT9x.js → flowDb-c6c81e3f-DjiCStMN.js} +1 -1
- package/dist/assets/{flowDiagram-50d868cf-WOi0KARY.js → flowDiagram-50d868cf-CSDi0-RD.js} +1 -1
- package/dist/assets/flowDiagram-v2-4f6560a1-_13Sz5Wh.js +1 -0
- package/dist/assets/{flowchart-elk-definition-6af322e1-i_Yd0LCE.js → flowchart-elk-definition-6af322e1-DrhIMas7.js} +1 -1
- package/dist/assets/{ganttDiagram-a2739b55-CFH9zF14.js → ganttDiagram-a2739b55-CTZnUP5z.js} +1 -1
- package/dist/assets/{gitGraphDiagram-82fe8481-DglKfMze.js → gitGraphDiagram-82fe8481-COOW7jTi.js} +1 -1
- package/dist/assets/{graph-BKbBNGPf.js → graph-CIkpD4Kx.js} +1 -1
- package/dist/assets/{index-5325376f-BK7F9nSl.js → index-5325376f-aVVRRTIu.js} +1 -1
- package/dist/assets/index-D1giUI7r.css +1 -0
- package/dist/assets/index-DRSI_ZIL.js +514 -0
- package/dist/assets/{infoDiagram-8eee0895-BLFL77_D.js → infoDiagram-8eee0895-DQpZ1LVD.js} +1 -1
- package/dist/assets/{journeyDiagram-c64418c1-CS9XctDL.js → journeyDiagram-c64418c1-DoKguIuk.js} +1 -1
- package/dist/assets/{layout-By3JZZGt.js → layout-Tnmha8Nh.js} +1 -1
- package/dist/assets/{line-9GUsXbwv.js → line-BQR2SOyl.js} +1 -1
- package/dist/assets/{linear-DzGV4E9N.js → linear-DlG0eemV.js} +1 -1
- package/dist/assets/{mermaid.core-CG3Ib42Q.js → mermaid.core-BnwYO0He.js} +6 -6
- package/dist/assets/{mindmap-definition-8da855dc-WQ3LPKJU.js → mindmap-definition-8da855dc-BllYwDID.js} +1 -1
- package/dist/assets/{pieDiagram-a8764435-DHVIUZiN.js → pieDiagram-a8764435-DwCkhPVc.js} +1 -1
- package/dist/assets/{quadrantDiagram-1e28029f-C3G9Ye8-.js → quadrantDiagram-1e28029f-c40GKTU0.js} +1 -1
- package/dist/assets/{requirementDiagram-08caed73-C9ES1D5G.js → requirementDiagram-08caed73-DnQp2Tk6.js} +1 -1
- package/dist/assets/{sankeyDiagram-a04cb91d-B4BKXclQ.js → sankeyDiagram-a04cb91d-CnJrs13b.js} +1 -1
- package/dist/assets/{sequenceDiagram-c5b8d532-DrgEb25G.js → sequenceDiagram-c5b8d532-1YBwnpKu.js} +1 -1
- package/dist/assets/{stateDiagram-1ecb1508-CF1XWARJ.js → stateDiagram-1ecb1508-BFBxQ6Fh.js} +1 -1
- package/dist/assets/{stateDiagram-v2-c2b004d7-IO3i3yXv.js → stateDiagram-v2-c2b004d7-Dmechvv2.js} +1 -1
- package/dist/assets/{styles-b4e223ce-DACN9aSc.js → styles-b4e223ce-DWWfWX8O.js} +1 -1
- package/dist/assets/{styles-ca3715f6-bekm2WLP.js → styles-ca3715f6-CKKvZxaU.js} +1 -1
- package/dist/assets/{styles-d45a18b0-OzTDVBb8.js → styles-d45a18b0-dKMOUh9p.js} +1 -1
- package/dist/assets/{svgDrawCommon-b86b1483-BWroJerr.js → svgDrawCommon-b86b1483-CBgjChPM.js} +1 -1
- package/dist/assets/{timeline-definition-faaaa080-CCfRNigO.js → timeline-definition-faaaa080-NCt-HHmb.js} +1 -1
- package/dist/assets/{xychartDiagram-f5964ef8-C3cbfVqN.js → xychartDiagram-f5964ef8-BJhXS4dG.js} +1 -1
- package/dist/index.html +2 -7
- package/index.html +0 -5
- package/package.json +11 -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 +82 -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 +37 -29
- package/src/components/{chat/CodeBlock.tsx → CodeBlock.tsx} +3 -1
- package/src/components/ConfigView.tsx +7 -0
- 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 +89 -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} +80 -0
- package/src/components/chat/{Sender.tsx → Sender/Sender.tsx} +161 -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/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/hooks/chat/use-chat-interaction.ts +26 -0
- package/src/{components/chat/useChatModels.tsx → hooks/chat/use-chat-models.tsx} +46 -15
- 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 +147 -0
- package/src/hooks/chat/use-chat-session-messages.ts +250 -0
- package/src/hooks/chat/use-chat-session.ts +57 -0
- package/src/hooks/chat/use-chat-view.ts +39 -0
- package/src/main.tsx +10 -13
- 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,397 @@
|
|
|
1
|
+
import './ChannelRecordEditor.scss'
|
|
2
|
+
|
|
3
|
+
import { Button, Input, InputNumber, Select, Switch, Tooltip } from 'antd'
|
|
4
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
5
|
+
import type { ZodRawShape, ZodTypeAny } from 'zod'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
import type { ChannelDescriptor } from '@vibe-forge/core/channel'
|
|
9
|
+
import { channelBaseSchema } from '@vibe-forge/core/channel'
|
|
10
|
+
|
|
11
|
+
import { ComplexTextEditor, StringArrayEditor } from '../ConfigEditors'
|
|
12
|
+
import { FieldRow } from '../ConfigFieldRow'
|
|
13
|
+
import { channelDefinitions } from '../channelDefinitions'
|
|
14
|
+
import { getTypeIcon, isSensitiveKey } from '../configUtils'
|
|
15
|
+
import type { TranslationFn } from '../configUtils'
|
|
16
|
+
|
|
17
|
+
type ChannelRecordValue = Record<string, unknown>
|
|
18
|
+
|
|
19
|
+
const getObjectShape = (schema: ZodTypeAny): ZodRawShape => {
|
|
20
|
+
if (schema instanceof z.ZodObject) return schema.shape
|
|
21
|
+
if (schema instanceof z.ZodEffects) return getObjectShape(schema.innerType())
|
|
22
|
+
if (schema instanceof z.ZodOptional) return getObjectShape(schema.unwrap())
|
|
23
|
+
if (schema instanceof z.ZodDefault) return getObjectShape(schema.removeDefault())
|
|
24
|
+
if (schema instanceof z.ZodNullable) return getObjectShape(schema.unwrap())
|
|
25
|
+
return {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const unwrapSchema = (schema: ZodTypeAny): ZodTypeAny => {
|
|
29
|
+
if (schema instanceof z.ZodEffects) return unwrapSchema(schema.innerType())
|
|
30
|
+
if (schema instanceof z.ZodOptional) return unwrapSchema(schema.unwrap())
|
|
31
|
+
if (schema instanceof z.ZodDefault) return unwrapSchema(schema.removeDefault())
|
|
32
|
+
if (schema instanceof z.ZodNullable) return unwrapSchema(schema.unwrap())
|
|
33
|
+
return schema
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const getDefaultValue = (schema: ZodTypeAny): unknown => {
|
|
37
|
+
if (schema instanceof z.ZodDefault) return schema._def.defaultValue()
|
|
38
|
+
if (schema instanceof z.ZodOptional) return undefined
|
|
39
|
+
if (schema instanceof z.ZodNullable) return null
|
|
40
|
+
if (schema instanceof z.ZodLiteral) return schema.value
|
|
41
|
+
if (schema instanceof z.ZodEnum) return schema.options[0]
|
|
42
|
+
if (schema instanceof z.ZodNativeEnum) {
|
|
43
|
+
const values = Object.values(schema.enum)
|
|
44
|
+
return values.length > 0 ? values[0] : undefined
|
|
45
|
+
}
|
|
46
|
+
if (schema instanceof z.ZodString) return ''
|
|
47
|
+
if (schema instanceof z.ZodNumber) return 0
|
|
48
|
+
if (schema instanceof z.ZodBoolean) return false
|
|
49
|
+
if (schema instanceof z.ZodArray) return []
|
|
50
|
+
if (schema instanceof z.ZodObject) {
|
|
51
|
+
const shape = getObjectShape(schema)
|
|
52
|
+
return Object.fromEntries(Object.entries(shape).map(([key, value]) => [key, getDefaultValue(value)]))
|
|
53
|
+
}
|
|
54
|
+
if (schema instanceof z.ZodRecord) return {}
|
|
55
|
+
if (schema instanceof z.ZodEffects) return getDefaultValue(schema.innerType())
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const toLabel = (key: string) =>
|
|
60
|
+
key
|
|
61
|
+
.replace(/_/g, ' ')
|
|
62
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
63
|
+
.replace(/\b\w/g, char => char.toUpperCase())
|
|
64
|
+
|
|
65
|
+
const buildRecordDefaults = (definition: ChannelDescriptor) => {
|
|
66
|
+
const shape = getObjectShape(definition.configSchema)
|
|
67
|
+
const entries = Object.entries(shape)
|
|
68
|
+
return entries.reduce<Record<string, unknown>>((acc, [key, schema]) => {
|
|
69
|
+
acc[key] = getDefaultValue(schema)
|
|
70
|
+
return acc
|
|
71
|
+
}, {})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const getChannelDefinition = (type: string | undefined) =>
|
|
75
|
+
channelDefinitions.find(definition => definition.type === type)
|
|
76
|
+
|
|
77
|
+
export const ChannelRecordEditor = ({
|
|
78
|
+
value,
|
|
79
|
+
onChange,
|
|
80
|
+
t,
|
|
81
|
+
keyPlaceholder
|
|
82
|
+
}: {
|
|
83
|
+
value: Record<string, unknown>
|
|
84
|
+
onChange: (nextValue: Record<string, unknown>) => void
|
|
85
|
+
t: TranslationFn
|
|
86
|
+
keyPlaceholder: string
|
|
87
|
+
}) => {
|
|
88
|
+
const [newKey, setNewKey] = useState('')
|
|
89
|
+
const [newType, setNewType] = useState(channelDefinitions[0]?.type ?? '')
|
|
90
|
+
const entries = useMemo(() => Object.entries(value), [value])
|
|
91
|
+
const [collapsedKeys, setCollapsedKeys] = useState<Record<string, boolean>>(() => (
|
|
92
|
+
Object.fromEntries(entries.map(([key]) => [key, true]))
|
|
93
|
+
))
|
|
94
|
+
const baseKeys = useMemo(() => new Set(Object.keys(getObjectShape(channelBaseSchema))), [])
|
|
95
|
+
const typeOptions = useMemo(() =>
|
|
96
|
+
channelDefinitions.map(definition => ({
|
|
97
|
+
value: definition.type,
|
|
98
|
+
label: definition.label
|
|
99
|
+
})), [])
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
setCollapsedKeys(prev => {
|
|
103
|
+
const next: Record<string, boolean> = {}
|
|
104
|
+
for (const [key] of entries) {
|
|
105
|
+
next[key] = prev[key] ?? true
|
|
106
|
+
}
|
|
107
|
+
return next
|
|
108
|
+
})
|
|
109
|
+
}, [entries])
|
|
110
|
+
|
|
111
|
+
const updateChannel = (key: string, nextValue: ChannelRecordValue) => {
|
|
112
|
+
onChange({ ...value, [key]: nextValue })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const renderField = (
|
|
116
|
+
channelKey: string,
|
|
117
|
+
recordValue: ChannelRecordValue,
|
|
118
|
+
fieldKey: string,
|
|
119
|
+
fieldSchema: ZodTypeAny
|
|
120
|
+
) => {
|
|
121
|
+
const rawSchema = unwrapSchema(fieldSchema)
|
|
122
|
+
const description = rawSchema.description ?? ''
|
|
123
|
+
const label = toLabel(fieldKey)
|
|
124
|
+
const currentValue = recordValue[fieldKey]
|
|
125
|
+
const valueToUse = currentValue !== undefined ? currentValue : getDefaultValue(fieldSchema)
|
|
126
|
+
|
|
127
|
+
if (rawSchema instanceof z.ZodString) {
|
|
128
|
+
const isSensitive = isSensitiveKey(fieldKey)
|
|
129
|
+
return (
|
|
130
|
+
<FieldRow key={fieldKey} title={label} description={description} icon={getTypeIcon('string')}>
|
|
131
|
+
{isSensitive
|
|
132
|
+
? (
|
|
133
|
+
<Input.Password
|
|
134
|
+
value={typeof valueToUse === 'string' ? valueToUse : ''}
|
|
135
|
+
onChange={(event) => {
|
|
136
|
+
updateChannel(channelKey, { ...recordValue, [fieldKey]: event.target.value })
|
|
137
|
+
}}
|
|
138
|
+
placeholder={t('config.editor.secretPlaceholder')}
|
|
139
|
+
/>
|
|
140
|
+
)
|
|
141
|
+
: (
|
|
142
|
+
<Input
|
|
143
|
+
value={typeof valueToUse === 'string' ? valueToUse : ''}
|
|
144
|
+
onChange={(event) => {
|
|
145
|
+
updateChannel(channelKey, { ...recordValue, [fieldKey]: event.target.value })
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
</FieldRow>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (rawSchema instanceof z.ZodNumber) {
|
|
154
|
+
return (
|
|
155
|
+
<FieldRow key={fieldKey} title={label} description={description} icon={getTypeIcon('number')}>
|
|
156
|
+
<InputNumber
|
|
157
|
+
value={typeof valueToUse === 'number' ? valueToUse : 0}
|
|
158
|
+
onChange={(nextValue) => {
|
|
159
|
+
updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue ?? 0 })
|
|
160
|
+
}}
|
|
161
|
+
/>
|
|
162
|
+
</FieldRow>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (rawSchema instanceof z.ZodBoolean) {
|
|
167
|
+
return (
|
|
168
|
+
<FieldRow key={fieldKey} title={label} description={description} icon={getTypeIcon('boolean')}>
|
|
169
|
+
<Switch
|
|
170
|
+
checked={Boolean(valueToUse)}
|
|
171
|
+
onChange={(checked) => {
|
|
172
|
+
updateChannel(channelKey, { ...recordValue, [fieldKey]: checked })
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
</FieldRow>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (rawSchema instanceof z.ZodEnum || rawSchema instanceof z.ZodNativeEnum) {
|
|
180
|
+
const optionValues: string[] = (
|
|
181
|
+
rawSchema instanceof z.ZodEnum
|
|
182
|
+
? rawSchema.options
|
|
183
|
+
: Object.values(rawSchema.enum)
|
|
184
|
+
).map((option: string | number) => String(option))
|
|
185
|
+
return (
|
|
186
|
+
<FieldRow key={fieldKey} title={label} description={description} icon={getTypeIcon('string')}>
|
|
187
|
+
<Select
|
|
188
|
+
value={typeof valueToUse === 'string' ? valueToUse : undefined}
|
|
189
|
+
options={optionValues.map((option) => ({ value: option, label: option }))}
|
|
190
|
+
onChange={(nextValue) => {
|
|
191
|
+
updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue })
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
</FieldRow>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (rawSchema instanceof z.ZodArray) {
|
|
199
|
+
const inner = unwrapSchema(rawSchema.element)
|
|
200
|
+
if (inner instanceof z.ZodString) {
|
|
201
|
+
const items = Array.isArray(valueToUse)
|
|
202
|
+
? valueToUse.filter(item => typeof item === 'string')
|
|
203
|
+
: []
|
|
204
|
+
return (
|
|
205
|
+
<FieldRow
|
|
206
|
+
key={fieldKey}
|
|
207
|
+
title={label}
|
|
208
|
+
description={description}
|
|
209
|
+
icon={getTypeIcon('array')}
|
|
210
|
+
layout='stacked'
|
|
211
|
+
>
|
|
212
|
+
<StringArrayEditor
|
|
213
|
+
value={items}
|
|
214
|
+
onChange={(nextValue) => {
|
|
215
|
+
updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue })
|
|
216
|
+
}}
|
|
217
|
+
t={t}
|
|
218
|
+
/>
|
|
219
|
+
</FieldRow>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
return (
|
|
223
|
+
<FieldRow
|
|
224
|
+
key={fieldKey}
|
|
225
|
+
title={label}
|
|
226
|
+
description={description}
|
|
227
|
+
icon={getTypeIcon('array')}
|
|
228
|
+
layout='stacked'
|
|
229
|
+
>
|
|
230
|
+
<ComplexTextEditor
|
|
231
|
+
value={Array.isArray(valueToUse) ? valueToUse : []}
|
|
232
|
+
onChange={(nextValue) => {
|
|
233
|
+
updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue })
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
</FieldRow>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<FieldRow
|
|
242
|
+
key={fieldKey}
|
|
243
|
+
title={label}
|
|
244
|
+
description={description}
|
|
245
|
+
icon={getTypeIcon('object')}
|
|
246
|
+
layout='stacked'
|
|
247
|
+
>
|
|
248
|
+
<ComplexTextEditor
|
|
249
|
+
value={valueToUse ?? {}}
|
|
250
|
+
onChange={(nextValue) => {
|
|
251
|
+
updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue })
|
|
252
|
+
}}
|
|
253
|
+
/>
|
|
254
|
+
</FieldRow>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<div className='config-view__record-list'>
|
|
260
|
+
{entries.map(([key, itemValue]) => {
|
|
261
|
+
const recordValue = (itemValue != null && typeof itemValue === 'object')
|
|
262
|
+
? itemValue as ChannelRecordValue
|
|
263
|
+
: {}
|
|
264
|
+
const type = typeof recordValue.type === 'string' ? recordValue.type : undefined
|
|
265
|
+
const definition = getChannelDefinition(type)
|
|
266
|
+
const titleValue = typeof recordValue.title === 'string' ? recordValue.title : ''
|
|
267
|
+
const descriptionValue = typeof recordValue.description === 'string' ? recordValue.description : ''
|
|
268
|
+
const displayName = titleValue.trim() !== '' ? titleValue : key
|
|
269
|
+
const typeLabel = definition?.label ?? type ?? t('config.editor.unknownChannelType')
|
|
270
|
+
const isCollapsed = collapsedKeys[key] === true
|
|
271
|
+
const shape = definition ? getObjectShape(definition.configSchema) : {}
|
|
272
|
+
const fieldEntries = Object.entries(shape)
|
|
273
|
+
.filter(([fieldKey]) => fieldKey !== 'type')
|
|
274
|
+
.sort(([a], [b]) => {
|
|
275
|
+
const order = ['title', 'description', 'enabled', 'admins']
|
|
276
|
+
const aIndex = order.indexOf(a)
|
|
277
|
+
const bIndex = order.indexOf(b)
|
|
278
|
+
if (aIndex === -1 && bIndex === -1) return a.localeCompare(b)
|
|
279
|
+
if (aIndex === -1) return 1
|
|
280
|
+
if (bIndex === -1) return -1
|
|
281
|
+
return aIndex - bIndex
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div
|
|
286
|
+
key={key}
|
|
287
|
+
className={`config-view__record-card${isCollapsed ? ' config-view__record-card--collapsed' : ''}`}
|
|
288
|
+
>
|
|
289
|
+
<div className='config-view__record-title'>
|
|
290
|
+
<div className='config-view__record-title-left'>
|
|
291
|
+
<Tooltip title={isCollapsed ? t('config.editor.expand') : t('config.editor.collapse')}>
|
|
292
|
+
<Button
|
|
293
|
+
size='small'
|
|
294
|
+
type='text'
|
|
295
|
+
className='config-view__icon-button config-view__icon-button--compact'
|
|
296
|
+
aria-label={isCollapsed ? t('config.editor.expand') : t('config.editor.collapse')}
|
|
297
|
+
icon={
|
|
298
|
+
<span className='material-symbols-rounded'>{isCollapsed ? 'chevron_right' : 'expand_more'}</span>
|
|
299
|
+
}
|
|
300
|
+
onClick={() => {
|
|
301
|
+
setCollapsedKeys(prev => ({ ...prev, [key]: !isCollapsed }))
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
</Tooltip>
|
|
305
|
+
<div className='config-view__record-heading'>
|
|
306
|
+
<div>{displayName}</div>
|
|
307
|
+
<div className='config-view__record-subtitle'>
|
|
308
|
+
{key} · {typeLabel}
|
|
309
|
+
</div>
|
|
310
|
+
{descriptionValue !== '' && (
|
|
311
|
+
<div className='config-view__record-desc'>{descriptionValue}</div>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
<div className='config-view__record-actions'>
|
|
316
|
+
<Tooltip title={t('config.editor.remove')}>
|
|
317
|
+
<Button
|
|
318
|
+
size='small'
|
|
319
|
+
type='text'
|
|
320
|
+
danger
|
|
321
|
+
className='config-view__icon-button config-view__icon-button--compact'
|
|
322
|
+
aria-label={t('config.editor.remove')}
|
|
323
|
+
icon={<span className='material-symbols-rounded'>delete</span>}
|
|
324
|
+
onClick={() => {
|
|
325
|
+
const updated = { ...value }
|
|
326
|
+
delete updated[key]
|
|
327
|
+
onChange(updated)
|
|
328
|
+
}}
|
|
329
|
+
/>
|
|
330
|
+
</Tooltip>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
<div className='config-view__record-body'>
|
|
334
|
+
<div className='config-view__record-fields'>
|
|
335
|
+
<FieldRow
|
|
336
|
+
title={t('config.editor.channelType')}
|
|
337
|
+
description={definition?.description ?? ''}
|
|
338
|
+
icon={getTypeIcon('string')}
|
|
339
|
+
>
|
|
340
|
+
<Select
|
|
341
|
+
value={type}
|
|
342
|
+
options={typeOptions}
|
|
343
|
+
onChange={(nextType) => {
|
|
344
|
+
const nextDefinition = getChannelDefinition(nextType)
|
|
345
|
+
if (!nextDefinition) return
|
|
346
|
+
const defaults = buildRecordDefaults(nextDefinition)
|
|
347
|
+
const preserved = Object.fromEntries(
|
|
348
|
+
Object.entries(recordValue).filter(([fieldKey]) => baseKeys.has(fieldKey))
|
|
349
|
+
)
|
|
350
|
+
updateChannel(key, { ...defaults, ...preserved, type: nextType })
|
|
351
|
+
}}
|
|
352
|
+
/>
|
|
353
|
+
</FieldRow>
|
|
354
|
+
{fieldEntries.map(([fieldKey, fieldSchema]) => (
|
|
355
|
+
renderField(key, recordValue, fieldKey, fieldSchema)
|
|
356
|
+
))}
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
)
|
|
361
|
+
})}
|
|
362
|
+
<div className='config-view__record-add'>
|
|
363
|
+
<div className='config-view__record-add-inputs'>
|
|
364
|
+
<Input
|
|
365
|
+
value={newKey}
|
|
366
|
+
placeholder={keyPlaceholder}
|
|
367
|
+
onChange={(event) => setNewKey(event.target.value)}
|
|
368
|
+
/>
|
|
369
|
+
<Select
|
|
370
|
+
value={newType}
|
|
371
|
+
options={typeOptions}
|
|
372
|
+
onChange={(nextValue) => setNewType(nextValue)}
|
|
373
|
+
/>
|
|
374
|
+
<Tooltip title={t('common.confirm')}>
|
|
375
|
+
<Button
|
|
376
|
+
size='small'
|
|
377
|
+
type='primary'
|
|
378
|
+
className='config-view__icon-button'
|
|
379
|
+
aria-label={t('common.confirm')}
|
|
380
|
+
icon={<span className='material-symbols-rounded'>check</span>}
|
|
381
|
+
disabled={newKey.trim() === '' || Object.hasOwn(value, newKey) || newType === ''}
|
|
382
|
+
onClick={() => {
|
|
383
|
+
const definition = getChannelDefinition(newType)
|
|
384
|
+
if (!definition) return
|
|
385
|
+
onChange({
|
|
386
|
+
...value,
|
|
387
|
+
[newKey]: buildRecordDefaults(definition)
|
|
388
|
+
})
|
|
389
|
+
setNewKey('')
|
|
390
|
+
}}
|
|
391
|
+
/>
|
|
392
|
+
</Tooltip>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
)
|
|
397
|
+
}
|
|
@@ -5,7 +5,7 @@ import useSWR from 'swr'
|
|
|
5
5
|
|
|
6
6
|
import type { RuleDetail, RuleSummary } from '#~/api.js'
|
|
7
7
|
import { getRuleDetail } from '#~/api.js'
|
|
8
|
-
import { MarkdownContent } from '#~/components/
|
|
8
|
+
import { MarkdownContent } from '#~/components/MarkdownContent'
|
|
9
9
|
import { LoadingState } from './LoadingState'
|
|
10
10
|
|
|
11
11
|
type RuleItemProps = {
|
|
@@ -7,7 +7,7 @@ import useSWR from 'swr'
|
|
|
7
7
|
|
|
8
8
|
import type { SpecDetail, SpecSummary } from '#~/api.js'
|
|
9
9
|
import { getSpecDetail } from '#~/api.js'
|
|
10
|
-
import { MarkdownContent } from '#~/components/
|
|
10
|
+
import { MarkdownContent } from '#~/components/MarkdownContent'
|
|
11
11
|
import { LoadingState } from './LoadingState'
|
|
12
12
|
import { MetaList } from './MetaList'
|
|
13
13
|
|
|
@@ -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,17 +1,17 @@
|
|
|
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
5
|
import type { ConfigResponse, ModelServiceConfig, RecommendedModelConfig } from '@vibe-forge/core'
|
|
6
|
-
import { getConfig } from '
|
|
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
|
}
|
|
@@ -46,7 +46,9 @@ export function useChatModels() {
|
|
|
46
46
|
const availableModels = useMemo(() => {
|
|
47
47
|
const list: Array<{ model: string; serviceKey: string; serviceTitle: string }> = []
|
|
48
48
|
for (const [serviceKey, serviceValue] of modelServiceEntries) {
|
|
49
|
-
const service = (serviceValue != null && typeof serviceValue === 'object')
|
|
49
|
+
const service = (serviceValue != null && typeof serviceValue === 'object')
|
|
50
|
+
? serviceValue as ModelServiceConfig
|
|
51
|
+
: undefined
|
|
50
52
|
const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
|
|
51
53
|
const models = Array.isArray(service?.models) ? service?.models.filter(item => typeof item === 'string') : []
|
|
52
54
|
for (const model of models) {
|
|
@@ -60,8 +62,24 @@ export function useChatModels() {
|
|
|
60
62
|
const availableModelKey = useMemo(() => availableModelValues.join('|'), [availableModelValues])
|
|
61
63
|
const availableModelSet = useMemo(() => new Set(availableModelValues), [availableModelKey])
|
|
62
64
|
const hasAvailableModels = availableModelValues.length > 0
|
|
65
|
+
const modelToService = useMemo(() => {
|
|
66
|
+
const map = new Map<string, { key: string; title: string }>()
|
|
67
|
+
for (const entry of availableModels) {
|
|
68
|
+
if (!map.has(entry.model)) {
|
|
69
|
+
map.set(entry.model, { key: entry.serviceKey, title: entry.serviceTitle })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return map
|
|
73
|
+
}, [availableModels])
|
|
63
74
|
const defaultModelService = configRes?.sources?.merged?.general?.defaultModelService
|
|
64
75
|
const defaultModel = configRes?.sources?.merged?.general?.defaultModel
|
|
76
|
+
const formatModelWithService = useCallback((model: string | undefined) => {
|
|
77
|
+
const normalizedModel = typeof model === 'string' ? model.trim() : ''
|
|
78
|
+
if (normalizedModel === '') return undefined
|
|
79
|
+
if (normalizedModel.includes(',')) return normalizedModel
|
|
80
|
+
const resolvedService = modelToService.get(normalizedModel)?.key ?? defaultModelService
|
|
81
|
+
return resolvedService ? `${resolvedService},${normalizedModel}` : normalizedModel
|
|
82
|
+
}, [defaultModelService, modelToService])
|
|
65
83
|
const resolvedDefaultModel = useMemo(() => {
|
|
66
84
|
if (!hasAvailableModels) return undefined
|
|
67
85
|
if (defaultModel && availableModelSet.has(defaultModel)) return defaultModel
|
|
@@ -71,7 +89,17 @@ export function useChatModels() {
|
|
|
71
89
|
if (models.length > 0) return models[0]
|
|
72
90
|
}
|
|
73
91
|
return availableModelValues[0]
|
|
74
|
-
}, [
|
|
92
|
+
}, [
|
|
93
|
+
availableModelSet,
|
|
94
|
+
availableModelValues,
|
|
95
|
+
defaultModel,
|
|
96
|
+
defaultModelService,
|
|
97
|
+
hasAvailableModels,
|
|
98
|
+
mergedModelServices
|
|
99
|
+
])
|
|
100
|
+
const selectedModelWithService = useMemo(() => (
|
|
101
|
+
formatModelWithService(selectedModel)
|
|
102
|
+
), [formatModelWithService, selectedModel])
|
|
75
103
|
|
|
76
104
|
useEffect(() => {
|
|
77
105
|
if (!hasAvailableModels) {
|
|
@@ -126,13 +154,6 @@ export function useChatModels() {
|
|
|
126
154
|
}
|
|
127
155
|
}
|
|
128
156
|
|
|
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
157
|
const resolveFirstAlias = (modelsAlias: Record<string, string[]> | undefined, model: string) => {
|
|
137
158
|
if (!modelsAlias) return undefined
|
|
138
159
|
for (const [alias, aliasModels] of Object.entries(modelsAlias)) {
|
|
@@ -144,7 +165,9 @@ export function useChatModels() {
|
|
|
144
165
|
|
|
145
166
|
const serviceGroups = modelServiceEntries
|
|
146
167
|
.map(([serviceKey, serviceValue]) => {
|
|
147
|
-
const service = (serviceValue != null && typeof serviceValue === 'object')
|
|
168
|
+
const service = (serviceValue != null && typeof serviceValue === 'object')
|
|
169
|
+
? serviceValue as ModelServiceConfig
|
|
170
|
+
: undefined
|
|
148
171
|
const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
|
|
149
172
|
const groupTitle = serviceTitle?.trim() !== '' ? serviceTitle : serviceKey
|
|
150
173
|
const serviceDescription = service?.description
|
|
@@ -213,10 +236,18 @@ export function useChatModels() {
|
|
|
213
236
|
})
|
|
214
237
|
}
|
|
215
238
|
return [...groups, ...serviceGroups]
|
|
216
|
-
}, [
|
|
239
|
+
}, [
|
|
240
|
+
availableModelSet,
|
|
241
|
+
modelToService,
|
|
242
|
+
mergedModelServices,
|
|
243
|
+
modelServiceEntries,
|
|
244
|
+
recommendedModels,
|
|
245
|
+
t
|
|
246
|
+
])
|
|
217
247
|
|
|
218
248
|
return {
|
|
219
249
|
selectedModel,
|
|
250
|
+
selectedModelWithService,
|
|
220
251
|
setSelectedModel,
|
|
221
252
|
modelOptions,
|
|
222
253
|
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
|
+
}
|