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.
Files changed (118) hide show
  1. package/README.md +188 -4
  2. package/dist/commands/add.js +93 -0
  3. package/dist/commands/diff.js +54 -0
  4. package/dist/commands/init.js +96 -0
  5. package/dist/commands/list.js +25 -0
  6. package/dist/index.js +37 -0
  7. package/dist/utils/config.js +53 -0
  8. package/dist/utils/registry.js +34 -0
  9. package/dist/utils/tokens.js +176 -0
  10. package/dist/utils/transform.js +19 -0
  11. package/package.json +33 -10
  12. package/registry/__tests__/basic/button.test.tsx +333 -0
  13. package/registry/__tests__/chat/markdown.test.tsx +387 -0
  14. package/registry/__tests__/chat/thinking-indicator.test.tsx +244 -0
  15. package/registry/__tests__/chat/tool-invocation-card.test.tsx +346 -0
  16. package/registry/basic/alert-dialog.tsx +180 -0
  17. package/registry/basic/avatar.tsx +120 -0
  18. package/registry/basic/button.tsx +100 -0
  19. package/registry/basic/collapse.tsx +94 -0
  20. package/registry/basic/collapsible-card.tsx +230 -0
  21. package/registry/basic/collapsible.tsx +21 -0
  22. package/registry/basic/dropdown-menu.tsx +254 -0
  23. package/registry/basic/icon-button.tsx +66 -0
  24. package/registry/basic/icons-inline.tsx +206 -0
  25. package/registry/basic/kbd.tsx +50 -0
  26. package/registry/basic/option-list.tsx +125 -0
  27. package/registry/basic/pagination.tsx +132 -0
  28. package/registry/basic/progress.tsx +42 -0
  29. package/registry/basic/radio-group.tsx +69 -0
  30. package/registry/basic/resizable.tsx +67 -0
  31. package/registry/basic/scrollbar.tsx +114 -0
  32. package/registry/basic/select.tsx +177 -0
  33. package/registry/basic/shimmering-text.tsx +115 -0
  34. package/registry/basic/sidebar-menu.tsx +177 -0
  35. package/registry/basic/skeleton.tsx +33 -0
  36. package/registry/basic/slider.tsx +55 -0
  37. package/registry/basic/sonner.tsx +104 -0
  38. package/registry/basic/spinner.tsx +17 -0
  39. package/registry/basic/switch.tsx +49 -0
  40. package/registry/basic/table.tsx +117 -0
  41. package/registry/basic/tabs.tsx +85 -0
  42. package/registry/basic/tag.tsx +161 -0
  43. package/registry/basic/theme-from-document.ts +10 -0
  44. package/registry/basic/toggle.tsx +223 -0
  45. package/registry/basic/tooltip.tsx +80 -0
  46. package/registry/basic/typography.tsx +201 -0
  47. package/registry/chat/ask-user-part.tsx +70 -0
  48. package/registry/chat/browser-action-part.tsx +166 -0
  49. package/registry/chat/chat-input/chat-input-folder-selector.tsx +185 -0
  50. package/registry/chat/chat-input/chat-input-model-switcher.tsx +131 -0
  51. package/registry/chat/chat-input/chat-input-textarea.tsx +67 -0
  52. package/registry/chat/chat-input/compound.tsx +334 -0
  53. package/registry/chat/chat-input/context.tsx +189 -0
  54. package/registry/chat/chat-input/folder-permission-dialog.tsx +61 -0
  55. package/registry/chat/chat-input/index.tsx +123 -0
  56. package/registry/chat/chat-input/types.ts +77 -0
  57. package/registry/chat/chat-input/useAutoResizeTextarea.ts +20 -0
  58. package/registry/chat/code-block-part.tsx +151 -0
  59. package/registry/chat/file-attachment.tsx +44 -0
  60. package/registry/chat/file-card.tsx +68 -0
  61. package/registry/chat/file-review-part.tsx +259 -0
  62. package/registry/chat/folder-button.tsx +169 -0
  63. package/registry/chat/generated-images-grid.tsx +56 -0
  64. package/registry/chat/generation-status-bar.tsx +72 -0
  65. package/registry/chat/hint-banner.tsx +165 -0
  66. package/registry/chat/image-attachment.tsx +166 -0
  67. package/registry/chat/image-generating.tsx +281 -0
  68. package/registry/chat/markdown.tsx +146 -0
  69. package/registry/chat/mermaid-part.tsx +90 -0
  70. package/registry/chat/permission-card.tsx +178 -0
  71. package/registry/chat/plan-part.tsx +168 -0
  72. package/registry/chat/queue-indicator.tsx +234 -0
  73. package/registry/chat/reasoning-step/compound.tsx +336 -0
  74. package/registry/chat/reasoning-step/context.tsx +114 -0
  75. package/registry/chat/reasoning-step/index.tsx +45 -0
  76. package/registry/chat/reasoning-step/types.ts +109 -0
  77. package/registry/chat/related-prompts.tsx +91 -0
  78. package/registry/chat/response/compound.tsx +210 -0
  79. package/registry/chat/response/context.tsx +200 -0
  80. package/registry/chat/response/index.tsx +87 -0
  81. package/registry/chat/response/types.ts +123 -0
  82. package/registry/chat/send-button.tsx +94 -0
  83. package/registry/chat/streaming-markdown-block.tsx +111 -0
  84. package/registry/chat/task-part.tsx +109 -0
  85. package/registry/chat/terminal-code-block-part.tsx +69 -0
  86. package/registry/chat/thinking-indicator.tsx +91 -0
  87. package/registry/chat/tool-invocation-card.tsx +132 -0
  88. package/registry/chat/user-message.tsx +38 -0
  89. package/registry/chat/user-question/UserQuestionCard.tsx +198 -0
  90. package/registry/chat/user-question/UserQuestionFooter.tsx +66 -0
  91. package/registry/chat/user-question/UserQuestionHeader.tsx +64 -0
  92. package/registry/chat/user-question/compound.tsx +324 -0
  93. package/registry/chat/user-question/context.tsx +456 -0
  94. package/registry/chat/user-question/index.tsx +95 -0
  95. package/registry/chat/user-question/types.ts +61 -0
  96. package/registry/chat/user-question/useUserQuestionKeyboard.ts +126 -0
  97. package/registry/chat/user-question/useUserQuestionState.ts +165 -0
  98. package/registry/chat/user-question-answer.tsx +62 -0
  99. package/registry/lib/file-icon-maps.ts +150 -0
  100. package/registry/lib/use-mermaid-render.ts +76 -0
  101. package/registry/lib/utils.ts +6 -0
  102. package/registry/meta.json +1 -0
  103. package/registry/tokens/index.css +31 -0
  104. package/registry/tokens/scale/computed.css +103 -0
  105. package/registry/tokens/scale/config.css +110 -0
  106. package/registry/tokens/scale/index.css +30 -0
  107. package/registry/tokens/scale/presets/compact.css +30 -0
  108. package/registry/tokens/scale/presets/dense.css +64 -0
  109. package/registry/tokens/scale/presets/sharp.css +40 -0
  110. package/registry/tokens/scale/presets/soft.css +16 -0
  111. package/registry/tokens/scale.css +13 -0
  112. package/registry/tokens/scrollbar-utility.css +35 -0
  113. package/registry/tokens/theme.css +633 -0
  114. package/registry/tokens/themes/dark-parchment.css +132 -0
  115. package/registry/tokens/themes/dark-qoder.css +132 -0
  116. package/registry/tokens/themes/light-parchment.css +123 -0
  117. package/registry/tokens/themes/light-qoder.css +131 -0
  118. 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 &quot;{projectName}&quot;?
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'