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,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DropdownMenu,
|
|
3
|
+
DropdownMenuTrigger,
|
|
4
|
+
DropdownMenuContent,
|
|
5
|
+
DropdownMenuItem,
|
|
6
|
+
} from '../../basic/dropdown-menu'
|
|
7
|
+
import { ArrowDownSLine, CheckLine } from '../../basic/icons-inline'
|
|
8
|
+
import type { FooterLeftModelSwitchConfig } from './types'
|
|
9
|
+
|
|
10
|
+
export interface ChatInputModelSwitcherProps {
|
|
11
|
+
config: FooterLeftModelSwitchConfig
|
|
12
|
+
disabled: boolean
|
|
13
|
+
arrowDownIcon?: React.ReactNode
|
|
14
|
+
dataStyle?: string
|
|
15
|
+
dataTheme?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ChatInputModelSwitcher({
|
|
19
|
+
config,
|
|
20
|
+
disabled,
|
|
21
|
+
dataStyle,
|
|
22
|
+
dataTheme,
|
|
23
|
+
arrowDownIcon = <ArrowDownSLine className="h-3.5 w-3.5 shrink-0 text-text-secondary" />,
|
|
24
|
+
}: ChatInputModelSwitcherProps) {
|
|
25
|
+
const currentModeOption = config.modeSwitch.options.find((o) => o.value === config.modeSwitch.value)
|
|
26
|
+
const modeTriggerIcon = currentModeOption?.icon ?? config.modeSwitch.icon
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex items-center gap-3 text-sm leading-sm text-text-secondary">
|
|
30
|
+
<DropdownMenu>
|
|
31
|
+
<DropdownMenuTrigger asChild>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
className="inline-flex items-center gap-1.5 rounded-md bg-fill-secondary px-2.5 py-1.5 text-sm leading-sm text-text hover:bg-fill focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
35
|
+
disabled={disabled}
|
|
36
|
+
>
|
|
37
|
+
{modeTriggerIcon != null && (
|
|
38
|
+
<span className="flex shrink-0 items-center [&>*]:block [&>*]:leading-none">
|
|
39
|
+
{modeTriggerIcon}
|
|
40
|
+
</span>
|
|
41
|
+
)}
|
|
42
|
+
<span>{config.modeSwitch.label}</span>
|
|
43
|
+
{arrowDownIcon}
|
|
44
|
+
</button>
|
|
45
|
+
</DropdownMenuTrigger>
|
|
46
|
+
<DropdownMenuContent align="start" side="top" className="min-w-30" dataStyle={dataStyle} dataTheme={dataTheme}>
|
|
47
|
+
{config.modeSwitch.options.map((opt) => (
|
|
48
|
+
<DropdownMenuItem
|
|
49
|
+
key={opt.value}
|
|
50
|
+
onClick={() => config.modeSwitch.onChange(opt.value)}
|
|
51
|
+
className="flex items-center justify-between gap-2"
|
|
52
|
+
>
|
|
53
|
+
<span className="flex items-center gap-2">
|
|
54
|
+
{opt.icon != null && (
|
|
55
|
+
<span className="flex shrink-0 [&>*]:block [&>*]:leading-none [&>*]:size-4">
|
|
56
|
+
{opt.icon}
|
|
57
|
+
</span>
|
|
58
|
+
)}
|
|
59
|
+
{opt.label}
|
|
60
|
+
</span>
|
|
61
|
+
{config.modeSwitch.value === opt.value && (
|
|
62
|
+
<CheckLine className="h-4 w-4 shrink-0 text-primary" />
|
|
63
|
+
)}
|
|
64
|
+
</DropdownMenuItem>
|
|
65
|
+
))}
|
|
66
|
+
</DropdownMenuContent>
|
|
67
|
+
</DropdownMenu>
|
|
68
|
+
|
|
69
|
+
<DropdownMenu>
|
|
70
|
+
<DropdownMenuTrigger asChild>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
className="inline-flex items-center gap-1 text-text-secondary hover:text-text focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1 py-0.5 disabled:opacity-50"
|
|
74
|
+
disabled={disabled}
|
|
75
|
+
>
|
|
76
|
+
<span>{config.modelSwitch.label}</span>
|
|
77
|
+
{arrowDownIcon}
|
|
78
|
+
</button>
|
|
79
|
+
</DropdownMenuTrigger>
|
|
80
|
+
<DropdownMenuContent align="start" side="top" className="min-w-25" dataStyle={dataStyle} dataTheme={dataTheme}>
|
|
81
|
+
{config.modelSwitch.options.map((opt) => (
|
|
82
|
+
<DropdownMenuItem
|
|
83
|
+
key={opt.value}
|
|
84
|
+
onClick={() => config.modelSwitch.onChange(opt.value)}
|
|
85
|
+
className="flex items-center justify-between gap-2"
|
|
86
|
+
>
|
|
87
|
+
{opt.label}
|
|
88
|
+
{config.modelSwitch.value === opt.value && (
|
|
89
|
+
<CheckLine className="h-4 w-4 shrink-0 text-primary" />
|
|
90
|
+
)}
|
|
91
|
+
</DropdownMenuItem>
|
|
92
|
+
))}
|
|
93
|
+
</DropdownMenuContent>
|
|
94
|
+
</DropdownMenu>
|
|
95
|
+
|
|
96
|
+
{config.consumptionMultiplier.options != null &&
|
|
97
|
+
config.consumptionMultiplier.options.length > 0 &&
|
|
98
|
+
config.consumptionMultiplier.onChange != null ? (
|
|
99
|
+
<DropdownMenu>
|
|
100
|
+
<DropdownMenuTrigger asChild>
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
className="inline-flex items-center text-text-secondary hover:text-text focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1 py-0.5 disabled:opacity-50"
|
|
104
|
+
disabled={disabled}
|
|
105
|
+
>
|
|
106
|
+
{config.consumptionMultiplier.value}
|
|
107
|
+
</button>
|
|
108
|
+
</DropdownMenuTrigger>
|
|
109
|
+
<DropdownMenuContent align="start" side="top" className="min-w-20" dataStyle={dataStyle} dataTheme={dataTheme}>
|
|
110
|
+
{config.consumptionMultiplier.options.map((opt) => (
|
|
111
|
+
<DropdownMenuItem
|
|
112
|
+
key={opt.value}
|
|
113
|
+
onClick={() => config.consumptionMultiplier.onChange?.(opt.value)}
|
|
114
|
+
className="flex items-center justify-between gap-2"
|
|
115
|
+
>
|
|
116
|
+
{opt.label}
|
|
117
|
+
{config.consumptionMultiplier.value === opt.value && (
|
|
118
|
+
<CheckLine className="h-4 w-4 shrink-0 text-primary" />
|
|
119
|
+
)}
|
|
120
|
+
</DropdownMenuItem>
|
|
121
|
+
))}
|
|
122
|
+
</DropdownMenuContent>
|
|
123
|
+
</DropdownMenu>
|
|
124
|
+
) : (
|
|
125
|
+
<span className="text-text-secondary">{config.consumptionMultiplier.value}</span>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
ChatInputModelSwitcher.displayName = 'ChatInputModelSwitcher'
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { forwardRef, useCallback, useRef } from 'react'
|
|
2
|
+
import { useAutoResizeTextarea } from './useAutoResizeTextarea'
|
|
3
|
+
|
|
4
|
+
export interface ChatInputTextareaProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'value' | 'onChange'> {
|
|
5
|
+
value: string
|
|
6
|
+
onChange?: (value: string) => void
|
|
7
|
+
placeholder?: string
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
maxRows?: number
|
|
10
|
+
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ChatInputTextarea = forwardRef<HTMLTextAreaElement, ChatInputTextareaProps>(
|
|
14
|
+
(
|
|
15
|
+
{
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
placeholder = 'What can I help you with today?',
|
|
19
|
+
disabled = false,
|
|
20
|
+
maxRows = 6,
|
|
21
|
+
onKeyDown,
|
|
22
|
+
className,
|
|
23
|
+
style,
|
|
24
|
+
...props
|
|
25
|
+
},
|
|
26
|
+
ref
|
|
27
|
+
) => {
|
|
28
|
+
const innerRef = useRef<HTMLTextAreaElement>(null)
|
|
29
|
+
useAutoResizeTextarea(innerRef, value, maxRows)
|
|
30
|
+
|
|
31
|
+
const setRef = useCallback(
|
|
32
|
+
(node: HTMLTextAreaElement | null) => {
|
|
33
|
+
(innerRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node
|
|
34
|
+
if (typeof ref === 'function') ref(node)
|
|
35
|
+
else if (ref) (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node
|
|
36
|
+
},
|
|
37
|
+
[ref]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const handleChange = useCallback(
|
|
41
|
+
(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange?.(e.target.value),
|
|
42
|
+
[onChange]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const textareaNoFocusStyle = { outline: 'none', border: 'none', boxShadow: 'none' } as const
|
|
46
|
+
const textareaStyles =
|
|
47
|
+
'w-full resize-none border-none bg-transparent text-sm leading-sm font-normal placeholder:text-text-tertiary overflow-y-auto scrollbar-auto outline-none shadow-none focus:outline-none focus:ring-0 focus:shadow-none focus-visible:outline-none focus-visible:ring-0 focus-visible:shadow-none [&:focus-visible]:outline-none [&:focus-visible]:ring-0 [&:focus-visible]:ring-offset-0 [&:focus-visible]:shadow-none ' +
|
|
48
|
+
(value?.trim() ? 'text-text' : 'text-text-quaternary')
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<textarea
|
|
52
|
+
ref={setRef}
|
|
53
|
+
value={value}
|
|
54
|
+
onChange={handleChange}
|
|
55
|
+
onKeyDown={onKeyDown}
|
|
56
|
+
placeholder={placeholder}
|
|
57
|
+
disabled={disabled}
|
|
58
|
+
rows={1}
|
|
59
|
+
className={textareaStyles}
|
|
60
|
+
{...props}
|
|
61
|
+
style={{ ...(style ?? {}), ...textareaNoFocusStyle }}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
ChatInputTextarea.displayName = 'ChatInputTextarea'
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { cva } from 'class-variance-authority'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
import { SendButton } from '../send-button'
|
|
6
|
+
import { IconButton } from '../../basic/icon-button'
|
|
7
|
+
import { MoreLine } from '../../basic/icons-inline'
|
|
8
|
+
import { GenerationStatusBar } from '../generation-status-bar'
|
|
9
|
+
import { ChatInputTextarea } from './chat-input-textarea'
|
|
10
|
+
import { ChatInputFolderSelector } from './chat-input-folder-selector'
|
|
11
|
+
import { ChatInputModelSwitcher } from './chat-input-model-switcher'
|
|
12
|
+
import { useChatInputContext } from './context'
|
|
13
|
+
|
|
14
|
+
const chatInputContainerVariants = cva(
|
|
15
|
+
[
|
|
16
|
+
'flex flex-col w-full bg-bg-base border border-primary-border rounded-xl transition-all duration-200',
|
|
17
|
+
/* 参考 Claude 输入框:2 级阴影默认,4 级 focus,使用本项目 1~4 级 shadow token */
|
|
18
|
+
'shadow-[0_0.25rem_1.25rem_var(--color-shadow-secondary)]',
|
|
19
|
+
'hover:shadow-[0_0.25rem_1.25rem_var(--color-shadow-secondary)]',
|
|
20
|
+
'focus-within:shadow-[0_0.25rem_1.25rem_var(--color-shadow-quaternary)]',
|
|
21
|
+
'hover:focus-within:shadow-[0_0.25rem_1.25rem_var(--color-shadow-quaternary)]',
|
|
22
|
+
].join(' '),
|
|
23
|
+
{
|
|
24
|
+
variants: { disabled: { true: 'opacity-50 cursor-not-allowed', false: '' } },
|
|
25
|
+
defaultVariants: { disabled: false },
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
// Box 使用 rounded-xl (= --radius-xl)
|
|
29
|
+
|
|
30
|
+
const attachmentRowStyles = 'flex flex-wrap items-center gap-2 px-3 pt-3 pb-2'
|
|
31
|
+
const contentStyles = 'flex flex-1 items-center min-h-6 p-3'
|
|
32
|
+
const footerStyles = 'flex shrink-0 items-center justify-between w-full self-stretch gap-2 p-3'
|
|
33
|
+
|
|
34
|
+
// ----- 框外 -----
|
|
35
|
+
// Above 高度 = 主题下 md 按钮高度(h-9),文字 text-sm;宽度 = Box 宽度 - 2*圆角,居中;无水平 padding
|
|
36
|
+
export interface ChatInputAboveProps {
|
|
37
|
+
className?: string
|
|
38
|
+
style?: React.CSSProperties
|
|
39
|
+
children?: ReactNode
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ChatInputAbove({ className, style, children }: ChatInputAboveProps) {
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
className={cn(
|
|
46
|
+
'flex min-h-9 h-9 w-[calc(100%-2*var(--radius-xl))] mx-auto items-center overflow-hidden rounded-t-xl text-sm leading-sm',
|
|
47
|
+
className
|
|
48
|
+
)}
|
|
49
|
+
style={style}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
ChatInputAbove.displayName = 'ChatInputAbove'
|
|
56
|
+
|
|
57
|
+
// ----- 框容器 -----
|
|
58
|
+
export interface ChatInputBoxProps {
|
|
59
|
+
className?: string
|
|
60
|
+
children?: ReactNode
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ChatInputBox({ className, children }: ChatInputBoxProps) {
|
|
64
|
+
const ctx = useChatInputContext()
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
className={chatInputContainerVariants({
|
|
68
|
+
disabled: ctx.disabled,
|
|
69
|
+
className: cn('relative', className),
|
|
70
|
+
})}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
ChatInputBox.displayName = 'ChatInputBox'
|
|
77
|
+
|
|
78
|
+
// ----- 框内·第一层 附件区 -----
|
|
79
|
+
export interface ChatInputAttachmentsProps {
|
|
80
|
+
className?: string
|
|
81
|
+
children?: ReactNode
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function ChatInputAttachments({ className, children }: ChatInputAttachmentsProps) {
|
|
85
|
+
if (!children) return null
|
|
86
|
+
return <div className={cn(attachmentRowStyles, className)}>{children}</div>
|
|
87
|
+
}
|
|
88
|
+
ChatInputAttachments.displayName = 'ChatInputAttachments'
|
|
89
|
+
|
|
90
|
+
// ----- 框内·第二层 输入区 -----
|
|
91
|
+
export interface ChatInputInputProps extends Omit<React.ComponentProps<typeof ChatInputTextarea>, 'value' | 'onChange' | 'onKeyDown'> {
|
|
92
|
+
placeholder?: string
|
|
93
|
+
maxRows?: number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const ChatInputInput = forwardRef<HTMLTextAreaElement, ChatInputInputProps>(
|
|
97
|
+
({ placeholder = 'What can I help you with today?', maxRows = 6, ...props }, ref) => {
|
|
98
|
+
const ctx = useChatInputContext()
|
|
99
|
+
const setRef = useCallback(
|
|
100
|
+
(node: HTMLTextAreaElement | null) => {
|
|
101
|
+
ctx.setTextareaRef(node)
|
|
102
|
+
if (typeof ref === 'function') ref(node)
|
|
103
|
+
else if (ref) (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node
|
|
104
|
+
},
|
|
105
|
+
[ctx, ref]
|
|
106
|
+
)
|
|
107
|
+
return (
|
|
108
|
+
<div className={contentStyles}>
|
|
109
|
+
<ChatInputTextarea
|
|
110
|
+
ref={setRef}
|
|
111
|
+
value={ctx.value}
|
|
112
|
+
onChange={ctx.onChange}
|
|
113
|
+
onKeyDown={ctx.handleKeyDown}
|
|
114
|
+
placeholder={placeholder}
|
|
115
|
+
disabled={ctx.disabled}
|
|
116
|
+
maxRows={maxRows}
|
|
117
|
+
{...props}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
ChatInputInput.displayName = 'ChatInputInput'
|
|
124
|
+
|
|
125
|
+
// ----- 框内·第三层 操作区 -----
|
|
126
|
+
export interface ChatInputActionsProps {
|
|
127
|
+
className?: string
|
|
128
|
+
children?: ReactNode
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function ChatInputActions({ className, children }: ChatInputActionsProps) {
|
|
132
|
+
return <div className={cn(footerStyles, className)}>{children}</div>
|
|
133
|
+
}
|
|
134
|
+
ChatInputActions.displayName = 'ChatInputActions'
|
|
135
|
+
|
|
136
|
+
export interface ChatInputActionsLeftProps {
|
|
137
|
+
className?: string
|
|
138
|
+
children?: ReactNode
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function ChatInputActionsLeft({ className, children }: ChatInputActionsLeftProps) {
|
|
142
|
+
return <div className={cn('flex items-center gap-2', className)}>{children}</div>
|
|
143
|
+
}
|
|
144
|
+
ChatInputActionsLeft.displayName = 'ChatInputActionsLeft'
|
|
145
|
+
|
|
146
|
+
export interface ChatInputActionsRightProps {
|
|
147
|
+
className?: string
|
|
148
|
+
children?: ReactNode
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function ChatInputActionsRight({ className, children }: ChatInputActionsRightProps) {
|
|
152
|
+
return <div className={cn('flex items-center gap-2', className)}>{children}</div>
|
|
153
|
+
}
|
|
154
|
+
ChatInputActionsRight.displayName = 'ChatInputActionsRight'
|
|
155
|
+
|
|
156
|
+
// ----- 固定·框内右上角放大按钮 -----
|
|
157
|
+
export interface ChatInputExpandButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
|
158
|
+
children?: ReactNode
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function ChatInputExpandButton({ className, style, ...props }: ChatInputExpandButtonProps) {
|
|
162
|
+
return (
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
className={cn(
|
|
166
|
+
'absolute top-2 right-2 z-10 flex h-8 w-8 items-center justify-center rounded-lg text-text-secondary hover:bg-fill-secondary hover:text-text focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50',
|
|
167
|
+
className
|
|
168
|
+
)}
|
|
169
|
+
style={{ ...style }}
|
|
170
|
+
{...props}
|
|
171
|
+
/>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
ChatInputExpandButton.displayName = 'ChatInputExpandButton'
|
|
175
|
+
|
|
176
|
+
// ----- 操作区子组件(从 Context 消费) -----
|
|
177
|
+
export function ChatInputFolderButton() {
|
|
178
|
+
const ctx = useChatInputContext()
|
|
179
|
+
if (!ctx.showFolderButton) return null
|
|
180
|
+
return (
|
|
181
|
+
<ChatInputFolderSelector
|
|
182
|
+
disabled={ctx.disabled}
|
|
183
|
+
folderButtonLabel={ctx.folderButtonLabel}
|
|
184
|
+
folderPermission={ctx.folderPermission}
|
|
185
|
+
onFolderButtonClick={ctx.onFolderButtonClick}
|
|
186
|
+
onFolderClear={ctx.handleFolderClear}
|
|
187
|
+
onFolderSelect={ctx.handleFolderSelect}
|
|
188
|
+
hasFolderSelected={ctx.hasFolderSelected}
|
|
189
|
+
dataStyle={ctx.dataStyle}
|
|
190
|
+
dataTheme={ctx.dataTheme}
|
|
191
|
+
folderIcon={ctx.folderIcon}
|
|
192
|
+
folderOpenIcon={ctx.folderOpenIcon}
|
|
193
|
+
historyIcon={ctx.historyIcon}
|
|
194
|
+
subMenuArrowIcon={ctx.subMenuArrowIcon}
|
|
195
|
+
folderFillIcon={ctx.folderFillIcon}
|
|
196
|
+
closeIcon={ctx.closeIcon}
|
|
197
|
+
/>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
ChatInputFolderButton.displayName = 'ChatInputFolderButton'
|
|
201
|
+
|
|
202
|
+
export function ChatInputMoreButton() {
|
|
203
|
+
const ctx = useChatInputContext()
|
|
204
|
+
if (!ctx.showMoreButton) return null
|
|
205
|
+
return (
|
|
206
|
+
<IconButton
|
|
207
|
+
variant="ghost"
|
|
208
|
+
icon={ctx.addLargeIcon ?? <MoreLine className="h-5 w-5" />}
|
|
209
|
+
onClick={ctx.onMoreButtonClick}
|
|
210
|
+
disabled={ctx.disabled}
|
|
211
|
+
/>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
ChatInputMoreButton.displayName = 'ChatInputMoreButton'
|
|
215
|
+
|
|
216
|
+
// 使用 chat 内共用的 SendButton,仅从 Context 注入 disabled/status/onClick|form
|
|
217
|
+
export function ChatInputSendButton() {
|
|
218
|
+
const ctx = useChatInputContext()
|
|
219
|
+
return (
|
|
220
|
+
<SendButton
|
|
221
|
+
disabled={!ctx.canSend || ctx.disabled}
|
|
222
|
+
status={ctx.sendButtonStatus}
|
|
223
|
+
{...(ctx.form ? { type: 'submit' as const, form: ctx.form } : { onClick: ctx.handleSend })}
|
|
224
|
+
/>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
ChatInputSendButton.displayName = 'ChatInputSendButton'
|
|
228
|
+
|
|
229
|
+
// 左侧操作区默认内容:有 footerLeftConfig 时渲染模式/模型切换,否则渲染 FolderButton + MoreButton
|
|
230
|
+
export function ChatInputFooterLeft() {
|
|
231
|
+
const ctx = useChatInputContext()
|
|
232
|
+
if (ctx.footerLeftConfig) {
|
|
233
|
+
return (
|
|
234
|
+
<ChatInputModelSwitcher
|
|
235
|
+
config={ctx.footerLeftConfig}
|
|
236
|
+
disabled={ctx.disabled}
|
|
237
|
+
arrowDownIcon={ctx.arrowDownIcon}
|
|
238
|
+
dataStyle={ctx.dataStyle}
|
|
239
|
+
dataTheme={ctx.dataTheme}
|
|
240
|
+
/>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
return (
|
|
244
|
+
<>
|
|
245
|
+
<ChatInputFolderButton />
|
|
246
|
+
<ChatInputMoreButton />
|
|
247
|
+
</>
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
ChatInputFooterLeft.displayName = 'ChatInputFooterLeft'
|
|
251
|
+
|
|
252
|
+
// ----- 默认布局(ChatInput 便捷导出使用) -----
|
|
253
|
+
export interface ChatInputDefaultLayoutProps {
|
|
254
|
+
className?: string
|
|
255
|
+
hasAbove?: boolean
|
|
256
|
+
hasAttachments?: boolean
|
|
257
|
+
hasFooter?: boolean
|
|
258
|
+
aboveOverlap?: boolean
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export const ChatInputDefaultLayout = forwardRef<HTMLTextAreaElement, ChatInputDefaultLayoutProps>(
|
|
262
|
+
function ChatInputDefaultLayout(
|
|
263
|
+
{ className, hasAbove = false, hasAttachments = false, hasFooter = true, aboveOverlap = true },
|
|
264
|
+
ref
|
|
265
|
+
) {
|
|
266
|
+
const ctx = useChatInputContext()
|
|
267
|
+
|
|
268
|
+
const renderAttachments = () => {
|
|
269
|
+
if (!hasAttachments || ctx.attachments == null || ctx.attachments === false) return null
|
|
270
|
+
const att = ctx.attachments
|
|
271
|
+
if (typeof att === 'object' && !React.isValidElement(att) && ('files' in att || 'images' in att)) {
|
|
272
|
+
return (
|
|
273
|
+
<>
|
|
274
|
+
{(att as { files?: ReactNode }).files && (
|
|
275
|
+
<div className={attachmentRowStyles}>{(att as { files?: ReactNode }).files}</div>
|
|
276
|
+
)}
|
|
277
|
+
{(att as { images?: ReactNode }).images && (
|
|
278
|
+
<div className={attachmentRowStyles}>{(att as { images?: ReactNode }).images}</div>
|
|
279
|
+
)}
|
|
280
|
+
</>
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
return <div className={attachmentRowStyles}>{att as ReactNode}</div>
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const renderFooterLeft = () => <ChatInputFooterLeft />
|
|
287
|
+
|
|
288
|
+
const boxContent = (
|
|
289
|
+
<ChatInputBox className={cn(!hasFooter && !hasAttachments && 'min-h-14')}>
|
|
290
|
+
{renderAttachments()}
|
|
291
|
+
<ChatInputInput ref={ref} />
|
|
292
|
+
{/* 始终保留操作区右侧 SendButton:hasFooter 时显示完整 footer,否则仅显示右侧发送按钮(Minimal 模式) */}
|
|
293
|
+
<ChatInputActions>
|
|
294
|
+
{hasFooter ? (
|
|
295
|
+
<>
|
|
296
|
+
<ChatInputActionsLeft>{renderFooterLeft()}</ChatInputActionsLeft>
|
|
297
|
+
<div className="flex-1 h-5" />
|
|
298
|
+
<ChatInputActionsRight>
|
|
299
|
+
<ChatInputSendButton />
|
|
300
|
+
</ChatInputActionsRight>
|
|
301
|
+
</>
|
|
302
|
+
) : (
|
|
303
|
+
<>
|
|
304
|
+
<div className="flex-1 h-5" />
|
|
305
|
+
<ChatInputActionsRight>
|
|
306
|
+
<ChatInputSendButton />
|
|
307
|
+
</ChatInputActionsRight>
|
|
308
|
+
</>
|
|
309
|
+
)}
|
|
310
|
+
</ChatInputActions>
|
|
311
|
+
</ChatInputBox>
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if (hasAbove && ctx.showGenerationStatus && ctx.generationStatus) {
|
|
315
|
+
return (
|
|
316
|
+
<div className={cn('flex flex-col w-full', className)}>
|
|
317
|
+
<ChatInputAbove>
|
|
318
|
+
<GenerationStatusBar
|
|
319
|
+
status={typeof ctx.generationStatus?.content === 'string' ? ctx.generationStatus.content : undefined}
|
|
320
|
+
icon={ctx.generationStatus?.icon}
|
|
321
|
+
time={ctx.generationStatus?.time}
|
|
322
|
+
>
|
|
323
|
+
{ctx.generationStatus?.content}
|
|
324
|
+
</GenerationStatusBar>
|
|
325
|
+
</ChatInputAbove>
|
|
326
|
+
{boxContent}
|
|
327
|
+
</div>
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return <div className={className}>{boxContent}</div>
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
ChatInputDefaultLayout.displayName = 'ChatInputDefaultLayout'
|