import React, { forwardRef, useCallback } from 'react'
import type { ReactNode } from 'react'
import { cva } from 'class-variance-authority'
import { cn } from '@/lib/utils'
import { SendButton } from '../send-button'
import { IconButton } from '../../basic/icon-button'
import { MoreLine } from '../../basic/icons-inline'
import { GenerationStatusBar } from '../generation-status-bar'
import { ChatInputTextarea } from './chat-input-textarea'
import { ChatInputFolderSelector } from './chat-input-folder-selector'
import { ChatInputModelSwitcher } from './chat-input-model-switcher'
import { useChatInputContext } from './context'
const chatInputContainerVariants = cva(
[
'flex flex-col w-full bg-bg-base border border-primary-border rounded-xl transition-all duration-200',
/* 参考 Claude 输入框:2 级阴影默认,4 级 focus,使用本项目 1~4 级 shadow token */
'shadow-[0_0.25rem_1.25rem_var(--color-shadow-secondary)]',
'hover:shadow-[0_0.25rem_1.25rem_var(--color-shadow-secondary)]',
'focus-within:shadow-[0_0.25rem_1.25rem_var(--color-shadow-quaternary)]',
'hover:focus-within:shadow-[0_0.25rem_1.25rem_var(--color-shadow-quaternary)]',
].join(' '),
{
variants: { disabled: { true: 'opacity-50 cursor-not-allowed', false: '' } },
defaultVariants: { disabled: false },
}
)
// Box 使用 rounded-xl (= --radius-xl)
const attachmentRowStyles = 'flex flex-wrap items-center gap-2 px-3 pt-3 pb-2'
const contentStyles = 'flex flex-1 items-center min-h-6 p-3'
const footerStyles = 'flex shrink-0 items-center justify-between w-full self-stretch gap-2 p-3'
// ----- 框外 -----
// Above 高度 = 主题下 md 控件高度;宽度 = Box 宽度 - 2*圆角,居中;无水平 padding
export interface ChatInputAboveProps {
className?: string
style?: React.CSSProperties
children?: ReactNode
}
export function ChatInputAbove({ className, style, children }: ChatInputAboveProps) {
return (
{children}
)
}
ChatInputAbove.displayName = 'ChatInputAbove'
// ----- 框容器 -----
export interface ChatInputBoxProps {
className?: string
children?: ReactNode
}
export function ChatInputBox({ className, children }: ChatInputBoxProps) {
const ctx = useChatInputContext()
return (
{children}
)
}
ChatInputBox.displayName = 'ChatInputBox'
// ----- 框内·第一层 附件区 -----
export interface ChatInputAttachmentsProps {
className?: string
children?: ReactNode
}
export function ChatInputAttachments({ className, children }: ChatInputAttachmentsProps) {
if (!children) return null
return {children}
}
ChatInputAttachments.displayName = 'ChatInputAttachments'
// ----- 框内·第二层 输入区 -----
export interface ChatInputInputProps extends Omit, 'value' | 'onChange' | 'onKeyDown'> {
placeholder?: string
maxRows?: number
}
export const ChatInputInput = forwardRef(
({ placeholder, maxRows, ...props }, ref) => {
const ctx = useChatInputContext()
const setRef = useCallback(
(node: HTMLTextAreaElement | null) => {
ctx.setTextareaRef(node)
if (typeof ref === 'function') ref(node)
else if (ref) (ref as React.MutableRefObject).current = node
},
[ctx, ref]
)
return (
)
}
)
ChatInputInput.displayName = 'ChatInputInput'
// ----- 框内·第三层 操作区 -----
export interface ChatInputActionsProps {
className?: string
children?: ReactNode
}
export function ChatInputActions({ className, children }: ChatInputActionsProps) {
return {children}
}
ChatInputActions.displayName = 'ChatInputActions'
export interface ChatInputActionsLeftProps {
className?: string
children?: ReactNode
}
export function ChatInputActionsLeft({ className, children }: ChatInputActionsLeftProps) {
return {children}
}
ChatInputActionsLeft.displayName = 'ChatInputActionsLeft'
export interface ChatInputActionsRightProps {
className?: string
children?: ReactNode
}
export function ChatInputActionsRight({ className, children }: ChatInputActionsRightProps) {
return {children}
}
ChatInputActionsRight.displayName = 'ChatInputActionsRight'
// ----- 固定·框内右上角放大按钮 -----
export interface ChatInputExpandButtonProps extends Omit, 'children'> {
children?: ReactNode
}
export function ChatInputExpandButton({ className, style, ...props }: ChatInputExpandButtonProps) {
return (
)
}
ChatInputExpandButton.displayName = 'ChatInputExpandButton'
// ----- 操作区子组件(从 Context 消费) -----
export function ChatInputFolderButton() {
const ctx = useChatInputContext()
if (!ctx.showFolderButton) return null
return (
)
}
ChatInputFolderButton.displayName = 'ChatInputFolderButton'
export function ChatInputMoreButton() {
const ctx = useChatInputContext()
if (!ctx.showMoreButton) return null
return (
}
onClick={ctx.onMoreButtonClick}
disabled={ctx.disabled}
aria-label="More actions"
/>
)
}
ChatInputMoreButton.displayName = 'ChatInputMoreButton'
// 使用 chat 内共用的 SendButton,仅从 Context 注入 disabled/status/onClick|form
export function ChatInputSendButton() {
const ctx = useChatInputContext()
return (
)
}
ChatInputSendButton.displayName = 'ChatInputSendButton'
// 左侧操作区默认内容:有 footerLeftConfig 时渲染模式/模型切换,否则渲染 FolderButton + MoreButton
export function ChatInputFooterLeft() {
const ctx = useChatInputContext()
if (ctx.footerLeftConfig) {
return (
)
}
return (
<>
>
)
}
ChatInputFooterLeft.displayName = 'ChatInputFooterLeft'
// ----- 默认布局(ChatInput 便捷导出使用) -----
export interface ChatInputDefaultLayoutProps {
className?: string
hasAbove?: boolean
hasAttachments?: boolean
hasFooter?: boolean
aboveOverlap?: boolean
}
export const ChatInputDefaultLayout = forwardRef(
function ChatInputDefaultLayout(
{ className, hasAbove = false, hasAttachments = false, hasFooter = true, aboveOverlap = true },
ref
) {
const ctx = useChatInputContext()
void aboveOverlap
const renderAttachments = () => {
if (!hasAttachments || ctx.attachments == null || ctx.attachments === false) return null
const att = ctx.attachments
if (typeof att === 'object' && !React.isValidElement(att) && ('files' in att || 'images' in att)) {
return (
<>
{(att as { files?: ReactNode }).files && (
{(att as { files?: ReactNode }).files}
)}
{(att as { images?: ReactNode }).images && (
{(att as { images?: ReactNode }).images}
)}
>
)
}
return {att as ReactNode}
}
const renderFooterLeft = () =>
const boxContent = (
{renderAttachments()}
{/* 始终保留操作区右侧 SendButton:hasFooter 时显示完整 footer,否则仅显示右侧发送按钮(Minimal 模式) */}
{hasFooter ? (
<>
{renderFooterLeft()}
>
) : (
<>
>
)}
)
if (hasAbove && ctx.showGenerationStatus && ctx.generationStatus) {
return (
{ctx.generationStatus?.content}
{boxContent}
)
}
return {boxContent}
}
)
ChatInputDefaultLayout.displayName = 'ChatInputDefaultLayout'