sparkdesign 0.0.1 → 0.1.10
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 +188 -4
- package/dist/commands/add.js +93 -0
- package/dist/commands/diff.js +54 -0
- package/dist/commands/init.js +96 -0
- package/dist/commands/list.js +25 -0
- package/dist/index.js +37 -0
- package/dist/utils/config.js +53 -0
- package/dist/utils/registry.js +34 -0
- package/dist/utils/tokens.js +176 -0
- package/dist/utils/transform.js +19 -0
- package/package.json +33 -10
- package/registry/__tests__/basic/button.test.tsx +333 -0
- package/registry/__tests__/chat/markdown.test.tsx +387 -0
- package/registry/__tests__/chat/thinking-indicator.test.tsx +244 -0
- package/registry/__tests__/chat/tool-invocation-card.test.tsx +346 -0
- package/registry/basic/alert-dialog.tsx +180 -0
- package/registry/basic/avatar.tsx +120 -0
- package/registry/basic/button.tsx +100 -0
- package/registry/basic/collapse.tsx +94 -0
- package/registry/basic/collapsible-card.tsx +230 -0
- package/registry/basic/collapsible.tsx +21 -0
- package/registry/basic/dropdown-menu.tsx +254 -0
- package/registry/basic/icon-button.tsx +66 -0
- package/registry/basic/icons-inline.tsx +206 -0
- package/registry/basic/kbd.tsx +50 -0
- package/registry/basic/option-list.tsx +125 -0
- package/registry/basic/pagination.tsx +132 -0
- package/registry/basic/progress.tsx +42 -0
- package/registry/basic/radio-group.tsx +69 -0
- package/registry/basic/resizable.tsx +67 -0
- package/registry/basic/scrollbar.tsx +114 -0
- package/registry/basic/select.tsx +177 -0
- package/registry/basic/shimmering-text.tsx +115 -0
- package/registry/basic/sidebar-menu.tsx +177 -0
- package/registry/basic/skeleton.tsx +33 -0
- package/registry/basic/slider.tsx +55 -0
- package/registry/basic/sonner.tsx +104 -0
- package/registry/basic/spinner.tsx +17 -0
- package/registry/basic/switch.tsx +49 -0
- package/registry/basic/table.tsx +117 -0
- package/registry/basic/tabs.tsx +85 -0
- package/registry/basic/tag.tsx +161 -0
- package/registry/basic/theme-from-document.ts +10 -0
- package/registry/basic/toggle.tsx +223 -0
- package/registry/basic/tooltip.tsx +80 -0
- package/registry/basic/typography.tsx +201 -0
- package/registry/chat/ask-user-part.tsx +70 -0
- package/registry/chat/browser-action-part.tsx +166 -0
- package/registry/chat/chat-input/chat-input-folder-selector.tsx +185 -0
- package/registry/chat/chat-input/chat-input-model-switcher.tsx +131 -0
- package/registry/chat/chat-input/chat-input-textarea.tsx +67 -0
- package/registry/chat/chat-input/compound.tsx +334 -0
- package/registry/chat/chat-input/context.tsx +189 -0
- package/registry/chat/chat-input/folder-permission-dialog.tsx +61 -0
- package/registry/chat/chat-input/index.tsx +123 -0
- package/registry/chat/chat-input/types.ts +77 -0
- package/registry/chat/chat-input/useAutoResizeTextarea.ts +20 -0
- package/registry/chat/code-block-part.tsx +151 -0
- package/registry/chat/file-attachment.tsx +44 -0
- package/registry/chat/file-card.tsx +68 -0
- package/registry/chat/file-review-part.tsx +259 -0
- package/registry/chat/folder-button.tsx +169 -0
- package/registry/chat/generated-images-grid.tsx +56 -0
- package/registry/chat/generation-status-bar.tsx +72 -0
- package/registry/chat/hint-banner.tsx +165 -0
- package/registry/chat/image-attachment.tsx +166 -0
- package/registry/chat/image-generating.tsx +281 -0
- package/registry/chat/markdown.tsx +146 -0
- package/registry/chat/mermaid-part.tsx +90 -0
- package/registry/chat/permission-card.tsx +178 -0
- package/registry/chat/plan-part.tsx +168 -0
- package/registry/chat/queue-indicator.tsx +234 -0
- package/registry/chat/reasoning-step/compound.tsx +336 -0
- package/registry/chat/reasoning-step/context.tsx +114 -0
- package/registry/chat/reasoning-step/index.tsx +45 -0
- package/registry/chat/reasoning-step/types.ts +109 -0
- package/registry/chat/related-prompts.tsx +91 -0
- package/registry/chat/response/compound.tsx +210 -0
- package/registry/chat/response/context.tsx +200 -0
- package/registry/chat/response/index.tsx +87 -0
- package/registry/chat/response/types.ts +123 -0
- package/registry/chat/send-button.tsx +94 -0
- package/registry/chat/streaming-markdown-block.tsx +111 -0
- package/registry/chat/task-part.tsx +109 -0
- package/registry/chat/terminal-code-block-part.tsx +69 -0
- package/registry/chat/thinking-indicator.tsx +91 -0
- package/registry/chat/tool-invocation-card.tsx +132 -0
- package/registry/chat/user-message.tsx +38 -0
- package/registry/chat/user-question/UserQuestionCard.tsx +198 -0
- package/registry/chat/user-question/UserQuestionFooter.tsx +66 -0
- package/registry/chat/user-question/UserQuestionHeader.tsx +64 -0
- package/registry/chat/user-question/compound.tsx +324 -0
- package/registry/chat/user-question/context.tsx +456 -0
- package/registry/chat/user-question/index.tsx +95 -0
- package/registry/chat/user-question/types.ts +61 -0
- package/registry/chat/user-question/useUserQuestionKeyboard.ts +126 -0
- package/registry/chat/user-question/useUserQuestionState.ts +165 -0
- package/registry/chat/user-question-answer.tsx +62 -0
- package/registry/lib/file-icon-maps.ts +150 -0
- package/registry/lib/use-mermaid-render.ts +76 -0
- package/registry/lib/utils.ts +6 -0
- package/registry/meta.json +1 -0
- package/registry/tokens/index.css +31 -0
- package/registry/tokens/scale/computed.css +103 -0
- package/registry/tokens/scale/config.css +110 -0
- package/registry/tokens/scale/index.css +30 -0
- package/registry/tokens/scale/presets/compact.css +30 -0
- package/registry/tokens/scale/presets/dense.css +64 -0
- package/registry/tokens/scale/presets/sharp.css +40 -0
- package/registry/tokens/scale/presets/soft.css +16 -0
- package/registry/tokens/scale.css +13 -0
- package/registry/tokens/scrollbar-utility.css +35 -0
- package/registry/tokens/theme.css +633 -0
- package/registry/tokens/themes/dark-parchment.css +132 -0
- package/registry/tokens/themes/dark-qoder.css +132 -0
- package/registry/tokens/themes/light-parchment.css +123 -0
- package/registry/tokens/themes/light-qoder.css +131 -0
- package/index.js +0 -5
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { memo, useState, useCallback, useEffect } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion'
|
|
5
|
+
import { Tooltip } from '../basic/tooltip'
|
|
6
|
+
import { IconButton } from '../basic/icon-button'
|
|
7
|
+
import { ArrowUpLine, CloseLine, ArrowDownSLine } from '../basic/icons-inline'
|
|
8
|
+
import { Scrollbar } from '../basic/scrollbar'
|
|
9
|
+
|
|
10
|
+
export interface QueueIndicatorItem {
|
|
11
|
+
id: string
|
|
12
|
+
label: ReactNode
|
|
13
|
+
attachmentCount?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface QueueIndicatorLabels {
|
|
17
|
+
title?: string
|
|
18
|
+
sendNowTip?: string
|
|
19
|
+
removeTip?: string
|
|
20
|
+
fileLabel?: string
|
|
21
|
+
filesLabel?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_LABELS: Required<QueueIndicatorLabels> = {
|
|
25
|
+
title: 'In queue',
|
|
26
|
+
sendNowTip: 'Send now',
|
|
27
|
+
removeTip: 'Remove',
|
|
28
|
+
fileLabel: 'file',
|
|
29
|
+
filesLabel: 'files',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface QueueIndicatorProps {
|
|
33
|
+
items: QueueIndicatorItem[]
|
|
34
|
+
onRemoveItem?: (itemId: string) => void
|
|
35
|
+
onSendNow?: (itemId: string) => void
|
|
36
|
+
isStreaming?: boolean
|
|
37
|
+
hasStatusCardBelow?: boolean
|
|
38
|
+
labels?: QueueIndicatorLabels
|
|
39
|
+
defaultExpanded?: boolean
|
|
40
|
+
storageKey?: string
|
|
41
|
+
arrowUpIcon?: ReactNode
|
|
42
|
+
closeIcon?: ReactNode
|
|
43
|
+
arrowDownSIcon?: ReactNode
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const arrowUpClass = 'w-3.5 h-3.5'
|
|
47
|
+
const closeClass = 'w-3.5 h-3.5'
|
|
48
|
+
const arrowDownClass = 'w-4 h-4 text-text-tertiary transition-transform duration-200'
|
|
49
|
+
|
|
50
|
+
const QueueItemRow = memo(function QueueItemRow({
|
|
51
|
+
item,
|
|
52
|
+
onRemove,
|
|
53
|
+
onSendNow,
|
|
54
|
+
labels,
|
|
55
|
+
arrowUpIcon,
|
|
56
|
+
closeIcon,
|
|
57
|
+
}: {
|
|
58
|
+
item: QueueIndicatorItem
|
|
59
|
+
onRemove?: (itemId: string) => void
|
|
60
|
+
onSendNow?: (itemId: string) => void
|
|
61
|
+
labels: Required<QueueIndicatorLabels>
|
|
62
|
+
arrowUpIcon: ReactNode
|
|
63
|
+
closeIcon: ReactNode
|
|
64
|
+
}) {
|
|
65
|
+
const handleRemove = useCallback(
|
|
66
|
+
(e: React.MouseEvent) => {
|
|
67
|
+
e.stopPropagation()
|
|
68
|
+
onRemove?.(item.id)
|
|
69
|
+
},
|
|
70
|
+
[item.id, onRemove]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const handleSendNow = useCallback(
|
|
74
|
+
(e: React.MouseEvent) => {
|
|
75
|
+
e.stopPropagation()
|
|
76
|
+
onSendNow?.(item.id)
|
|
77
|
+
},
|
|
78
|
+
[item.id, onSendNow]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const attachmentCount = item.attachmentCount ?? 0
|
|
82
|
+
const fileWord = attachmentCount === 1 ? labels.fileLabel : labels.filesLabel
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="flex items-center gap-2 px-3 py-1.5 text-xs leading-xs hover:bg-fill-tertiary transition-colors cursor-default">
|
|
86
|
+
<span className="truncate flex-1 text-text">{item.label}</span>
|
|
87
|
+
{attachmentCount > 0 && (
|
|
88
|
+
<span className="shrink-0 text-xs leading-xs text-text-tertiary">
|
|
89
|
+
+{attachmentCount} {fileWord}
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
<div className="flex items-center gap-0.5">
|
|
93
|
+
{onSendNow && (
|
|
94
|
+
<Tooltip content={labels.sendNowTip} side="top">
|
|
95
|
+
<IconButton
|
|
96
|
+
size="sm"
|
|
97
|
+
variant="ghost"
|
|
98
|
+
icon={arrowUpIcon}
|
|
99
|
+
aria-label={labels.sendNowTip}
|
|
100
|
+
className="shrink-0"
|
|
101
|
+
onClick={handleSendNow}
|
|
102
|
+
/>
|
|
103
|
+
</Tooltip>
|
|
104
|
+
)}
|
|
105
|
+
{onRemove && (
|
|
106
|
+
<Tooltip content={labels.removeTip} side="top">
|
|
107
|
+
<IconButton
|
|
108
|
+
size="sm"
|
|
109
|
+
variant="ghost"
|
|
110
|
+
icon={closeIcon}
|
|
111
|
+
aria-label={labels.removeTip}
|
|
112
|
+
className="shrink-0"
|
|
113
|
+
onClick={handleRemove}
|
|
114
|
+
/>
|
|
115
|
+
</Tooltip>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
export const QueueIndicator = memo(function QueueIndicator({
|
|
123
|
+
items,
|
|
124
|
+
onRemoveItem,
|
|
125
|
+
onSendNow,
|
|
126
|
+
isStreaming = false,
|
|
127
|
+
hasStatusCardBelow = false,
|
|
128
|
+
labels: labelsProp,
|
|
129
|
+
defaultExpanded = true,
|
|
130
|
+
storageKey,
|
|
131
|
+
arrowUpIcon,
|
|
132
|
+
closeIcon,
|
|
133
|
+
arrowDownSIcon,
|
|
134
|
+
}: QueueIndicatorProps) {
|
|
135
|
+
const labels = { ...DEFAULT_LABELS, ...labelsProp }
|
|
136
|
+
|
|
137
|
+
const defaultArrowUp = arrowUpIcon ?? <ArrowUpLine className={arrowUpClass} />
|
|
138
|
+
const defaultClose = closeIcon ?? <CloseLine className={closeClass} />
|
|
139
|
+
const defaultArrowDown =
|
|
140
|
+
arrowDownSIcon ?? <ArrowDownSLine className={arrowDownClass} />
|
|
141
|
+
|
|
142
|
+
const [isExpanded, setIsExpanded] = useState(() => {
|
|
143
|
+
if (typeof window === 'undefined') return defaultExpanded
|
|
144
|
+
if (storageKey) {
|
|
145
|
+
const saved = localStorage.getItem(storageKey)
|
|
146
|
+
return saved !== null ? saved === 'true' : defaultExpanded
|
|
147
|
+
}
|
|
148
|
+
return defaultExpanded
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (storageKey) {
|
|
153
|
+
localStorage.setItem(storageKey, String(isExpanded))
|
|
154
|
+
}
|
|
155
|
+
}, [storageKey, isExpanded])
|
|
156
|
+
|
|
157
|
+
if (items.length === 0) return null
|
|
158
|
+
|
|
159
|
+
const containerStyles = cn(
|
|
160
|
+
'w-full overflow-hidden flex flex-col',
|
|
161
|
+
'border border-b-0 border-border border-border-tertiary bg-bg-base',
|
|
162
|
+
!hasStatusCardBelow && 'rounded-t-xl'
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<motion.div
|
|
167
|
+
initial={{ opacity: 0, y: 8 }}
|
|
168
|
+
animate={{ opacity: 1, y: 0 }}
|
|
169
|
+
transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
|
|
170
|
+
className={containerStyles}
|
|
171
|
+
>
|
|
172
|
+
<div
|
|
173
|
+
role="button"
|
|
174
|
+
tabIndex={0}
|
|
175
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
176
|
+
onKeyDown={(e) => {
|
|
177
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
178
|
+
e.preventDefault()
|
|
179
|
+
setIsExpanded(!isExpanded)
|
|
180
|
+
}
|
|
181
|
+
}}
|
|
182
|
+
aria-expanded={isExpanded}
|
|
183
|
+
aria-label={`${isExpanded ? 'Collapse' : 'Expand'} queue`}
|
|
184
|
+
className="flex items-center justify-between px-3 py-1 h-7 cursor-pointer bg-fill-tertiary hover:bg-fill-secondary transition-colors duration-150 focus:outline-none"
|
|
185
|
+
>
|
|
186
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
187
|
+
<span
|
|
188
|
+
className={cn(
|
|
189
|
+
'inline-flex items-center justify-center',
|
|
190
|
+
!isExpanded && '-rotate-90'
|
|
191
|
+
)}
|
|
192
|
+
>
|
|
193
|
+
{defaultArrowDown}
|
|
194
|
+
</span>
|
|
195
|
+
<span className="text-xs leading-xs font-medium text-text-secondary">
|
|
196
|
+
{labels.title}
|
|
197
|
+
</span>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<AnimatePresence initial={false}>
|
|
202
|
+
{isExpanded && (
|
|
203
|
+
<motion.div
|
|
204
|
+
initial={{ height: 0, opacity: 0 }}
|
|
205
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
206
|
+
exit={{ height: 0, opacity: 0 }}
|
|
207
|
+
transition={{ duration: 0.2, ease: [0.23, 1, 0.32, 1] }}
|
|
208
|
+
className="overflow-hidden"
|
|
209
|
+
>
|
|
210
|
+
<Scrollbar
|
|
211
|
+
scrollbarVisibility="auto"
|
|
212
|
+
maxHeight={208}
|
|
213
|
+
className="border-t border-border-tertiary scroll-smooth"
|
|
214
|
+
>
|
|
215
|
+
{items.map((item) => (
|
|
216
|
+
<QueueItemRow
|
|
217
|
+
key={item.id}
|
|
218
|
+
item={item}
|
|
219
|
+
onRemove={onRemoveItem}
|
|
220
|
+
onSendNow={onSendNow}
|
|
221
|
+
labels={labels}
|
|
222
|
+
arrowUpIcon={defaultArrowUp}
|
|
223
|
+
closeIcon={defaultClose}
|
|
224
|
+
/>
|
|
225
|
+
))}
|
|
226
|
+
</Scrollbar>
|
|
227
|
+
</motion.div>
|
|
228
|
+
)}
|
|
229
|
+
</AnimatePresence>
|
|
230
|
+
</motion.div>
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
QueueIndicator.displayName = 'QueueIndicator'
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { memo, useMemo, useCallback, Fragment } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { motion } from 'framer-motion'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
import { useReasoningStepContext } from './context'
|
|
6
|
+
import type {
|
|
7
|
+
ReasoningStepHeaderProps,
|
|
8
|
+
ReasoningStepDetailsProps,
|
|
9
|
+
ReasoningStepDetailRowProps,
|
|
10
|
+
ReasoningStepDetail,
|
|
11
|
+
} from './types'
|
|
12
|
+
|
|
13
|
+
// ===== 样式常量 =====
|
|
14
|
+
|
|
15
|
+
const iconSizeStyle = { width: 'var(--font-size-xs)', height: 'var(--font-size-xs)' }
|
|
16
|
+
|
|
17
|
+
const FILE_ICON_COLOR_MAP: Record<string, string> = {
|
|
18
|
+
ts: 'text-info', tsx: 'text-info', js: 'text-info', jsx: 'text-info',
|
|
19
|
+
py: 'text-teal',
|
|
20
|
+
md: 'text-text-secondary', mdx: 'text-text-secondary',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getFileIconColorClass(fileType?: string, filename?: string): string {
|
|
24
|
+
const ext = (fileType ?? filename?.split('.').pop() ?? '').toLowerCase()
|
|
25
|
+
return FILE_ICON_COLOR_MAP[ext] ?? 'text-text-tertiary'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ===== 辅助组件 =====
|
|
29
|
+
|
|
30
|
+
const FileIconPlaceholder = memo(function FileIconPlaceholder({ className }: { className?: string }) {
|
|
31
|
+
return (
|
|
32
|
+
<span
|
|
33
|
+
className={cn('inline-flex items-center justify-center text-xs leading-xs font-mono', className)}
|
|
34
|
+
style={iconSizeStyle}
|
|
35
|
+
aria-hidden
|
|
36
|
+
>
|
|
37
|
+
⎔
|
|
38
|
+
</span>
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const InlineRow = memo(function InlineRow({
|
|
43
|
+
prefix,
|
|
44
|
+
primary,
|
|
45
|
+
secondary,
|
|
46
|
+
}: {
|
|
47
|
+
prefix?: ReactNode
|
|
48
|
+
primary: string
|
|
49
|
+
secondary?: string
|
|
50
|
+
}) {
|
|
51
|
+
return (
|
|
52
|
+
<div className="flex flex-row items-center gap-1.5 min-w-0 flex-1">
|
|
53
|
+
{prefix ? <div className="flex items-center justify-center shrink-0">{prefix}</div> : null}
|
|
54
|
+
<div className="flex flex-row items-center gap-1 min-w-0 overflow-hidden flex-1">
|
|
55
|
+
<span className="text-xs leading-xs font-normal text-text-tertiary truncate" title={primary}>
|
|
56
|
+
{primary}
|
|
57
|
+
</span>
|
|
58
|
+
{secondary ? (
|
|
59
|
+
<span className="text-xs leading-xs font-normal text-text-quaternary truncate" title={secondary}>
|
|
60
|
+
{secondary}
|
|
61
|
+
</span>
|
|
62
|
+
) : null}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// ===== ReasoningStep.Header =====
|
|
69
|
+
|
|
70
|
+
export function ReasoningStepHeader({ className }: ReasoningStepHeaderProps) {
|
|
71
|
+
const ctx = useReasoningStepContext()
|
|
72
|
+
|
|
73
|
+
const dynamicSpread = useMemo(() => {
|
|
74
|
+
if (typeof ctx.text === 'string') return ctx.text.length * ctx.spread
|
|
75
|
+
return 10 * ctx.spread
|
|
76
|
+
}, [ctx.text, ctx.spread])
|
|
77
|
+
|
|
78
|
+
const getIcon = () => (ctx.icon ? ctx.icon : ctx.eyeIcon)
|
|
79
|
+
|
|
80
|
+
const renderPrimaryText = () => {
|
|
81
|
+
if (typeof ctx.text !== 'string') return ctx.text
|
|
82
|
+
if (ctx.status === 'in-progress') {
|
|
83
|
+
return (
|
|
84
|
+
<motion.span
|
|
85
|
+
className="text-xs leading-xs font-normal relative inline-block bg-clip-text text-transparent bg-[length:250%_100%,auto] bg-no-repeat truncate"
|
|
86
|
+
style={{
|
|
87
|
+
backgroundImage: `linear-gradient(90deg, transparent calc(50% - ${dynamicSpread}px), var(--color-text), transparent calc(50% + ${dynamicSpread}px)), linear-gradient(var(--color-text-tertiary), var(--color-text-tertiary))`,
|
|
88
|
+
}}
|
|
89
|
+
initial={{ backgroundPosition: '100% center' }}
|
|
90
|
+
animate={{ backgroundPosition: '0% center' }}
|
|
91
|
+
transition={{ repeat: Infinity, duration: ctx.duration, ease: 'linear' }}
|
|
92
|
+
>
|
|
93
|
+
{ctx.text}
|
|
94
|
+
</motion.span>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
return (
|
|
98
|
+
<span className="text-xs leading-xs font-normal text-text-tertiary truncate">
|
|
99
|
+
{ctx.text}
|
|
100
|
+
</span>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const renderDescription = () => {
|
|
105
|
+
if (!ctx.description) return null
|
|
106
|
+
return (
|
|
107
|
+
<span className="text-xs leading-xs font-normal text-text-quaternary truncate">
|
|
108
|
+
{ctx.description}
|
|
109
|
+
</span>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
className={cn('flex flex-row items-center gap-1.5', ctx.hasExpandableContent && 'cursor-pointer', className)}
|
|
116
|
+
onClick={ctx.toggleExpanded}
|
|
117
|
+
>
|
|
118
|
+
{ctx.effectiveShowIcon && (
|
|
119
|
+
<div className="flex items-center justify-center shrink-0" style={iconSizeStyle}>
|
|
120
|
+
{getIcon()}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
<div className="flex flex-row items-center gap-1 flex-1 min-w-0">
|
|
124
|
+
{renderPrimaryText()}
|
|
125
|
+
{ctx.description ? renderDescription() : null}
|
|
126
|
+
{ctx.hasExpandableContent && (
|
|
127
|
+
<div
|
|
128
|
+
className={cn(
|
|
129
|
+
'flex items-center justify-center shrink-0 text-text-tertiary transition-opacity duration-200 ease-in-out',
|
|
130
|
+
ctx.effectiveExpanded ? 'opacity-100' : ctx.isHovered ? 'opacity-100' : 'opacity-0',
|
|
131
|
+
ctx.effectiveExpanded ? 'rotate-90' : 'rotate-0'
|
|
132
|
+
)}
|
|
133
|
+
style={iconSizeStyle}
|
|
134
|
+
>
|
|
135
|
+
{ctx.arrowRightIcon}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
ReasoningStepHeader.displayName = 'ReasoningStepHeader'
|
|
144
|
+
|
|
145
|
+
// ===== ReasoningStep.DetailRow(内部递归组件)=====
|
|
146
|
+
|
|
147
|
+
interface DetailRowInternalProps {
|
|
148
|
+
detail: ReasoningStepDetail
|
|
149
|
+
depth: number
|
|
150
|
+
pathKey: string
|
|
151
|
+
detailIndent: string
|
|
152
|
+
disableExpandAnimation: boolean
|
|
153
|
+
expandedNestedKeys: Set<string>
|
|
154
|
+
onToggleNested: (pathKey: string) => void
|
|
155
|
+
arrowRightIcon: ReactNode
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const DetailRowInternal = memo(function DetailRowInternal({
|
|
159
|
+
detail,
|
|
160
|
+
depth,
|
|
161
|
+
pathKey,
|
|
162
|
+
detailIndent,
|
|
163
|
+
expandedNestedKeys,
|
|
164
|
+
onToggleNested,
|
|
165
|
+
arrowRightIcon,
|
|
166
|
+
}: DetailRowInternalProps) {
|
|
167
|
+
const hasNested = detail.details != null && detail.details.length > 0
|
|
168
|
+
const isExpandedNested = expandedNestedKeys.has(pathKey)
|
|
169
|
+
const showIconOnRow = !hasNested
|
|
170
|
+
const primary = detail.type === 'action' ? detail.action : detail.filename
|
|
171
|
+
const secondary =
|
|
172
|
+
detail.type === 'action' ? detail.desc : [detail.lines, detail.path].filter(Boolean).join(' ')
|
|
173
|
+
const prefix =
|
|
174
|
+
showIconOnRow && detail.type === 'file' ? (
|
|
175
|
+
<FileIconPlaceholder className={getFileIconColorClass(detail.fileType, detail.filename)} />
|
|
176
|
+
) : undefined
|
|
177
|
+
|
|
178
|
+
const handleClick = useCallback(() => {
|
|
179
|
+
if (hasNested) onToggleNested(pathKey)
|
|
180
|
+
}, [hasNested, onToggleNested, pathKey])
|
|
181
|
+
|
|
182
|
+
const rowContent = (
|
|
183
|
+
<div
|
|
184
|
+
className={cn(
|
|
185
|
+
'flex flex-row items-center gap-1.5 self-stretch min-h-5 min-w-0',
|
|
186
|
+
hasNested && 'cursor-pointer'
|
|
187
|
+
)}
|
|
188
|
+
onClick={hasNested ? handleClick : undefined}
|
|
189
|
+
>
|
|
190
|
+
<InlineRow prefix={prefix} primary={primary} secondary={secondary} />
|
|
191
|
+
{hasNested && (
|
|
192
|
+
<div
|
|
193
|
+
className={cn(
|
|
194
|
+
'flex items-center justify-center shrink-0 text-text-tertiary',
|
|
195
|
+
isExpandedNested ? 'rotate-90' : 'rotate-0'
|
|
196
|
+
)}
|
|
197
|
+
style={iconSizeStyle}
|
|
198
|
+
>
|
|
199
|
+
{arrowRightIcon}
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
const rowWithOptionalLine = (
|
|
206
|
+
<div
|
|
207
|
+
className="flex flex-row items-center gap-0 self-stretch"
|
|
208
|
+
style={{ marginLeft: detailIndent }}
|
|
209
|
+
>
|
|
210
|
+
<div className="shrink-0 w-[var(--font-size-xs)] h-5 border-l border-border-tertiary" />
|
|
211
|
+
{rowContent}
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if (!hasNested) return <Fragment key={pathKey}>{rowWithOptionalLine}</Fragment>
|
|
216
|
+
|
|
217
|
+
const isThirdLevelContainer = depth === 1
|
|
218
|
+
const nestedWrapperClass = isThirdLevelContainer
|
|
219
|
+
? 'rounded-lg border border-border-tertiary bg-bg-base p-2 flex flex-col gap-0'
|
|
220
|
+
: 'flex flex-col'
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<div key={pathKey} className="flex flex-col self-stretch">
|
|
224
|
+
{rowWithOptionalLine}
|
|
225
|
+
<div
|
|
226
|
+
className={cn(
|
|
227
|
+
'grid transition-[grid-template-rows,opacity] duration-150 ease-out',
|
|
228
|
+
isExpandedNested ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
|
|
229
|
+
)}
|
|
230
|
+
>
|
|
231
|
+
<div className="overflow-hidden">
|
|
232
|
+
<div className={cn(nestedWrapperClass)}>
|
|
233
|
+
{detail.details?.map((d, i) => (
|
|
234
|
+
<DetailRowInternal
|
|
235
|
+
key={`${pathKey}-${i}`}
|
|
236
|
+
detail={d}
|
|
237
|
+
depth={depth + 1}
|
|
238
|
+
pathKey={`${pathKey}-${i}`}
|
|
239
|
+
detailIndent={detailIndent}
|
|
240
|
+
disableExpandAnimation={false}
|
|
241
|
+
expandedNestedKeys={expandedNestedKeys}
|
|
242
|
+
onToggleNested={onToggleNested}
|
|
243
|
+
arrowRightIcon={arrowRightIcon}
|
|
244
|
+
/>
|
|
245
|
+
))}
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// ===== ReasoningStep.DetailRow(公开 API)=====
|
|
254
|
+
|
|
255
|
+
export function ReasoningStepDetailRow({
|
|
256
|
+
detail,
|
|
257
|
+
depth = 0,
|
|
258
|
+
pathKey = '0',
|
|
259
|
+
}: ReasoningStepDetailRowProps) {
|
|
260
|
+
const ctx = useReasoningStepContext()
|
|
261
|
+
return (
|
|
262
|
+
<DetailRowInternal
|
|
263
|
+
detail={detail}
|
|
264
|
+
depth={depth}
|
|
265
|
+
pathKey={pathKey}
|
|
266
|
+
detailIndent={ctx.detailIndent}
|
|
267
|
+
disableExpandAnimation={ctx.disableExpandAnimation}
|
|
268
|
+
expandedNestedKeys={ctx.expandedNestedKeys}
|
|
269
|
+
onToggleNested={ctx.toggleNested}
|
|
270
|
+
arrowRightIcon={ctx.arrowRightIcon}
|
|
271
|
+
/>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
ReasoningStepDetailRow.displayName = 'ReasoningStepDetailRow'
|
|
276
|
+
|
|
277
|
+
// ===== ReasoningStep.Details =====
|
|
278
|
+
|
|
279
|
+
export function ReasoningStepDetails({ className, children }: ReasoningStepDetailsProps) {
|
|
280
|
+
const ctx = useReasoningStepContext()
|
|
281
|
+
const hasDetails = ctx.details != null && ctx.details.length > 0
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<div
|
|
285
|
+
className={cn(
|
|
286
|
+
'grid transition-[grid-template-rows,opacity] duration-200 ease-out',
|
|
287
|
+
ctx.effectiveExpanded ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0',
|
|
288
|
+
className
|
|
289
|
+
)}
|
|
290
|
+
>
|
|
291
|
+
<div className="overflow-hidden">
|
|
292
|
+
{hasDetails ? (
|
|
293
|
+
<div className="flex flex-col">
|
|
294
|
+
{ctx.details?.map((d, i) => (
|
|
295
|
+
<DetailRowInternal
|
|
296
|
+
key={String(i)}
|
|
297
|
+
detail={d}
|
|
298
|
+
depth={0}
|
|
299
|
+
pathKey={String(i)}
|
|
300
|
+
detailIndent={ctx.detailIndent}
|
|
301
|
+
disableExpandAnimation={ctx.disableExpandAnimation}
|
|
302
|
+
expandedNestedKeys={ctx.expandedNestedKeys}
|
|
303
|
+
onToggleNested={ctx.toggleNested}
|
|
304
|
+
arrowRightIcon={ctx.arrowRightIcon}
|
|
305
|
+
/>
|
|
306
|
+
))}
|
|
307
|
+
</div>
|
|
308
|
+
) : (
|
|
309
|
+
children ?? ctx.detailsChildren
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
ReasoningStepDetails.displayName = 'ReasoningStepDetails'
|
|
317
|
+
|
|
318
|
+
// ===== ReasoningStep.DefaultLayout =====
|
|
319
|
+
|
|
320
|
+
export function ReasoningStepDefaultLayout() {
|
|
321
|
+
const ctx = useReasoningStepContext()
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<div
|
|
325
|
+
data-reasoning-step
|
|
326
|
+
className="flex flex-col gap-1 py-0.5"
|
|
327
|
+
onMouseEnter={() => ctx.setIsHovered(true)}
|
|
328
|
+
onMouseLeave={() => ctx.setIsHovered(false)}
|
|
329
|
+
>
|
|
330
|
+
<ReasoningStepHeader />
|
|
331
|
+
{ctx.hasExpandableContent && <ReasoningStepDetails />}
|
|
332
|
+
</div>
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
ReasoningStepDefaultLayout.displayName = 'ReasoningStepDefaultLayout'
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useCallback, useMemo } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { EyeLine, ArrowRightLine } from '../../basic/icons-inline'
|
|
4
|
+
import type { ReasoningStepContextValue, ReasoningStepRootProps } from './types'
|
|
5
|
+
|
|
6
|
+
// ===== 样式常量 =====
|
|
7
|
+
|
|
8
|
+
const eyeClass = 'w-full h-full text-text-tertiary transition-transform duration-200 ease-in-out'
|
|
9
|
+
const arrowClass = 'w-full h-full transition-transform duration-200 ease-in-out'
|
|
10
|
+
|
|
11
|
+
// ===== Context =====
|
|
12
|
+
|
|
13
|
+
const ReasoningStepContext = createContext<ReasoningStepContextValue | null>(null)
|
|
14
|
+
ReasoningStepContext.displayName = 'ReasoningStepContext'
|
|
15
|
+
|
|
16
|
+
export function useReasoningStepContext() {
|
|
17
|
+
const ctx = useContext(ReasoningStepContext)
|
|
18
|
+
if (!ctx) throw new Error('ReasoningStep compound components must be used within ReasoningStep.Root')
|
|
19
|
+
return ctx
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ===== Provider =====
|
|
23
|
+
|
|
24
|
+
export function ReasoningStepRootProvider({
|
|
25
|
+
text,
|
|
26
|
+
description,
|
|
27
|
+
status = 'completed',
|
|
28
|
+
icon,
|
|
29
|
+
showIcon = true,
|
|
30
|
+
details,
|
|
31
|
+
children,
|
|
32
|
+
detailsChildren,
|
|
33
|
+
duration = 2,
|
|
34
|
+
spread = 2,
|
|
35
|
+
disableExpandAnimation = false,
|
|
36
|
+
defaultExpanded,
|
|
37
|
+
expanded: expandedProp,
|
|
38
|
+
onExpandedChange,
|
|
39
|
+
eyeIcon,
|
|
40
|
+
arrowRightIcon,
|
|
41
|
+
}: ReasoningStepRootProps & { children?: ReactNode; detailsChildren?: ReactNode }) {
|
|
42
|
+
const isControlled = expandedProp !== undefined
|
|
43
|
+
const [isExpandedInternal, setIsExpandedInternal] = useState(defaultExpanded ?? false)
|
|
44
|
+
const isExpanded = isControlled ? expandedProp : isExpandedInternal
|
|
45
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
46
|
+
|
|
47
|
+
const effectiveExpanded = disableExpandAnimation && defaultExpanded !== undefined
|
|
48
|
+
? defaultExpanded
|
|
49
|
+
: isExpanded
|
|
50
|
+
|
|
51
|
+
const effectiveShowIcon = showIcon && typeof text === 'string'
|
|
52
|
+
const detailIndent = effectiveShowIcon ? 'var(--spacing-1.5)' : 'var(--spacing-2.5)'
|
|
53
|
+
|
|
54
|
+
const hasDetails = details != null && details.length > 0
|
|
55
|
+
const hasExpandableContent = hasDetails || detailsChildren != null
|
|
56
|
+
|
|
57
|
+
const toggleExpanded = useCallback(() => {
|
|
58
|
+
if (hasExpandableContent) {
|
|
59
|
+
if (isControlled) onExpandedChange?.(!expandedProp)
|
|
60
|
+
else setIsExpandedInternal((e) => !e)
|
|
61
|
+
}
|
|
62
|
+
}, [hasExpandableContent, isControlled, expandedProp, onExpandedChange])
|
|
63
|
+
|
|
64
|
+
const [expandedNestedKeys, setExpandedNestedKeys] = useState<Set<string>>(new Set())
|
|
65
|
+
const toggleNested = useCallback((pathKey: string) => {
|
|
66
|
+
setExpandedNestedKeys((prev) => {
|
|
67
|
+
const next = new Set(prev)
|
|
68
|
+
if (next.has(pathKey)) next.delete(pathKey)
|
|
69
|
+
else next.add(pathKey)
|
|
70
|
+
return next
|
|
71
|
+
})
|
|
72
|
+
}, [])
|
|
73
|
+
|
|
74
|
+
const defaultEye = useMemo(
|
|
75
|
+
() => eyeIcon ?? <EyeLine className={eyeClass} />,
|
|
76
|
+
[eyeIcon]
|
|
77
|
+
)
|
|
78
|
+
const defaultArrow = useMemo(
|
|
79
|
+
() => arrowRightIcon ?? <ArrowRightLine className={arrowClass} />,
|
|
80
|
+
[arrowRightIcon]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const ctxValue: ReasoningStepContextValue = {
|
|
84
|
+
status,
|
|
85
|
+
isExpanded,
|
|
86
|
+
isHovered,
|
|
87
|
+
setIsHovered,
|
|
88
|
+
toggleExpanded,
|
|
89
|
+
effectiveExpanded,
|
|
90
|
+
effectiveShowIcon,
|
|
91
|
+
hasExpandableContent,
|
|
92
|
+
text,
|
|
93
|
+
description,
|
|
94
|
+
details,
|
|
95
|
+
detailsChildren,
|
|
96
|
+
duration,
|
|
97
|
+
spread,
|
|
98
|
+
disableExpandAnimation,
|
|
99
|
+
detailIndent,
|
|
100
|
+
expandedNestedKeys,
|
|
101
|
+
toggleNested,
|
|
102
|
+
icon,
|
|
103
|
+
eyeIcon: defaultEye,
|
|
104
|
+
arrowRightIcon: defaultArrow,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<ReasoningStepContext.Provider value={ctxValue}>
|
|
109
|
+
{children}
|
|
110
|
+
</ReasoningStepContext.Provider>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ReasoningStepRootProvider.displayName = 'ReasoningStepRoot'
|