refactor: chat composer new design

This commit is contained in:
simosmik
2026-04-20 14:47:49 +00:00
parent 7763e60fb3
commit 5758bee8a0
5 changed files with 392 additions and 243 deletions

View File

@@ -0,0 +1,219 @@
"use client";
import * as React from 'react';
import { SendHorizonalIcon, SquareIcon } from 'lucide-react';
import { cn } from '../../../lib/utils';
import { Button } from './Button';
import Tooltip from './Tooltip';
/* ─── Context ────────────────────────────────────────────────────── */
type PromptInputStatus = 'ready' | 'submitted' | 'streaming' | 'error';
interface PromptInputContextValue {
status: PromptInputStatus;
}
const PromptInputContext = React.createContext<PromptInputContextValue | null>(null);
const usePromptInput = () => {
const context = React.useContext(PromptInputContext);
if (!context) {
throw new Error('PromptInput components must be used within PromptInput');
}
return context;
};
/* ─── PromptInput (root form) ────────────────────────────────────── */
export interface PromptInputProps extends React.FormHTMLAttributes<HTMLFormElement> {
status?: PromptInputStatus;
}
export const PromptInput = React.forwardRef<HTMLFormElement, PromptInputProps>(
({ className, status = 'ready', children, ...props }, ref) => {
const contextValue = React.useMemo(() => ({ status }), [status]);
return (
<PromptInputContext.Provider value={contextValue}>
<form
ref={ref}
data-slot="prompt-input"
className={cn(
'relative overflow-hidden rounded-xl border border-border/50 bg-card/80 shadow-sm backdrop-blur-sm transition-all duration-200 focus-within:border-primary/30 focus-within:shadow-md focus-within:ring-1 focus-within:ring-primary/15',
className
)}
{...props}
>
{children}
</form>
</PromptInputContext.Provider>
);
}
);
PromptInput.displayName = 'PromptInput';
/* ─── PromptInputHeader ──────────────────────────────────────────── */
export const PromptInputHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-slot="prompt-input-header"
className={cn('px-3 pt-3', className)}
{...props}
/>
));
PromptInputHeader.displayName = 'PromptInputHeader';
/* ─── PromptInputBody ────────────────────────────────────────────── */
export const PromptInputBody = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-slot="prompt-input-body"
className={cn('relative', className)}
{...props}
/>
));
PromptInputBody.displayName = 'PromptInputBody';
/* ─── PromptInputTextarea ────────────────────────────────────────── */
export const PromptInputTextarea = React.forwardRef<
HTMLTextAreaElement,
React.TextareaHTMLAttributes<HTMLTextAreaElement>
>(({ className, ...props }, ref) => (
<textarea
ref={ref}
data-slot="prompt-input-textarea"
className={cn(
'chat-input-placeholder block max-h-[40vh] w-full resize-none overflow-y-auto bg-transparent px-4 py-2 text-sm leading-6 text-foreground placeholder-muted-foreground/50 focus:outline-none sm:max-h-[300px]',
className
)}
{...props}
/>
));
PromptInputTextarea.displayName = 'PromptInputTextarea';
/* ─── PromptInputFooter ──────────────────────────────────────────── */
export const PromptInputFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-slot="prompt-input-footer"
className={cn('flex items-center justify-between border-t border-border/30 px-3 py-2', className)}
{...props}
/>
));
PromptInputFooter.displayName = 'PromptInputFooter';
/* ─── PromptInputTools ───────────────────────────────────────────── */
export const PromptInputTools = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-slot="prompt-input-tools"
className={cn('flex items-center gap-1', className)}
{...props}
/>
));
PromptInputTools.displayName = 'PromptInputTools';
/* ─── PromptInputButton ──────────────────────────────────────────── */
export interface PromptInputButtonTooltip {
content: React.ReactNode;
shortcut?: string;
side?: 'top' | 'bottom' | 'left' | 'right';
}
export interface PromptInputButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
tooltip?: PromptInputButtonTooltip;
}
export const PromptInputButton = React.forwardRef<HTMLButtonElement, PromptInputButtonProps>(
({ className, tooltip, children, ...props }, ref) => {
const button = (
<Button
ref={ref}
type="button"
variant="ghost"
size="icon"
className={cn('h-8 w-8 [&_svg]:size-4', className)}
{...props}
>
{children}
</Button>
);
if (tooltip) {
return (
<Tooltip
content={
tooltip.shortcut ? (
<span className="flex items-center gap-1.5">
{tooltip.content}
<kbd className="rounded bg-white/20 px-1 text-[10px]">{tooltip.shortcut}</kbd>
</span>
) : (
tooltip.content
)
}
position={tooltip.side ?? 'top'}
>
{button}
</Tooltip>
);
}
return button;
}
);
PromptInputButton.displayName = 'PromptInputButton';
/* ─── PromptInputSubmit ──────────────────────────────────────────── */
export interface PromptInputSubmitProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
status?: PromptInputStatus;
}
export const PromptInputSubmit = React.forwardRef<HTMLButtonElement, PromptInputSubmitProps>(
({ className, status: statusProp, children, ...props }, ref) => {
const context = React.useContext(PromptInputContext);
const status = statusProp ?? context?.status ?? 'ready';
const isActive = status === 'submitted' || status === 'streaming';
return (
<Button
ref={ref}
type={isActive ? 'button' : 'submit'}
variant="default"
size="icon"
className={cn('h-8 w-8 rounded-lg', className)}
{...props}
>
{children ?? (isActive ? (
<SquareIcon className="h-3.5 w-3.5 fill-current" />
) : (
<SendHorizonalIcon className="h-4 w-4" />
))}
</Button>
);
}
);
PromptInputSubmit.displayName = 'PromptInputSubmit';
export { usePromptInput };

View File

@@ -12,4 +12,5 @@ export { ScrollArea } from './ScrollArea';
export { Reasoning, ReasoningTrigger, ReasoningContent, useReasoning } from './Reasoning';
export { Shimmer } from './Shimmer';
export { default as Tooltip } from './Tooltip';
export { PromptInput, PromptInputHeader, PromptInputBody, PromptInputTextarea, PromptInputFooter, PromptInputTools, PromptInputButton, PromptInputSubmit } from './PromptInput';
export { PillBar, Pill } from './PillBar';