MingruiZhang's picture
feat: Final error display and step duration (#88)
3c8b24f unverified
'use client';
import {
useState,
useEffect,
useRef,
forwardRef,
useImperativeHandle,
} from 'react';
import { Button } from '@/components/ui/Button';
import { useEnterSubmit } from '@/lib/hooks/useEnterSubmit';
import Img from '../ui/Img';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/Tooltip';
import { IconImage, IconArrowUp, IconClose } from '@/components/ui/Icons';
import { cn } from '@/lib/utils';
import Chip from '../ui/Chip';
import Textarea from 'react-textarea-autosize';
import useMediaUpload from '@/lib/hooks/useMediaUpload';
export interface ComposerProps {
onSubmit: (params: { input: string; mediaUrl: string }) => Promise<void>;
disabled?: boolean;
title?: string;
initMediaUrl?: string;
initInput?: string;
}
export interface ComposerRef {
setMediaUrl: (url: string) => void;
setInput: (input: string) => void;
}
const Composer = forwardRef<ComposerRef, ComposerProps>(
({ disabled, onSubmit, initMediaUrl, initInput }, ref) => {
const { formRef, onKeyDown } = useEnterSubmit();
const inputRef = useRef<HTMLTextAreaElement>(null);
const [localMediaUrl, setLocalMediaUrl] = useState<string | undefined>(
initMediaUrl,
);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [input, setLocalInput] = useState(initInput ?? '');
const noMediaValidation = !localMediaUrl && !!input;
const {
getRootProps,
getInputProps,
isDragActive,
isUploading,
openUpload,
} = useMediaUpload(uploadUrl => setLocalMediaUrl(uploadUrl));
const finalLoading = isUploading || isSubmitting;
const finalDisabled = finalLoading || disabled;
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
useImperativeHandle(ref, () => ({
setMediaUrl(mediaUrl) {
setLocalMediaUrl(mediaUrl);
},
setInput(input) {
setLocalInput(input);
},
}));
const mediaName = localMediaUrl?.split('/').pop();
return (
<div
{...getRootProps()}
className={cn(
'mx-auto w-[42rem] max-w-full px-6 py-4 bg-zinc-600 rounded-xl relative shadow-lg shadow-zinc-600/40 z-50',
isDragActive && 'bg-indigo-700/50',
)}
>
<input {...getInputProps()} />
<div
className={cn(
'w-1/3 h-1 rounded-full overflow-hidden bg-zinc-700 absolute left-1/2 -translate-x-1/2 top-2',
finalLoading ? 'opacity-100' : 'opacity-0',
)}
>
<div className="h-full bg-primary animate-progress origin-left-right" />
</div>
{localMediaUrl ? (
<Chip className="mb-0.5">
<div className="flex flex-row items-center space-x-2">
<Tooltip>
<TooltipTrigger>
<div className="flex flex-row items-center space-x-2">
<IconImage className="size-3" />
<p>{mediaName ?? 'unnamed_media'}</p>
</div>
</TooltipTrigger>
<TooltipContent sideOffset={12} className="max-w-2xl">
<Img
src={localMediaUrl}
className="m-1"
quality={100}
alt="zoomed-in-image"
/>
</TooltipContent>
</Tooltip>
<Button
size="icon"
variant="ghost"
disabled={finalDisabled}
className="size-4"
onClick={() => setLocalMediaUrl(undefined)}
>
<IconClose className="size-3" />
</Button>
</div>
</Chip>
) : (
<Button
variant="ghost"
size="sm"
className={cn(
'ml-[-10px] border border-transparent',
noMediaValidation && 'border-red-500/50 text-red-500',
)}
onClick={openUpload}
>
<IconImage className="mr-2 size-4" />
{noMediaValidation ? 'Select media (required)' : 'Select media'}
</Button>
)}
<form
onSubmit={async e => {
e.preventDefault();
if (!input?.trim() || !localMediaUrl) {
return;
}
setIsSubmitting(true);
try {
await onSubmit({ input, mediaUrl: localMediaUrl });
} finally {
setIsSubmitting(false);
setLocalInput('');
}
}}
ref={formRef}
className="h-full mt-4"
>
{/* <div className="border-gray-500 flex overflow-hidden size-full flex flex-row items-center"> */}
<Textarea
ref={inputRef}
tabIndex={0}
onKeyDown={onKeyDown}
rows={1}
value={input}
disabled={finalDisabled}
onChange={e => setLocalInput(e.target.value)}
placeholder={
finalDisabled ? '🤖 Agent working ✨' : 'Message Vision Agent'
}
spellCheck={false}
className="w-full grow resize-none bg-transparent focus-within:outline-none"
/>
{/* Submit Icon */}
<Tooltip>
<TooltipTrigger asChild>
<Button
type="submit"
size="icon"
className={cn('size-6 absolute bottom-3 right-3')}
disabled={finalDisabled || input === '' || noMediaValidation}
>
<IconArrowUp className="size-3" />
</Button>
</TooltipTrigger>
<TooltipContent>Message Vision Agent</TooltipContent>
</Tooltip>
</form>
</div>
);
},
);
Composer.displayName = 'Composer';
export default Composer;