@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,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 左侧导航栏组件
|
|
3
|
+
* @module dsh-group-chat/client/components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ReactNode, KeyboardEvent as ReactKeyboardEvent } from 'react'
|
|
7
|
+
import { Icon, P } from '../lib/ui.ts'
|
|
8
|
+
import { groupById, sessById, sessStatus, SESS_STATUS_LABEL, type ClientSnapshot } from '../lib/model.ts'
|
|
9
|
+
|
|
10
|
+
interface NavPanelProps {
|
|
11
|
+
snap: ClientSnapshot
|
|
12
|
+
search: string
|
|
13
|
+
setSearch: (val: string) => void
|
|
14
|
+
collapsedGroups: Set<string>
|
|
15
|
+
setCollapsedGroups: (fn: (prev: Set<string>) => Set<string>) => void
|
|
16
|
+
gid: string | null
|
|
17
|
+
sid: string | null
|
|
18
|
+
setGid: (val: string) => void
|
|
19
|
+
setSid: (val: string) => void
|
|
20
|
+
setPartsSel: (val: string[] | null) => void
|
|
21
|
+
renameDraft: { kind: 'group' | 'session', id: string, value: string } | null
|
|
22
|
+
setRenameDraft: (val: { kind: 'group' | 'session', id: string, value: string } | null) => void
|
|
23
|
+
confirmDel: { kind: 'group' | 'session', id: string } | null
|
|
24
|
+
setConfirmDel: (val: { kind: 'group' | 'session', id: string } | null) => void
|
|
25
|
+
mutate: (args: Record<string, unknown>) => Promise<unknown>
|
|
26
|
+
navOpen: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function NavPanel(props: NavPanelProps): ReactNode {
|
|
30
|
+
const { snap, search, setSearch, collapsedGroups, setCollapsedGroups, gid, sid, setGid, setSid, setPartsSel, renameDraft, setRenameDraft, confirmDel, setConfirmDel, mutate, navOpen } = props
|
|
31
|
+
|
|
32
|
+
const commitRename = async (): Promise<void> => {
|
|
33
|
+
if (!renameDraft) return
|
|
34
|
+
const d = renameDraft
|
|
35
|
+
setRenameDraft(null)
|
|
36
|
+
const value = String(d.value || '').trim()
|
|
37
|
+
if (!value) return
|
|
38
|
+
if (d.kind === 'group') await mutate({ op: 'renameGroup', groupId: d.id, name: value })
|
|
39
|
+
else await mutate({ op: 'renameSession', sessionId: d.id, name: value })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const renameKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>): void => {
|
|
43
|
+
if (e.key === 'Enter') {
|
|
44
|
+
e.preventDefault()
|
|
45
|
+
void commitRename()
|
|
46
|
+
} else if (e.key === 'Escape') {
|
|
47
|
+
e.preventDefault()
|
|
48
|
+
setRenameDraft(null)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const doDelete = async (): Promise<void> => {
|
|
53
|
+
if (!confirmDel) return
|
|
54
|
+
const d = confirmDel
|
|
55
|
+
setConfirmDel(null)
|
|
56
|
+
if (d.kind === 'group') await mutate({ op: 'deleteGroup', groupId: d.id })
|
|
57
|
+
else await mutate({ op: 'deleteSession', sessionId: d.id })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const q = search.trim().toLowerCase()
|
|
61
|
+
const groupMatches = (g: ClientSnapshot['groups'][number]): { show: boolean, filterSessions: boolean } => {
|
|
62
|
+
if (!q) return { show: true, filterSessions: false }
|
|
63
|
+
if (g.name.toLowerCase().includes(q)) return { show: true, filterSessions: false }
|
|
64
|
+
const hitSessions = g.sessionIds.map((id) => sessById(snap, id)).filter((s) => s && s.name.toLowerCase().includes(q))
|
|
65
|
+
return { show: hitSessions.length > 0, filterSessions: true }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const group = gid ? groupById(snap, gid) : null
|
|
69
|
+
|
|
70
|
+
const treeNodes: ReactNode[] = []
|
|
71
|
+
for (const g of snap.groups) {
|
|
72
|
+
const match = groupMatches(g)
|
|
73
|
+
if (!match.show) continue
|
|
74
|
+
const expanded = q ? true : !collapsedGroups.has(g.id)
|
|
75
|
+
const isRenameGroup = renameDraft && renameDraft.kind === 'group' && renameDraft.id === g.id
|
|
76
|
+
const isConfirmGroup = confirmDel && confirmDel.kind === 'group' && confirmDel.id === g.id
|
|
77
|
+
const groupChildren: ReactNode[] = []
|
|
78
|
+
|
|
79
|
+
if (expanded) {
|
|
80
|
+
for (const sessionId of g.sessionIds) {
|
|
81
|
+
const s = sessById(snap, sessionId)
|
|
82
|
+
if (!s) continue
|
|
83
|
+
if (match.filterSessions && !s.name.toLowerCase().includes(q)) continue
|
|
84
|
+
const sess = sid ? sessById(snap, sid) : null
|
|
85
|
+
const isActive = sess && s.id === sess.id && g.id === group?.id
|
|
86
|
+
const isRenameSess = renameDraft && renameDraft.kind === 'session' && renameDraft.id === s.id
|
|
87
|
+
const isConfirm = confirmDel && confirmDel.kind === 'session' && confirmDel.id === s.id
|
|
88
|
+
// 会话状态(对齐主 GUI StateDot:idle 不渲染点)
|
|
89
|
+
const st = sessStatus(snap.run, s.id)
|
|
90
|
+
|
|
91
|
+
groupChildren.push(
|
|
92
|
+
<div
|
|
93
|
+
key={s.id}
|
|
94
|
+
className={'dsgc-sess-row' + (isActive ? ' on' : '')}
|
|
95
|
+
role="button"
|
|
96
|
+
tabIndex={0}
|
|
97
|
+
onClick={() => { setGid(g.id); setSid(s.id); setPartsSel(null); setConfirmDel(null); setRenameDraft(null) }}
|
|
98
|
+
onKeyDown={(e) => {
|
|
99
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
100
|
+
e.preventDefault()
|
|
101
|
+
setGid(g.id); setSid(s.id); setPartsSel(null); setConfirmDel(null); setRenameDraft(null)
|
|
102
|
+
}
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{/* 固定宽度状态槽(对齐主 GUI:槽恒在、点按状态渲染——行间文案对齐) */}
|
|
106
|
+
<span className="dsgc-sess-status">
|
|
107
|
+
{st !== 'idle'
|
|
108
|
+
? (
|
|
109
|
+
<span role="img" title={SESS_STATUS_LABEL[st]} aria-label={SESS_STATUS_LABEL[st]}>
|
|
110
|
+
<P.StateDot state={st} size={8} />
|
|
111
|
+
</span>
|
|
112
|
+
)
|
|
113
|
+
: null}
|
|
114
|
+
</span>
|
|
115
|
+
{isRenameSess
|
|
116
|
+
? (
|
|
117
|
+
<input
|
|
118
|
+
className="dsgc-rename"
|
|
119
|
+
value={renameDraft!.value}
|
|
120
|
+
autoFocus
|
|
121
|
+
onChange={(e) => { setRenameDraft({ kind: 'session', id: s.id, value: e.target.value }) }}
|
|
122
|
+
onBlur={() => { void commitRename() }}
|
|
123
|
+
onKeyDown={renameKeyDown}
|
|
124
|
+
onClick={(e) => { e.stopPropagation() }}
|
|
125
|
+
/>
|
|
126
|
+
)
|
|
127
|
+
: <span className="dsgc-sess-name">{s.name}</span>}
|
|
128
|
+
<span className="dsgc-nodeops">
|
|
129
|
+
<button
|
|
130
|
+
className="dsgc-opbtn"
|
|
131
|
+
title="重命名会话"
|
|
132
|
+
aria-label="重命名会话"
|
|
133
|
+
onClick={(e) => { e.stopPropagation(); setRenameDraft({ kind: 'session', id: s.id, value: s.name }) }}
|
|
134
|
+
>
|
|
135
|
+
{Icon(P.IconEditOutline16, 14)}
|
|
136
|
+
</button>
|
|
137
|
+
<button
|
|
138
|
+
className={'dsgc-opbtn' + (isConfirm ? ' danger' : '')}
|
|
139
|
+
title={isConfirm ? '再次点击确认删除' : '删除会话'}
|
|
140
|
+
aria-label="删除会话"
|
|
141
|
+
onClick={(e) => {
|
|
142
|
+
e.stopPropagation()
|
|
143
|
+
if (isConfirm) void doDelete()
|
|
144
|
+
else setConfirmDel({ kind: 'session', id: s.id })
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
{Icon(P.IconTrashOutline16, 14)}
|
|
148
|
+
</button>
|
|
149
|
+
</span>
|
|
150
|
+
</div>,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
groupChildren.push(
|
|
155
|
+
<button key="__add" className="dsgc-addsess" onClick={() => { void mutate({ op: 'createSession', groupId: g.id }) }}>
|
|
156
|
+
{Icon(P.IconPlusOutline16, 14)}新会话
|
|
157
|
+
</button>,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
treeNodes.push(
|
|
162
|
+
<div key={g.id} className="dsgc-gnode">
|
|
163
|
+
<div
|
|
164
|
+
className={'dsgc-grow-row' + (g.id === group?.id ? ' on' : '')}
|
|
165
|
+
role="button"
|
|
166
|
+
tabIndex={0}
|
|
167
|
+
onClick={() => { setGid(g.id); setPartsSel(null); setConfirmDel(null); setRenameDraft(null) }}
|
|
168
|
+
onKeyDown={(e) => {
|
|
169
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
170
|
+
e.preventDefault()
|
|
171
|
+
setGid(g.id); setPartsSel(null); setConfirmDel(null); setRenameDraft(null)
|
|
172
|
+
}
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
<button
|
|
176
|
+
className={'dsgc-twist' + (expanded ? '' : ' closed')}
|
|
177
|
+
title={expanded ? '收起' : '展开'}
|
|
178
|
+
aria-label={expanded ? '收起' : '展开'}
|
|
179
|
+
onClick={(e) => {
|
|
180
|
+
e.stopPropagation()
|
|
181
|
+
setCollapsedGroups((prev) => {
|
|
182
|
+
const next = new Set(prev)
|
|
183
|
+
if (next.has(g.id)) next.delete(g.id)
|
|
184
|
+
else next.add(g.id)
|
|
185
|
+
return next
|
|
186
|
+
})
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
{Icon(P.IconChevronDownOutline14, 14)}
|
|
190
|
+
</button>
|
|
191
|
+
{isRenameGroup
|
|
192
|
+
? (
|
|
193
|
+
<input
|
|
194
|
+
className="dsgc-rename"
|
|
195
|
+
value={renameDraft!.value}
|
|
196
|
+
autoFocus
|
|
197
|
+
onChange={(e) => { setRenameDraft({ kind: 'group', id: g.id, value: e.target.value }) }}
|
|
198
|
+
onBlur={() => { void commitRename() }}
|
|
199
|
+
onKeyDown={renameKeyDown}
|
|
200
|
+
onClick={(e) => { e.stopPropagation() }}
|
|
201
|
+
/>
|
|
202
|
+
)
|
|
203
|
+
: <span className="dsgc-gname">{g.name}</span>}
|
|
204
|
+
<span className="dsgc-nodeops">
|
|
205
|
+
<button
|
|
206
|
+
className="dsgc-opbtn"
|
|
207
|
+
title="重命名群组"
|
|
208
|
+
aria-label="重命名群组"
|
|
209
|
+
onClick={(e) => { e.stopPropagation(); setRenameDraft({ kind: 'group', id: g.id, value: g.name }) }}
|
|
210
|
+
>
|
|
211
|
+
{Icon(P.IconEditOutline16, 14)}
|
|
212
|
+
</button>
|
|
213
|
+
<button
|
|
214
|
+
className={'dsgc-opbtn' + (isConfirmGroup ? ' danger' : '')}
|
|
215
|
+
title={isConfirmGroup ? '再次点击确认删除' : '删除群组'}
|
|
216
|
+
aria-label="删除群组"
|
|
217
|
+
onClick={(e) => {
|
|
218
|
+
e.stopPropagation()
|
|
219
|
+
if (isConfirmGroup) void doDelete()
|
|
220
|
+
else setConfirmDel({ kind: 'group', id: g.id })
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
{Icon(P.IconTrashOutline16, 14)}
|
|
224
|
+
</button>
|
|
225
|
+
</span>
|
|
226
|
+
</div>
|
|
227
|
+
{expanded ? <div className="dsgc-sess-list">{groupChildren}</div> : null}
|
|
228
|
+
</div>,
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div className={'dsgc-nav' + (navOpen ? '' : ' closed')} aria-hidden={navOpen ? undefined : 'true'}>
|
|
234
|
+
<P.Input
|
|
235
|
+
icon={Icon(P.IconSearchOutline16, 16)}
|
|
236
|
+
className="dsgc-search"
|
|
237
|
+
placeholder="搜索群组与会话…"
|
|
238
|
+
value={search}
|
|
239
|
+
onChange={(e: { target: { value: string } }) => setSearch(e.target.value)}
|
|
240
|
+
/>
|
|
241
|
+
{treeNodes.length
|
|
242
|
+
? <div className="dsgc-tree">{treeNodes}</div>
|
|
243
|
+
: <div className="dsgc-hint">{q ? '没有匹配「' + search.trim() + '」的群组或会话' : '暂无群组'}</div>}
|
|
244
|
+
<P.Button variant="outline" onClick={() => { void mutate({ op: 'createGroup' }) }}>
|
|
245
|
+
{Icon(P.IconPlusOutline16, 16)}新建群组
|
|
246
|
+
</P.Button>
|
|
247
|
+
</div>
|
|
248
|
+
)
|
|
249
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 群组权限档位选择器(对齐主会话 composer 左下角的 /permission 选择器):
|
|
3
|
+
* 触发芯片(盾形图标 + 档位名 + chevron)+ Menu 上弹三档 + 完全权限风险
|
|
4
|
+
* 确认弹窗(与主会话同款 RiskConfirmation 原语与文案)。
|
|
5
|
+
* @module dsh-group-chat/client/permission-select
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useState, type ReactNode } from 'react'
|
|
9
|
+
import { Icon, P } from '../lib/ui.ts'
|
|
10
|
+
import type { PermissionTier } from '../../core/types.ts'
|
|
11
|
+
import { asPermissionTier, PERMISSION_TIERS } from '../../core/types.ts'
|
|
12
|
+
|
|
13
|
+
export interface PermissionSelectProps {
|
|
14
|
+
tier: PermissionTier
|
|
15
|
+
onSelect: (tier: PermissionTier) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 档位展示名(与主会话中文标签一致)。 */
|
|
19
|
+
const TIER_LABELS: Record<PermissionTier, string> = {
|
|
20
|
+
view_only: '仅可查看',
|
|
21
|
+
workspace_write: '工作区内修改',
|
|
22
|
+
full_access: '完全权限',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 档位悬停说明(群聊语境下的真实约束)。 */
|
|
26
|
+
const TIER_DESCRIPTIONS: Record<PermissionTier, string> = {
|
|
27
|
+
view_only: '角色只能读取工作区文件,不能执行命令',
|
|
28
|
+
workspace_write: '角色可执行命令(cwd 固定在工作区),每条命令需你在会话中确认',
|
|
29
|
+
full_access: '角色可免确认直接执行命令,存在风险',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 完全权限风险确认文案(与主会话逐字一致)。 */
|
|
33
|
+
const CONFIRM_TEXT = {
|
|
34
|
+
title: '确认启用完全权限?',
|
|
35
|
+
description: '启用完全权限后,智能体将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。',
|
|
36
|
+
acknowledge: '我已了解风险,并愿意继续',
|
|
37
|
+
cancel: '取消',
|
|
38
|
+
enable: '启用完全权限',
|
|
39
|
+
} as const
|
|
40
|
+
|
|
41
|
+
// ---- 盾形图标(路径取自主会话 PermissionSelect 的设计稿字形) ----
|
|
42
|
+
|
|
43
|
+
const SHIELD_OUTLINE = 'M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z'
|
|
44
|
+
|
|
45
|
+
function GlyphViewOnly(): ReactNode {
|
|
46
|
+
return (
|
|
47
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
48
|
+
<path d={SHIELD_OUTLINE} stroke="currentColor" strokeWidth="1.31831" strokeLinejoin="round" />
|
|
49
|
+
<path d="M12.1654 5.7552L8.9447 9.41475C8.73044 9.65816 8.53628 9.8804 8.35774 10.0423C8.1713 10.2114 7.94235 10.3717 7.64016 10.4254C7.48207 10.4535 7.32 10.4552 7.16151 10.4294C6.85843 10.3801 6.62728 10.2223 6.43836 10.0559C6.25752 9.89653 6.06037 9.67732 5.84264 9.43705L4.72925 8.20897L5.63557 7.38707L6.74897 8.61594C6.98603 8.87755 7.12974 9.03533 7.24673 9.13839C7.31033 9.19443 7.34485 9.21476 7.35823 9.22122C7.38068 9.22484 7.40352 9.22515 7.42593 9.22122C7.40522 9.22502 7.42893 9.23294 7.53583 9.136C7.65132 9.03126 7.79316 8.87139 8.02643 8.60638L11.2479 4.94763L12.1654 5.7552Z" fill="currentColor" />
|
|
50
|
+
</svg>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function GlyphWorkspaceWrite(): ReactNode {
|
|
55
|
+
return (
|
|
56
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
57
|
+
<path d="M8.08887 0.251709C8.20479 0.23085 8.32486 0.241168 8.43652 0.282959L15.0215 2.75171C15.2787 2.84819 15.4492 3.09414 15.4492 3.3689V7.0105C15.4492 7.10986 15.4441 7.2081 15.4414 7.30542C15.0285 7.07175 14.5905 6.87695 14.1309 6.73022V3.82495L8.20508 1.60327L2.2793 3.82495V7.0105C2.27936 9.7171 3.4745 11.5379 5.02734 12.7947C5.01025 12.9942 5 13.1962 5 13.4001C5.00001 13.7617 5.02722 14.1169 5.08008 14.4636C2.91555 13.0393 0.961014 10.752 0.960938 7.0105V3.3689C0.960938 3.09417 1.13146 2.84821 1.38867 2.75171L7.97461 0.282959L8.08887 0.251709Z" fill="currentColor" />
|
|
58
|
+
<path d="M11.3525 5.64688V6.85688H5V5.64688H11.3525Z" fill="currentColor" />
|
|
59
|
+
<path d="M9.5824 8.29376V9.50376H5V8.29376H9.5824Z" fill="currentColor" />
|
|
60
|
+
<path d="M14.6647 15.6852H10.0338C10.3878 15.3751 10.7567 15.0517 11.0772 14.7706C11.2531 14.6164 11.4144 14.4746 11.5511 14.3547H14.6647V15.6852Z" fill="currentColor" />
|
|
61
|
+
<path d="M8.14852 14.1308L7.33925 15.4976C7.22458 15.6912 7.42245 15.9194 7.63037 15.8333L9.09785 15.2254L15.0399 10.0719L14.0905 8.97733L8.14852 14.1308Z" fill="currentColor" />
|
|
62
|
+
</svg>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function GlyphFullAccess(): ReactNode {
|
|
67
|
+
return (
|
|
68
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
69
|
+
<path d={SHIELD_OUTLINE} stroke="currentColor" strokeWidth="1.31831" strokeLinejoin="round" />
|
|
70
|
+
<path d="M9.10094 4.5V8.75939H7.59888V4.5H9.10094Z" fill="currentColor" />
|
|
71
|
+
<path d="M9.10094 9.8114V11.5H7.59888V9.8114Z" fill="currentColor" />
|
|
72
|
+
</svg>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const TIER_GLYPHS: Record<PermissionTier, () => ReactNode> = {
|
|
77
|
+
view_only: GlyphViewOnly,
|
|
78
|
+
workspace_write: GlyphWorkspaceWrite,
|
|
79
|
+
full_access: GlyphFullAccess,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function PermissionSelect(props: PermissionSelectProps): ReactNode {
|
|
83
|
+
const [open, setOpen] = useState(false)
|
|
84
|
+
const [confirming, setConfirming] = useState(false)
|
|
85
|
+
const [acknowledged, setAcknowledged] = useState(false)
|
|
86
|
+
// 版本错位兜底:旧 host 快照无 permissionTier(或值非法)时按最安全的仅可查看呈现,
|
|
87
|
+
// 不让面板整体崩溃
|
|
88
|
+
const tier = asPermissionTier(props.tier) ?? 'view_only'
|
|
89
|
+
|
|
90
|
+
// 档位被外部改回(如迁移/其他端变更)时收起弹层与确认态
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
setOpen(false)
|
|
93
|
+
setAcknowledged(false)
|
|
94
|
+
setConfirming(false)
|
|
95
|
+
}, [tier])
|
|
96
|
+
|
|
97
|
+
const choose = (id: string): void => {
|
|
98
|
+
setOpen(false)
|
|
99
|
+
if (id === tier) return
|
|
100
|
+
if (id === 'full_access') {
|
|
101
|
+
setAcknowledged(false)
|
|
102
|
+
setConfirming(true)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
props.onSelect(id as PermissionTier)
|
|
106
|
+
}
|
|
107
|
+
const closeConfirm = (): void => {
|
|
108
|
+
setAcknowledged(false)
|
|
109
|
+
setConfirming(false)
|
|
110
|
+
}
|
|
111
|
+
const confirmFullAccess = (): void => {
|
|
112
|
+
if (!acknowledged) return
|
|
113
|
+
closeConfirm()
|
|
114
|
+
props.onSelect('full_access')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const items = PERMISSION_TIERS.map((t) => ({ id: t, label: TIER_LABELS[t], icon: TIER_GLYPHS[t]() }))
|
|
118
|
+
const Glyph = TIER_GLYPHS[tier] ?? GlyphViewOnly
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<>
|
|
122
|
+
<P.Menu
|
|
123
|
+
open={open}
|
|
124
|
+
items={items}
|
|
125
|
+
selectedId={tier}
|
|
126
|
+
onSelect={choose}
|
|
127
|
+
onClose={() => { setOpen(false) }}
|
|
128
|
+
side="top"
|
|
129
|
+
anchor={(
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
className="dsgc-permtrigger"
|
|
133
|
+
aria-label={'权限模式:' + TIER_LABELS[tier]}
|
|
134
|
+
title={TIER_DESCRIPTIONS[tier]}
|
|
135
|
+
disabled={confirming}
|
|
136
|
+
onClick={() => { setOpen(!open) }}
|
|
137
|
+
>
|
|
138
|
+
<span className="dsgc-permicon" aria-hidden>{Glyph()}</span>
|
|
139
|
+
<span className="dsgc-permlabel">{TIER_LABELS[tier]}</span>
|
|
140
|
+
<span className={'dsgc-permchevron' + (open ? ' open' : '')} aria-hidden>{Icon(P.IconChevronDownOutline14, 14)}</span>
|
|
141
|
+
</button>
|
|
142
|
+
)}
|
|
143
|
+
/>
|
|
144
|
+
<P.RiskConfirmation
|
|
145
|
+
open={confirming}
|
|
146
|
+
title={CONFIRM_TEXT.title}
|
|
147
|
+
description={CONFIRM_TEXT.description}
|
|
148
|
+
acknowledgeLabel={CONFIRM_TEXT.acknowledge}
|
|
149
|
+
cancelLabel={CONFIRM_TEXT.cancel}
|
|
150
|
+
closeLabel="关闭"
|
|
151
|
+
confirmLabel={CONFIRM_TEXT.enable}
|
|
152
|
+
acknowledged={acknowledged}
|
|
153
|
+
onAcknowledgedChange={setAcknowledged}
|
|
154
|
+
onCancel={closeConfirm}
|
|
155
|
+
onConfirm={confirmFullAccess}
|
|
156
|
+
/>
|
|
157
|
+
</>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 角色编辑抽屉(右侧滑出,不遮挡会话流阅读)。
|
|
3
|
+
* @module dsh-group-chat/client/drawer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useEffect, useState, type ReactNode } from 'react'
|
|
7
|
+
import { Icon, P } from '../lib/ui.ts'
|
|
8
|
+
import { api } from '../lib/api.ts'
|
|
9
|
+
import { PALETTE, type EffortsResponse, type ModelsResponse, type RoleDraft } from '../lib/model.ts'
|
|
10
|
+
|
|
11
|
+
export interface RoleDrawerProps {
|
|
12
|
+
draft: RoleDraft
|
|
13
|
+
set: (draft: RoleDraft) => void
|
|
14
|
+
models: ModelsResponse | null
|
|
15
|
+
modelsError: string | null
|
|
16
|
+
onRetryModels: () => void
|
|
17
|
+
onSave: () => void
|
|
18
|
+
onCancel: () => void
|
|
19
|
+
formError: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function RoleDrawer(props: RoleDrawerProps): ReactNode {
|
|
23
|
+
const { draft, set } = props
|
|
24
|
+
const models = props.models
|
|
25
|
+
const providers = (models && models.providers) || []
|
|
26
|
+
const modelsOf = (models && models.modelsByProvider && models.modelsByProvider[draft.provider]) || []
|
|
27
|
+
const [closing, setClosing] = useState(false)
|
|
28
|
+
const [effortsInfo, setEffortsInfo] = useState<EffortsResponse | null>(null)
|
|
29
|
+
const close = (): void => {
|
|
30
|
+
if (closing) return
|
|
31
|
+
setClosing(true)
|
|
32
|
+
window.setTimeout(() => props.onCancel(), 140)
|
|
33
|
+
}
|
|
34
|
+
const onProvider = (e: { target: { value: string } }): void => {
|
|
35
|
+
const p = e.target.value
|
|
36
|
+
const list = (models && models.modelsByProvider && models.modelsByProvider[p]) || []
|
|
37
|
+
// 换模型路线后旧推理级别失效,重置为默认
|
|
38
|
+
set(Object.assign({}, draft, { provider: p, model: list.length ? list[0].id : '', reasoningEffort: 'default' }))
|
|
39
|
+
}
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const onKey = (e: KeyboardEvent): void => { if (e.key === 'Escape') close() }
|
|
42
|
+
document.addEventListener('keydown', onKey)
|
|
43
|
+
return () => document.removeEventListener('keydown', onKey)
|
|
44
|
+
}, [])
|
|
45
|
+
// 推理级别选项:跟随抽屉内 provider/model 变化重新查询(Host 60s 缓存)
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
let live = true
|
|
48
|
+
setEffortsInfo(null)
|
|
49
|
+
if (!draft.provider || !draft.model) return
|
|
50
|
+
api.action({ kind: 'efforts', provider: draft.provider, model: draft.model }).then((res) => {
|
|
51
|
+
if (!live) return
|
|
52
|
+
const r = res as EffortsResponse
|
|
53
|
+
if (r && r.ok) setEffortsInfo({ ok: true, efforts: r.efforts || [], defaultEffort: r.defaultEffort })
|
|
54
|
+
else setEffortsInfo({ ok: false, efforts: [], defaultEffort: undefined, error: (r && r.error) || '查询失败' })
|
|
55
|
+
}).catch((e: unknown) => { if (live) setEffortsInfo({ ok: false, efforts: [], defaultEffort: undefined, error: String((e && (e as Error).message) || e) }) })
|
|
56
|
+
return () => { live = false }
|
|
57
|
+
}, [draft.provider, draft.model])
|
|
58
|
+
return (
|
|
59
|
+
<div className={'dsgc-drawer' + (closing ? ' closing' : '')} role="dialog" aria-modal="false" aria-label={draft.id ? '编辑角色' : '添加角色'}>
|
|
60
|
+
<div className="dsgc-drawerhead">
|
|
61
|
+
{Icon(P.IconUserOutline16, 16)}
|
|
62
|
+
<span className="dsgc-drawertitle">{draft.id ? '编辑角色' : '添加角色'}</span>
|
|
63
|
+
<P.Button variant="ghost" size="sm" onClick={close} aria-label="关闭">{Icon(P.IconCloseOutline16, 16)}</P.Button>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="dsgc-drawerbody">
|
|
66
|
+
<div className="dsgc-form">
|
|
67
|
+
<div className="dsgc-field">
|
|
68
|
+
<label>名称</label>
|
|
69
|
+
<input
|
|
70
|
+
className="dsgc-input"
|
|
71
|
+
value={draft.name}
|
|
72
|
+
autoFocus={!draft.id}
|
|
73
|
+
onChange={(e) => { set(Object.assign({}, draft, { name: e.target.value })) }}
|
|
74
|
+
placeholder="例如:产品经理"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="dsgc-field">
|
|
78
|
+
<label>标识色</label>
|
|
79
|
+
<div className="dsgc-palette">
|
|
80
|
+
{PALETTE.map((c) => (
|
|
81
|
+
<button
|
|
82
|
+
key={c}
|
|
83
|
+
type="button"
|
|
84
|
+
className={'dsgc-dot' + (draft.color === c ? ' on' : '')}
|
|
85
|
+
style={{ background: c }}
|
|
86
|
+
onClick={() => { set(Object.assign({}, draft, { color: c })) }}
|
|
87
|
+
aria-label={'选择标识色 ' + c}
|
|
88
|
+
/>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
<div className="dsgc-field">
|
|
93
|
+
<label>人设 / 角色设定</label>
|
|
94
|
+
<textarea
|
|
95
|
+
className="dsgc-textarea"
|
|
96
|
+
rows={4}
|
|
97
|
+
value={draft.persona}
|
|
98
|
+
onChange={(e) => { set(Object.assign({}, draft, { persona: e.target.value })) }}
|
|
99
|
+
placeholder="性格、立场、说话风格、专业背景……"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
<div className="dsgc-field">
|
|
103
|
+
<label>模型提供方</label>
|
|
104
|
+
{providers.length
|
|
105
|
+
? (
|
|
106
|
+
<select className="dsgc-select" value={draft.provider} onChange={onProvider}>
|
|
107
|
+
<option value="">选择提供方…</option>
|
|
108
|
+
{providers.map((p) => <option key={p.id} value={p.id}>{p.name || p.id}</option>)}
|
|
109
|
+
</select>
|
|
110
|
+
)
|
|
111
|
+
: (
|
|
112
|
+
<div className="dsgc-selhint">
|
|
113
|
+
{(props.modelsError ? '模型目录加载失败:' + props.modelsError : '暂无可用 provider') + ' '}
|
|
114
|
+
<P.Button variant="outline" size="sm" onClick={props.onRetryModels}>重试</P.Button>
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
<div className="dsgc-field">
|
|
119
|
+
<label>模型</label>
|
|
120
|
+
{modelsOf.length
|
|
121
|
+
? (
|
|
122
|
+
<select
|
|
123
|
+
className="dsgc-select"
|
|
124
|
+
value={draft.model}
|
|
125
|
+
onChange={(e) => { set(Object.assign({}, draft, { model: e.target.value, reasoningEffort: 'default' })) }}
|
|
126
|
+
>
|
|
127
|
+
{modelsOf.map((m) => <option key={m.id} value={m.id}>{m.name || m.id}</option>)}
|
|
128
|
+
</select>
|
|
129
|
+
)
|
|
130
|
+
: <div className="dsgc-selhint">请先选择有可用模型的提供方</div>}
|
|
131
|
+
</div>
|
|
132
|
+
<div className="dsgc-field">
|
|
133
|
+
<label>温度(可选)</label>
|
|
134
|
+
<input
|
|
135
|
+
className="dsgc-input"
|
|
136
|
+
type="number"
|
|
137
|
+
step="0.1"
|
|
138
|
+
min="0"
|
|
139
|
+
max="2"
|
|
140
|
+
value={draft.temperature == null ? '' : String(draft.temperature)}
|
|
141
|
+
onChange={(e) => { set(Object.assign({}, draft, { temperature: e.target.value === '' ? undefined : Number(e.target.value) })) }}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
<div className="dsgc-field">
|
|
145
|
+
<label>深度思考</label>
|
|
146
|
+
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
|
|
147
|
+
<P.Switch checked={draft.thinking === true} onChange={(v: boolean) => { set(Object.assign({}, draft, { thinking: v })) }} label="深度思考" />
|
|
148
|
+
<span className="dsgc-hint">开启后角色发言前先思考(支持思考的模型)</span>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
{draft.thinking === true
|
|
152
|
+
? (
|
|
153
|
+
<div className="dsgc-field">
|
|
154
|
+
<label>推理级别</label>
|
|
155
|
+
{effortsInfo === null
|
|
156
|
+
? <div className="dsgc-selhint">查询推理级别…</div>
|
|
157
|
+
: !effortsInfo.ok
|
|
158
|
+
? <div className="dsgc-selhint">推理级别查询失败:{effortsInfo.error}(若刚更新插件,请重启 dsh web 使 host 半生效)</div>
|
|
159
|
+
: (effortsInfo.efforts || []).length
|
|
160
|
+
? (
|
|
161
|
+
<select
|
|
162
|
+
className="dsgc-select"
|
|
163
|
+
value={draft.reasoningEffort && draft.reasoningEffort !== 'default' ? draft.reasoningEffort : 'default'}
|
|
164
|
+
onChange={(e) => { set(Object.assign({}, draft, { reasoningEffort: e.target.value })) }}
|
|
165
|
+
title="取自当前模型设置中的推理选项;默认 = 使用该模型的默认推理级别"
|
|
166
|
+
>
|
|
167
|
+
<option value="default">
|
|
168
|
+
默认{effortsInfo.defaultEffort ? '(当前默认:' + ((effortsInfo.efforts || []).find((x) => x.id === effortsInfo.defaultEffort) || { name: '' }).name + ')' : ''}
|
|
169
|
+
</option>
|
|
170
|
+
{(effortsInfo.efforts || []).map((e) => <option key={e.id} value={e.id} title={e.description || ''}>{e.name || e.id}</option>)}
|
|
171
|
+
</select>
|
|
172
|
+
)
|
|
173
|
+
: <div className="dsgc-selhint">当前模型未提供推理等级,将使用默认</div>}
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
: null}
|
|
177
|
+
{props.formError ? <div className="dsgc-formerr">{props.formError}</div> : null}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
<div className="dsgc-drawerfoot">
|
|
181
|
+
<P.Button variant="outline" onClick={close}>取消</P.Button>
|
|
182
|
+
<P.Button variant="primary" onClick={props.onSave}>{draft.id ? '保存' : '添加'}</P.Button>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 思考折叠行(对标宿主 ReasoningRow:折叠摘要 / 展开全文)。
|
|
3
|
+
* @module dsh-group-chat/client/ThinkRow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, type ReactNode } from 'react'
|
|
7
|
+
import { P } from '../lib/ui.ts'
|
|
8
|
+
import { firstLine, latestLine } from '../lib/model.ts'
|
|
9
|
+
|
|
10
|
+
export function ThinkRow(props: { text: string, running?: boolean }): ReactNode {
|
|
11
|
+
const text = props.text || ''
|
|
12
|
+
const running = !!props.running
|
|
13
|
+
const [expanded, setExpanded] = useState(false)
|
|
14
|
+
const plain = (line: string): string => line.replace(/^(\s*#{1,6}\s+|\s*[-*+]\s+|\s*>\s*)+/, '').replace(/[*`_~]/g, '').replace(/\s+/g, ' ').trim()
|
|
15
|
+
const summary = plain(running ? latestLine(text) : firstLine(text))
|
|
16
|
+
return (
|
|
17
|
+
<div className="dsgc-think">
|
|
18
|
+
<P.DisclosureRow
|
|
19
|
+
rowClassName="dsgc-thinkrow"
|
|
20
|
+
icon={<P.IconThinkOutline14 size={14} />}
|
|
21
|
+
title="思考"
|
|
22
|
+
open={expanded}
|
|
23
|
+
expandable
|
|
24
|
+
expandOnRowClick
|
|
25
|
+
onToggle={() => { setExpanded((v) => !v) }}
|
|
26
|
+
collapsedContent={text ? <span className="dsgc-thinksummary">{summary}</span> : null}
|
|
27
|
+
>
|
|
28
|
+
<div className="dsgc-thinkbody">{text}</div>
|
|
29
|
+
</P.DisclosureRow>
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具调用折叠行(对标思考折叠:摘要行 / 展开输出)。
|
|
3
|
+
* @module dsh-group-chat/client/ToolRow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, type ReactNode } from 'react'
|
|
7
|
+
import { Icon, P, pickPrimitive } from '../lib/ui.ts'
|
|
8
|
+
import type { ToolCallView } from '../lib/model.ts'
|
|
9
|
+
|
|
10
|
+
const TOOL_ICONS: Record<string, string> = { read_file: 'IconBrowseOutline16', list_dir: 'IconFolderOpenOutline16', run_command: 'IconCodeOutline16' }
|
|
11
|
+
|
|
12
|
+
export function ToolRow(props: { c: ToolCallView }): ReactNode {
|
|
13
|
+
const c = props.c
|
|
14
|
+
const [expanded, setExpanded] = useState(false)
|
|
15
|
+
let brief = ''
|
|
16
|
+
try {
|
|
17
|
+
brief = JSON.stringify(c.args) || ''
|
|
18
|
+
} catch {
|
|
19
|
+
brief = ''
|
|
20
|
+
}
|
|
21
|
+
if (brief.length > 40) brief = brief.slice(0, 40) + '…'
|
|
22
|
+
const IconOf = pickPrimitive(TOOL_ICONS[c.tool] || 'IconCodeOutline16')
|
|
23
|
+
const statusText = c.status === 'ok' ? '成功' : c.status === 'denied' ? '用户拒绝' : '失败'
|
|
24
|
+
const StatusIcon = c.status === 'ok' ? P.IconCheckOutline14 : c.status === 'denied' ? P.IconCloseOutline16 : P.IconWarningOutline16
|
|
25
|
+
const dur = c.durationMs ? (c.durationMs >= 1000 ? (c.durationMs / 1000).toFixed(1) + 's' : c.durationMs + 'ms') : ''
|
|
26
|
+
return (
|
|
27
|
+
<div className="dsgc-tool">
|
|
28
|
+
<P.DisclosureRow
|
|
29
|
+
rowClassName="dsgc-toolrow"
|
|
30
|
+
icon={<IconOf size={14} />}
|
|
31
|
+
title={c.tool + (brief ? ' ' + brief : '')}
|
|
32
|
+
open={expanded}
|
|
33
|
+
expandable
|
|
34
|
+
expandOnRowClick
|
|
35
|
+
onToggle={() => { setExpanded((v) => !v) }}
|
|
36
|
+
collapsedContent={
|
|
37
|
+
<span className="dsgc-toolsummary">
|
|
38
|
+
{Icon(StatusIcon, 12, {
|
|
39
|
+
verticalAlign: '-2px',
|
|
40
|
+
marginRight: 4,
|
|
41
|
+
color: c.status === 'ok' ? 'var(--dsw-alias-label-tertiary,inherit)' : 'var(--dsw-alias-state-error-primary,#e5484d)',
|
|
42
|
+
})}
|
|
43
|
+
{statusText + (dur ? ' · ' + dur : '')}
|
|
44
|
+
</span>
|
|
45
|
+
}
|
|
46
|
+
>
|
|
47
|
+
<div className="dsgc-toolbody">{c.output || '(无输出)'}</div>
|
|
48
|
+
</P.DisclosureRow>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|