@jcy2387/dsh-conversation-ui 0.1.0-rc.12
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.en.md +123 -0
- package/README.md +123 -0
- package/conversation-ui-off.yml +3 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +3672 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +302 -0
- package/package.json +182 -0
- package/src/boot-config.ts +28 -0
- package/src/client/AnimatedDisclosure.tsx +89 -0
- package/src/client/ConversationCard.module.css +253 -0
- package/src/client/ConversationCard.tsx +122 -0
- package/src/client/DeliverablesCard.module.css +187 -0
- package/src/client/DeliverablesCard.tsx +142 -0
- package/src/client/FollowHost.tsx +25 -0
- package/src/client/ToolActivityGroup.tsx +341 -0
- package/src/client/TurnPreludeUserNodeView.tsx +258 -0
- package/src/client/TurnProcessMember.tsx +92 -0
- package/src/client/TypewriterAssistantNodeView.module.css +1260 -0
- package/src/client/TypewriterAssistantNodeView.tsx +390 -0
- package/src/client/TypewriterToolNodeView.tsx +916 -0
- package/src/client/conversation-ui-card-controller.ts +162 -0
- package/src/client/conversation-ui-settings-api.ts +65 -0
- package/src/client/deliverables.ts +263 -0
- package/src/client/index.ts +242 -0
- package/src/client/locales.ts +74 -0
- package/src/client/teleprompterGlide.ts +590 -0
- package/src/client/turnElapsed.ts +27 -0
- package/src/client/turnProcessFold.ts +224 -0
- package/src/client/useConversationContent.ts +471 -0
- package/src/client/useFpsGuard.ts +84 -0
- package/src/config.ts +44 -0
- package/src/css-modules.d.ts +9 -0
- package/src/index.ts +2 -0
- package/src/package-meta.ts +39 -0
- package/src/plugin.ts +155 -0
- package/src/profile-installation.ts +121 -0
- package/src/settings-api.ts +33 -0
- package/src/settings.ts +30 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/* Mirrors the shipped PluginCard chrome without importing an internal module
|
|
2
|
+
from another client package. */
|
|
3
|
+
|
|
4
|
+
.card {
|
|
5
|
+
list-style: none;
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
9
|
+
border-radius: 12px;
|
|
10
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
11
|
+
transition: border-color .16s, background .16s;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.card:hover {
|
|
15
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.cardOpen {
|
|
19
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
20
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.header {
|
|
24
|
+
appearance: none;
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: 12px;
|
|
28
|
+
width: 100%;
|
|
29
|
+
padding: 14px 16px;
|
|
30
|
+
border: 0;
|
|
31
|
+
border-radius: 12px;
|
|
32
|
+
background: none;
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
text-align: left;
|
|
35
|
+
font: inherit;
|
|
36
|
+
color: inherit;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.header:focus-visible {
|
|
40
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
41
|
+
outline-offset: -2px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.headText {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
gap: 4px;
|
|
48
|
+
min-width: 0;
|
|
49
|
+
flex: 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.name {
|
|
53
|
+
font-weight: 600;
|
|
54
|
+
font-size: 15px;
|
|
55
|
+
line-height: 1.4;
|
|
56
|
+
color: var(--dsw-alias-label-primary);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.description {
|
|
60
|
+
font-size: 13px;
|
|
61
|
+
line-height: 1.5;
|
|
62
|
+
color: var(--dsw-alias-label-tertiary);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.version {
|
|
66
|
+
flex: none;
|
|
67
|
+
max-width: 16rem;
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
text-overflow: ellipsis;
|
|
70
|
+
white-space: nowrap;
|
|
71
|
+
font-size: 12px;
|
|
72
|
+
line-height: 1.5;
|
|
73
|
+
color: var(--dsw-alias-label-tertiary);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.pending {
|
|
77
|
+
flex: none;
|
|
78
|
+
padding: 1px 8px;
|
|
79
|
+
border-radius: 999px;
|
|
80
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
81
|
+
color: var(--dsw-alias-label-secondary);
|
|
82
|
+
font-size: 11px;
|
|
83
|
+
font-weight: 500;
|
|
84
|
+
line-height: 17px;
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.chevron {
|
|
89
|
+
flex: none;
|
|
90
|
+
color: var(--dsw-alias-label-tertiary);
|
|
91
|
+
transition: transform .16s;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.chevronOpen {
|
|
95
|
+
transform: rotate(180deg);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.body {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
margin: 0 16px;
|
|
102
|
+
padding-bottom: 8px;
|
|
103
|
+
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.readOnly {
|
|
107
|
+
margin: 12px 0 0;
|
|
108
|
+
font-size: 12px;
|
|
109
|
+
line-height: 1.5;
|
|
110
|
+
color: var(--dsw-alias-label-tertiary);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.field {
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
gap: 6px;
|
|
117
|
+
padding: 12px 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.fieldHead {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: space-between;
|
|
124
|
+
gap: 8px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.label {
|
|
128
|
+
flex: 1;
|
|
129
|
+
min-width: 0;
|
|
130
|
+
font-size: 13px;
|
|
131
|
+
font-weight: 500;
|
|
132
|
+
line-height: 1.5;
|
|
133
|
+
color: var(--dsw-alias-label-primary);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.toggle {
|
|
137
|
+
width: 16px;
|
|
138
|
+
height: 16px;
|
|
139
|
+
accent-color: var(--dsw-alias-brand-primary);
|
|
140
|
+
cursor: pointer;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.toggle:disabled {
|
|
144
|
+
cursor: default;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.toggle:focus-visible {
|
|
148
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
149
|
+
outline-offset: 2px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.hint {
|
|
153
|
+
margin: 0;
|
|
154
|
+
font-size: 12px;
|
|
155
|
+
line-height: 1.5;
|
|
156
|
+
color: var(--dsw-alias-label-tertiary);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.failure {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: space-between;
|
|
163
|
+
gap: 12px;
|
|
164
|
+
padding: 12px 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.updateRow {
|
|
168
|
+
display: flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
justify-content: space-between;
|
|
171
|
+
gap: 12px;
|
|
172
|
+
padding: 12px 0;
|
|
173
|
+
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.updateCopy {
|
|
177
|
+
display: flex;
|
|
178
|
+
flex-direction: column;
|
|
179
|
+
min-width: 0;
|
|
180
|
+
gap: 4px;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.footer {
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
justify-content: flex-end;
|
|
187
|
+
gap: 8px;
|
|
188
|
+
padding: 12px 0 4px;
|
|
189
|
+
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.failed {
|
|
193
|
+
flex: 1;
|
|
194
|
+
min-width: 0;
|
|
195
|
+
margin: 0;
|
|
196
|
+
font-size: 12px;
|
|
197
|
+
line-height: 1.5;
|
|
198
|
+
color: var(--dsw-alias-label-error);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.save,
|
|
202
|
+
.discard,
|
|
203
|
+
.update {
|
|
204
|
+
appearance: none;
|
|
205
|
+
border: 1px solid transparent;
|
|
206
|
+
border-radius: 8px;
|
|
207
|
+
font: inherit;
|
|
208
|
+
font-size: 13px;
|
|
209
|
+
line-height: 1.5;
|
|
210
|
+
padding: 5px 14px;
|
|
211
|
+
cursor: pointer;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.save {
|
|
215
|
+
background: var(--dsw-alias-label-primary);
|
|
216
|
+
color: var(--dsw-alias-bg-layer-3);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.discard {
|
|
220
|
+
border-color: var(--dsw-alias-border-l2);
|
|
221
|
+
background: none;
|
|
222
|
+
color: var(--dsw-alias-label-secondary);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.discard:hover:not(:disabled),
|
|
226
|
+
.update:hover:not(:disabled) {
|
|
227
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
228
|
+
color: var(--dsw-alias-label-primary);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.update {
|
|
232
|
+
display: inline-flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
flex: none;
|
|
235
|
+
gap: 6px;
|
|
236
|
+
border-color: var(--dsw-alias-border-l2);
|
|
237
|
+
background: none;
|
|
238
|
+
color: var(--dsw-alias-label-secondary);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.discard:disabled,
|
|
242
|
+
.save:disabled,
|
|
243
|
+
.update:disabled {
|
|
244
|
+
opacity: 0.4;
|
|
245
|
+
cursor: default;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.discard:focus-visible,
|
|
249
|
+
.save:focus-visible,
|
|
250
|
+
.update:focus-visible {
|
|
251
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
252
|
+
outline-offset: 1px;
|
|
253
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conversation-ui plugin configuration card, rendered inside the Web
|
|
3
|
+
* Settings "plugin configuration" page. One toggle, staged until the user
|
|
4
|
+
* saves — the same shape as the Host-shipped cards, hand-drawn because the
|
|
5
|
+
* Host cards' chrome is not exported for reuse.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState } from 'react'
|
|
9
|
+
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
10
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
|
|
11
|
+
import { IconChevronDownOutline14, IconRefreshOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
12
|
+
import type { ConversationCardFace } from './conversation-ui-card-controller.ts'
|
|
13
|
+
import css from './ConversationCard.module.css'
|
|
14
|
+
|
|
15
|
+
/** Props the renderer binds for the conversation-ui card. */
|
|
16
|
+
export type ConversationCardProps =
|
|
17
|
+
PropsRuntime<'settings.plugin.item'>
|
|
18
|
+
& PropsLocale<'settings.conversationUi'>
|
|
19
|
+
& InjectFace<ConversationCardFace>
|
|
20
|
+
|
|
21
|
+
/** Render the conversation-ui card independently of the core settings namespace allowlist. */
|
|
22
|
+
export function ConversationCard(props: ConversationCardProps) {
|
|
23
|
+
const { t } = props
|
|
24
|
+
const [open, setOpen] = useState(false)
|
|
25
|
+
const state = props.useConversationUiCard(snapshot => snapshot)
|
|
26
|
+
const blocked = !state.dirty || state.saving || state.status !== 'ready'
|
|
27
|
+
const versionLabel = state.version === undefined
|
|
28
|
+
? null
|
|
29
|
+
: t(state.installation === 'development' ? 'developmentVersion' : 'version')
|
|
30
|
+
.replace('{version}', state.version)
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<li className={open ? `${css.card} ${css.cardOpen}` : css.card}>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
className={css.header}
|
|
37
|
+
aria-expanded={open}
|
|
38
|
+
onClick={() => { setOpen(!open) }}
|
|
39
|
+
>
|
|
40
|
+
<span className={css.headText}>
|
|
41
|
+
<span className={css.name}>{t('title')}</span>
|
|
42
|
+
<span className={css.description}>{t('description')}</span>
|
|
43
|
+
</span>
|
|
44
|
+
{versionLabel === null ? null : <span className={css.version}>{versionLabel}</span>}
|
|
45
|
+
{state.dirty ? <span className={css.pending}>{t('unsaved')}</span> : null}
|
|
46
|
+
<IconChevronDownOutline14 className={open ? `${css.chevron} ${css.chevronOpen}` : css.chevron} />
|
|
47
|
+
</button>
|
|
48
|
+
{open
|
|
49
|
+
? (
|
|
50
|
+
<div className={css.body}>
|
|
51
|
+
{state.status === 'loading' ? <p className={css.readOnly} role="status">{t('loading')}</p> : null}
|
|
52
|
+
{state.status === 'unavailable' ? (
|
|
53
|
+
<div className={css.failure}>
|
|
54
|
+
<p className={css.readOnly} role="status">{t('unavailable')}</p>
|
|
55
|
+
<button type="button" className={css.discard} onClick={props.reload}>{t('retry')}</button>
|
|
56
|
+
</div>
|
|
57
|
+
) : null}
|
|
58
|
+
{state.status === 'ready' ? (
|
|
59
|
+
<>
|
|
60
|
+
{!state.writable ? <p className={css.readOnly} role="status">{t('readOnly')}</p> : null}
|
|
61
|
+
<label className={css.field}>
|
|
62
|
+
<span className={css.fieldHead}>
|
|
63
|
+
<span className={css.label}>{t('thinkAutoExpand')}</span>
|
|
64
|
+
<input
|
|
65
|
+
type="checkbox"
|
|
66
|
+
className={css.toggle}
|
|
67
|
+
checked={state.thinkAutoExpand}
|
|
68
|
+
disabled={!state.writable}
|
|
69
|
+
onChange={(event) => { props.edit(event.target.checked) }}
|
|
70
|
+
/>
|
|
71
|
+
</span>
|
|
72
|
+
<span className={css.hint}>{t('thinkAutoExpandHint')}</span>
|
|
73
|
+
</label>
|
|
74
|
+
<div className={css.updateRow}>
|
|
75
|
+
<span className={css.updateCopy}>
|
|
76
|
+
<span className={css.label}>{t('updates')}</span>
|
|
77
|
+
<span className={css.hint}>
|
|
78
|
+
{state.restartRequired
|
|
79
|
+
? t('restartRequired')
|
|
80
|
+
: state.installation === 'npm' ? t('updateHint')
|
|
81
|
+
: state.installation === 'development' ? t('developmentBuild') : t('updateUnavailable')}
|
|
82
|
+
</span>
|
|
83
|
+
</span>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
className={css.update}
|
|
87
|
+
disabled={!state.canUpgrade || state.upgrading || state.restartRequired}
|
|
88
|
+
title={state.canUpgrade ? undefined : t('updateUnavailable')}
|
|
89
|
+
onClick={props.upgrade}
|
|
90
|
+
>
|
|
91
|
+
<span aria-hidden="true"><IconRefreshOutline14 /></span>
|
|
92
|
+
{t(state.upgrading ? 'updating' : 'update')}
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
{state.upgradeFailed ? <p className={css.failed} role="status">{t('updateFailed')}</p> : null}
|
|
96
|
+
<div className={css.footer}>
|
|
97
|
+
{state.failed ? <p className={css.failed} role="status">{t('saveFailed')}</p> : null}
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
className={css.discard}
|
|
101
|
+
disabled={!state.dirty || state.saving}
|
|
102
|
+
onClick={props.discard}
|
|
103
|
+
>
|
|
104
|
+
{t('discard')}
|
|
105
|
+
</button>
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
className={css.save}
|
|
109
|
+
disabled={blocked}
|
|
110
|
+
onClick={props.save}
|
|
111
|
+
>
|
|
112
|
+
{t(state.saving ? 'saving' : 'save')}
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
</>
|
|
116
|
+
) : null}
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
: null}
|
|
120
|
+
</li>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
:global([data-turn-tail]:has([data-stream-deliverables-card]) > *:has([data-produced-files-row])) {
|
|
2
|
+
display: none !important;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.root {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: 0;
|
|
9
|
+
margin-top: 16px;
|
|
10
|
+
padding: 12px 12px 10px;
|
|
11
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
12
|
+
border-radius: 12px;
|
|
13
|
+
background: var(--dsw-alias-bg-primary);
|
|
14
|
+
color: var(--dsw-alias-label-secondary);
|
|
15
|
+
font-size: 14px;
|
|
16
|
+
line-height: 22px;
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.header {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: flex-start;
|
|
23
|
+
gap: 12px;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
padding: 4px 8px 12px;
|
|
26
|
+
border-bottom: 1px solid var(--dsw-alias-border-l2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.headerIcon {
|
|
30
|
+
display: inline-flex;
|
|
31
|
+
width: 40px;
|
|
32
|
+
height: 40px;
|
|
33
|
+
flex: none;
|
|
34
|
+
align-items: center;
|
|
35
|
+
justify-content: center;
|
|
36
|
+
border-radius: 10px;
|
|
37
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
38
|
+
color: var(--dsw-alias-label-secondary);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.heading {
|
|
42
|
+
display: flex;
|
|
43
|
+
min-width: 0;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
gap: 1px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.title {
|
|
49
|
+
color: var(--dsw-alias-label-primary);
|
|
50
|
+
font-size: 16px;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
line-height: 24px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.subtitle {
|
|
56
|
+
margin: 0;
|
|
57
|
+
padding: 0;
|
|
58
|
+
border: 0;
|
|
59
|
+
background: transparent;
|
|
60
|
+
color: var(--dsw-alias-label-tertiary);
|
|
61
|
+
font: inherit;
|
|
62
|
+
font-size: 14px;
|
|
63
|
+
line-height: 20px;
|
|
64
|
+
text-align: start;
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.subtitle:hover {
|
|
69
|
+
color: var(--dsw-alias-label-secondary);
|
|
70
|
+
text-decoration: underline;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.list {
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
min-width: 0;
|
|
77
|
+
padding: 10px 8px 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.item {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
width: 100%;
|
|
84
|
+
min-width: 0;
|
|
85
|
+
min-height: 28px;
|
|
86
|
+
gap: 0;
|
|
87
|
+
margin: 0;
|
|
88
|
+
padding: 2px 0;
|
|
89
|
+
border: 0;
|
|
90
|
+
border-radius: 5px;
|
|
91
|
+
background: transparent;
|
|
92
|
+
color: inherit;
|
|
93
|
+
font: inherit;
|
|
94
|
+
line-height: 22px;
|
|
95
|
+
text-align: start;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.item:hover {
|
|
100
|
+
color: var(--dsw-alias-label-primary);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.item:focus-visible,
|
|
104
|
+
.more:focus-visible,
|
|
105
|
+
.subtitle:focus-visible {
|
|
106
|
+
outline: 2px solid var(--dsw-alias-border-l3);
|
|
107
|
+
outline-offset: 1px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.filePath,
|
|
111
|
+
.websitePath {
|
|
112
|
+
display: flex;
|
|
113
|
+
min-width: 0;
|
|
114
|
+
flex: 1 1 auto;
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.parentPath {
|
|
120
|
+
min-width: 0;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
color: var(--dsw-alias-label-tertiary);
|
|
123
|
+
text-overflow: ellipsis;
|
|
124
|
+
white-space: nowrap;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.fileName {
|
|
128
|
+
flex: none;
|
|
129
|
+
color: var(--dsw-alias-label-secondary);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.websitePath {
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
color: var(--dsw-alias-label-secondary);
|
|
135
|
+
text-overflow: ellipsis;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.stats {
|
|
139
|
+
display: inline-flex;
|
|
140
|
+
flex: none;
|
|
141
|
+
gap: 6px;
|
|
142
|
+
margin-inline-start: 8px;
|
|
143
|
+
font-size: 13px;
|
|
144
|
+
line-height: 22px;
|
|
145
|
+
white-space: nowrap;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.added {
|
|
149
|
+
color: var(--dsw-alias-label-success, #2f9e44);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.removed {
|
|
153
|
+
color: var(--dsw-alias-label-danger, #d94841);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.external {
|
|
157
|
+
flex: none;
|
|
158
|
+
color: var(--dsw-alias-label-tertiary);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.more {
|
|
162
|
+
display: inline-flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
align-self: flex-start;
|
|
165
|
+
gap: 5px;
|
|
166
|
+
margin: 8px 8px 0;
|
|
167
|
+
padding: 0;
|
|
168
|
+
border: 0;
|
|
169
|
+
background: transparent;
|
|
170
|
+
color: var(--dsw-alias-label-secondary);
|
|
171
|
+
font: inherit;
|
|
172
|
+
line-height: 22px;
|
|
173
|
+
cursor: pointer;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.more:hover {
|
|
177
|
+
color: var(--dsw-alias-label-primary);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.moreIcon {
|
|
181
|
+
display: inline-flex;
|
|
182
|
+
width: 14px;
|
|
183
|
+
height: 14px;
|
|
184
|
+
align-items: center;
|
|
185
|
+
justify-content: center;
|
|
186
|
+
flex: none;
|
|
187
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { useLayoutEffect, useState } from 'react'
|
|
2
|
+
import type { TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
3
|
+
import {
|
|
4
|
+
IconChevronDownOutline14,
|
|
5
|
+
IconChevronUpOutline14,
|
|
6
|
+
IconGlobeOutline14,
|
|
7
|
+
IconProjectAddOutline16,
|
|
8
|
+
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
9
|
+
import type { DeliverableEntry } from './deliverables.ts'
|
|
10
|
+
import css from './DeliverablesCard.module.css'
|
|
11
|
+
|
|
12
|
+
export interface DeliverablesCardProps extends TurnTailOwnerProps {
|
|
13
|
+
readonly matched: readonly DeliverableEntry[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isWebsite(entry: DeliverableEntry): boolean {
|
|
17
|
+
return entry.kind === 'website' || /^https?:\/\//i.test(entry.path)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function openWebsite(path: string): void {
|
|
21
|
+
if (typeof window !== 'undefined') window.open(path, '_blank', 'noopener,noreferrer')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pathParts(path: string): { parent: string; name: string } {
|
|
25
|
+
const separator = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
|
|
26
|
+
if (separator < 0) return { parent: '', name: path }
|
|
27
|
+
return { parent: path.slice(0, separator + 1), name: path.slice(separator + 1) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hiddenKindLabel(entries: readonly DeliverableEntry[]): string {
|
|
31
|
+
if (entries.every(isWebsite)) return '网站'
|
|
32
|
+
if (entries.every(entry => !isWebsite(entry))) return '文件'
|
|
33
|
+
return '产物'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function countLabel(entries: readonly DeliverableEntry[]): string {
|
|
37
|
+
const files = entries.filter(entry => !isWebsite(entry)).length
|
|
38
|
+
const websites = entries.length - files
|
|
39
|
+
if (files > 0 && websites === 0) return `已编辑 ${files} 个文件`
|
|
40
|
+
if (files === 0) return `交付 ${websites} 个网站`
|
|
41
|
+
return `已交付 ${entries.length} 项产物`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface EditStats {
|
|
45
|
+
readonly added: number
|
|
46
|
+
readonly removed: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function collectEditStats(turn: number): ReadonlyMap<string, EditStats> {
|
|
50
|
+
const result = new Map<string, EditStats>()
|
|
51
|
+
if (typeof document === 'undefined') return result
|
|
52
|
+
for (const element of document.querySelectorAll<HTMLElement>('[data-stream-edit-stats]')) {
|
|
53
|
+
const row = element.closest<HTMLElement>('[data-stream-turn]')
|
|
54
|
+
if (row?.getAttribute('data-stream-turn') !== String(turn)) continue
|
|
55
|
+
const path = element.getAttribute('data-stream-edit-file')
|
|
56
|
+
const match = /\+(\d+)\s+-\s*(\d+)/.exec(element.textContent ?? '')
|
|
57
|
+
if (path === null || match === null) continue
|
|
58
|
+
result.set(path, { added: Number(match[1]), removed: Number(match[2]) })
|
|
59
|
+
}
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Codex-style turn-tail card for edited files and deployed websites. */
|
|
64
|
+
export function DeliverablesCard({ matched, turn, openFile }: DeliverablesCardProps) {
|
|
65
|
+
const [expanded, setExpanded] = useState(false)
|
|
66
|
+
const [domStats, setDomStats] = useState<ReadonlyMap<string, EditStats>>(() => new Map())
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
setDomStats(collectEditStats(turn.turn))
|
|
69
|
+
}, [turn.turn, matched])
|
|
70
|
+
const entries = matched.map(entry => {
|
|
71
|
+
const stats = domStats.get(entry.path)
|
|
72
|
+
return stats === undefined || entry.added > 0 || entry.removed > 0 ? entry : { ...entry, ...stats }
|
|
73
|
+
})
|
|
74
|
+
const visible = expanded ? entries : entries.slice(0, 3)
|
|
75
|
+
const hidden = entries.length - visible.length
|
|
76
|
+
const summary = countLabel(entries)
|
|
77
|
+
const hasFiles = entries.some(entry => !isWebsite(entry))
|
|
78
|
+
const firstWebsite = entries.find(isWebsite)
|
|
79
|
+
return (
|
|
80
|
+
<section className={css.root} data-stream-deliverables-card data-stream-deliverables-count={entries.length}>
|
|
81
|
+
<div className={css.header}>
|
|
82
|
+
<span className={css.headerIcon} aria-hidden>
|
|
83
|
+
{hasFiles ? <IconProjectAddOutline16 size={20} /> : <IconGlobeOutline14 size={20} />}
|
|
84
|
+
</span>
|
|
85
|
+
<div className={css.heading}>
|
|
86
|
+
<strong className={css.title}>{summary}</strong>
|
|
87
|
+
{hasFiles ? (
|
|
88
|
+
<button type="button" className={css.subtitle} onClick={() => { setExpanded(true) }}>
|
|
89
|
+
查看更改 ↗
|
|
90
|
+
</button>
|
|
91
|
+
) : firstWebsite !== undefined ? (
|
|
92
|
+
<button type="button" className={css.subtitle} onClick={() => { openWebsite(firstWebsite.path) }}>
|
|
93
|
+
打开网站 ↗
|
|
94
|
+
</button>
|
|
95
|
+
) : null}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<div className={css.list}>
|
|
99
|
+
{visible.map(entry => {
|
|
100
|
+
const website = isWebsite(entry)
|
|
101
|
+
const parts = pathParts(entry.path)
|
|
102
|
+
return (
|
|
103
|
+
<button
|
|
104
|
+
key={`${entry.kind}:${entry.path}`}
|
|
105
|
+
type="button"
|
|
106
|
+
className={css.item}
|
|
107
|
+
title={entry.path}
|
|
108
|
+
onClick={() => { website ? openWebsite(entry.path) : openFile(entry.path) }}
|
|
109
|
+
>
|
|
110
|
+
{website ? (
|
|
111
|
+
<span className={css.websitePath}>{entry.path}</span>
|
|
112
|
+
) : (
|
|
113
|
+
<span className={css.filePath}>
|
|
114
|
+
{parts.parent !== '' && <span className={css.parentPath}>{parts.parent}</span>}
|
|
115
|
+
<span className={css.fileName}>{parts.name || entry.path}</span>
|
|
116
|
+
</span>
|
|
117
|
+
)}
|
|
118
|
+
{!website && (entry.added > 0 || entry.removed > 0) && (
|
|
119
|
+
<span className={css.stats}>
|
|
120
|
+
{entry.added > 0 && <span className={css.added}>+{entry.added}</span>}
|
|
121
|
+
{entry.removed > 0 && <span className={css.removed}>-{entry.removed}</span>}
|
|
122
|
+
</span>
|
|
123
|
+
)}
|
|
124
|
+
{website && <span className={css.external} aria-hidden>↗</span>}
|
|
125
|
+
</button>
|
|
126
|
+
)
|
|
127
|
+
})}
|
|
128
|
+
</div>
|
|
129
|
+
{hidden > 0 && (
|
|
130
|
+
<button type="button" className={css.more} onClick={() => { setExpanded(true) }}>
|
|
131
|
+
再显示 {hidden} 个{hiddenKindLabel(entries.slice(visible.length))}
|
|
132
|
+
<span className={css.moreIcon} aria-hidden><IconChevronDownOutline14 /></span>
|
|
133
|
+
</button>
|
|
134
|
+
)}
|
|
135
|
+
{expanded && entries.length > 3 && (
|
|
136
|
+
<button type="button" className={css.more} onClick={() => { setExpanded(false) }}>
|
|
137
|
+
收起 <span className={css.moreIcon} aria-hidden><IconChevronUpOutline14 /></span>
|
|
138
|
+
</button>
|
|
139
|
+
)}
|
|
140
|
+
</section>
|
|
141
|
+
)
|
|
142
|
+
}
|