tahamajs/IE / Projects /front /src /components /multimodal /MultimodalMessageInput.tsx
tahamajs's picture
download
raw
5.54 kB
// src/components/multimodal/MultimodalMessageInput.tsx
import { useRef, useState } from "react";
import type { ProcessedFile } from "@/types/multimodal";
import { FilePreviewCard } from "./FilePreviewCard";
import { Send, Paperclip, Mic, X, Sparkles, Loader2 } from "lucide-react";
import { VoiceMessageButton } from "@/components/voice/VoiceMessageButton";
interface MultimodalMessageInputProps {
value: string;
onChange: (val: string) => void;
onSend: () => void;
files: ProcessedFile[];
onFilesChange: (files: ProcessedFile[]) => void;
onFileRemove: (id: string) => void;
onAnalyzeFile: (file: ProcessedFile) => void;
onTyping: () => void;
sending: boolean;
showAiButton?: boolean;
onAiChat?: () => void;
}
export function MultimodalMessageInput({
value, onChange, onSend, files, onFilesChange, onFileRemove,
onAnalyzeFile, onTyping, sending, showAiButton, onAiChat,
}: MultimodalMessageInputProps) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [pasted, setPasted] = useState(false);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
onSend();
}
};
// Handle paste (images from clipboard)
const handlePaste = (e: React.ClipboardEvent) => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of Array.from(items)) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
e.preventDefault();
// Convert to a pseudo‑FileList
const dt = new DataTransfer();
dt.items.add(file);
const fakeEvent = {
target: { files: dt.files },
} as unknown as React.ChangeEvent<HTMLInputElement>;
handleFileInput(fakeEvent);
}
}
}
};
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = e.target.files;
if (selectedFiles?.length) {
// Import dynamically to avoid circular deps – use the hook in parent instead
// Here we just signal the parent; parent uses useFileProcessor
window.dispatchEvent(
new CustomEvent("multimodal-files-selected", {
detail: { files: Array.from(selectedFiles) },
})
);
}
// Reset input so same file can be re‑selected
if (fileInputRef.current) fileInputRef.current.value = "";
};
return (
<div className="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
{/* File preview strip */}
{files.length > 0 && (
<div className="px-3 pt-3 flex gap-2 overflow-x-auto pb-1">
{files.map((file) => (
<div key={file.id} className="shrink-0 w-40">
<FilePreviewCard
file={file}
onRemove={onFileRemove}
onAnalyze={file.uploadStatus === "done" ? onAnalyzeFile : undefined}
/>
</div>
))}
</div>
)}
{/* Input row */}
<div className="flex items-end gap-2 p-3">
{/* Attach button */}
<button
onClick={() => fileInputRef.current?.click()}
className="p-2 text-gray-400 hover:text-primary hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition"
title="Attach file"
>
<Paperclip className="w-5 h-5" />
</button>
<input
ref={fileInputRef}
type="file"
multiple
className="hidden"
onChange={handleFileInput}
accept="image/*,video/*,.csv,.pdf,.json,.xml,.yaml,.yml,.py,.js,.ts,.jsx,.tsx,.java,.c,.cpp,.go,.rs,.rb,.tex,.mol,.sdf,.pdb,.cif"
/>
{/* Voice input */}
<VoiceMessageButton
onTranscript={(text) => onChange(value + " " + text)}
/>
{/* AI Chat button */}
{showAiButton && (
<button
onClick={onAiChat}
className="p-2 text-purple-400 hover:text-purple-600 hover:bg-purple-50 dark:hover:bg-purple-900/20 rounded-lg transition"
title="Ask AI about files"
>
<Sparkles className="w-5 h-5" />
</button>
)}
{/* Text input */}
<div className="flex-1 relative" onPaste={handlePaste}>
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={handleKeyDown}
onInput={onTyping}
placeholder="Type a message or drop files here..."
rows={1}
className="w-full resize-none px-4 py-2.5 bg-gray-100 dark:bg-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary text-gray-800 dark:text-gray-100 placeholder-gray-400 text-sm"
style={{ minHeight: "42px", maxHeight: "120px" }}
/>
{/* Paste indicator */}
{pasted && (
<span className="absolute top-1 right-3 text-[10px] text-green-500">
Image pasted!
</span>
)}
</div>
{/* Send button */}
<button
onClick={onSend}
disabled={(!value.trim() && files.length === 0) || sending}
className="p-2.5 bg-primary text-white rounded-xl hover:bg-primary-dark disabled:opacity-50 transition shadow"
>
{sending ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<Send className="w-5 h-5" />
)}
</button>
</div>
</div>
);
}

Xet Storage Details

Size:
5.54 kB
·
Xet hash:
145e784ff43c3a1f12dbf78c0fe665be876fb916135aee94f26da639dbd7e949

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.