@roaming-ai/dsh-group-chat 0.2.2 → 0.3.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/README.md +3 -2
- package/lib/client.js +984 -296
- package/lib/client.js.map +1 -1
- package/lib/index.js +524 -52
- package/lib/types/client/components/Bubble.d.ts +6 -3
- package/lib/types/client/components/ChatPanel.d.ts +1 -0
- package/lib/types/client/components/ConstraintList.d.ts +11 -0
- package/lib/types/client/components/FailCard.d.ts +9 -0
- package/lib/types/client/components/Fold.d.ts +19 -0
- package/lib/types/client/components/HoverTip.d.ts +22 -0
- package/lib/types/client/components/MessageFlow.d.ts +1 -0
- package/lib/types/client/components/MsgActions.d.ts +14 -0
- package/lib/types/client/hooks/useComposer.d.ts +6 -1
- package/lib/types/client/lib/composer-draft.d.ts +17 -0
- package/lib/types/core/constraints.d.ts +61 -0
- package/lib/types/core/errors.d.ts +54 -0
- package/lib/types/core/types.d.ts +22 -0
- package/lib/types/host/api/actions.d.ts +1 -1
- package/lib/types/host/engine/conversation.d.ts +5 -3
- package/lib/types/host/engine/fold.d.ts +17 -0
- package/lib/types/host/engine/index.d.ts +1 -0
- package/lib/types/host/engine/retitle.d.ts +5 -5
- package/lib/types/host/service.d.ts +1 -1
- package/lib/types/host/state.d.ts +2 -0
- package/lib/types/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/client/GroupChatPanel.tsx +18 -3
- package/src/client/components/AsidePanel.tsx +10 -8
- package/src/client/components/Bubble.tsx +45 -18
- package/src/client/components/ChatPanel.tsx +17 -12
- package/src/client/components/Composer.tsx +26 -20
- package/src/client/components/ConstraintList.tsx +69 -0
- package/src/client/components/FailCard.tsx +49 -0
- package/src/client/components/Fold.tsx +72 -0
- package/src/client/components/HoverTip.tsx +134 -0
- package/src/client/components/MessageFlow.tsx +77 -54
- package/src/client/components/MsgActions.tsx +85 -0
- package/src/client/components/NavPanel.tsx +6 -1
- package/src/client/components/ThinkRow.tsx +7 -3
- package/src/client/components/ToolRow.tsx +7 -3
- package/src/client/hooks/useComposer.ts +40 -3
- package/src/client/hooks/useGroupChatState.ts +5 -4
- package/src/client/lib/composer-draft.ts +47 -0
- package/src/client/lib/styles.ts +73 -12
- package/src/client/react-dom-shim.d.ts +2 -0
- package/src/core/constraints.ts +210 -0
- package/src/core/errors.ts +184 -0
- package/src/core/json.ts +1 -0
- package/src/core/types.ts +28 -3
- package/src/host/api/actions.ts +35 -1
- package/src/host/broadcast.ts +3 -3
- package/src/host/engine/conversation.ts +103 -38
- package/src/host/engine/fold.ts +114 -0
- package/src/host/engine/index.ts +1 -0
- package/src/host/engine/retitle.ts +16 -11
- package/src/host/persistence/persistence.ts +18 -1
- package/src/host/service.ts +1 -1
- package/src/host/state.ts +5 -4
- package/src/index.ts +2 -0
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ReactNode, CSSProperties } from 'react'
|
|
7
|
+
import { WINDOW_SIZE } from '../../core/constraints.ts'
|
|
7
8
|
import { Icon, P } from '../lib/ui.ts'
|
|
8
9
|
import { Bubble } from './Bubble.tsx'
|
|
10
|
+
import { ConstraintList } from './ConstraintList.tsx'
|
|
9
11
|
import { ThinkRow } from './ThinkRow.tsx'
|
|
12
|
+
import { resolveFailedRole } from '../../core/errors.ts'
|
|
10
13
|
import { roleById, type ClientSnapshot } from '../lib/model.ts'
|
|
11
14
|
import { MD_LABELS } from '../lib/model.ts'
|
|
12
15
|
|
|
@@ -16,68 +19,56 @@ interface MessageFlowProps {
|
|
|
16
19
|
busyNow: boolean
|
|
17
20
|
msgById: Record<string, ClientSnapshot['messages'][number]>
|
|
18
21
|
action: (payload: Record<string, unknown>) => Promise<unknown>
|
|
22
|
+
onRetrySpeak: (messageId: string) => void
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export function MessageFlow(props: MessageFlowProps): ReactNode {
|
|
22
|
-
const { snap, sess, busyNow, msgById, action } = props
|
|
26
|
+
const { snap, sess, busyNow, msgById, action, onRetrySpeak } = props
|
|
23
27
|
|
|
24
28
|
const bubbles: ReactNode[] = []
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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)}
|
|
29
|
+
const replaceId = busyNow ? snap.run.replaceMessageId : null
|
|
30
|
+
const replaceMsg = replaceId ? msgById[replaceId] : null
|
|
31
|
+
const liveRoleId = snap.run.currentRoleId || (replaceMsg && (replaceMsg.failedRoleId || replaceMsg.speaker)) || null
|
|
32
|
+
const lr = busyNow && liveRoleId ? roleById(snap, liveRoleId) : null
|
|
33
|
+
|
|
34
|
+
const live = lr
|
|
35
|
+
? (
|
|
36
|
+
<div key="__live" className="dsgc-msg live">
|
|
37
|
+
<div
|
|
38
|
+
className="dsgc-avatar"
|
|
39
|
+
style={{ border: '2px solid ' + (lr.color || '#888'), '--role-color': lr.color || '#888' } as CSSProperties}
|
|
40
|
+
>
|
|
41
|
+
{lr.name.slice(0, 1)}
|
|
42
|
+
</div>
|
|
43
|
+
<div className="dsgc-msgbody">
|
|
44
|
+
<div className="dsgc-msghead">
|
|
45
|
+
<span className="dsgc-msgname">{lr.name}</span>
|
|
46
|
+
<span className="dsgc-msgmodel">{lr.provider} / {lr.model}</span>
|
|
47
|
+
<span className="dsgc-msgtime dsgc-typing">深度求索...</span>
|
|
48
48
|
</div>
|
|
49
|
-
<div className="dsgc-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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>
|
|
49
|
+
<div className="dsgc-msgtext live">
|
|
50
|
+
{snap.run.partialReasoning ? <ThinkRow text={snap.run.partialReasoning} running /> : null}
|
|
51
|
+
{snap.run.partial ? <P.MarkdownText text={snap.run.partial} streaming labels={MD_LABELS} /> : null}
|
|
52
|
+
{/* 首 delta 前的「思考中」占位(深度思考模型首字节可能等数秒到数十秒,
|
|
53
|
+
空白气泡会被感知为卡死);delta 到达后被真实思考行/正文自然替换 */}
|
|
54
|
+
{!snap.run.partial && !snap.run.partialReasoning
|
|
55
|
+
? (
|
|
56
|
+
<div className="dsgc-pending">
|
|
57
|
+
{Icon(P.IconThinkOutline14, 14)}
|
|
58
|
+
<span>思考中</span>
|
|
59
|
+
<span className="dsgc-pendingdots" aria-hidden="true"><i /><i /><i /></span>
|
|
60
|
+
</div>
|
|
67
61
|
)
|
|
68
|
-
|
|
69
|
-
</div>
|
|
62
|
+
: null}
|
|
70
63
|
</div>
|
|
71
|
-
</div
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
72
66
|
)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// 命令确认卡片
|
|
67
|
+
: null
|
|
68
|
+
|
|
77
69
|
const pc = busyNow && snap.run.pendingConfirm && sess && snap.run.sessionId === sess.id ? snap.run.pendingConfirm : null
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
bubbles.push(
|
|
70
|
+
const confirm = pc
|
|
71
|
+
? (
|
|
81
72
|
<div key="__confirm" className="dsgc-confirm">
|
|
82
73
|
<div className="dsgc-confirmtitle">
|
|
83
74
|
{Icon(P.IconWarningOutline16, 14)}
|
|
@@ -89,9 +80,41 @@ export function MessageFlow(props: MessageFlowProps): ReactNode {
|
|
|
89
80
|
<P.Button variant="primary" size="sm" onClick={() => { void action({ kind: 'confirmCommand', toolCallId: pc.toolCallId, allow: true }) }}>允许</P.Button>
|
|
90
81
|
<P.Button variant="outline" size="sm" onClick={() => { void action({ kind: 'confirmCommand', toolCallId: pc.toolCallId, allow: false }) }}>拒绝</P.Button>
|
|
91
82
|
</div>
|
|
92
|
-
</div
|
|
93
|
-
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
: null
|
|
86
|
+
|
|
87
|
+
let livePlaced = false
|
|
88
|
+
|
|
89
|
+
if (sess) {
|
|
90
|
+
const ids = sess.messageIds
|
|
91
|
+
const memo = sess.constraints && sess.constraints.length
|
|
92
|
+
? <ConstraintList key={sess.id + '-constraints'} items={sess.constraints} />
|
|
93
|
+
: null
|
|
94
|
+
// 折点 = 最近 WINDOW_SIZE 条之前。消息都还在窗口内时卡放流顶。
|
|
95
|
+
const foldAt = memo && ids.length > WINDOW_SIZE ? ids.length - WINDOW_SIZE : 0
|
|
96
|
+
if (memo && foldAt === 0) bubbles.push(memo)
|
|
97
|
+
for (let i = 0; i < ids.length; i++) {
|
|
98
|
+
if (memo && foldAt > 0 && i === foldAt) bubbles.push(memo)
|
|
99
|
+
const m = msgById[ids[i]]
|
|
100
|
+
if (!m) continue
|
|
101
|
+
// 角色在父级解析:Bubble 按值 memo(不依赖 snap identity),流式帧
|
|
102
|
+
// 不再触发已完成消息的全量重渲(每帧仅 live 行与派生列表变化)
|
|
103
|
+
const groupRoles = snap.roles.filter((r) => r.groupId === sess.groupId)
|
|
104
|
+
const role = resolveFailedRole(m, groupRoles) || (m.speaker !== 'user' && m.speaker !== 'system' ? roleById(snap, m.speaker) : null)
|
|
105
|
+
// 原地重试:该槽位换成 live(确认卡紧随其后),不要钉在列表末尾
|
|
106
|
+
if (replaceId && m.id === replaceId) {
|
|
107
|
+
if (live) bubbles.push(live)
|
|
108
|
+
if (confirm) bubbles.push(confirm)
|
|
109
|
+
livePlaced = true
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
bubbles.push(<Bubble key={m.id} m={m} role={role} busy={!!snap.run.running} onRetry={onRetrySpeak} />)
|
|
113
|
+
}
|
|
94
114
|
}
|
|
95
115
|
|
|
116
|
+
if (live && !livePlaced) bubbles.push(live)
|
|
117
|
+
if (confirm && !livePlaced) bubbles.push(confirm)
|
|
118
|
+
|
|
96
119
|
return <>{bubbles}</>
|
|
97
120
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 消息操作条:贴在气泡外下方。用户/角色消息悬停出「复制」;失败卡常驻「复制 / 重试」。
|
|
3
|
+
* @module dsh-group-chat/client/MsgActions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
7
|
+
import { Icon, P } from '../lib/ui.ts'
|
|
8
|
+
|
|
9
|
+
export interface MsgActionsProps {
|
|
10
|
+
copyText: string
|
|
11
|
+
onRetry?: () => void
|
|
12
|
+
retryDisabled?: boolean
|
|
13
|
+
retryTitle?: string
|
|
14
|
+
/** 失败卡:不依赖悬停,始终可见。 */
|
|
15
|
+
always?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function writeClipboard(text: string): Promise<boolean> {
|
|
19
|
+
try {
|
|
20
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
21
|
+
await navigator.clipboard.writeText(text)
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
} catch { /* fall through */ }
|
|
25
|
+
try {
|
|
26
|
+
const el = document.createElement('textarea')
|
|
27
|
+
el.value = text
|
|
28
|
+
el.setAttribute('readonly', '')
|
|
29
|
+
el.style.position = 'fixed'
|
|
30
|
+
el.style.left = '-9999px'
|
|
31
|
+
document.body.appendChild(el)
|
|
32
|
+
el.select()
|
|
33
|
+
const ok = document.execCommand('copy')
|
|
34
|
+
document.body.removeChild(el)
|
|
35
|
+
return ok
|
|
36
|
+
} catch {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function MsgActions(props: MsgActionsProps): ReactNode {
|
|
42
|
+
const { copyText, onRetry, retryDisabled, retryTitle, always } = props
|
|
43
|
+
const [copied, setCopied] = useState(false)
|
|
44
|
+
const copiedTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
45
|
+
useEffect(() => () => { if (copiedTimer.current) clearTimeout(copiedTimer.current) }, [])
|
|
46
|
+
|
|
47
|
+
const copy = async (): Promise<void> => {
|
|
48
|
+
if (!copyText) return
|
|
49
|
+
const ok = await writeClipboard(copyText)
|
|
50
|
+
if (!ok) return
|
|
51
|
+
setCopied(true)
|
|
52
|
+
if (copiedTimer.current) clearTimeout(copiedTimer.current)
|
|
53
|
+
copiedTimer.current = setTimeout(() => { setCopied(false) }, 1400)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className={'dsgc-msgops' + (always ? ' always' : '')} role="group" aria-label="消息操作">
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
className={'dsgc-msgop' + (copied ? ' done' : '')}
|
|
61
|
+
title={copied ? '已复制' : '复制'}
|
|
62
|
+
aria-label={copied ? '已复制' : '复制'}
|
|
63
|
+
onClick={(e) => { e.stopPropagation(); void copy() }}
|
|
64
|
+
>
|
|
65
|
+
{Icon(copied ? P.IconCheckOutline14 : P.IconCopyOutline16, 14)}
|
|
66
|
+
<span>{copied ? '已复制' : '复制'}</span>
|
|
67
|
+
</button>
|
|
68
|
+
{onRetry
|
|
69
|
+
? (
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
className="dsgc-msgop retry"
|
|
73
|
+
title={retryTitle || '重试'}
|
|
74
|
+
aria-label={retryTitle || '重试'}
|
|
75
|
+
disabled={retryDisabled}
|
|
76
|
+
onClick={(e) => { e.stopPropagation(); if (!retryDisabled) onRetry() }}
|
|
77
|
+
>
|
|
78
|
+
{Icon(P.IconRefreshOutline14, 14)}
|
|
79
|
+
<span>重试</span>
|
|
80
|
+
</button>
|
|
81
|
+
)
|
|
82
|
+
: null}
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ReactNode, KeyboardEvent as ReactKeyboardEvent } from 'react'
|
|
7
7
|
import { Icon, P } from '../lib/ui.ts'
|
|
8
|
+
import { HoverTip } from './HoverTip.tsx'
|
|
8
9
|
import { groupById, sessById, sessStatus, SESS_STATUS_LABEL, type ClientSnapshot } from '../lib/model.ts'
|
|
9
10
|
|
|
10
11
|
interface NavPanelProps {
|
|
@@ -124,7 +125,11 @@ export function NavPanel(props: NavPanelProps): ReactNode {
|
|
|
124
125
|
onClick={(e) => { e.stopPropagation() }}
|
|
125
126
|
/>
|
|
126
127
|
)
|
|
127
|
-
:
|
|
128
|
+
: (
|
|
129
|
+
<HoverTip label={s.name} side="right" delayMs={500} className="dsgc-sess-name">
|
|
130
|
+
{s.name}
|
|
131
|
+
</HoverTip>
|
|
132
|
+
)}
|
|
128
133
|
<span className="dsgc-nodeops">
|
|
129
134
|
<button
|
|
130
135
|
className="dsgc-opbtn"
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { useState, type ReactNode } from 'react'
|
|
7
7
|
import { P } from '../lib/ui.ts'
|
|
8
8
|
import { firstLine, latestLine } from '../lib/model.ts'
|
|
9
|
+
import { ClipWell, Fold } from './Fold.tsx'
|
|
9
10
|
|
|
10
11
|
export function ThinkRow(props: { text: string, running?: boolean }): ReactNode {
|
|
11
12
|
const text = props.text || ''
|
|
@@ -24,9 +25,12 @@ export function ThinkRow(props: { text: string, running?: boolean }): ReactNode
|
|
|
24
25
|
expandOnRowClick
|
|
25
26
|
onToggle={() => { setExpanded((v) => !v) }}
|
|
26
27
|
collapsedContent={text ? <span className="dsgc-thinksummary">{summary}</span> : null}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
/>
|
|
29
|
+
<Fold open={expanded}>
|
|
30
|
+
<ClipWell maxHeight={320} watch={text}>
|
|
31
|
+
<div className="dsgc-thinkbody">{text}</div>
|
|
32
|
+
</ClipWell>
|
|
33
|
+
</Fold>
|
|
30
34
|
</div>
|
|
31
35
|
)
|
|
32
36
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { useState, type ReactNode } from 'react'
|
|
7
7
|
import { Icon, P, pickPrimitive } from '../lib/ui.ts'
|
|
8
8
|
import type { ToolCallView } from '../lib/model.ts'
|
|
9
|
+
import { ClipWell, Fold } from './Fold.tsx'
|
|
9
10
|
|
|
10
11
|
const TOOL_ICONS: Record<string, string> = { read_file: 'IconBrowseOutline16', list_dir: 'IconFolderOpenOutline16', run_command: 'IconCodeOutline16' }
|
|
11
12
|
|
|
@@ -43,9 +44,12 @@ export function ToolRow(props: { c: ToolCallView }): ReactNode {
|
|
|
43
44
|
{statusText + (dur ? ' · ' + dur : '')}
|
|
44
45
|
</span>
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
/>
|
|
48
|
+
<Fold open={expanded}>
|
|
49
|
+
<ClipWell maxHeight={260} watch={c.output}>
|
|
50
|
+
<div className="dsgc-toolbody">{c.output || '(无输出)'}</div>
|
|
51
|
+
</ClipWell>
|
|
52
|
+
</Fold>
|
|
49
53
|
</div>
|
|
50
54
|
)
|
|
51
55
|
}
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* @module dsh-group-chat/client/hooks
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useCallback, useEffect, type ClipboardEvent as ReactClipboardEvent, type DragEvent as ReactDragEvent, type KeyboardEvent as ReactKeyboardEvent } from 'react'
|
|
6
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, type ClipboardEvent as ReactClipboardEvent, type DragEvent as ReactDragEvent, type KeyboardEvent as ReactKeyboardEvent } from 'react'
|
|
7
7
|
import { execCommand, queryAtCaret, serializeInput, chipHtml, fileChipHtml } from '../utils/utils.ts'
|
|
8
|
+
import { readComposerDraft, writeComposerDraft } from '../lib/composer-draft.ts'
|
|
8
9
|
import type { AtToken } from '../../shared/file-mention-grammar.ts'
|
|
9
10
|
import type { SnapshotRole } from '../lib/model.ts'
|
|
10
11
|
|
|
@@ -44,12 +45,18 @@ export function useComposerInput(
|
|
|
44
45
|
setInput: (val: string) => void,
|
|
45
46
|
setMention: (val: AtToken | null) => void,
|
|
46
47
|
setMentionIdx: (val: number) => void,
|
|
48
|
+
sessionId?: string | null,
|
|
47
49
|
) {
|
|
48
|
-
|
|
50
|
+
const sessionIdRef = useRef(sessionId)
|
|
51
|
+
sessionIdRef.current = sessionId
|
|
52
|
+
|
|
53
|
+
/** 从 DOM 同步 input 状态(序列化),并写入当前会话草稿槽。 */
|
|
49
54
|
const syncFromDOM = useCallback((): void => {
|
|
50
55
|
const el = inputRef.current
|
|
51
56
|
if (!el) return
|
|
52
|
-
|
|
57
|
+
const text = serializeInput(el)
|
|
58
|
+
setInput(text)
|
|
59
|
+
writeComposerDraft(sessionIdRef.current, el.innerHTML, text)
|
|
53
60
|
}, [inputRef, setInput])
|
|
54
61
|
|
|
55
62
|
const onInputCE = useCallback((): void => {
|
|
@@ -84,6 +91,36 @@ export function useComposerInput(
|
|
|
84
91
|
}
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
/**
|
|
95
|
+
* 按会话恢复草稿:切会话时先把当前 HTML 写入旧槽,再灌入新槽;
|
|
96
|
+
* 面板重挂载(主会话⇄群聊)时 editor 是新节点,从模块缓存灌回。
|
|
97
|
+
*/
|
|
98
|
+
export function useComposerDraft(
|
|
99
|
+
sessionId: string | null | undefined,
|
|
100
|
+
inputRef: React.RefObject<HTMLDivElement>,
|
|
101
|
+
setInput: (val: string) => void,
|
|
102
|
+
setMention: (val: AtToken | null) => void,
|
|
103
|
+
): void {
|
|
104
|
+
const prevIdRef = useRef<string | null>(null)
|
|
105
|
+
useLayoutEffect(() => {
|
|
106
|
+
const el = inputRef.current
|
|
107
|
+
const prev = prevIdRef.current
|
|
108
|
+
const next = sessionId || null
|
|
109
|
+
if (el && prev && prev !== next) writeComposerDraft(prev, el.innerHTML, serializeInput(el))
|
|
110
|
+
prevIdRef.current = next
|
|
111
|
+
if (!next) return
|
|
112
|
+
const draft = readComposerDraft(next)
|
|
113
|
+
if (el) el.innerHTML = draft.html
|
|
114
|
+
setInput(draft.text)
|
|
115
|
+
setMention(null)
|
|
116
|
+
return () => {
|
|
117
|
+
const node = inputRef.current
|
|
118
|
+
const id = prevIdRef.current
|
|
119
|
+
if (node && id) writeComposerDraft(id, node.innerHTML, serializeInput(node))
|
|
120
|
+
}
|
|
121
|
+
}, [sessionId, inputRef, setInput, setMention])
|
|
122
|
+
}
|
|
123
|
+
|
|
87
124
|
export function useMentionChip(
|
|
88
125
|
inputRef: React.RefObject<HTMLDivElement>,
|
|
89
126
|
setMention: (val: AtToken | null) => void,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
7
7
|
import { api } from '../lib/api.ts'
|
|
8
|
+
import { readComposerDraft } from '../lib/composer-draft.ts'
|
|
8
9
|
import type { ClientSnapshot, ModelsResponse, RoleDraft } from '../lib/model.ts'
|
|
9
10
|
import type { AtToken } from '../../shared/file-mention-grammar.ts'
|
|
10
11
|
|
|
@@ -22,8 +23,8 @@ export interface ActionOk {
|
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* 面板切换缓存(模块级,跨挂载存活):main 为 keyed 槽,主会话⇄群聊切换会
|
|
25
|
-
*
|
|
26
|
-
*
|
|
26
|
+
* 整体卸载重挂面板组件;缓存最近快照、选中项与按会话分槽的未发送草稿,
|
|
27
|
+
* 让重挂载瞬时恢复上次视图。挂载后的 state 拉取 / SSE 首帧再行校正。
|
|
27
28
|
*/
|
|
28
29
|
let cachedSnap: ClientSnapshot | null = null
|
|
29
30
|
let cachedGid: string | null = null
|
|
@@ -84,8 +85,8 @@ export function useGroupChatState() {
|
|
|
84
85
|
const [partsSel, setPartsSel] = useState<string[] | null>(null)
|
|
85
86
|
const [rounds, setRounds] = useState(1)
|
|
86
87
|
|
|
87
|
-
// 输入与
|
|
88
|
-
const [input, setInput] = useState(
|
|
88
|
+
// 输入与 @提及(文本初值取当前选中会话草稿,HTML 由 useComposerDraft 灌回)
|
|
89
|
+
const [input, setInput] = useState(() => readComposerDraft(cachedSid).text)
|
|
89
90
|
const [mention, setMention] = useState<AtToken | null>(null)
|
|
90
91
|
const [mentionIdx, setMentionIdx] = useState(0)
|
|
91
92
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 未发送草稿:按会话 id 分槽,模块级跨挂载存活。
|
|
3
|
+
* 主会话⇄群聊会整页卸载 composer,contentEditable 的 HTML 必须自行记住。
|
|
4
|
+
* @module dsh-group-chat/client/composer-draft
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ComposerDraft {
|
|
8
|
+
html: string
|
|
9
|
+
text: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LIMIT = 32
|
|
13
|
+
const drafts = new Map<string, ComposerDraft>()
|
|
14
|
+
|
|
15
|
+
const empty = (): ComposerDraft => ({ html: '', text: '' })
|
|
16
|
+
|
|
17
|
+
/** 读指定会话草稿;命中则提到 LRU 最近端。 */
|
|
18
|
+
export function readComposerDraft(sessionId: string | null | undefined): ComposerDraft {
|
|
19
|
+
if (!sessionId) return empty()
|
|
20
|
+
const hit = drafts.get(sessionId)
|
|
21
|
+
if (!hit) return empty()
|
|
22
|
+
drafts.delete(sessionId)
|
|
23
|
+
drafts.set(sessionId, hit)
|
|
24
|
+
return hit
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 写入;空草稿删槽。超出上限淘汰最旧槽。 */
|
|
28
|
+
export function writeComposerDraft(sessionId: string | null | undefined, html: string, text: string): void {
|
|
29
|
+
if (!sessionId) return
|
|
30
|
+
drafts.delete(sessionId)
|
|
31
|
+
if (!html && !text) return
|
|
32
|
+
drafts.set(sessionId, { html, text })
|
|
33
|
+
if (drafts.size > LIMIT) {
|
|
34
|
+
const oldest = drafts.keys().next().value
|
|
35
|
+
if (oldest !== undefined) drafts.delete(oldest)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 发送成功或明确丢弃时摘槽。 */
|
|
40
|
+
export function clearComposerDraft(sessionId: string | null | undefined): void {
|
|
41
|
+
if (sessionId) drafts.delete(sessionId)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 测试用:清空全部槽位。 */
|
|
45
|
+
export function resetComposerDrafts(): void {
|
|
46
|
+
drafts.clear()
|
|
47
|
+
}
|
package/src/client/lib/styles.ts
CHANGED
|
@@ -62,7 +62,32 @@ export const CSS = [
|
|
|
62
62
|
'.dsgc-seambtn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.15));color:var(--dsw-alias-label-primary,inherit);border-color:var(--dsw-alias-border-l3,rgba(128,128,128,.4))}',
|
|
63
63
|
'.dsgc-seambtn.left{left:0;border-left:none;border-radius:0 8px 8px 0;box-shadow:3px 0 10px rgba(0,0,0,.07)}',
|
|
64
64
|
'.dsgc-seambtn.right{right:0;border-right:none;border-radius:8px 0 0 8px;box-shadow:-3px 0 10px rgba(0,0,0,.07)}',
|
|
65
|
-
'.dsgc-chathead{display:flex;
|
|
65
|
+
'.dsgc-chathead{display:flex;flex-direction:column;gap:6px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.2))}',
|
|
66
|
+
'.dsgc-chathead-row{display:flex;gap:10px;align-items:center;min-width:0}',
|
|
67
|
+
'.dsgc-constraints{align-self:center;box-sizing:border-box;width:76%;max-width:76%;display:flex;flex-direction:column;gap:8px;min-width:0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));background:var(--dsw-alias-bg-layer-3,rgba(128,128,128,.06));border-radius:12px;animation:dsgc-fade-in .18s ease-out}',
|
|
68
|
+
'.dsgc-chead{display:flex;flex-direction:column;gap:2px;min-width:0}',
|
|
69
|
+
'.dsgc-ctitle{margin:0;font:inherit;font-size:12px;font-weight:600;line-height:1.5;color:var(--dsw-alias-label-primary,inherit)}',
|
|
70
|
+
'.dsgc-cdesc{margin:0;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-tertiary,inherit)}',
|
|
71
|
+
'.dsgc-fold{display:grid;grid-template-rows:0fr;transition:grid-template-rows .16s ease-in}',
|
|
72
|
+
'.dsgc-fold.open{grid-template-rows:1fr;transition:grid-template-rows .22s cubic-bezier(.16,1,.3,1)}',
|
|
73
|
+
'.dsgc-fold-inner{min-height:0;overflow:hidden;display:flex;flex-direction:column;gap:2px;opacity:0;transition:opacity .12s ease-in}',
|
|
74
|
+
'.dsgc-fold.open .dsgc-fold-inner{opacity:1;transition:opacity .18s cubic-bezier(.16,1,.3,1)}',
|
|
75
|
+
'.dsgc-clip{--dsgc-clip-from:var(--dsw-alias-bg-layer-3,rgba(128,128,128,.18));position:relative;min-width:0}',
|
|
76
|
+
'.dsgc-clip::before,.dsgc-clip::after{content:"";position:absolute;left:0;right:0;height:14px;pointer-events:none;z-index:1;opacity:0;transition:opacity .12s}',
|
|
77
|
+
'.dsgc-clip::before{top:0;background:linear-gradient(to bottom,var(--dsgc-clip-from),transparent)}',
|
|
78
|
+
'.dsgc-clip::after{bottom:0;background:linear-gradient(to top,var(--dsgc-clip-from),transparent)}',
|
|
79
|
+
'.dsgc-clip.can-up::before,.dsgc-clip.can-down::after{opacity:1}',
|
|
80
|
+
'.dsgc-clip-scroll{min-width:0;overflow-y:auto;overscroll-behavior:contain;overflow-anchor:none;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-scrollbar-bg-l2,rgba(128,128,128,.35)) transparent}',
|
|
81
|
+
'.dsgc-clist{display:flex;flex-direction:column;gap:2px;min-width:0}',
|
|
82
|
+
'.dsgc-constraint{display:flex;gap:8px;align-items:flex-start;min-width:0;font-size:11.5px;line-height:1.5}',
|
|
83
|
+
'.dsgc-ckind{flex:none;font-weight:500}',
|
|
84
|
+
'.dsgc-ckind.decided{color:var(--dsw-alias-label-secondary,inherit)}',
|
|
85
|
+
'.dsgc-ckind.rejected{color:var(--dsw-alias-state-error-primary,#e5484d)}',
|
|
86
|
+
'.dsgc-ckind.open{color:var(--dsw-alias-label-tertiary,inherit)}',
|
|
87
|
+
'.dsgc-ctext{min-width:0;color:var(--dsw-alias-label-primary,inherit);overflow-wrap:break-word}',
|
|
88
|
+
'.dsgc-cmore{align-self:flex-start;border:none;background:none;padding:0;font:inherit;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-tertiary,inherit);cursor:pointer}',
|
|
89
|
+
'.dsgc-cmore:hover{color:var(--dsw-alias-label-secondary,inherit)}',
|
|
90
|
+
'.dsgc-cmore:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary,#4f6ef7));outline-offset:1px}',
|
|
66
91
|
'.dsgc-sess-title{flex:none;font-weight:600;font-size:14px;color:var(--dsw-alias-label-primary,inherit);max-width:30%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
67
92
|
'.dsgc-topic{flex:1;background:transparent;border:none;border-bottom:1px solid transparent;padding:2px;min-height:20px;color:var(--dsw-alias-label-secondary,inherit);font:inherit;outline:none;min-width:0;transition:color .12s,border-color .12s}',
|
|
68
93
|
'.dsgc-topic:hover{border-bottom-color:var(--dsw-alias-border-l2,rgba(128,128,128,.3))}',
|
|
@@ -99,7 +124,8 @@ export const CSS = [
|
|
|
99
124
|
'.dsgc-think .dsgc-thinkrow{font-size:12px;color:var(--dsw-alias-label-secondary,inherit);border-radius:8px;padding:3px 8px;transition:background-color .12s}',
|
|
100
125
|
'.dsgc-think .dsgc-thinkrow:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1))}',
|
|
101
126
|
'.dsgc-think .dsgc-thinksummary{color:var(--dsw-alias-label-tertiary,inherit);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:44ch;font-size:11.5px;margin-left:6px}',
|
|
102
|
-
'.dsgc-think .dsgc-
|
|
127
|
+
'.dsgc-think .dsgc-clip{margin:2px 0 4px;padding-left:10px;border-left:2px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25))}',
|
|
128
|
+
'.dsgc-think .dsgc-thinkbody{font-size:12.5px;line-height:1.7;color:var(--dsw-alias-label-secondary,inherit);white-space:pre-wrap;word-break:break-word}',
|
|
103
129
|
/* 首 delta 前的「思考中」占位行:ThinkRow 同语言(思考图标 + 次要色 12px 小字)
|
|
104
130
|
+ 三点交错呼吸动画——深度思考模型首字节可能等数秒到数十秒,空白气泡会被
|
|
105
131
|
感知为卡死;delta 到达后被真实思考行/正文自然替换 */
|
|
@@ -111,17 +137,42 @@ export const CSS = [
|
|
|
111
137
|
'.dsgc-pendingdots i:nth-child(3){animation-delay:.4s}',
|
|
112
138
|
'@keyframes dsgc-dot-breathe{0%,100%{opacity:.25}50%{opacity:1}}',
|
|
113
139
|
'.dsgc-sysmsg{align-self:center;font-size:11.5px;color:var(--dsw-alias-label-secondary,inherit);background:var(--dsw-alias-bg-module-platform,rgba(128,128,128,.1));border-radius:999px;padding:2px 10px;text-align:center;max-width:90%;animation:dsgc-sys-in .24s ease-out}',
|
|
140
|
+
/* 消息操作条:气泡外下方;角色左齐、自己右齐。占位常在,悬停显现;失败卡 always 常驻 */
|
|
141
|
+
'.dsgc-msgops{display:inline-flex;align-items:center;gap:2px;margin-top:2px;min-height:24px;visibility:hidden;opacity:0;transition:opacity .12s,visibility .12s}',
|
|
142
|
+
'.dsgc-msg:hover .dsgc-msgops,.dsgc-msg:focus-within .dsgc-msgops,.dsgc-msgops.always{visibility:visible;opacity:1}',
|
|
143
|
+
'@media (hover:none){.dsgc-msg .dsgc-msgops{visibility:visible;opacity:1}}',
|
|
144
|
+
'.dsgc-msg.mine .dsgc-msgops{justify-content:flex-end}',
|
|
145
|
+
'.dsgc-msgop{border:none;background:none;color:var(--dsw-alias-label-tertiary,inherit);cursor:pointer;padding:3px 6px;border-radius:6px;display:inline-flex;align-items:center;gap:4px;font:inherit;font-size:11.5px;line-height:16px;transition:background-color .12s,color .12s}',
|
|
146
|
+
'.dsgc-msgop svg{flex:none}',
|
|
147
|
+
'.dsgc-msgop:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.15));color:var(--dsw-alias-label-primary,inherit)}',
|
|
148
|
+
'.dsgc-msgop:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary,#4f6ef7));outline-offset:1px}',
|
|
149
|
+
'.dsgc-msgop.done{color:var(--dsw-alias-state-success-primary,#30a46c)}',
|
|
150
|
+
'.dsgc-msgop.retry{color:var(--dsw-alias-state-error-primary,#e5484d)}',
|
|
151
|
+
'.dsgc-msgop.retry:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 10%,transparent);color:var(--dsw-alias-state-error-primary,#e5484d)}',
|
|
152
|
+
'.dsgc-msgop:disabled{opacity:.4;cursor:default}',
|
|
153
|
+
/* 失败卡:角色消息形态,人话标题 + 可展开原文;不用左侧色条,避免 craft-floor 禁令 */
|
|
154
|
+
'.dsgc-fail{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 8%,var(--dsw-alias-bg-layer-3,rgba(128,128,128,.06)));border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 28%,var(--dsw-alias-border-l1,rgba(128,128,128,.15)));border-radius:12px;padding:10px 12px;display:flex;flex-direction:column;gap:6px;min-width:0;animation:dsgc-fade-in .18s ease-out}',
|
|
155
|
+
'.dsgc-failhead{display:flex;align-items:flex-start;gap:8px;min-width:0}',
|
|
156
|
+
'.dsgc-failicon{flex:none;width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-state-error-primary,#e5484d);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 12%,transparent);margin-top:1px}',
|
|
157
|
+
'.dsgc-failcopy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}',
|
|
158
|
+
'.dsgc-failtitle{font-size:13px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary,inherit)}',
|
|
159
|
+
'.dsgc-faildetail{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary,inherit);overflow-wrap:anywhere}',
|
|
160
|
+
'.dsgc-failmore{align-self:flex-start;border:none;background:none;padding:0;font:inherit;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-tertiary,inherit);cursor:pointer}',
|
|
161
|
+
'.dsgc-failmore:hover{color:var(--dsw-alias-label-secondary,inherit)}',
|
|
162
|
+
'.dsgc-failmore:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary,#4f6ef7));outline-offset:1px}',
|
|
163
|
+
'.dsgc-fail .dsgc-clip{--dsgc-clip-from:var(--dsw-alias-bg-layer-2,rgba(128,128,128,.12));border-radius:8px}',
|
|
164
|
+
'.dsgc-failraw{margin:0;font-size:11.5px;line-height:1.55;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,transparent);color:var(--dsw-alias-label-secondary,inherit)}',
|
|
114
165
|
/* ===== 工具调用行 / 确认卡片(对标思考折叠语言) ===== */
|
|
115
166
|
'.dsgc-tool{margin:2px 0}',
|
|
116
167
|
'.dsgc-tool .dsgc-toolrow{font-size:12px;color:var(--dsw-alias-label-secondary,inherit);border-radius:8px;padding:3px 8px;transition:background-color .12s}',
|
|
117
168
|
'.dsgc-tool .dsgc-toolrow:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1))}',
|
|
118
169
|
'.dsgc-tool .dsgc-toolsummary{color:var(--dsw-alias-label-tertiary,inherit);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:40ch;font-size:11.5px;margin-left:6px}',
|
|
119
|
-
'.dsgc-tool .dsgc-
|
|
170
|
+
'.dsgc-tool .dsgc-clip{margin:2px 0 4px;padding-left:10px;border-left:2px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25))}',
|
|
171
|
+
'.dsgc-tool .dsgc-toolbody{font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary,inherit);white-space:pre-wrap;word-break:break-word}',
|
|
120
172
|
'.dsgc-confirm{border:1px solid var(--dsw-alias-state-business-primary,#4f6ef7);border-radius:12px;background:var(--dsw-alias-bg-layer-3,rgba(128,128,128,.06));padding:10px 12px;display:flex;flex-direction:column;gap:8px;max-width:76%;animation:dsgc-fade-in .18s ease-out}',
|
|
121
173
|
'.dsgc-confirmtitle{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,inherit);display:flex;align-items:center;gap:6px}',
|
|
122
174
|
'.dsgc-confirmcmd{font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));border-radius:8px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2,transparent);color:var(--dsw-alias-label-primary,inherit);max-height:240px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-scrollbar-bg-l2,rgba(128,128,128,.35)) transparent}',
|
|
123
175
|
'.dsgc-confirmops{display:flex;gap:8px;justify-content:flex-end}',
|
|
124
|
-
'.dsgc-sysmsg.err{color:var(--dsw-alias-state-error-primary,#e5484d)}',
|
|
125
176
|
/* 清空确认弹窗内的说明列表 */
|
|
126
177
|
'.dsgc-clearnotes{margin:0;padding-left:18px;font-size:13px;line-height:1.8;color:var(--dsw-alias-label-secondary,inherit);display:flex;flex-direction:column;gap:2px}',
|
|
127
178
|
'.dsgc-err{font-size:12px;color:var(--dsw-alias-state-error-primary,#e5484d);animation:dsgc-fade-in .18s ease-out}',
|
|
@@ -152,8 +203,11 @@ export const CSS = [
|
|
|
152
203
|
'.dsgc-card .dsgc-sendrow{padding:0 12px 10px}',
|
|
153
204
|
'.dsgc-stopbtn{background:var(--dsw-alias-state-error-primary,#e5484d);border-color:transparent;color:#fff;font-weight:600}',
|
|
154
205
|
'.dsgc-stopbtn:hover:not(:disabled){filter:brightness(1.08);color:#fff}',
|
|
155
|
-
|
|
156
|
-
|
|
206
|
+
/* 标签固定在左;芯片在右侧独立换行,后续行与首行芯片左缘对齐(不折到「参与角色」下面)。
|
|
207
|
+
单行 24px;角色过多时随内容增高,避免固定高度把后续行溢出叠到下方输入卡上 */
|
|
208
|
+
'.dsgc-parts{display:flex;align-items:flex-start;gap:6px;min-height:24px}',
|
|
209
|
+
'.dsgc-partslabel{font-size:12px;line-height:24px;color:var(--dsw-alias-label-tertiary,inherit);flex:none}',
|
|
210
|
+
'.dsgc-partlist{display:flex;flex-wrap:wrap;align-items:center;gap:6px;row-gap:4px;flex:1;min-width:0}',
|
|
157
211
|
'.dsgc-partchip{display:inline-flex;gap:6px;align-items:center;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));background:transparent;color:var(--dsw-alias-label-secondary,inherit);border-radius:999px;padding:2px 10px;font-size:12px;line-height:1.5;cursor:pointer;transition:background-color .12s,color .12s,border-color .12s;font-family:inherit}',
|
|
158
212
|
'.dsgc-partchip:hover{color:var(--dsw-alias-label-primary,inherit)}',
|
|
159
213
|
'.dsgc-partchip.on{background:var(--dsw-alias-interactive-bg-active,rgba(128,128,128,.18));color:var(--dsw-alias-label-primary,inherit);border-color:var(--dsw-alias-border-l2,rgba(128,128,128,.3))}',
|
|
@@ -176,6 +230,12 @@ export const CSS = [
|
|
|
176
230
|
'.dsgc-rounds .dsgc-roundbtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.15));color:var(--dsw-alias-label-primary,inherit)}',
|
|
177
231
|
'.dsgc-rounds .dsgc-roundbtn:disabled{opacity:.4;cursor:default}',
|
|
178
232
|
'.dsgc-rounds .dsgc-roundnum{width:26px;text-align:center;font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary,inherit)}',
|
|
233
|
+
/* hover 气泡:对齐宿主 Tooltip.module.css,portal 到 body 以躲开 container-type */
|
|
234
|
+
'.dsgc-hovertip{position:fixed;z-index:100;width:max-content;max-width:50vw;padding:3px 7px;border-radius:8px;background:var(--dsw-alias-tooltip-bg,#1f1f1f);color:var(--dsw-static-neutral-bluish-00,#fff);font-size:13px;line-height:20px;white-space:pre-line;overflow-wrap:break-word;pointer-events:none;animation:dsgc-hovertip-in 150ms var(--ds-ease-in-out,ease)}',
|
|
235
|
+
'.dsgc-hovertip[data-side="right"]{transform:translateY(-50%)}',
|
|
236
|
+
'.dsgc-hovertip[data-side="top"]{transform:translate(-50%,-100%)}',
|
|
237
|
+
'.dsgc-hovertip[data-side="bottom"]{transform:translateX(-50%)}',
|
|
238
|
+
'@keyframes dsgc-hovertip-in{from{opacity:0}}',
|
|
179
239
|
/* ===== @成员 / @文件弹层 ===== */
|
|
180
240
|
'.dsgc-mentionwrap{position:relative}',
|
|
181
241
|
'.dsgc-mention{animation:dsgc-pop-in .16s cubic-bezier(.16,1,.3,1);position:absolute;left:0;right:0;bottom:calc(100% + 6px);background:var(--dsw-alias-bg-layer-3,#fff);border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));border-radius:10px;box-shadow:var(--dsw-shadow-lv3,0 8px 24px rgba(0,0,0,.18));max-height:220px;overflow-y:auto;padding:4px;z-index:30;display:flex;flex-direction:column;gap:2px;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-scrollbar-bg-l2,rgba(128,128,128,.35)) transparent}',
|
|
@@ -214,10 +274,12 @@ export const CSS = [
|
|
|
214
274
|
'.dsgc-rolename{font-weight:600;font-size:13px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary,inherit)}',
|
|
215
275
|
'.dsgc-role.off .dsgc-rolename{color:var(--dsw-alias-label-tertiary,inherit)}',
|
|
216
276
|
'.dsgc-rolepersona{font-size:11.5px;color:var(--dsw-alias-label-tertiary,inherit);line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:0}',
|
|
217
|
-
'.dsgc-
|
|
218
|
-
'.dsgc-
|
|
277
|
+
'.dsgc-rolemeta{display:flex;align-items:center;gap:6px;min-width:0;flex:1}',
|
|
278
|
+
'.dsgc-rolemodel{min-width:0;flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background:var(--dsw-alias-bg-module-platform,rgba(128,128,128,.12));border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;color:var(--dsw-alias-label-secondary,inherit)}',
|
|
279
|
+
'.dsgc-rolethink{display:inline-flex;align-items:center;flex:none;color:var(--dsw-alias-label-tertiary,inherit)}',
|
|
280
|
+
'.dsgc-roleops{display:flex;gap:2px;flex:none;margin-left:auto;opacity:0;transition:opacity .12s}',
|
|
219
281
|
'.dsgc-role:hover .dsgc-roleops,.dsgc-role:focus-within .dsgc-roleops{opacity:1}',
|
|
220
|
-
'.dsgc-rolemenu{display:flex;align-items:center;
|
|
282
|
+
'.dsgc-rolemenu{display:flex;align-items:center;gap:8px}',
|
|
221
283
|
/* ===== 表单与控件(角色抽屉内) ===== */
|
|
222
284
|
'.dsgc-field{display:flex;flex-direction:column;gap:5px}',
|
|
223
285
|
'.dsgc-field>label{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary,inherit)}',
|
|
@@ -239,7 +301,6 @@ export const CSS = [
|
|
|
239
301
|
'@keyframes dsgc-drawer-out{to{transform:translateX(32px);opacity:0}}',
|
|
240
302
|
'@keyframes dsgc-msg-in{from{opacity:0;transform:translateY(6px)}}',
|
|
241
303
|
'@keyframes dsgc-sys-in{from{opacity:0;transform:scale(.96)}}',
|
|
242
|
-
'@keyframes dsgc-think-in{from{opacity:0;transform:translateY(-3px)}}',
|
|
243
304
|
'@keyframes dsgc-pop-in{from{opacity:0;transform:translateY(4px)}}',
|
|
244
305
|
'@keyframes dsgc-rise-in{from{opacity:0;transform:translate(-50%,6px)}}',
|
|
245
306
|
'@keyframes dsgc-fade-in{from{opacity:0}}',
|
|
@@ -269,7 +330,7 @@ export const CSS = [
|
|
|
269
330
|
'.dsgc-partchip:focus-visible,.dsgc-mentionitem:focus-visible,.dsgc-dot:focus-visible,.dsgc-opbtn:focus-visible,.dsgc-twist:focus-visible,.dsgc-addsess:focus-visible,.dsgc-grow-row:focus-visible,.dsgc-sess-row:focus-visible,.dsgc-rename:focus-visible,.dsgc-fbrow:focus-visible,.dsgc-role:focus-visible,.dsgc-roundbtn:focus-visible,.dsgc-tobtn:focus-visible,.dsgc-seambtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary,#4f6ef7));outline-offset:1px}',
|
|
270
331
|
/* ===== 窄容器:右栏覆盖式呈现 ===== */
|
|
271
332
|
'@container (max-width: 880px){.dsgc-aside{position:absolute;top:0;right:0;bottom:0;z-index:15;width:min(304px,88%);box-shadow:-12px 0 32px rgba(0,0,0,.16);border-left:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));transition:transform .3s cubic-bezier(.16,1,.3,1),opacity .2s ease}.dsgc-aside.closed{width:min(304px,88%);padding-left:12px;padding-right:12px;border-left-width:1px;transform:translateX(calc(100% + 14px));opacity:0;transition:transform .26s ease-in,opacity .18s ease-in,visibility 0s .24s}.dsgc-seambtn.right:not(.closed){right:min(304px,88%)}}',
|
|
272
|
-
'@container (max-width: 640px){.dsgc-nav{width:200px}.dsgc-msgbody{max-width:88%}}',
|
|
333
|
+
'@container (max-width: 640px){.dsgc-nav{width:200px}.dsgc-msgbody{max-width:88%}.dsgc-constraints{width:88%;max-width:88%}}',
|
|
273
334
|
/* ===== 侧边栏入口对齐(外壳将插槽内容包在 panelGlyph span 内,必须用后代 :has) ===== */
|
|
274
335
|
'[class*="panelList"]:has(.dsgc-entryOverlay){margin-top:4px}',
|
|
275
336
|
'[class*="panelRow"]:has(.dsgc-entryOverlay){position:relative;box-sizing:border-box;height:36px;min-height:36px;padding:0 10px;font-size:13px;transition:background-color .12s,color .12s}',
|
|
@@ -292,7 +353,7 @@ export const CSS = [
|
|
|
292
353
|
'.dsgc-entryIcon svg{width:18px;height:18px;display:block}',
|
|
293
354
|
'[data-sidebar-collapsed] .dsgc-entryOverlay,[class*="collapsed"] .dsgc-entryOverlay{justify-content:center;padding:0}',
|
|
294
355
|
'[data-sidebar-collapsed] [class*="panelRow"]:has(.dsgc-entryOverlay),[class*="collapsed"] [class*="panelRow"]:has(.dsgc-entryOverlay){margin:0 auto}',
|
|
295
|
-
'@media (prefers-reduced-motion:reduce){.dsgc-dot,.dsgc-partchip,.dsgc-mentionitem,.dsgc-grow-row,.dsgc-sess-row,.dsgc-opbtn,.dsgc-addsess,.dsgc-topic,.dsgc-role,.dsgc-rename,.dsgc-input,.dsgc-select,.dsgc-textarea,.dsgc-fbrow,.dsgc-twist svg,.dsgc-roleops,.dsgc-roundbtn,.dsgc-permtrigger,.dsgc-permchevron,.dsgc-tobtn,.dsgc-seambtn,.dsgc-aside,.dsgc-aside.closed,.dsgc-nav,.dsgc-nav.closed,.dsgc-clearbtn,[class*="panelRow"]:has(.dsgc-entryOverlay){transition:none}.dsgc-dot:hover{transform:none}.dsgc-drawer,.dsgc-msg,.dsgc-sysmsg,.dsgc-mention,.dsgc-tobottom,.dsgc-err,.dsgc-empty,.dsgc-fb,.dsgc-
|
|
356
|
+
'@media (prefers-reduced-motion:reduce){.dsgc-dot,.dsgc-partchip,.dsgc-mentionitem,.dsgc-grow-row,.dsgc-sess-row,.dsgc-opbtn,.dsgc-addsess,.dsgc-topic,.dsgc-role,.dsgc-rename,.dsgc-input,.dsgc-select,.dsgc-textarea,.dsgc-fbrow,.dsgc-twist svg,.dsgc-roleops,.dsgc-roundbtn,.dsgc-permtrigger,.dsgc-permchevron,.dsgc-tobtn,.dsgc-seambtn,.dsgc-aside,.dsgc-aside.closed,.dsgc-nav,.dsgc-nav.closed,.dsgc-clearbtn,.dsgc-cmore,.dsgc-msgops,.dsgc-msgop,.dsgc-failmore,.dsgc-clip::before,.dsgc-clip::after,.dsgc-fold,.dsgc-fold-inner,.dsgc-fold.open,.dsgc-fold.open .dsgc-fold-inner,[class*="panelRow"]:has(.dsgc-entryOverlay){transition:none}.dsgc-dot:hover{transform:none}.dsgc-drawer,.dsgc-msg,.dsgc-sysmsg,.dsgc-mention,.dsgc-tobottom,.dsgc-err,.dsgc-empty,.dsgc-fb,.dsgc-msg.live .dsgc-avatar,.dsgc-loading svg,.dsgc-pendingdots i,.dsgc-typing,.dsgc-constraints,.dsgc-hovertip,.dsgc-fail{animation:none}.dsgc-typing{background-position:0 0;background-size:100% 100%}}',
|
|
296
357
|
/* ===== 设置页(同一卡片语言) ===== */
|
|
297
358
|
'.dgcs-page{display:flex;flex-direction:column;gap:14px;padding:4px 0}',
|
|
298
359
|
'.dgcs-head{display:flex;flex-direction:column;gap:6px}',
|