@roaming-ai/dsh-group-chat 0.2.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/LICENSE +21 -0
- package/README.md +56 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +3308 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +2583 -0
- package/lib/types/client/Bubble.d.ts +10 -0
- package/lib/types/client/Glyph.d.ts +12 -0
- package/lib/types/client/GroupChatPanel.d.ts +10 -0
- package/lib/types/client/GroupChatPanel.old.d.ts +10 -0
- package/lib/types/client/GroupChatSettingsSection.d.ts +15 -0
- package/lib/types/client/PermissionSelect.d.ts +13 -0
- package/lib/types/client/RoleDrawer.d.ts +17 -0
- package/lib/types/client/ThinkRow.d.ts +9 -0
- package/lib/types/client/ToolRow.d.ts +9 -0
- package/lib/types/client/api.d.ts +8 -0
- package/lib/types/client/components/AsidePanel.d.ts +31 -0
- package/lib/types/client/components/Bubble.d.ts +19 -0
- package/lib/types/client/components/ChatPanel.d.ts +57 -0
- package/lib/types/client/components/Composer.d.ts +48 -0
- package/lib/types/client/components/Glyph.d.ts +12 -0
- package/lib/types/client/components/GroupChatSettingsSection.d.ts +15 -0
- package/lib/types/client/components/MessageFlow.d.ts +15 -0
- package/lib/types/client/components/NavPanel.d.ts +40 -0
- package/lib/types/client/components/PermissionSelect.d.ts +13 -0
- package/lib/types/client/components/RoleDrawer.d.ts +17 -0
- package/lib/types/client/components/ThinkRow.d.ts +9 -0
- package/lib/types/client/components/ToolRow.d.ts +9 -0
- package/lib/types/client/components/index.d.ts +9 -0
- package/lib/types/client/components.d.ts +22 -0
- package/lib/types/client/drawer.d.ts +17 -0
- package/lib/types/client/glyph.d.ts +12 -0
- package/lib/types/client/hooks/index.d.ts +6 -0
- package/lib/types/client/hooks/useComposer.d.ts +27 -0
- package/lib/types/client/hooks/useFileSearch.d.ts +19 -0
- package/lib/types/client/hooks/useGroupChatState.d.ts +95 -0
- package/lib/types/client/hooks/useMentionState.d.ts +27 -0
- package/lib/types/client/index.d.ts +52 -0
- package/lib/types/client/lib/api.d.ts +8 -0
- package/lib/types/client/lib/model.d.ts +80 -0
- package/lib/types/client/lib/styles.d.ts +10 -0
- package/lib/types/client/lib/ui.d.ts +17 -0
- package/lib/types/client/model.d.ts +78 -0
- package/lib/types/client/panel.d.ts +10 -0
- package/lib/types/client/settings.d.ts +15 -0
- package/lib/types/client/styles.d.ts +10 -0
- package/lib/types/client/ui.d.ts +17 -0
- package/lib/types/client/utils/utils.d.ts +29 -0
- package/lib/types/client/utils.d.ts +22 -0
- package/lib/types/core/json.d.ts +9 -0
- package/lib/types/core/status.d.ts +18 -0
- package/lib/types/core/tools.d.ts +59 -0
- package/lib/types/core/types.d.ts +256 -0
- package/lib/types/host/actions.d.ts +34 -0
- package/lib/types/host/api/actions.d.ts +36 -0
- package/lib/types/host/api/http.d.ts +16 -0
- package/lib/types/host/api/index.d.ts +7 -0
- package/lib/types/host/api/routes.d.ts +11 -0
- package/lib/types/host/broadcast.d.ts +20 -0
- package/lib/types/host/conversation.d.ts +25 -0
- package/lib/types/host/engine/conversation.d.ts +25 -0
- package/lib/types/host/engine/index.d.ts +7 -0
- package/lib/types/host/engine/retitle.d.ts +20 -0
- package/lib/types/host/http.d.ts +16 -0
- package/lib/types/host/index.d.ts +8 -0
- package/lib/types/host/materials/index.d.ts +6 -0
- package/lib/types/host/materials/materials.d.ts +40 -0
- package/lib/types/host/materials.d.ts +32 -0
- package/lib/types/host/persistence/index.d.ts +6 -0
- package/lib/types/host/persistence/persistence.d.ts +31 -0
- package/lib/types/host/persistence/store.d.ts +65 -0
- package/lib/types/host/persistence.d.ts +31 -0
- package/lib/types/host/routes.d.ts +11 -0
- package/lib/types/host/service.d.ts +28 -0
- package/lib/types/host/state.d.ts +45 -0
- package/lib/types/host/store.d.ts +65 -0
- package/lib/types/host/tools/index.d.ts +6 -0
- package/lib/types/host/tools/tools.d.ts +28 -0
- package/lib/types/host/tools.d.ts +28 -0
- package/lib/types/index.d.ts +41 -0
- package/lib/types/mount-once.d.ts +9 -0
- package/lib/types/shared/file-mention-grammar.d.ts +37 -0
- package/lib/types/shared/file-mention-grammar.test.d.ts +5 -0
- package/package.json +98 -0
- package/src/client/GroupChatPanel.tsx +385 -0
- package/src/client/components/AsidePanel.tsx +188 -0
- package/src/client/components/Bubble.tsx +76 -0
- package/src/client/components/ChatPanel.tsx +214 -0
- package/src/client/components/Composer.tsx +238 -0
- package/src/client/components/Glyph.tsx +27 -0
- package/src/client/components/GroupChatSettingsSection.tsx +74 -0
- package/src/client/components/MessageFlow.tsx +97 -0
- package/src/client/components/NavPanel.tsx +249 -0
- package/src/client/components/PermissionSelect.tsx +159 -0
- package/src/client/components/RoleDrawer.tsx +186 -0
- package/src/client/components/ThinkRow.tsx +32 -0
- package/src/client/components/ToolRow.tsx +51 -0
- package/src/client/components/index.ts +10 -0
- package/src/client/hooks/index.ts +7 -0
- package/src/client/hooks/useComposer.ts +272 -0
- package/src/client/hooks/useFileSearch.ts +115 -0
- package/src/client/hooks/useGroupChatState.ts +214 -0
- package/src/client/hooks/useMentionState.ts +56 -0
- package/src/client/index.ts +110 -0
- package/src/client/lib/api.ts +23 -0
- package/src/client/lib/model.ts +119 -0
- package/src/client/lib/styles.ts +317 -0
- package/src/client/lib/ui.ts +27 -0
- package/src/client/react-dom-shim.d.ts +13 -0
- package/src/client/utils/utils.ts +104 -0
- package/src/core/json.ts +28 -0
- package/src/core/status.ts +33 -0
- package/src/core/tools.ts +41 -0
- package/src/core/types.ts +222 -0
- package/src/host/api/actions.ts +335 -0
- package/src/host/api/http.ts +80 -0
- package/src/host/api/index.ts +8 -0
- package/src/host/api/routes.ts +95 -0
- package/src/host/broadcast.ts +65 -0
- package/src/host/engine/conversation.ts +331 -0
- package/src/host/engine/index.ts +8 -0
- package/src/host/engine/retitle.ts +135 -0
- package/src/host/index.ts +9 -0
- package/src/host/materials/index.ts +7 -0
- package/src/host/materials/materials.ts +193 -0
- package/src/host/persistence/index.ts +7 -0
- package/src/host/persistence/persistence.ts +311 -0
- package/src/host/persistence/store.ts +375 -0
- package/src/host/service.ts +78 -0
- package/src/host/state.ts +85 -0
- package/src/host/tools/index.ts +7 -0
- package/src/host/tools/tools.ts +194 -0
- package/src/index.ts +99 -0
- package/src/mount-once.ts +49 -0
- package/src/shared/file-mention-grammar.test.ts +77 -0
- package/src/shared/file-mention-grammar.ts +82 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 中央会话面板组件
|
|
3
|
+
* @module dsh-group-chat/client/components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ReactNode } from 'react'
|
|
7
|
+
import { Icon, P } from '../lib/ui.ts'
|
|
8
|
+
import { MessageFlow } from './MessageFlow.tsx'
|
|
9
|
+
import { Composer } from './Composer.tsx'
|
|
10
|
+
import type { ClientSnapshot, SnapshotRole } from '../lib/model.ts'
|
|
11
|
+
import type { AtToken } from '../../shared/file-mention-grammar.ts'
|
|
12
|
+
|
|
13
|
+
interface ChatPanelProps {
|
|
14
|
+
snap: ClientSnapshot
|
|
15
|
+
sess: ClientSnapshot['sessions'][number] | null
|
|
16
|
+
group: ClientSnapshot['groups'][number]
|
|
17
|
+
enabledRoles: SnapshotRole[]
|
|
18
|
+
participants: string[]
|
|
19
|
+
mentionedRoles: SnapshotRole[]
|
|
20
|
+
busyNow: boolean
|
|
21
|
+
input: string
|
|
22
|
+
mention: AtToken | null
|
|
23
|
+
mentionCandidates: SnapshotRole[]
|
|
24
|
+
mentionIdxC: number
|
|
25
|
+
fileCandidates: Array<{ path: string, isDir: boolean }>
|
|
26
|
+
fileSearchError: string | null
|
|
27
|
+
fileSearchLoading: boolean
|
|
28
|
+
rounds: number
|
|
29
|
+
err: string
|
|
30
|
+
atBottom: boolean
|
|
31
|
+
topicDraft: string | null
|
|
32
|
+
msgById: Record<string, ClientSnapshot['messages'][number]>
|
|
33
|
+
navOpen: boolean
|
|
34
|
+
asideOpen: boolean
|
|
35
|
+
inputRef: React.RefObject<HTMLDivElement>
|
|
36
|
+
scrollRef: React.RefObject<HTMLDivElement>
|
|
37
|
+
setTopicDraft: (val: string | null) => void
|
|
38
|
+
setConfirmClear: (val: boolean) => void
|
|
39
|
+
setMentionIdx: (val: number) => void
|
|
40
|
+
setRounds: (val: number) => void
|
|
41
|
+
setNavOpen: (val: boolean) => void
|
|
42
|
+
setAsideOpen: (val: boolean) => void
|
|
43
|
+
togglePart: (rid: string) => void
|
|
44
|
+
onMsgsScroll: () => void
|
|
45
|
+
onInputCE: () => void
|
|
46
|
+
onInputKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void
|
|
47
|
+
onPasteCE: (e: React.ClipboardEvent<HTMLDivElement>) => void
|
|
48
|
+
onDropCE: (e: React.DragEvent<HTMLDivElement>) => void
|
|
49
|
+
onDragOverCE: (e: React.DragEvent<HTMLDivElement>) => void
|
|
50
|
+
insertChip: (role: SnapshotRole) => void
|
|
51
|
+
insertFileChip: (path: string, kind: 'file' | 'directory') => void
|
|
52
|
+
sendMsg: () => Promise<void>
|
|
53
|
+
stopRun: () => Promise<void>
|
|
54
|
+
action: (payload: Record<string, unknown>) => Promise<unknown>
|
|
55
|
+
mutate: (args: Record<string, unknown>) => Promise<unknown>
|
|
56
|
+
setMention: (val: AtToken | null) => void
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function ChatPanel(props: ChatPanelProps): ReactNode {
|
|
60
|
+
const {
|
|
61
|
+
snap,
|
|
62
|
+
sess,
|
|
63
|
+
group,
|
|
64
|
+
enabledRoles,
|
|
65
|
+
participants,
|
|
66
|
+
mentionedRoles,
|
|
67
|
+
busyNow,
|
|
68
|
+
input,
|
|
69
|
+
mention,
|
|
70
|
+
mentionCandidates,
|
|
71
|
+
mentionIdxC,
|
|
72
|
+
fileCandidates,
|
|
73
|
+
fileSearchError,
|
|
74
|
+
fileSearchLoading,
|
|
75
|
+
rounds,
|
|
76
|
+
err,
|
|
77
|
+
atBottom,
|
|
78
|
+
topicDraft,
|
|
79
|
+
msgById,
|
|
80
|
+
navOpen,
|
|
81
|
+
asideOpen,
|
|
82
|
+
inputRef,
|
|
83
|
+
scrollRef,
|
|
84
|
+
setTopicDraft,
|
|
85
|
+
setConfirmClear,
|
|
86
|
+
setMentionIdx,
|
|
87
|
+
setRounds,
|
|
88
|
+
setNavOpen,
|
|
89
|
+
setAsideOpen,
|
|
90
|
+
togglePart,
|
|
91
|
+
onMsgsScroll,
|
|
92
|
+
onInputCE,
|
|
93
|
+
onInputKeyDown,
|
|
94
|
+
onPasteCE,
|
|
95
|
+
onDropCE,
|
|
96
|
+
onDragOverCE,
|
|
97
|
+
insertChip,
|
|
98
|
+
insertFileChip,
|
|
99
|
+
sendMsg,
|
|
100
|
+
stopRun,
|
|
101
|
+
action,
|
|
102
|
+
mutate,
|
|
103
|
+
setMention,
|
|
104
|
+
} = props
|
|
105
|
+
|
|
106
|
+
const commitTopic = (): void => {
|
|
107
|
+
if (topicDraft !== null && sess && topicDraft !== sess.topic) void mutate({ op: 'setTopic', sessionId: sess.id, topic: topicDraft })
|
|
108
|
+
setTopicDraft(null)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const bubbles = (
|
|
112
|
+
<MessageFlow
|
|
113
|
+
snap={snap}
|
|
114
|
+
sess={sess}
|
|
115
|
+
busyNow={busyNow}
|
|
116
|
+
msgById={msgById}
|
|
117
|
+
action={action}
|
|
118
|
+
/>
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<section className="dsgc-chat">
|
|
123
|
+
{/* 接缝收合钮 */}
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
className={'dsgc-seambtn left' + (navOpen ? '' : ' closed')}
|
|
127
|
+
aria-label={navOpen ? '收起群组导航栏' : '展开群组导航栏'}
|
|
128
|
+
title={navOpen ? '收起群组导航栏' : '展开群组导航栏'}
|
|
129
|
+
aria-expanded={navOpen}
|
|
130
|
+
onClick={() => { setNavOpen(!navOpen) }}
|
|
131
|
+
>
|
|
132
|
+
{Icon(P.IconChevronLeftOutline14, 16)}
|
|
133
|
+
</button>
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
className={'dsgc-seambtn right' + (asideOpen ? '' : ' closed')}
|
|
137
|
+
aria-label={asideOpen ? '收起成员与工作区栏' : '展开成员与工作区栏'}
|
|
138
|
+
title={asideOpen ? '收起成员与工作区栏' : '展开成员与工作区栏'}
|
|
139
|
+
aria-expanded={asideOpen}
|
|
140
|
+
onClick={() => { setAsideOpen(!asideOpen) }}
|
|
141
|
+
>
|
|
142
|
+
{Icon(P.IconChevronRightOutline14, 16)}
|
|
143
|
+
</button>
|
|
144
|
+
|
|
145
|
+
<div className="dsgc-chathead">
|
|
146
|
+
{sess ? <span className="dsgc-sess-title" title={'当前会话:' + sess.name}>{sess.name}</span> : null}
|
|
147
|
+
<input
|
|
148
|
+
className="dsgc-topic"
|
|
149
|
+
value={topicDraft === null ? (sess ? sess.topic : '') : topicDraft}
|
|
150
|
+
placeholder="设置本会话主题(可选)…"
|
|
151
|
+
onChange={(e) => { setTopicDraft(e.target.value) }}
|
|
152
|
+
onBlur={commitTopic}
|
|
153
|
+
onKeyDown={(e) => { if (e.key === 'Enter') (e.target as HTMLInputElement).blur() }}
|
|
154
|
+
/>
|
|
155
|
+
<P.Button variant="ghost" size="sm" className="dsgc-clearbtn" title="清空当前会话的消息记录" onClick={() => { if (sess) setConfirmClear(true) }}>
|
|
156
|
+
清空
|
|
157
|
+
</P.Button>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="dsgc-msgs" ref={scrollRef} onScroll={onMsgsScroll}>
|
|
161
|
+
{sess && sess.messageIds.length > 0
|
|
162
|
+
? bubbles
|
|
163
|
+
: (
|
|
164
|
+
<div className="dsgc-empty">
|
|
165
|
+
{Icon(P.IconSparkle16, 20)}
|
|
166
|
+
<div className="dsgc-emptytitle">{sess && sess.topic ? '「' + sess.topic + '」' : '会话已就绪'}</div>
|
|
167
|
+
<div className="dsgc-hint">
|
|
168
|
+
{enabledRoles.length
|
|
169
|
+
? '发送消息开始讨论;@成员 点名让其优先回应;留空直接发送可让角色自由讨论——每轮全体参与角色按顺序各发言一次,可用右下角轮数控制(1–10 轮)'
|
|
170
|
+
: '先在右侧添加角色(每个角色可绑定不同模型),再回到这里发起讨论。'}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<Composer
|
|
177
|
+
snap={snap}
|
|
178
|
+
sess={sess}
|
|
179
|
+
group={group}
|
|
180
|
+
enabledRoles={enabledRoles}
|
|
181
|
+
participants={participants}
|
|
182
|
+
mentionedRoles={mentionedRoles}
|
|
183
|
+
busyNow={busyNow}
|
|
184
|
+
input={input}
|
|
185
|
+
mention={mention}
|
|
186
|
+
mentionCandidates={mentionCandidates}
|
|
187
|
+
mentionIdxC={mentionIdxC}
|
|
188
|
+
fileCandidates={fileCandidates}
|
|
189
|
+
fileSearchError={fileSearchError}
|
|
190
|
+
fileSearchLoading={fileSearchLoading}
|
|
191
|
+
rounds={rounds}
|
|
192
|
+
err={err}
|
|
193
|
+
atBottom={atBottom}
|
|
194
|
+
bubblesLength={sess?.messageIds.length || 0}
|
|
195
|
+
inputRef={inputRef}
|
|
196
|
+
scrollRef={scrollRef}
|
|
197
|
+
setMentionIdx={setMentionIdx}
|
|
198
|
+
setRounds={setRounds}
|
|
199
|
+
togglePart={togglePart}
|
|
200
|
+
onInputCE={onInputCE}
|
|
201
|
+
onInputKeyDown={onInputKeyDown}
|
|
202
|
+
onPasteCE={onPasteCE}
|
|
203
|
+
onDropCE={onDropCE}
|
|
204
|
+
onDragOverCE={onDragOverCE}
|
|
205
|
+
insertChip={insertChip}
|
|
206
|
+
insertFileChip={insertFileChip}
|
|
207
|
+
sendMsg={sendMsg}
|
|
208
|
+
stopRun={stopRun}
|
|
209
|
+
mutate={mutate}
|
|
210
|
+
setMention={setMention}
|
|
211
|
+
/>
|
|
212
|
+
</section>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 消息输入编辑器组件
|
|
3
|
+
* @module dsh-group-chat/client/components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ReactNode, KeyboardEvent as ReactKeyboardEvent, ClipboardEvent as ReactClipboardEvent, DragEvent as ReactDragEvent } from 'react'
|
|
7
|
+
import { Icon, P } from '../lib/ui.ts'
|
|
8
|
+
import { PermissionSelect } from './PermissionSelect.tsx'
|
|
9
|
+
import { escapeRegExp, type ClientSnapshot, type SnapshotRole } from '../lib/model.ts'
|
|
10
|
+
import type { AtToken } from '../../shared/file-mention-grammar.ts'
|
|
11
|
+
|
|
12
|
+
interface ComposerProps {
|
|
13
|
+
snap: ClientSnapshot
|
|
14
|
+
sess: ClientSnapshot['sessions'][number] | null
|
|
15
|
+
group: ClientSnapshot['groups'][number]
|
|
16
|
+
enabledRoles: SnapshotRole[]
|
|
17
|
+
participants: string[]
|
|
18
|
+
mentionedRoles: SnapshotRole[]
|
|
19
|
+
busyNow: boolean
|
|
20
|
+
input: string
|
|
21
|
+
mention: AtToken | null
|
|
22
|
+
mentionCandidates: SnapshotRole[]
|
|
23
|
+
mentionIdxC: number
|
|
24
|
+
fileCandidates: Array<{ path: string, isDir: boolean }>
|
|
25
|
+
fileSearchError: string | null
|
|
26
|
+
fileSearchLoading: boolean
|
|
27
|
+
rounds: number
|
|
28
|
+
err: string
|
|
29
|
+
atBottom: boolean
|
|
30
|
+
bubblesLength: number
|
|
31
|
+
inputRef: React.RefObject<HTMLDivElement>
|
|
32
|
+
scrollRef: React.RefObject<HTMLDivElement>
|
|
33
|
+
setMentionIdx: (val: number) => void
|
|
34
|
+
setRounds: (val: number) => void
|
|
35
|
+
togglePart: (rid: string) => void
|
|
36
|
+
onInputCE: () => void
|
|
37
|
+
onInputKeyDown: (e: ReactKeyboardEvent<HTMLDivElement>) => void
|
|
38
|
+
onPasteCE: (e: ReactClipboardEvent<HTMLDivElement>) => void
|
|
39
|
+
onDropCE: (e: ReactDragEvent<HTMLDivElement>) => void
|
|
40
|
+
onDragOverCE: (e: ReactDragEvent<HTMLDivElement>) => void
|
|
41
|
+
insertChip: (role: SnapshotRole) => void
|
|
42
|
+
insertFileChip: (path: string, kind: 'file' | 'directory') => void
|
|
43
|
+
sendMsg: () => Promise<void>
|
|
44
|
+
stopRun: () => Promise<void>
|
|
45
|
+
mutate: (args: Record<string, unknown>) => Promise<unknown>
|
|
46
|
+
setMention: (val: AtToken | null) => void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function Composer(props: ComposerProps): ReactNode {
|
|
50
|
+
const {
|
|
51
|
+
snap,
|
|
52
|
+
sess,
|
|
53
|
+
group,
|
|
54
|
+
enabledRoles,
|
|
55
|
+
participants,
|
|
56
|
+
mentionedRoles,
|
|
57
|
+
busyNow,
|
|
58
|
+
input,
|
|
59
|
+
mention,
|
|
60
|
+
mentionCandidates,
|
|
61
|
+
mentionIdxC,
|
|
62
|
+
fileCandidates,
|
|
63
|
+
fileSearchError,
|
|
64
|
+
fileSearchLoading,
|
|
65
|
+
rounds,
|
|
66
|
+
err,
|
|
67
|
+
atBottom,
|
|
68
|
+
bubblesLength,
|
|
69
|
+
inputRef,
|
|
70
|
+
scrollRef,
|
|
71
|
+
setMentionIdx,
|
|
72
|
+
setRounds,
|
|
73
|
+
togglePart,
|
|
74
|
+
onInputCE,
|
|
75
|
+
onInputKeyDown,
|
|
76
|
+
onPasteCE,
|
|
77
|
+
onDropCE,
|
|
78
|
+
onDragOverCE,
|
|
79
|
+
insertChip,
|
|
80
|
+
insertFileChip,
|
|
81
|
+
sendMsg,
|
|
82
|
+
stopRun,
|
|
83
|
+
mutate,
|
|
84
|
+
setMention,
|
|
85
|
+
} = props
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="dsgc-composer">
|
|
89
|
+
{!atBottom && bubblesLength
|
|
90
|
+
? (
|
|
91
|
+
<div className="dsgc-tobottom">
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
className="dsgc-tobtn"
|
|
95
|
+
onClick={() => { const el = scrollRef.current; if (el) el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }) }}
|
|
96
|
+
>
|
|
97
|
+
{Icon(P.IconChevronDownOutline14, 14)}回到底部
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
: null}
|
|
102
|
+
{err ? <div className="dsgc-err">{err}</div> : null}
|
|
103
|
+
<div className="dsgc-parts">
|
|
104
|
+
<span className="dsgc-partslabel">参与角色</span>
|
|
105
|
+
{mentionedRoles.length
|
|
106
|
+
? <span className="dsgc-partslabel">已 @ {mentionedRoles.map((r) => r.name).join('、')}(本轮仅被点名成员发言)</span>
|
|
107
|
+
: enabledRoles.length
|
|
108
|
+
? enabledRoles.map((r) => (
|
|
109
|
+
<button
|
|
110
|
+
key={r.id}
|
|
111
|
+
type="button"
|
|
112
|
+
className={'dsgc-partchip' + (participants.includes(r.id) ? ' on' : '')}
|
|
113
|
+
onClick={() => togglePart(r.id)}
|
|
114
|
+
disabled={busyNow}
|
|
115
|
+
title={busyNow ? '对话进行中,暂停调整' : '点击切换本轮是否参与'}
|
|
116
|
+
>
|
|
117
|
+
<span className="dsgc-chipdot" style={{ background: r.color || '#888' }} />
|
|
118
|
+
{r.name}
|
|
119
|
+
</button>
|
|
120
|
+
))
|
|
121
|
+
: <span className="dsgc-hint">还没有启用的角色,请在右侧添加</span>}
|
|
122
|
+
</div>
|
|
123
|
+
<div className="dsgc-card">
|
|
124
|
+
<div className="dsgc-mentionwrap">
|
|
125
|
+
{mention !== null && mention.query === '' && mentionCandidates.length > 0
|
|
126
|
+
? (
|
|
127
|
+
<div className="dsgc-mention" role="listbox">
|
|
128
|
+
{mentionCandidates.map((r, i) => (
|
|
129
|
+
<button
|
|
130
|
+
key={r.id}
|
|
131
|
+
type="button"
|
|
132
|
+
className={'dsgc-mentionitem' + (i === mentionIdxC ? ' on' : '')}
|
|
133
|
+
role="option"
|
|
134
|
+
aria-selected={i === mentionIdxC ? 'true' : 'false'}
|
|
135
|
+
onMouseDown={(e) => { e.preventDefault() }}
|
|
136
|
+
onClick={() => { insertChip(r) }}
|
|
137
|
+
onMouseEnter={() => setMentionIdx(i)}
|
|
138
|
+
>
|
|
139
|
+
<span className="dsgc-chipdot" style={{ background: r.color || '#888' }} />
|
|
140
|
+
<span className="dsgc-mentionname">{r.name}</span>
|
|
141
|
+
<span className="dsgc-mentionmodel">{r.provider} / {r.model}</span>
|
|
142
|
+
</button>
|
|
143
|
+
))}
|
|
144
|
+
<span className="dsgc-mentionhint">↑↓ 选择 · Enter/Tab 插入</span>
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
: mention !== null && mention.query !== ''
|
|
148
|
+
? (
|
|
149
|
+
<div className="dsgc-mention dsgc-mention-file" role="listbox">
|
|
150
|
+
{fileCandidates.length
|
|
151
|
+
? fileCandidates.map((item, i) => (
|
|
152
|
+
<button
|
|
153
|
+
key={item.path}
|
|
154
|
+
type="button"
|
|
155
|
+
className={'dsgc-mentionitem' + (i === mentionIdxC ? ' on' : '')}
|
|
156
|
+
role="option"
|
|
157
|
+
aria-selected={i === mentionIdxC ? 'true' : 'false'}
|
|
158
|
+
onMouseDown={(e) => { e.preventDefault() }}
|
|
159
|
+
onClick={() => {
|
|
160
|
+
insertFileChip(item.path, item.isDir ? 'directory' : 'file')
|
|
161
|
+
}}
|
|
162
|
+
onMouseEnter={() => setMentionIdx(i)}
|
|
163
|
+
>
|
|
164
|
+
<span className={'dsgc-fileglyph' + (item.isDir ? ' dir' : ' file')} aria-hidden="true">
|
|
165
|
+
{item.isDir
|
|
166
|
+
? <P.FileTypeIcon kind="folder" size={16} />
|
|
167
|
+
: <P.FileTypeIcon path={item.path} size={16} />}
|
|
168
|
+
</span>
|
|
169
|
+
<span className="dsgc-mentionname">{item.path}</span>
|
|
170
|
+
{item.isDir ? <span className="dsgc-filedrill">{Icon(P.IconChevronRightOutline14, 12)}</span> : null}
|
|
171
|
+
</button>
|
|
172
|
+
))
|
|
173
|
+
: fileSearchLoading
|
|
174
|
+
? <div className="dsgc-mentionitem dsgc-hint">检索中…</div>
|
|
175
|
+
: fileSearchError
|
|
176
|
+
? <div className="dsgc-mentionitem dsgc-hint">{fileSearchError}</div>
|
|
177
|
+
: <div className="dsgc-mentionitem dsgc-hint">无匹配文件</div>}
|
|
178
|
+
<span className="dsgc-mentionhint">
|
|
179
|
+
{fileSearchLoading && fileCandidates.length
|
|
180
|
+
? '检索中…'
|
|
181
|
+
: fileCandidates.some((f) => f.isDir)
|
|
182
|
+
? '↑↓ 选择 · Enter 插入 · Tab 进入目录 · Esc 关闭'
|
|
183
|
+
: '↑↓ 选择 · Enter 插入 · Esc 关闭'}
|
|
184
|
+
</span>
|
|
185
|
+
</div>
|
|
186
|
+
)
|
|
187
|
+
: null}
|
|
188
|
+
{!input.trim()
|
|
189
|
+
? <div className="dsgc-ph" aria-hidden="true">发消息给全群,@成员 点名让其回应(留空则让角色自由讨论)…</div>
|
|
190
|
+
: null}
|
|
191
|
+
<div
|
|
192
|
+
className="dsgc-edit"
|
|
193
|
+
ref={inputRef}
|
|
194
|
+
contentEditable
|
|
195
|
+
suppressContentEditableWarning
|
|
196
|
+
role="textbox"
|
|
197
|
+
aria-multiline="true"
|
|
198
|
+
aria-label="群聊消息输入框"
|
|
199
|
+
onInput={onInputCE}
|
|
200
|
+
onKeyDown={onInputKeyDown}
|
|
201
|
+
onPaste={onPasteCE}
|
|
202
|
+
onDrop={onDropCE}
|
|
203
|
+
onDragOver={onDragOverCE}
|
|
204
|
+
onBlur={() => { setMention(null) }}
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
207
|
+
<div className="dsgc-sendrow">
|
|
208
|
+
<PermissionSelect
|
|
209
|
+
tier={group.permissionTier}
|
|
210
|
+
onSelect={(tier) => { void mutate({ op: 'setPermissionTier', groupId: group.id, tier }) }}
|
|
211
|
+
/>
|
|
212
|
+
<span style={{ flex: 1 }} />
|
|
213
|
+
<div className="dsgc-rounds" title="自由讨论的轮数(1–10):一轮 = 全体参与角色按顺序各发言一次">
|
|
214
|
+
<button type="button" className="dsgc-roundbtn" aria-label="减少轮数" disabled={rounds <= 1} onClick={() => { setRounds(Math.max(1, rounds - 1)) }}>
|
|
215
|
+
{Icon(P.IconChevronLeftOutline14, 12)}
|
|
216
|
+
</button>
|
|
217
|
+
<span className="dsgc-roundnum" title="轮数">{rounds}</span>
|
|
218
|
+
<button type="button" className="dsgc-roundbtn" aria-label="增加轮数" disabled={rounds >= 10} onClick={() => { setRounds(Math.min(10, rounds + 1)) }}>
|
|
219
|
+
{Icon(P.IconChevronRightOutline14, 12)}
|
|
220
|
+
</button>
|
|
221
|
+
<span style={{ padding: '0 6px 0 2px' }}>轮</span>
|
|
222
|
+
</div>
|
|
223
|
+
{busyNow
|
|
224
|
+
? (
|
|
225
|
+
<P.Button variant="outline" className="dsgc-stopbtn" onClick={() => { void stopRun() }}>
|
|
226
|
+
{Icon(P.IconStopFill16, 16)}停止
|
|
227
|
+
</P.Button>
|
|
228
|
+
)
|
|
229
|
+
: (
|
|
230
|
+
<P.Button variant="primary" onClick={() => { void sendMsg() }} disabled={(!participants.length && !mentionedRoles.length) || !sess}>
|
|
231
|
+
{Icon(P.IconSendOutline16, 16)}发送
|
|
232
|
+
</P.Button>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 侧边栏入口图标。
|
|
3
|
+
*
|
|
4
|
+
* 整行命中层携带 “newSession” 类名:任务看板等 DOM 注入式面板用
|
|
5
|
+
* [class*="newSession"] 识别“侧边栏导航点击”并自动收起自己,与点击
|
|
6
|
+
* “新建会话”行为一致——类名不可哈希化。
|
|
7
|
+
* @module dsh-group-chat/client/glyph
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ReactNode } from 'react'
|
|
11
|
+
|
|
12
|
+
export function Glyph(props: { size?: number }): ReactNode {
|
|
13
|
+
const size = (props && props.size) || 18
|
|
14
|
+
return (
|
|
15
|
+
<span className="dsgc-entryOverlay newSession">
|
|
16
|
+
<span className="dsgc-entryIcon">
|
|
17
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
18
|
+
strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
19
|
+
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
|
20
|
+
<circle cx={9} cy={7} r={4} />
|
|
21
|
+
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
|
22
|
+
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
|
23
|
+
</svg>
|
|
24
|
+
</span>
|
|
25
|
+
</span>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设置页:启停开关(settings.section 插槽)。写 host 的 group-chat 设置
|
|
3
|
+
* 命名空间(settings.yaml 持久化)。
|
|
4
|
+
* @module dsh-group-chat/client/settings
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useEffect, useState, type ReactNode } from 'react'
|
|
8
|
+
import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
9
|
+
import { P } from '../lib/ui.ts'
|
|
10
|
+
|
|
11
|
+
/** Host 侧注册的设置命名空间 shape。 */
|
|
12
|
+
interface GroupChatSettings {
|
|
13
|
+
enabled?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function GroupChatSettingsSection(props: { settingsScope: SettingsScope<GroupChatSettings> }): ReactNode {
|
|
17
|
+
const scope = props.settingsScope
|
|
18
|
+
const [snap, setSnap] = useState<ReturnType<SettingsScope<GroupChatSettings>['getSnapshot']> | null>(null)
|
|
19
|
+
const [pending, setPending] = useState(false)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
let live = true
|
|
23
|
+
const read = (): void => {
|
|
24
|
+
if (!live) return
|
|
25
|
+
try {
|
|
26
|
+
setSnap(scope.getSnapshot())
|
|
27
|
+
} catch { /* ignore */ }
|
|
28
|
+
}
|
|
29
|
+
read()
|
|
30
|
+
const off = scope.subscribe(read)
|
|
31
|
+
return () => {
|
|
32
|
+
live = false
|
|
33
|
+
off()
|
|
34
|
+
}
|
|
35
|
+
}, [])
|
|
36
|
+
|
|
37
|
+
const ready = snap !== null && snap.status === 'ready'
|
|
38
|
+
const writable = ready ? snap.writable !== false : false
|
|
39
|
+
const enabled = ready ? (snap.value && snap.value.enabled) !== false : true
|
|
40
|
+
|
|
41
|
+
const toggle = async (): Promise<void> => {
|
|
42
|
+
if (!writable || pending) return
|
|
43
|
+
setPending(true)
|
|
44
|
+
try {
|
|
45
|
+
await scope.set('enabled', !enabled)
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error('[dsh-group-chat] settings write failed', e)
|
|
48
|
+
} finally {
|
|
49
|
+
setPending(false)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="dgcs-page">
|
|
55
|
+
<div className="dgcs-head">
|
|
56
|
+
<h3 className="dgcs-title">模型群聊</h3>
|
|
57
|
+
<p className="dgcs-desc">多模型角色群组对话面板:每个角色绑定不同的 provider/model;群组内多会话目录树管理,群内共享对话记录与工作区目录;消息以 markdown 渲染、支持思考折叠;输入框支持 @成员 / @文件。数据持久化于 ~/.dsh/storages/group-chat/,重启 dsh web 后恢复。</p>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="dgcs-card">
|
|
60
|
+
<div className="dgcs-cardtext">
|
|
61
|
+
<div className="dgcs-cardtitle">{enabled ? '已启用' : '已停用'}</div>
|
|
62
|
+
<div className="dgcs-cardhint">{writable ? (enabled ? '关闭后侧边栏将不再显示「群聊」入口,进行中的对话会被中止。' : '开启后侧边栏显示「群聊」入口。') : '当前设置不可写(可能被配置文件覆盖)。'}</div>
|
|
63
|
+
</div>
|
|
64
|
+
<P.Switch
|
|
65
|
+
checked={enabled}
|
|
66
|
+
onChange={() => { void toggle() }}
|
|
67
|
+
disabled={!writable || pending}
|
|
68
|
+
label="模型群聊启停"
|
|
69
|
+
title={pending ? '正在写入…' : enabled ? '点击停用' : '点击启用'}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 消息流显示组件
|
|
3
|
+
* @module dsh-group-chat/client/components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ReactNode, CSSProperties } from 'react'
|
|
7
|
+
import { Icon, P } from '../lib/ui.ts'
|
|
8
|
+
import { Bubble } from './Bubble.tsx'
|
|
9
|
+
import { ThinkRow } from './ThinkRow.tsx'
|
|
10
|
+
import { roleById, type ClientSnapshot } from '../lib/model.ts'
|
|
11
|
+
import { MD_LABELS } from '../lib/model.ts'
|
|
12
|
+
|
|
13
|
+
interface MessageFlowProps {
|
|
14
|
+
snap: ClientSnapshot
|
|
15
|
+
sess: ClientSnapshot['sessions'][number] | null
|
|
16
|
+
busyNow: boolean
|
|
17
|
+
msgById: Record<string, ClientSnapshot['messages'][number]>
|
|
18
|
+
action: (payload: Record<string, unknown>) => Promise<unknown>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function MessageFlow(props: MessageFlowProps): ReactNode {
|
|
22
|
+
const { snap, sess, busyNow, msgById, action } = props
|
|
23
|
+
|
|
24
|
+
const bubbles: ReactNode[] = []
|
|
25
|
+
|
|
26
|
+
if (sess) {
|
|
27
|
+
for (const mid of sess.messageIds) {
|
|
28
|
+
const m = msgById[mid]
|
|
29
|
+
if (!m) continue
|
|
30
|
+
// 角色在父级解析:Bubble 按值 memo(不依赖 snap identity),流式帧
|
|
31
|
+
// 不再触发已完成消息的全量重渲(每帧仅 live 行与派生列表变化)
|
|
32
|
+
const role = m.speaker !== 'user' && m.speaker !== 'system' ? roleById(snap, m.speaker) : null
|
|
33
|
+
bubbles.push(<Bubble key={m.id} m={m} role={role} />)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 流式尾巴
|
|
38
|
+
if (busyNow && snap.run.currentRoleId) {
|
|
39
|
+
const lr = roleById(snap, snap.run.currentRoleId)
|
|
40
|
+
if (lr) {
|
|
41
|
+
bubbles.push(
|
|
42
|
+
<div key="__live" className="dsgc-msg live">
|
|
43
|
+
<div
|
|
44
|
+
className="dsgc-avatar"
|
|
45
|
+
style={{ border: '2px solid ' + (lr.color || '#888'), '--role-color': lr.color || '#888' } as CSSProperties}
|
|
46
|
+
>
|
|
47
|
+
{lr.name.slice(0, 1)}
|
|
48
|
+
</div>
|
|
49
|
+
<div className="dsgc-msgbody">
|
|
50
|
+
<div className="dsgc-msghead">
|
|
51
|
+
<span className="dsgc-msgname">{lr.name}</span>
|
|
52
|
+
<span className="dsgc-msgmodel">{lr.provider} / {lr.model}</span>
|
|
53
|
+
<span className="dsgc-msgtime dsgc-typing">深度求索...</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="dsgc-msgtext live">
|
|
56
|
+
{snap.run.partialReasoning ? <ThinkRow text={snap.run.partialReasoning} running /> : null}
|
|
57
|
+
{snap.run.partial ? <P.MarkdownText text={snap.run.partial} streaming labels={MD_LABELS} /> : null}
|
|
58
|
+
{/* 首 delta 前的「思考中」占位(深度思考模型首字节可能等数秒到数十秒,
|
|
59
|
+
空白气泡会被感知为卡死);delta 到达后被真实思考行/正文自然替换 */}
|
|
60
|
+
{!snap.run.partial && !snap.run.partialReasoning
|
|
61
|
+
? (
|
|
62
|
+
<div className="dsgc-pending">
|
|
63
|
+
{Icon(P.IconThinkOutline14, 14)}
|
|
64
|
+
<span>思考中</span>
|
|
65
|
+
<span className="dsgc-pendingdots" aria-hidden="true"><i /><i /><i /></span>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
: null}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 命令确认卡片
|
|
77
|
+
const pc = busyNow && snap.run.pendingConfirm && sess && snap.run.sessionId === sess.id ? snap.run.pendingConfirm : null
|
|
78
|
+
if (pc) {
|
|
79
|
+
const lr = roleById(snap, snap.run.currentRoleId)
|
|
80
|
+
bubbles.push(
|
|
81
|
+
<div key="__confirm" className="dsgc-confirm">
|
|
82
|
+
<div className="dsgc-confirmtitle">
|
|
83
|
+
{Icon(P.IconWarningOutline16, 14)}
|
|
84
|
+
{(lr ? lr.name : '角色') + ' 请求执行命令(工作区内)'}
|
|
85
|
+
</div>
|
|
86
|
+
<div className="dsgc-confirmcmd">{String((pc.args && pc.args.command) || '')}</div>
|
|
87
|
+
<div className="dsgc-hint">允许后将在工作区目录执行;拒绝后角色将继续纯文本讨论</div>
|
|
88
|
+
<div className="dsgc-confirmops">
|
|
89
|
+
<P.Button variant="primary" size="sm" onClick={() => { void action({ kind: 'confirmCommand', toolCallId: pc.toolCallId, allow: true }) }}>允许</P.Button>
|
|
90
|
+
<P.Button variant="outline" size="sm" onClick={() => { void action({ kind: 'confirmCommand', toolCallId: pc.toolCallId, allow: false }) }}>拒绝</P.Button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return <>{bubbles}</>
|
|
97
|
+
}
|