github-actions[bot]
Deploy demo from GitHub Actions - 2025-12-24 02:23:20
6cdce85
'use client';
import { useState, useRef, useCallback, useImperativeHandle, forwardRef } from 'react';
import { Send, Image as ImageIcon, X, Loader2 } from 'lucide-react';
import { clsx } from 'clsx';
interface MessageInputProps {
onSend: (message: string, imageUrl?: string, imageBase64?: string) => void;
isLoading: boolean;
placeholder?: string;
}
export interface MessageInputRef {
setContent: (text: string, imageUrl?: string) => void;
clear: () => void;
}
export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
function MessageInput(
{
onSend,
isLoading,
placeholder = 'Ask about quantum computing, Qiskit, or upload a circuit diagram...',
},
ref
) {
const [message, setMessage] = useState('');
const [imageBase64, setImageBase64] = useState<string | null>(null);
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [imagePreview, setImagePreview] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
useImperativeHandle(ref, () => ({
setContent: (text: string, url?: string) => {
setMessage(text);
if (url) {
setImageUrl(url);
setImagePreview(url);
setImageBase64(null);
}
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
setTimeout(() => {
if (textareaRef.current) {
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`;
}
}, 0);
}
},
clear: () => {
setMessage('');
setImageBase64(null);
setImageUrl(null);
setImagePreview(null);
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
}
},
}));
const handleSubmit = useCallback(() => {
if ((!message.trim() && !imageBase64 && !imageUrl) || isLoading) return;
onSend(message.trim(), imageUrl || undefined, imageBase64 || undefined);
setMessage('');
setImageBase64(null);
setImageUrl(null);
setImagePreview(null);
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
}
}, [message, imageBase64, imageUrl, isLoading, onSend]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
};
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
alert('Please upload an image file');
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target?.result as string;
const base64 = result.split(',')[1];
setImageBase64(base64);
setImageUrl(null);
setImagePreview(result);
};
reader.readAsDataURL(file);
};
const removeImage = () => {
setImageBase64(null);
setImageUrl(null);
setImagePreview(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const adjustTextareaHeight = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const textarea = e.target;
textarea.style.height = 'auto';
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
setMessage(textarea.value);
};
const hasContent = message.trim() || imageBase64 || imageUrl;
return (
<div className="bg-zinc-800/60 border border-zinc-700/50 rounded-xl p-3">
{imagePreview && (
<div className="mb-3 relative inline-block">
<img
src={imagePreview}
alt="Upload preview"
className="h-24 rounded-lg border border-zinc-700/50 object-contain bg-zinc-900"
/>
<button
onClick={removeImage}
className="absolute -top-2 -right-2 p-1 bg-red-600/80 rounded-full hover:bg-red-600 transition-colors"
>
<X className="w-3 h-3 text-white" />
</button>
</div>
)}
<div className="flex items-end gap-2">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageUpload}
className="hidden"
/>
<button
onClick={() => fileInputRef.current?.click()}
disabled={isLoading}
className={clsx(
'p-3 rounded-lg transition-all duration-200',
'hover:bg-zinc-700/50 text-zinc-500 hover:text-zinc-300',
isLoading && 'opacity-50 cursor-not-allowed'
)}
title="Upload image"
>
<ImageIcon className="w-5 h-5" />
</button>
<textarea
ref={textareaRef}
value={message}
onChange={adjustTextareaHeight}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={isLoading}
rows={1}
className={clsx(
'flex-1 bg-transparent border-none outline-none resize-none',
'text-zinc-200 placeholder:text-zinc-500',
'min-h-[44px] max-h-[200px] py-3',
isLoading && 'opacity-50'
)}
/>
<button
onClick={handleSubmit}
disabled={!hasContent || isLoading}
className={clsx(
'p-3 rounded-lg transition-all duration-200',
hasContent
? 'bg-teal-700/80 hover:bg-teal-600/80 text-white'
: 'bg-zinc-700/50 text-zinc-500',
isLoading && 'opacity-50 cursor-not-allowed'
)}
>
{isLoading ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<Send className="w-5 h-5" />
)}
</button>
</div>
</div>
);
}
);