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,80 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { motion } from 'framer-motion'
|
|
3
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
import { getThemeFromDocument } from './theme-from-document'
|
|
6
|
+
|
|
7
|
+
const TooltipProvider = TooltipPrimitive.Provider
|
|
8
|
+
|
|
9
|
+
const TooltipRoot = TooltipPrimitive.Root
|
|
10
|
+
|
|
11
|
+
const TooltipTrigger = TooltipPrimitive.Trigger
|
|
12
|
+
|
|
13
|
+
const TooltipContent = React.forwardRef<
|
|
14
|
+
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
15
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {
|
|
16
|
+
/** When provided (e.g. from ThemeStyleProvider), used for portal wrapper instead of getThemeFromDocument */
|
|
17
|
+
dataStyle?: string
|
|
18
|
+
dataTheme?: string
|
|
19
|
+
}
|
|
20
|
+
>(({ className, sideOffset = 8, children, dataStyle, dataTheme, ...props }, ref) => {
|
|
21
|
+
const fromDoc = getThemeFromDocument()
|
|
22
|
+
const dataProps =
|
|
23
|
+
dataStyle !== undefined
|
|
24
|
+
? { 'data-style': dataStyle, 'data-theme': dataTheme ?? fromDoc['data-theme'] ?? '' }
|
|
25
|
+
: fromDoc
|
|
26
|
+
return (
|
|
27
|
+
<TooltipPrimitive.Portal>
|
|
28
|
+
<div style={{ display: 'contents' }} {...dataProps}>
|
|
29
|
+
<TooltipPrimitive.Content
|
|
30
|
+
ref={ref}
|
|
31
|
+
sideOffset={sideOffset}
|
|
32
|
+
className={cn(
|
|
33
|
+
'z-50 max-w-60 overflow-hidden rounded px-2 py-1 text-xs leading-5',
|
|
34
|
+
'bg-bg-highlight text-text-on-primary shadow-sm',
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.15 }}>
|
|
40
|
+
{children}
|
|
41
|
+
</motion.div>
|
|
42
|
+
</TooltipPrimitive.Content>
|
|
43
|
+
</div>
|
|
44
|
+
</TooltipPrimitive.Portal>
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
|
48
|
+
|
|
49
|
+
export interface TooltipProps {
|
|
50
|
+
content: React.ReactNode
|
|
51
|
+
children: React.ReactNode
|
|
52
|
+
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
53
|
+
contentClassName?: string
|
|
54
|
+
/** When provided, portal content uses this data-style (e.g. from ThemeStyleProvider) */
|
|
55
|
+
dataStyle?: string
|
|
56
|
+
/** When provided, portal content uses this data-theme */
|
|
57
|
+
dataTheme?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Tooltip({ content, children, side = 'top', contentClassName, dataStyle, dataTheme }: TooltipProps) {
|
|
61
|
+
return (
|
|
62
|
+
<TooltipProvider delayDuration={300}>
|
|
63
|
+
<TooltipRoot>
|
|
64
|
+
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
|
65
|
+
<TooltipContent side={side} className={contentClassName} dataStyle={dataStyle} dataTheme={dataTheme}>
|
|
66
|
+
{content}
|
|
67
|
+
</TooltipContent>
|
|
68
|
+
</TooltipRoot>
|
|
69
|
+
</TooltipProvider>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
Tooltip.displayName = 'Tooltip'
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
TooltipProvider,
|
|
76
|
+
TooltipRoot,
|
|
77
|
+
TooltipTrigger,
|
|
78
|
+
TooltipContent,
|
|
79
|
+
Tooltip,
|
|
80
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
import { Scrollbar } from './scrollbar'
|
|
4
|
+
|
|
5
|
+
export type TypographyH1Props = React.ComponentProps<'h1'>
|
|
6
|
+
function TypographyH1({ className, ...props }: TypographyH1Props) {
|
|
7
|
+
return (
|
|
8
|
+
<h1
|
|
9
|
+
className={cn(
|
|
10
|
+
'font-sans scroll-m-20 text-4xl font-extrabold tracking-tight text-balance text-text',
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
TypographyH1.displayName = 'TypographyH1'
|
|
18
|
+
|
|
19
|
+
export type TypographyH2Props = React.ComponentProps<'h2'>
|
|
20
|
+
function TypographyH2({ className, ...props }: TypographyH2Props) {
|
|
21
|
+
return (
|
|
22
|
+
<h2
|
|
23
|
+
className={cn(
|
|
24
|
+
'font-sans scroll-m-20 border-b border-border-tertiary pb-2 text-3xl font-semibold tracking-tight first:mt-0 text-text',
|
|
25
|
+
className
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
TypographyH2.displayName = 'TypographyH2'
|
|
32
|
+
|
|
33
|
+
export type TypographyH3Props = React.ComponentProps<'h3'>
|
|
34
|
+
function TypographyH3({ className, ...props }: TypographyH3Props) {
|
|
35
|
+
return (
|
|
36
|
+
<h3
|
|
37
|
+
className={cn(
|
|
38
|
+
'font-sans scroll-m-20 text-2xl font-semibold tracking-tight text-text',
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
TypographyH3.displayName = 'TypographyH3'
|
|
46
|
+
|
|
47
|
+
export type TypographyH4Props = React.ComponentProps<'h4'>
|
|
48
|
+
function TypographyH4({ className, ...props }: TypographyH4Props) {
|
|
49
|
+
return (
|
|
50
|
+
<h4
|
|
51
|
+
className={cn(
|
|
52
|
+
'font-sans scroll-m-20 text-xl font-semibold tracking-tight text-text',
|
|
53
|
+
className
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
TypographyH4.displayName = 'TypographyH4'
|
|
60
|
+
|
|
61
|
+
export type TypographyPProps = React.ComponentProps<'p'>
|
|
62
|
+
function TypographyP({ className, ...props }: TypographyPProps) {
|
|
63
|
+
return (
|
|
64
|
+
<p
|
|
65
|
+
className={cn(
|
|
66
|
+
'font-sans leading-7 text-text [&:not(:first-child)]:mt-6',
|
|
67
|
+
className
|
|
68
|
+
)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
TypographyP.displayName = 'TypographyP'
|
|
74
|
+
|
|
75
|
+
export type TypographyBlockquoteProps = React.ComponentProps<'blockquote'>
|
|
76
|
+
function TypographyBlockquote({ className, ...props }: TypographyBlockquoteProps) {
|
|
77
|
+
return (
|
|
78
|
+
<blockquote
|
|
79
|
+
className={cn(
|
|
80
|
+
'font-sans mt-6 border-l-2 border-border-tertiary pl-6 italic text-text-secondary',
|
|
81
|
+
className
|
|
82
|
+
)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
TypographyBlockquote.displayName = 'TypographyBlockquote'
|
|
88
|
+
|
|
89
|
+
export type TypographyTableProps = React.ComponentProps<'div'>
|
|
90
|
+
function TypographyTable({ className, children, ..._rest }: TypographyTableProps) {
|
|
91
|
+
void _rest
|
|
92
|
+
return (
|
|
93
|
+
<Scrollbar
|
|
94
|
+
scrollbarVisibility="auto"
|
|
95
|
+
className={cn('font-sans my-6 w-full', className)}
|
|
96
|
+
>
|
|
97
|
+
{children}
|
|
98
|
+
</Scrollbar>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
TypographyTable.displayName = 'TypographyTable'
|
|
102
|
+
|
|
103
|
+
export type TypographyListProps = React.ComponentProps<'ul'>
|
|
104
|
+
function TypographyList({ className, ...props }: TypographyListProps) {
|
|
105
|
+
return (
|
|
106
|
+
<ul
|
|
107
|
+
className={cn(
|
|
108
|
+
'font-sans my-6 ml-6 list-disc text-text [&>li]:mt-2',
|
|
109
|
+
className
|
|
110
|
+
)}
|
|
111
|
+
{...props}
|
|
112
|
+
/>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
TypographyList.displayName = 'TypographyList'
|
|
116
|
+
|
|
117
|
+
export type TypographyInlineCodeProps = React.ComponentProps<'code'>
|
|
118
|
+
function TypographyInlineCode({ className, ...props }: TypographyInlineCodeProps) {
|
|
119
|
+
return (
|
|
120
|
+
<code
|
|
121
|
+
className={cn(
|
|
122
|
+
'relative rounded bg-fill-secondary px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold text-text',
|
|
123
|
+
className
|
|
124
|
+
)}
|
|
125
|
+
{...props}
|
|
126
|
+
/>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
TypographyInlineCode.displayName = 'TypographyInlineCode'
|
|
130
|
+
|
|
131
|
+
export type TypographyLeadProps = React.ComponentProps<'p'>
|
|
132
|
+
function TypographyLead({ className, ...props }: TypographyLeadProps) {
|
|
133
|
+
return (
|
|
134
|
+
<p
|
|
135
|
+
className={cn(
|
|
136
|
+
'font-sans text-xl text-muted-foreground',
|
|
137
|
+
className
|
|
138
|
+
)}
|
|
139
|
+
{...props}
|
|
140
|
+
/>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
TypographyLead.displayName = 'TypographyLead'
|
|
144
|
+
|
|
145
|
+
export type TypographyLargeProps = React.ComponentProps<'div'>
|
|
146
|
+
function TypographyLarge({ className, ...props }: TypographyLargeProps) {
|
|
147
|
+
return (
|
|
148
|
+
<div
|
|
149
|
+
className={cn(
|
|
150
|
+
'font-sans text-lg font-semibold text-text',
|
|
151
|
+
className
|
|
152
|
+
)}
|
|
153
|
+
{...props}
|
|
154
|
+
/>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
TypographyLarge.displayName = 'TypographyLarge'
|
|
158
|
+
|
|
159
|
+
export type TypographySmallProps = React.ComponentProps<'small'>
|
|
160
|
+
function TypographySmall({ className, ...props }: TypographySmallProps) {
|
|
161
|
+
return (
|
|
162
|
+
<small
|
|
163
|
+
className={cn(
|
|
164
|
+
'font-sans text-sm font-medium leading-none text-text',
|
|
165
|
+
className
|
|
166
|
+
)}
|
|
167
|
+
{...props}
|
|
168
|
+
/>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
TypographySmall.displayName = 'TypographySmall'
|
|
172
|
+
|
|
173
|
+
export type TypographyMutedProps = React.ComponentProps<'p'>
|
|
174
|
+
function TypographyMuted({ className, ...props }: TypographyMutedProps) {
|
|
175
|
+
return (
|
|
176
|
+
<p
|
|
177
|
+
className={cn(
|
|
178
|
+
'font-sans text-sm text-muted-foreground',
|
|
179
|
+
className
|
|
180
|
+
)}
|
|
181
|
+
{...props}
|
|
182
|
+
/>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
TypographyMuted.displayName = 'TypographyMuted'
|
|
186
|
+
|
|
187
|
+
export {
|
|
188
|
+
TypographyH1,
|
|
189
|
+
TypographyH2,
|
|
190
|
+
TypographyH3,
|
|
191
|
+
TypographyH4,
|
|
192
|
+
TypographyP,
|
|
193
|
+
TypographyBlockquote,
|
|
194
|
+
TypographyTable,
|
|
195
|
+
TypographyList,
|
|
196
|
+
TypographyInlineCode,
|
|
197
|
+
TypographyLead,
|
|
198
|
+
TypographyLarge,
|
|
199
|
+
TypographySmall,
|
|
200
|
+
TypographyMuted,
|
|
201
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { CollapsibleCard } from '../basic/collapsible-card'
|
|
3
|
+
import { Button } from '../basic/button'
|
|
4
|
+
import { QuestionnaireLine, ArrowDownSLine } from '../basic/icons-inline'
|
|
5
|
+
|
|
6
|
+
const ICON_CLASS = 'w-[var(--font-size-sm)] h-[var(--font-size-sm)] shrink-0 text-text-secondary'
|
|
7
|
+
|
|
8
|
+
export interface AskUserPartProps {
|
|
9
|
+
title?: string
|
|
10
|
+
question: string
|
|
11
|
+
skipText?: string
|
|
12
|
+
runText?: string
|
|
13
|
+
askTimeText?: string
|
|
14
|
+
onSkip?: () => void
|
|
15
|
+
onRun?: () => void
|
|
16
|
+
onAskTimeChange?: (value: string) => void
|
|
17
|
+
contentMaxHeight?: number
|
|
18
|
+
collapsible?: boolean
|
|
19
|
+
className?: string
|
|
20
|
+
headerIcon?: ReactNode
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const defaultHeaderIcon = <QuestionnaireLine className={ICON_CLASS} />
|
|
24
|
+
|
|
25
|
+
export function AskUserPart({
|
|
26
|
+
title = 'Initializing agent…',
|
|
27
|
+
question,
|
|
28
|
+
skipText = 'Skip ⌘⌫',
|
|
29
|
+
runText = 'Run ⌘⏎',
|
|
30
|
+
askTimeText = 'Ask any time',
|
|
31
|
+
onSkip,
|
|
32
|
+
onRun,
|
|
33
|
+
contentMaxHeight,
|
|
34
|
+
collapsible = true,
|
|
35
|
+
className,
|
|
36
|
+
headerIcon,
|
|
37
|
+
}: AskUserPartProps) {
|
|
38
|
+
return (
|
|
39
|
+
<CollapsibleCard
|
|
40
|
+
headerIcon={headerIcon ?? defaultHeaderIcon}
|
|
41
|
+
headerTitle={<span className="text-left">{title}</span>}
|
|
42
|
+
contentPadding="var(--spacing-1\.5) var(--spacing-2)"
|
|
43
|
+
defaultExpanded
|
|
44
|
+
contentMaxHeight={contentMaxHeight}
|
|
45
|
+
collapsible={collapsible}
|
|
46
|
+
footer={
|
|
47
|
+
<>
|
|
48
|
+
<Button variant="ghost" size="sm" className="gap-1 py-0.5 px-0 text-text-secondary hover:text-text">
|
|
49
|
+
<span className="text-xs">{askTimeText}</span>
|
|
50
|
+
<ArrowDownSLine className="w-[var(--font-size-sm)] h-[var(--font-size-sm)] shrink-0" />
|
|
51
|
+
</Button>
|
|
52
|
+
<div className="flex-1" />
|
|
53
|
+
<Button variant="ghost" size="sm" onClick={onSkip} className="bg-fill text-text-tertiary hover:bg-fill-secondary hover:text-text-secondary">
|
|
54
|
+
{skipText}
|
|
55
|
+
</Button>
|
|
56
|
+
<Button variant="secondary" size="sm" onClick={onRun}>
|
|
57
|
+
{runText}
|
|
58
|
+
</Button>
|
|
59
|
+
</>
|
|
60
|
+
}
|
|
61
|
+
className={className}
|
|
62
|
+
>
|
|
63
|
+
<div className="text-sm text-left">
|
|
64
|
+
<span className="text-text">{question}</span>
|
|
65
|
+
</div>
|
|
66
|
+
</CollapsibleCard>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
AskUserPart.displayName = 'AskUserPart'
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { useState, type ReactNode } from 'react'
|
|
2
|
+
import { CollapsibleCard } from '../basic/collapsible-card'
|
|
3
|
+
import { Button } from '../basic/button'
|
|
4
|
+
import {
|
|
5
|
+
Select,
|
|
6
|
+
SelectContent,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectTrigger,
|
|
9
|
+
SelectValue,
|
|
10
|
+
} from '../basic/select'
|
|
11
|
+
import { GlobalLine, WarningLine, Forbid2Line } from '../basic/icons-inline'
|
|
12
|
+
|
|
13
|
+
const ICON_CLASS = 'w-[var(--font-size-sm)] h-[var(--font-size-sm)] shrink-0'
|
|
14
|
+
|
|
15
|
+
export type BrowserActionStatus = 'normal' | 'failed' | 'skipped'
|
|
16
|
+
|
|
17
|
+
export interface BrowserActionPartProps {
|
|
18
|
+
description?: string
|
|
19
|
+
content?: string
|
|
20
|
+
status?: BrowserActionStatus
|
|
21
|
+
onRun?: () => void
|
|
22
|
+
onSkip?: () => void
|
|
23
|
+
onViewDetail?: () => void
|
|
24
|
+
onAskEveryTimeChange?: (value: string) => void
|
|
25
|
+
className?: string
|
|
26
|
+
/** 供 Select 下拉浮层继承布局风格(如 dense),不传则从 document 读取 */
|
|
27
|
+
dataStyle?: string
|
|
28
|
+
/** 供 Select 下拉浮层继承颜色主题,不传则从 document 读取 */
|
|
29
|
+
dataTheme?: string
|
|
30
|
+
normalIcon?: ReactNode
|
|
31
|
+
failedIcon?: ReactNode
|
|
32
|
+
skippedIcon?: ReactNode
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const defaultNormalIcon = <GlobalLine className={`${ICON_CLASS} text-text-secondary`} />
|
|
36
|
+
const defaultFailedIcon = <WarningLine className={`${ICON_CLASS} text-warning`} />
|
|
37
|
+
const defaultSkippedIcon = <Forbid2Line className={`${ICON_CLASS} text-text-secondary`} />
|
|
38
|
+
|
|
39
|
+
function BrowserFooter({
|
|
40
|
+
askValue,
|
|
41
|
+
onAskChange,
|
|
42
|
+
onSkip,
|
|
43
|
+
onRun,
|
|
44
|
+
dataStyle,
|
|
45
|
+
dataTheme,
|
|
46
|
+
}: {
|
|
47
|
+
askValue: string
|
|
48
|
+
onAskChange: (value: string) => void
|
|
49
|
+
onSkip: () => void
|
|
50
|
+
onRun: () => void
|
|
51
|
+
dataStyle?: string
|
|
52
|
+
dataTheme?: string
|
|
53
|
+
}) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
56
|
+
<div className="shrink-0 w-fit">
|
|
57
|
+
<Select value={askValue} onValueChange={onAskChange}>
|
|
58
|
+
<SelectTrigger
|
|
59
|
+
className="h-7 w-auto min-w-0 border-0 bg-transparent px-2 py-0 text-xs text-text-tertiary hover:text-text [&>svg]:w-[var(--font-size-sm)] [&>svg]:h-[var(--font-size-sm)]"
|
|
60
|
+
onClick={(e) => e.stopPropagation()}
|
|
61
|
+
>
|
|
62
|
+
<SelectValue />
|
|
63
|
+
</SelectTrigger>
|
|
64
|
+
<SelectContent
|
|
65
|
+
dataStyle={dataStyle}
|
|
66
|
+
dataTheme={dataTheme}
|
|
67
|
+
onClick={(e) => e.stopPropagation()}
|
|
68
|
+
>
|
|
69
|
+
<SelectItem value="Ask every time">Ask every time</SelectItem>
|
|
70
|
+
<SelectItem value="Always allow">Always allow</SelectItem>
|
|
71
|
+
<SelectItem value="Always deny">Always deny</SelectItem>
|
|
72
|
+
</SelectContent>
|
|
73
|
+
</Select>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="flex-1 h-4 border-l border-border-tertiary opacity-10" />
|
|
76
|
+
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); onSkip() }}>
|
|
77
|
+
Skip ⌘⌫
|
|
78
|
+
</Button>
|
|
79
|
+
<Button variant="secondary" size="sm" onClick={(e) => { e.stopPropagation(); onRun() }}>
|
|
80
|
+
Run ⌘⏎
|
|
81
|
+
</Button>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function BrowserActionPart({
|
|
87
|
+
description,
|
|
88
|
+
content,
|
|
89
|
+
status = 'normal',
|
|
90
|
+
onRun,
|
|
91
|
+
onSkip,
|
|
92
|
+
onViewDetail,
|
|
93
|
+
onAskEveryTimeChange,
|
|
94
|
+
className,
|
|
95
|
+
dataStyle,
|
|
96
|
+
dataTheme,
|
|
97
|
+
normalIcon,
|
|
98
|
+
failedIcon,
|
|
99
|
+
skippedIcon,
|
|
100
|
+
}: BrowserActionPartProps) {
|
|
101
|
+
const [askValue, setAskValue] = useState('Ask every time')
|
|
102
|
+
|
|
103
|
+
const handleAskChange = (value: string) => {
|
|
104
|
+
setAskValue(value)
|
|
105
|
+
onAskEveryTimeChange?.(value)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const headerIcon =
|
|
109
|
+
status === 'failed'
|
|
110
|
+
? (failedIcon ?? defaultFailedIcon)
|
|
111
|
+
: status === 'skipped'
|
|
112
|
+
? (skippedIcon ?? defaultSkippedIcon)
|
|
113
|
+
: (normalIcon ?? defaultNormalIcon)
|
|
114
|
+
|
|
115
|
+
const headerTitle =
|
|
116
|
+
status === 'failed' ? (
|
|
117
|
+
<span>Browser Failed</span>
|
|
118
|
+
) : status === 'skipped' ? (
|
|
119
|
+
<span>Browser Skipped</span>
|
|
120
|
+
) : (
|
|
121
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
122
|
+
<span className="shrink-0">Browser</span>
|
|
123
|
+
{description != null && (
|
|
124
|
+
<span className="flex-1 min-w-0 truncate text-text-tertiary">{description}</span>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const headerRight =
|
|
130
|
+
status === 'failed' ? (
|
|
131
|
+
<Button variant="text" size="sm" onClick={(e) => { e.stopPropagation(); onViewDetail?.() }}>
|
|
132
|
+
View Detail
|
|
133
|
+
</Button>
|
|
134
|
+
) : undefined
|
|
135
|
+
|
|
136
|
+
const footer =
|
|
137
|
+
status === 'normal' ? (
|
|
138
|
+
<BrowserFooter
|
|
139
|
+
askValue={askValue}
|
|
140
|
+
onAskChange={handleAskChange}
|
|
141
|
+
onSkip={() => onSkip?.()}
|
|
142
|
+
onRun={() => onRun?.()}
|
|
143
|
+
dataStyle={dataStyle}
|
|
144
|
+
dataTheme={dataTheme}
|
|
145
|
+
/>
|
|
146
|
+
) : undefined
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<CollapsibleCard
|
|
150
|
+
headerIcon={headerIcon}
|
|
151
|
+
headerTitle={headerTitle}
|
|
152
|
+
headerRight={headerRight}
|
|
153
|
+
contentPadding="var(--spacing-1\.5) var(--spacing-2)"
|
|
154
|
+
defaultExpanded={status === 'normal'}
|
|
155
|
+
collapsible={status === 'normal'}
|
|
156
|
+
footer={footer}
|
|
157
|
+
className={className}
|
|
158
|
+
>
|
|
159
|
+
{content != null && content !== '' && (
|
|
160
|
+
<span className="text-xs leading-xs text-text whitespace-pre-wrap break-words">{content}</span>
|
|
161
|
+
)}
|
|
162
|
+
</CollapsibleCard>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
BrowserActionPart.displayName = 'BrowserActionPart'
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from 'react'
|
|
2
|
+
import { FolderButton } from '../folder-button'
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuTrigger,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuSub,
|
|
9
|
+
DropdownMenuSubTrigger,
|
|
10
|
+
DropdownMenuSubContent,
|
|
11
|
+
} from '../../basic/dropdown-menu'
|
|
12
|
+
import { FolderPermissionDialog } from './folder-permission-dialog'
|
|
13
|
+
import { FolderLine, FolderFillLine, TimeLine } from '../../basic/icons-inline'
|
|
14
|
+
import type { FolderPermissionConfig, InputPropsWithWebkitDirectory } from './types'
|
|
15
|
+
|
|
16
|
+
const MOCK_HISTORY_FOLDERS = ['~/Projects/qoder-work-ui', '~/Documents/workspace', '~/Desktop/demo', '~/code/my-app']
|
|
17
|
+
|
|
18
|
+
const iconClass = 'mr-2 w-4 h-4 shrink-0'
|
|
19
|
+
|
|
20
|
+
export interface ChatInputFolderSelectorProps {
|
|
21
|
+
disabled: boolean
|
|
22
|
+
folderButtonLabel: string
|
|
23
|
+
folderPermission?: FolderPermissionConfig
|
|
24
|
+
onFolderButtonClick?: () => void
|
|
25
|
+
onFolderClear?: () => void
|
|
26
|
+
onFolderSelect: (files: FileList) => void
|
|
27
|
+
hasFolderSelected: boolean
|
|
28
|
+
dataStyle?: string
|
|
29
|
+
dataTheme?: string
|
|
30
|
+
folderIcon?: React.ReactNode
|
|
31
|
+
folderOpenIcon?: React.ReactNode
|
|
32
|
+
historyIcon?: React.ReactNode
|
|
33
|
+
subMenuArrowIcon?: React.ReactNode
|
|
34
|
+
folderFillIcon?: React.ReactNode
|
|
35
|
+
closeIcon?: React.ReactNode
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ChatInputFolderSelector({
|
|
39
|
+
disabled,
|
|
40
|
+
folderButtonLabel,
|
|
41
|
+
folderPermission,
|
|
42
|
+
onFolderButtonClick,
|
|
43
|
+
onFolderClear,
|
|
44
|
+
onFolderSelect,
|
|
45
|
+
hasFolderSelected,
|
|
46
|
+
dataStyle,
|
|
47
|
+
dataTheme,
|
|
48
|
+
folderIcon = <FolderLine className={iconClass} />,
|
|
49
|
+
folderOpenIcon = <FolderFillLine className={iconClass} />,
|
|
50
|
+
historyIcon = <TimeLine className={iconClass} />,
|
|
51
|
+
subMenuArrowIcon,
|
|
52
|
+
folderFillIcon,
|
|
53
|
+
closeIcon,
|
|
54
|
+
}: ChatInputFolderSelectorProps) {
|
|
55
|
+
const enableFolderPermissionDialog = folderPermission?.enable ?? false
|
|
56
|
+
const folderPermissionProjectName = folderPermission?.projectName ?? '04_代码'
|
|
57
|
+
const [folderPermissionOpen, setFolderPermissionOpen] = useState(false)
|
|
58
|
+
const [folderPermissionAlwaysAllow, setFolderPermissionAlwaysAllow] = useState(false)
|
|
59
|
+
const [folderMenuOpen, setFolderMenuOpen] = useState(false)
|
|
60
|
+
const folderInputRef = useRef<HTMLInputElement>(null)
|
|
61
|
+
|
|
62
|
+
const handleFolderButtonClick = onFolderButtonClick ?? (() => {})
|
|
63
|
+
|
|
64
|
+
const openFolderPicker = () => {
|
|
65
|
+
folderInputRef.current?.click()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const handleFolderClear = () => {
|
|
69
|
+
if (folderInputRef.current) folderInputRef.current.value = ''
|
|
70
|
+
onFolderClear?.()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const requestFolderPermission = useCallback((mode: 'once' | 'always') => {
|
|
74
|
+
if (mode === 'always') setFolderPermissionAlwaysAllow(true)
|
|
75
|
+
setFolderPermissionOpen(false)
|
|
76
|
+
setFolderMenuOpen(true)
|
|
77
|
+
}, [])
|
|
78
|
+
|
|
79
|
+
const handleFolderMenuOpenChange = (next: boolean) => {
|
|
80
|
+
if (!enableFolderPermissionDialog) {
|
|
81
|
+
setFolderMenuOpen(next)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
if (!next) {
|
|
85
|
+
setFolderMenuOpen(false)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
if (folderPermissionAlwaysAllow) {
|
|
89
|
+
setFolderMenuOpen(true)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
setFolderPermissionOpen(true)
|
|
93
|
+
setFolderMenuOpen(false)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const handleFolderInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
97
|
+
const files = e.target.files
|
|
98
|
+
if (files && files.length > 0) onFolderSelect(files)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (onFolderButtonClick) {
|
|
102
|
+
return (
|
|
103
|
+
<FolderButton
|
|
104
|
+
label={folderButtonLabel}
|
|
105
|
+
onClick={handleFolderButtonClick}
|
|
106
|
+
disabled={disabled}
|
|
107
|
+
folderIcon={folderIcon}
|
|
108
|
+
folderFillIcon={folderFillIcon}
|
|
109
|
+
/>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<>
|
|
115
|
+
<DropdownMenu open={folderMenuOpen} onOpenChange={handleFolderMenuOpenChange}>
|
|
116
|
+
<DropdownMenuTrigger asChild>
|
|
117
|
+
<FolderButton
|
|
118
|
+
label={folderButtonLabel}
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
selected={hasFolderSelected}
|
|
121
|
+
onClear={handleFolderClear}
|
|
122
|
+
folderIcon={folderIcon}
|
|
123
|
+
folderFillIcon={folderFillIcon}
|
|
124
|
+
closeIcon={closeIcon}
|
|
125
|
+
className="focus:outline-none focus-visible:outline-none focus:ring-0 focus-visible:ring-0"
|
|
126
|
+
/>
|
|
127
|
+
</DropdownMenuTrigger>
|
|
128
|
+
<DropdownMenuContent
|
|
129
|
+
align="start"
|
|
130
|
+
side="top"
|
|
131
|
+
className="min-w-50"
|
|
132
|
+
dataStyle={dataStyle}
|
|
133
|
+
dataTheme={dataTheme}
|
|
134
|
+
>
|
|
135
|
+
<DropdownMenuItem onClick={openFolderPicker}>
|
|
136
|
+
{folderIcon}
|
|
137
|
+
Add folder
|
|
138
|
+
</DropdownMenuItem>
|
|
139
|
+
{hasFolderSelected && (
|
|
140
|
+
<DropdownMenuItem onClick={openFolderPicker}>
|
|
141
|
+
{folderOpenIcon}
|
|
142
|
+
Replace folder
|
|
143
|
+
</DropdownMenuItem>
|
|
144
|
+
)}
|
|
145
|
+
<DropdownMenuSub>
|
|
146
|
+
<DropdownMenuSubTrigger subTriggerIcon={subMenuArrowIcon}>
|
|
147
|
+
{historyIcon}
|
|
148
|
+
Recent folders
|
|
149
|
+
</DropdownMenuSubTrigger>
|
|
150
|
+
<DropdownMenuSubContent
|
|
151
|
+
collisionPadding={{ bottom: 24, left: 8 }}
|
|
152
|
+
dataStyle={dataStyle}
|
|
153
|
+
dataTheme={dataTheme}
|
|
154
|
+
>
|
|
155
|
+
{MOCK_HISTORY_FOLDERS.map((path) => (
|
|
156
|
+
<DropdownMenuItem key={path} onClick={() => {}}>
|
|
157
|
+
{path}
|
|
158
|
+
</DropdownMenuItem>
|
|
159
|
+
))}
|
|
160
|
+
</DropdownMenuSubContent>
|
|
161
|
+
</DropdownMenuSub>
|
|
162
|
+
</DropdownMenuContent>
|
|
163
|
+
</DropdownMenu>
|
|
164
|
+
<input
|
|
165
|
+
ref={folderInputRef}
|
|
166
|
+
type="file"
|
|
167
|
+
{...({ webkitdirectory: '' } as InputPropsWithWebkitDirectory)}
|
|
168
|
+
multiple
|
|
169
|
+
style={{ display: 'none' }}
|
|
170
|
+
onChange={handleFolderInputChange}
|
|
171
|
+
/>
|
|
172
|
+
{enableFolderPermissionDialog && (
|
|
173
|
+
<FolderPermissionDialog
|
|
174
|
+
open={folderPermissionOpen}
|
|
175
|
+
onOpenChange={setFolderPermissionOpen}
|
|
176
|
+
projectName={folderPermissionProjectName}
|
|
177
|
+
onAllowOnce={() => requestFolderPermission('once')}
|
|
178
|
+
onAllowAlways={() => requestFolderPermission('always')}
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
</>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
ChatInputFolderSelector.displayName = 'ChatInputFolderSelector'
|