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,189 @@
|
|
|
1
|
+
import React, { createContext, useContext, useCallback, useRef, useState } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import type { ChatInputProps, FolderPermissionConfig, GenerationStatusConfig, FooterLeftModelSwitchConfig } from './types'
|
|
4
|
+
|
|
5
|
+
export interface ChatInputContextValue {
|
|
6
|
+
// Portal 主题透传(供 DropdownMenuContent/SubContent 等弹层继承)
|
|
7
|
+
dataStyle?: string
|
|
8
|
+
dataTheme?: string
|
|
9
|
+
// 输入与发送
|
|
10
|
+
value: string
|
|
11
|
+
onChange: (value: string) => void
|
|
12
|
+
onSend?: (value: string) => void
|
|
13
|
+
disabled: boolean
|
|
14
|
+
form?: string
|
|
15
|
+
textareaRef: React.RefObject<HTMLTextAreaElement | null>
|
|
16
|
+
setTextareaRef: (node: HTMLTextAreaElement | null) => void
|
|
17
|
+
handleSend: () => void
|
|
18
|
+
handleKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
|
|
19
|
+
sendButtonStatus: 'normal' | 'generating'
|
|
20
|
+
canSend: boolean
|
|
21
|
+
// 文件夹 / 更多 / 模式切换(默认操作区左侧)
|
|
22
|
+
hasFolderSelected: boolean
|
|
23
|
+
setHasFolderSelected: (v: boolean) => void
|
|
24
|
+
handleFolderSelect: (files: FileList) => void
|
|
25
|
+
handleFolderClear: () => void
|
|
26
|
+
folderPermission?: FolderPermissionConfig
|
|
27
|
+
footerLeftConfig?: FooterLeftModelSwitchConfig
|
|
28
|
+
showFolderButton: boolean
|
|
29
|
+
showMoreButton: boolean
|
|
30
|
+
folderButtonLabel: string
|
|
31
|
+
onFolderButtonClick?: () => void
|
|
32
|
+
onFolderClear?: () => void
|
|
33
|
+
onMoreButtonClick?: () => void
|
|
34
|
+
addLargeIcon?: ReactNode
|
|
35
|
+
folderIcon?: ReactNode
|
|
36
|
+
folderOpenIcon?: ReactNode
|
|
37
|
+
historyIcon?: ReactNode
|
|
38
|
+
arrowDownIcon?: ReactNode
|
|
39
|
+
subMenuArrowIcon?: ReactNode
|
|
40
|
+
folderFillIcon?: ReactNode
|
|
41
|
+
closeIcon?: ReactNode
|
|
42
|
+
// 框外 / 框内显隐(默认布局用)
|
|
43
|
+
showGenerationStatus?: boolean
|
|
44
|
+
generationStatus?: GenerationStatusConfig
|
|
45
|
+
attachments?: { files?: ReactNode; images?: ReactNode } | ReactNode
|
|
46
|
+
showFooter?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ChatInputContext = createContext<ChatInputContextValue | null>(null)
|
|
50
|
+
|
|
51
|
+
export function useChatInputContext() {
|
|
52
|
+
const ctx = useContext(ChatInputContext)
|
|
53
|
+
if (!ctx) throw new Error('ChatInput compound components must be used within ChatInput.Root')
|
|
54
|
+
return ctx
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type ChatInputRootProps = ChatInputProps & { children?: ReactNode }
|
|
58
|
+
|
|
59
|
+
export function ChatInputRootProvider({
|
|
60
|
+
value: controlledValue,
|
|
61
|
+
onChange,
|
|
62
|
+
onSend,
|
|
63
|
+
placeholder = 'What can I help you with today?',
|
|
64
|
+
disabled = false,
|
|
65
|
+
maxRows = 6,
|
|
66
|
+
form,
|
|
67
|
+
sendButtonStatus = 'normal',
|
|
68
|
+
showFooter = true,
|
|
69
|
+
showFolderButton = true,
|
|
70
|
+
folderButtonLabel = 'Work in a Folder',
|
|
71
|
+
onFolderButtonClick,
|
|
72
|
+
onFolderClear,
|
|
73
|
+
folderPermission,
|
|
74
|
+
showMoreButton = true,
|
|
75
|
+
onMoreButtonClick,
|
|
76
|
+
footerLeftConfig,
|
|
77
|
+
showGenerationStatus = false,
|
|
78
|
+
generationStatus,
|
|
79
|
+
attachments,
|
|
80
|
+
addLargeIcon,
|
|
81
|
+
folderIcon,
|
|
82
|
+
folderOpenIcon,
|
|
83
|
+
historyIcon,
|
|
84
|
+
arrowDownIcon,
|
|
85
|
+
subMenuArrowIcon,
|
|
86
|
+
folderFillIcon,
|
|
87
|
+
closeIcon,
|
|
88
|
+
dataStyle,
|
|
89
|
+
dataTheme,
|
|
90
|
+
className,
|
|
91
|
+
children,
|
|
92
|
+
...rest
|
|
93
|
+
}: ChatInputRootProps) {
|
|
94
|
+
const [internalValue, setInternalValue] = useState('')
|
|
95
|
+
const textareaRef = useRef<HTMLTextAreaElement | null>(null)
|
|
96
|
+
const [hasFolderSelected, setHasFolderSelected] = useState(false)
|
|
97
|
+
const isControlled = controlledValue !== undefined
|
|
98
|
+
const value = isControlled ? controlledValue : internalValue
|
|
99
|
+
|
|
100
|
+
const setTextareaRef = useCallback((node: HTMLTextAreaElement | null) => {
|
|
101
|
+
(textareaRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node
|
|
102
|
+
}, [])
|
|
103
|
+
|
|
104
|
+
const handleChange = useCallback(
|
|
105
|
+
(v: string) => {
|
|
106
|
+
if (!isControlled) setInternalValue(v)
|
|
107
|
+
onChange?.(v)
|
|
108
|
+
},
|
|
109
|
+
[isControlled, onChange]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const handleSend = useCallback(() => {
|
|
113
|
+
const raw = textareaRef.current?.value ?? value
|
|
114
|
+
const trimmed = (raw ?? '').trim()
|
|
115
|
+
if (!trimmed || disabled || !onSend) return
|
|
116
|
+
onSend(trimmed)
|
|
117
|
+
if (!isControlled) setInternalValue('')
|
|
118
|
+
if (textareaRef.current) textareaRef.current.style.height = 'auto'
|
|
119
|
+
}, [value, disabled, onSend, isControlled])
|
|
120
|
+
|
|
121
|
+
const handleKeyDown = useCallback(
|
|
122
|
+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
123
|
+
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
124
|
+
e.preventDefault()
|
|
125
|
+
handleSend()
|
|
126
|
+
}
|
|
127
|
+
rest.onKeyDown?.(e)
|
|
128
|
+
},
|
|
129
|
+
[handleSend, rest]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const handleFolderSelect = useCallback(() => setHasFolderSelected(true), [])
|
|
133
|
+
const handleFolderClear = useCallback(() => {
|
|
134
|
+
setHasFolderSelected(false)
|
|
135
|
+
onFolderClear?.()
|
|
136
|
+
}, [onFolderClear])
|
|
137
|
+
|
|
138
|
+
const canSend = value.trim().length > 0 && !disabled
|
|
139
|
+
|
|
140
|
+
const ctxValue: ChatInputContextValue = {
|
|
141
|
+
dataStyle,
|
|
142
|
+
dataTheme,
|
|
143
|
+
value,
|
|
144
|
+
onChange: handleChange,
|
|
145
|
+
onSend,
|
|
146
|
+
disabled,
|
|
147
|
+
form,
|
|
148
|
+
textareaRef,
|
|
149
|
+
setTextareaRef,
|
|
150
|
+
handleSend,
|
|
151
|
+
handleKeyDown,
|
|
152
|
+
sendButtonStatus,
|
|
153
|
+
canSend,
|
|
154
|
+
hasFolderSelected,
|
|
155
|
+
setHasFolderSelected,
|
|
156
|
+
handleFolderSelect,
|
|
157
|
+
handleFolderClear,
|
|
158
|
+
folderPermission,
|
|
159
|
+
footerLeftConfig,
|
|
160
|
+
showFolderButton,
|
|
161
|
+
showMoreButton,
|
|
162
|
+
folderButtonLabel,
|
|
163
|
+
onFolderButtonClick,
|
|
164
|
+
onFolderClear,
|
|
165
|
+
onMoreButtonClick,
|
|
166
|
+
addLargeIcon,
|
|
167
|
+
folderIcon,
|
|
168
|
+
folderOpenIcon,
|
|
169
|
+
historyIcon,
|
|
170
|
+
arrowDownIcon,
|
|
171
|
+
subMenuArrowIcon,
|
|
172
|
+
folderFillIcon,
|
|
173
|
+
closeIcon,
|
|
174
|
+
showGenerationStatus,
|
|
175
|
+
generationStatus,
|
|
176
|
+
attachments,
|
|
177
|
+
showFooter,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<ChatInputContext.Provider value={ctxValue}>
|
|
182
|
+
{children}
|
|
183
|
+
</ChatInputContext.Provider>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
ChatInputRootProvider.displayName = 'ChatInputRootProvider'
|
|
188
|
+
|
|
189
|
+
export { ChatInputContext }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlertDialog,
|
|
3
|
+
AlertDialogContent,
|
|
4
|
+
AlertDialogHeader,
|
|
5
|
+
AlertDialogTitle,
|
|
6
|
+
AlertDialogDescription,
|
|
7
|
+
AlertDialogFooter,
|
|
8
|
+
AlertDialogCancel,
|
|
9
|
+
} from '../../basic/alert-dialog'
|
|
10
|
+
|
|
11
|
+
export interface FolderPermissionDialogProps {
|
|
12
|
+
open: boolean
|
|
13
|
+
onOpenChange: (open: boolean) => void
|
|
14
|
+
projectName: string
|
|
15
|
+
onAllowOnce: () => void
|
|
16
|
+
onAllowAlways: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function FolderPermissionDialog({
|
|
20
|
+
open,
|
|
21
|
+
onOpenChange,
|
|
22
|
+
projectName,
|
|
23
|
+
onAllowOnce,
|
|
24
|
+
onAllowAlways,
|
|
25
|
+
}: FolderPermissionDialogProps) {
|
|
26
|
+
return (
|
|
27
|
+
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
28
|
+
<AlertDialogContent className="p-6">
|
|
29
|
+
<AlertDialogHeader>
|
|
30
|
+
<AlertDialogTitle className="text-xl leading-xl font-semibold text-text">
|
|
31
|
+
Allow Qoder Work to access and modify files in "{projectName}"?
|
|
32
|
+
</AlertDialogTitle>
|
|
33
|
+
<AlertDialogDescription className="mt-2 text-sm leading-sm text-text-tertiary">
|
|
34
|
+
This permission applies to all files and subfolders in this directory. Qoder Work may read, update, and
|
|
35
|
+
permanently delete files here, and may share relevant file contents with connected tools when needed.
|
|
36
|
+
Only allow access to folders you trust.
|
|
37
|
+
</AlertDialogDescription>
|
|
38
|
+
</AlertDialogHeader>
|
|
39
|
+
<AlertDialogFooter className="mt-6 flex flex-row justify-end gap-2">
|
|
40
|
+
<AlertDialogCancel className="h-9 rounded-full px-4">Cancel</AlertDialogCancel>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
className="inline-flex h-9 items-center justify-center rounded-full px-4 text-sm leading-sm font-medium border border-border bg-bg-base text-text transition-colors hover:bg-fill-tertiary active:bg-fill-secondary"
|
|
44
|
+
onClick={onAllowAlways}
|
|
45
|
+
>
|
|
46
|
+
Always Allow
|
|
47
|
+
</button>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
className="inline-flex h-9 items-center justify-center rounded-full px-4 text-sm leading-sm font-medium bg-text text-text-on-primary transition-opacity hover:opacity-90 active:opacity-95"
|
|
51
|
+
onClick={onAllowOnce}
|
|
52
|
+
>
|
|
53
|
+
Allow
|
|
54
|
+
</button>
|
|
55
|
+
</AlertDialogFooter>
|
|
56
|
+
</AlertDialogContent>
|
|
57
|
+
</AlertDialog>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
FolderPermissionDialog.displayName = 'FolderPermissionDialog'
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { ChatInputRootProvider } from './context'
|
|
4
|
+
import {
|
|
5
|
+
ChatInputAbove,
|
|
6
|
+
ChatInputBox,
|
|
7
|
+
ChatInputAttachments,
|
|
8
|
+
ChatInputInput,
|
|
9
|
+
ChatInputActions,
|
|
10
|
+
ChatInputActionsLeft,
|
|
11
|
+
ChatInputActionsRight,
|
|
12
|
+
ChatInputExpandButton,
|
|
13
|
+
ChatInputFolderButton,
|
|
14
|
+
ChatInputMoreButton,
|
|
15
|
+
ChatInputSendButton,
|
|
16
|
+
ChatInputFooterLeft,
|
|
17
|
+
ChatInputDefaultLayout,
|
|
18
|
+
} from './compound'
|
|
19
|
+
import type { ChatInputProps } from './types'
|
|
20
|
+
|
|
21
|
+
export type {
|
|
22
|
+
ChatInputProps,
|
|
23
|
+
FolderPermissionConfig,
|
|
24
|
+
GenerationStatusConfig,
|
|
25
|
+
FooterLeftModelSwitchConfig,
|
|
26
|
+
FooterLeftModelSwitchItem,
|
|
27
|
+
} from './types'
|
|
28
|
+
|
|
29
|
+
function hasAttachmentsValue(attachments: ChatInputProps['attachments']): boolean {
|
|
30
|
+
if (attachments == null || attachments === false) return false
|
|
31
|
+
if (typeof attachments === 'object' && !React.isValidElement(attachments)) {
|
|
32
|
+
return !!(attachments as { files?: ReactNode }).files || !!(attachments as { images?: ReactNode }).images
|
|
33
|
+
}
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ChatInputCompound {
|
|
38
|
+
Root: typeof ChatInputRootProvider
|
|
39
|
+
Above: typeof ChatInputAbove
|
|
40
|
+
Box: typeof ChatInputBox
|
|
41
|
+
Attachments: typeof ChatInputAttachments
|
|
42
|
+
Input: typeof ChatInputInput
|
|
43
|
+
Actions: typeof ChatInputActions
|
|
44
|
+
ActionsLeft: typeof ChatInputActionsLeft
|
|
45
|
+
ActionsRight: typeof ChatInputActionsRight
|
|
46
|
+
ExpandButton: typeof ChatInputExpandButton
|
|
47
|
+
FolderButton: typeof ChatInputFolderButton
|
|
48
|
+
MoreButton: typeof ChatInputMoreButton
|
|
49
|
+
SendButton: typeof ChatInputSendButton
|
|
50
|
+
FooterLeft: typeof ChatInputFooterLeft
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ChatInputBase = forwardRef<HTMLTextAreaElement, ChatInputProps>(function ChatInput(props, ref) {
|
|
54
|
+
// ChatInput = 复合组件的默认组合(Root + DefaultLayout),便捷导出;主 API 为复合组件
|
|
55
|
+
const hasAttachments = hasAttachmentsValue(props.attachments)
|
|
56
|
+
return (
|
|
57
|
+
<ChatInputRootProvider {...props}>
|
|
58
|
+
<ChatInputDefaultLayout
|
|
59
|
+
ref={ref}
|
|
60
|
+
className={props.className}
|
|
61
|
+
hasAbove={props.showGenerationStatus}
|
|
62
|
+
hasAttachments={hasAttachments}
|
|
63
|
+
hasFooter={props.showFooter}
|
|
64
|
+
aboveOverlap
|
|
65
|
+
/>
|
|
66
|
+
</ChatInputRootProvider>
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// 挂载复合组件后断言为带命名空间的类型,便于赋值时通过类型检查
|
|
71
|
+
type ChatInputBaseType = React.ForwardRefExoticComponent<
|
|
72
|
+
ChatInputProps & React.RefAttributes<HTMLTextAreaElement>
|
|
73
|
+
> &
|
|
74
|
+
Partial<ChatInputCompound>
|
|
75
|
+
|
|
76
|
+
const ChatInputBaseWithSlots = ChatInputBase as ChatInputBaseType
|
|
77
|
+
ChatInputBaseWithSlots.displayName = 'ChatInput'
|
|
78
|
+
ChatInputBaseWithSlots.Root = ChatInputRootProvider
|
|
79
|
+
ChatInputBaseWithSlots.Above = ChatInputAbove
|
|
80
|
+
ChatInputBaseWithSlots.Box = ChatInputBox
|
|
81
|
+
ChatInputBaseWithSlots.Attachments = ChatInputAttachments
|
|
82
|
+
ChatInputBaseWithSlots.Input = ChatInputInput
|
|
83
|
+
ChatInputBaseWithSlots.Actions = ChatInputActions
|
|
84
|
+
ChatInputBaseWithSlots.ActionsLeft = ChatInputActionsLeft
|
|
85
|
+
ChatInputBaseWithSlots.ActionsRight = ChatInputActionsRight
|
|
86
|
+
ChatInputBaseWithSlots.ExpandButton = ChatInputExpandButton
|
|
87
|
+
ChatInputBaseWithSlots.FolderButton = ChatInputFolderButton
|
|
88
|
+
ChatInputBaseWithSlots.MoreButton = ChatInputMoreButton
|
|
89
|
+
ChatInputBaseWithSlots.SendButton = ChatInputSendButton
|
|
90
|
+
ChatInputBaseWithSlots.FooterLeft = ChatInputFooterLeft
|
|
91
|
+
|
|
92
|
+
export const ChatInput = ChatInputBaseWithSlots as React.ForwardRefExoticComponent<
|
|
93
|
+
ChatInputProps & React.RefAttributes<HTMLTextAreaElement>
|
|
94
|
+
> &
|
|
95
|
+
ChatInputCompound
|
|
96
|
+
|
|
97
|
+
// 扁平命名导出(Root、Above、Box、Attachments、Input、Actions 等)
|
|
98
|
+
export { ChatInputRootProvider as ChatInputRoot } from './context'
|
|
99
|
+
export {
|
|
100
|
+
ChatInputAbove,
|
|
101
|
+
ChatInputBox,
|
|
102
|
+
ChatInputAttachments,
|
|
103
|
+
ChatInputInput,
|
|
104
|
+
ChatInputActions,
|
|
105
|
+
ChatInputActionsLeft,
|
|
106
|
+
ChatInputActionsRight,
|
|
107
|
+
ChatInputExpandButton,
|
|
108
|
+
ChatInputFolderButton,
|
|
109
|
+
ChatInputMoreButton,
|
|
110
|
+
ChatInputSendButton,
|
|
111
|
+
ChatInputFooterLeft,
|
|
112
|
+
} from './compound'
|
|
113
|
+
export type { ChatInputRootProps } from './context'
|
|
114
|
+
export type {
|
|
115
|
+
ChatInputAboveProps,
|
|
116
|
+
ChatInputBoxProps,
|
|
117
|
+
ChatInputAttachmentsProps,
|
|
118
|
+
ChatInputInputProps,
|
|
119
|
+
ChatInputActionsProps,
|
|
120
|
+
ChatInputActionsLeftProps,
|
|
121
|
+
ChatInputActionsRightProps,
|
|
122
|
+
ChatInputExpandButtonProps,
|
|
123
|
+
} from './compound'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { TextareaHTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export type InputPropsWithWebkitDirectory = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
4
|
+
webkitdirectory?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface FolderPermissionConfig {
|
|
8
|
+
enable?: boolean
|
|
9
|
+
projectName?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GenerationStatusConfig {
|
|
13
|
+
content?: ReactNode
|
|
14
|
+
icon?: ReactNode
|
|
15
|
+
time?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FooterLeftModelSwitchItem {
|
|
19
|
+
value: string
|
|
20
|
+
label: string
|
|
21
|
+
icon?: ReactNode
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FooterLeftModelSwitchConfig {
|
|
25
|
+
modeSwitch: {
|
|
26
|
+
icon?: ReactNode
|
|
27
|
+
label: string
|
|
28
|
+
value: string
|
|
29
|
+
options: FooterLeftModelSwitchItem[]
|
|
30
|
+
onChange: (value: string) => void
|
|
31
|
+
}
|
|
32
|
+
modelSwitch: {
|
|
33
|
+
label: string
|
|
34
|
+
value: string
|
|
35
|
+
options: FooterLeftModelSwitchItem[]
|
|
36
|
+
onChange: (value: string) => void
|
|
37
|
+
}
|
|
38
|
+
consumptionMultiplier: {
|
|
39
|
+
value: string
|
|
40
|
+
options?: FooterLeftModelSwitchItem[]
|
|
41
|
+
onChange?: (value: string) => void
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ChatInputProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange' | 'value'> {
|
|
46
|
+
value?: string
|
|
47
|
+
onChange?: (value: string) => void
|
|
48
|
+
onSend?: (value: string) => void
|
|
49
|
+
placeholder?: string
|
|
50
|
+
disabled?: boolean
|
|
51
|
+
maxRows?: number
|
|
52
|
+
form?: string
|
|
53
|
+
attachments?: { files?: ReactNode; images?: ReactNode } | ReactNode
|
|
54
|
+
showFooter?: boolean
|
|
55
|
+
showFolderButton?: boolean
|
|
56
|
+
folderButtonLabel?: string
|
|
57
|
+
onFolderButtonClick?: () => void
|
|
58
|
+
onFolderClear?: () => void
|
|
59
|
+
folderPermission?: FolderPermissionConfig
|
|
60
|
+
showMoreButton?: boolean
|
|
61
|
+
onMoreButtonClick?: () => void
|
|
62
|
+
footerLeftConfig?: FooterLeftModelSwitchConfig
|
|
63
|
+
sendButtonStatus?: 'normal' | 'generating'
|
|
64
|
+
showGenerationStatus?: boolean
|
|
65
|
+
generationStatus?: GenerationStatusConfig
|
|
66
|
+
addLargeIcon?: ReactNode
|
|
67
|
+
folderIcon?: ReactNode
|
|
68
|
+
folderOpenIcon?: ReactNode
|
|
69
|
+
historyIcon?: ReactNode
|
|
70
|
+
arrowDownIcon?: ReactNode
|
|
71
|
+
subMenuArrowIcon?: ReactNode
|
|
72
|
+
folderFillIcon?: ReactNode
|
|
73
|
+
closeIcon?: ReactNode
|
|
74
|
+
/** 供 Portal 浮层继承(Showcase 预览区等场景),不传则由 dropdown-menu 使用 getThemeFromDocument() 兜底 */
|
|
75
|
+
dataStyle?: string
|
|
76
|
+
dataTheme?: string
|
|
77
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, type RefObject } from 'react'
|
|
2
|
+
|
|
3
|
+
export function useAutoResizeTextarea(
|
|
4
|
+
textareaRef: RefObject<HTMLTextAreaElement | null>,
|
|
5
|
+
value: string,
|
|
6
|
+
maxRows: number
|
|
7
|
+
): void {
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const textarea = textareaRef.current
|
|
10
|
+
if (!textarea) return
|
|
11
|
+
textarea.style.height = 'auto'
|
|
12
|
+
const lineHeight = parseFloat(getComputedStyle(textarea).lineHeight) || 24
|
|
13
|
+
const maxHeight = lineHeight * maxRows
|
|
14
|
+
if (textarea.scrollHeight <= maxHeight) {
|
|
15
|
+
textarea.style.height = `${textarea.scrollHeight}px`
|
|
16
|
+
} else {
|
|
17
|
+
textarea.style.height = `${maxHeight}px`
|
|
18
|
+
}
|
|
19
|
+
}, [textareaRef, value, maxRows])
|
|
20
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useMemo, type ReactNode } from 'react'
|
|
2
|
+
import { motion } from 'framer-motion'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import { Highlight, Prism } from 'prism-react-renderer'
|
|
5
|
+
import { CollapsibleCard } from '../basic/collapsible-card'
|
|
6
|
+
import { FileLine, LoaderLine } from '../basic/icons-inline'
|
|
7
|
+
import { getFileIconColorClass } from '../lib/file-icon-maps'
|
|
8
|
+
|
|
9
|
+
const ICON_CLASS = 'w-[var(--font-size-sm)] h-[var(--font-size-sm)] shrink-0'
|
|
10
|
+
|
|
11
|
+
/** Prism 主题:使用设计令牌,随 data-theme 生效 */
|
|
12
|
+
const codeBlockTheme = {
|
|
13
|
+
plain: { color: 'var(--color-text)' },
|
|
14
|
+
styles: [
|
|
15
|
+
{ types: ['comment', 'prolog', 'cdata'], style: { color: 'var(--color-text-tertiary)' } },
|
|
16
|
+
{ types: ['keyword', 'atrule'], style: { color: 'var(--color-blue)' } },
|
|
17
|
+
{ types: ['string', 'attr-value', 'inserted'], style: { color: 'var(--color-success)' } },
|
|
18
|
+
{ types: ['number', 'constant', 'boolean'], style: { color: 'var(--color-success)' } },
|
|
19
|
+
{ types: ['function', 'function-variable'], style: { color: 'var(--color-purple)' } },
|
|
20
|
+
{ types: ['punctuation', 'operator', 'deleted'], style: { color: 'var(--color-text-tertiary)' } },
|
|
21
|
+
{ types: ['property', 'tag', 'selector'], style: { color: 'var(--color-pink)' } },
|
|
22
|
+
{ types: ['regex', 'important', 'variable'], style: { color: 'var(--color-text)' } },
|
|
23
|
+
],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const LANG_EXT: Record<string, string> = {
|
|
27
|
+
javascript: 'js', typescript: 'ts', jsx: 'jsx', tsx: 'tsx',
|
|
28
|
+
python: 'py', java: 'java', go: 'go', rust: 'rs', html: 'html', css: 'css',
|
|
29
|
+
json: 'json', yaml: 'yml', markdown: 'md', shell: 'sh', bash: 'sh',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type CodeBlockDiffType = 'add' | 'remove'
|
|
33
|
+
|
|
34
|
+
export interface CodeBlockPartProps {
|
|
35
|
+
language?: string
|
|
36
|
+
code?: string
|
|
37
|
+
filename?: string
|
|
38
|
+
diffType?: CodeBlockDiffType | null
|
|
39
|
+
diffLines?: number | null
|
|
40
|
+
isGenerating?: boolean
|
|
41
|
+
defaultExpanded?: boolean
|
|
42
|
+
className?: string
|
|
43
|
+
headerIcon?: ReactNode
|
|
44
|
+
getFileIcon?: (fileType?: string, filename?: string) => ReactNode
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getDisplayFilename(filename?: string, language?: string): string {
|
|
48
|
+
if (filename && filename.trim()) return filename
|
|
49
|
+
const lang = (language ?? 'text').toLowerCase()
|
|
50
|
+
const ext = LANG_EXT[lang] ?? 'txt'
|
|
51
|
+
return `${lang}.${ext}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function CodeBlockPart({
|
|
55
|
+
language = 'javascript',
|
|
56
|
+
code = '',
|
|
57
|
+
filename,
|
|
58
|
+
diffType = null,
|
|
59
|
+
diffLines = null,
|
|
60
|
+
isGenerating = false,
|
|
61
|
+
defaultExpanded = true,
|
|
62
|
+
className,
|
|
63
|
+
headerIcon,
|
|
64
|
+
getFileIcon,
|
|
65
|
+
}: CodeBlockPartProps) {
|
|
66
|
+
const displayName = useMemo(
|
|
67
|
+
() => getDisplayFilename(filename, language),
|
|
68
|
+
[filename, language]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const calculatedDiffLines = useMemo(() => {
|
|
72
|
+
if (diffType && code) {
|
|
73
|
+
const lines = code.split('\n')
|
|
74
|
+
return lines.length
|
|
75
|
+
}
|
|
76
|
+
return diffLines ?? null
|
|
77
|
+
}, [code, diffType, diffLines])
|
|
78
|
+
|
|
79
|
+
const fileType = language?.toLowerCase()
|
|
80
|
+
const defaultHeaderIcon = isGenerating ? (
|
|
81
|
+
<motion.div
|
|
82
|
+
animate={{ rotate: 360 }}
|
|
83
|
+
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
|
84
|
+
>
|
|
85
|
+
<LoaderLine className={cn(ICON_CLASS, 'text-text-secondary')} />
|
|
86
|
+
</motion.div>
|
|
87
|
+
) : getFileIcon ? (
|
|
88
|
+
getFileIcon(fileType, displayName)
|
|
89
|
+
) : (
|
|
90
|
+
<span className={cn(getFileIconColorClass(fileType, displayName))}>
|
|
91
|
+
<FileLine className={ICON_CLASS} />
|
|
92
|
+
</span>
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const headerRight =
|
|
96
|
+
diffType && calculatedDiffLines != null ? (
|
|
97
|
+
<span
|
|
98
|
+
className={cn(
|
|
99
|
+
'font-normal text-xs leading-xs',
|
|
100
|
+
diffType === 'add' ? 'text-success' : 'text-error'
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
{diffType === 'add' ? '+' : '-'}{calculatedDiffLines}
|
|
104
|
+
</span>
|
|
105
|
+
) : null
|
|
106
|
+
|
|
107
|
+
const contentClass = cn(
|
|
108
|
+
'overflow-x-auto text-left scrollbar-auto',
|
|
109
|
+
diffType === 'add' &&
|
|
110
|
+
'bg-success-bg border-l-[3px] border-l-[var(--color-success-border)] -ml-5 pl-5',
|
|
111
|
+
diffType === 'remove' &&
|
|
112
|
+
'bg-error-bg border-l-[3px] border-l-[var(--color-error-border)] -ml-5 pl-5'
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const lang = (language ?? 'text').toLowerCase()
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<CollapsibleCard
|
|
119
|
+
headerIcon={headerIcon ?? defaultHeaderIcon}
|
|
120
|
+
headerTitle={<span className="font-normal">{displayName}</span>}
|
|
121
|
+
headerRight={headerRight}
|
|
122
|
+
defaultExpanded={defaultExpanded}
|
|
123
|
+
contentPadding="var(--spacing-2) 0 var(--spacing-2) var(--spacing-5)"
|
|
124
|
+
titleShimmer={isGenerating}
|
|
125
|
+
className={className}
|
|
126
|
+
>
|
|
127
|
+
<div className={contentClass}>
|
|
128
|
+
<Highlight theme={codeBlockTheme} code={code} language={lang}>
|
|
129
|
+
{({ className: preClassName, style, tokens, getLineProps, getTokenProps }) => (
|
|
130
|
+
<pre
|
|
131
|
+
className={cn(preClassName, 'm-0 text-left bg-transparent font-mono text-xs font-normal leading-xs whitespace-pre')}
|
|
132
|
+
style={style}
|
|
133
|
+
>
|
|
134
|
+
<code className="text-left block">
|
|
135
|
+
{tokens.map((line, i) => (
|
|
136
|
+
<div key={i} {...getLineProps({ line })}>
|
|
137
|
+
{line.map((token, k) => (
|
|
138
|
+
<span key={k} {...getTokenProps({ token })} />
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
))}
|
|
142
|
+
</code>
|
|
143
|
+
</pre>
|
|
144
|
+
)}
|
|
145
|
+
</Highlight>
|
|
146
|
+
</div>
|
|
147
|
+
</CollapsibleCard>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
CodeBlockPart.displayName = 'CodeBlockPart'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
import { forwardRef } from 'react'
|
|
3
|
+
import { Tag } from '../basic/tag'
|
|
4
|
+
import { FileLine } from '../basic/icons-inline'
|
|
5
|
+
|
|
6
|
+
const ICON_CLASS = 'h-3.5 w-3.5 shrink-0 text-current'
|
|
7
|
+
|
|
8
|
+
export interface FileAttachmentProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
|
|
9
|
+
filename: string
|
|
10
|
+
iconFilename?: string
|
|
11
|
+
onRemove?: (e: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>) => void
|
|
12
|
+
icon?: ReactNode
|
|
13
|
+
/** 主库可注入 useIcons().getFileIcon;不传则用 registry 内联 FileLine */
|
|
14
|
+
getFileIcon?: (filename: string, className?: string) => ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const FileAttachment = forwardRef<HTMLSpanElement, FileAttachmentProps>(
|
|
18
|
+
({ filename, iconFilename, onRemove, icon, getFileIcon, className, ...props }, ref) => {
|
|
19
|
+
const lookup = iconFilename ?? filename
|
|
20
|
+
const resolvedIcon: ReactNode =
|
|
21
|
+
icon ?? (getFileIcon ? getFileIcon(lookup, ICON_CLASS) : <FileLine className={ICON_CLASS} />)
|
|
22
|
+
|
|
23
|
+
const { color: _omitColor, ...restProps } = props
|
|
24
|
+
return (
|
|
25
|
+
<Tag
|
|
26
|
+
ref={ref}
|
|
27
|
+
appearance="filled"
|
|
28
|
+
color="mauve"
|
|
29
|
+
closable={typeof onRemove === 'function'}
|
|
30
|
+
onClose={onRemove}
|
|
31
|
+
closeAriaLabel="移除"
|
|
32
|
+
className={className}
|
|
33
|
+
{...restProps}
|
|
34
|
+
>
|
|
35
|
+
<span className="inline-flex items-center gap-1.5 shrink-0">
|
|
36
|
+
{resolvedIcon}
|
|
37
|
+
{filename}
|
|
38
|
+
</span>
|
|
39
|
+
</Tag>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
FileAttachment.displayName = 'FileAttachment'
|