Gemma-4-WebGPU / src /components /InputBar.jsx
shreyask's picture
Upload folder using huggingface_hub
c38a89e verified
import { useState, useRef } from "react";
export default function InputBar({ onSubmit, disabled }) {
const [image, setImage] = useState(null);
const [audio, setAudio] = useState(null);
const [text, setText] = useState("");
const [isRecording, setIsRecording] = useState(false);
const mediaRecorderRef = useRef(null);
const audioChunksRef = useRef([]);
const fileInputRef = useRef(null);
const recordingTimeoutRef = useRef(null);
function handleImageSelect(e) {
const file = e.target.files?.[0];
if (!file) return;
setImage({ url: URL.createObjectURL(file), file });
}
async function startRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorder(stream);
audioChunksRef.current = [];
recorder.ondataavailable = (e) => {
if (e.data.size > 0) audioChunksRef.current.push(e.data);
};
recorder.onstop = () => {
clearTimeout(recordingTimeoutRef.current);
const blob = new Blob(audioChunksRef.current, { type: "audio/webm" });
setAudio({ url: URL.createObjectURL(blob), blob });
stream.getTracks().forEach((t) => t.stop());
};
recordingTimeoutRef.current = setTimeout(() => {
if (recorder.state === "recording") recorder.stop();
}, 30_000);
mediaRecorderRef.current = recorder;
recorder.start();
setIsRecording(true);
} catch (err) {
console.error("Mic access denied:", err);
}
}
function stopRecording() {
mediaRecorderRef.current?.stop();
setIsRecording(false);
}
function handleSubmit() {
if (!image && !audio && !text.trim()) return;
onSubmit({
imageUrl: image?.url || null,
audioUrl: audio?.url || null,
text: text.trim() || null,
});
setImage(null);
setAudio(null);
setText("");
}
const hasInput = image || audio || text.trim();
return (
<div className="px-4 py-3 border-t border-[var(--color-outline)]">
{/* Previews */}
{(image || audio) && (
<div className="flex gap-2 mb-2">
{image && (
<div className="relative">
<img src={image.url} alt="Upload" className="h-16 w-16 object-cover rounded-xl border border-[var(--color-outline)]" />
<button
onClick={() => { URL.revokeObjectURL(image.url); setImage(null); }}
className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-[var(--color-surface-high)] rounded-full text-xs flex items-center justify-center hover:bg-[var(--color-surface)] cursor-pointer"
>
×
</button>
</div>
)}
{audio && (
<div className="flex items-center gap-2 px-2 py-1.5 bg-[var(--color-surface)] rounded-xl">
<audio controls src={audio.url} className="h-7 max-w-[200px]" />
<button
onClick={() => { URL.revokeObjectURL(audio.url); setAudio(null); }}
className="text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-text)] cursor-pointer"
>
×
</button>
</div>
)}
</div>
)}
{/* Input row */}
<div className="flex items-center gap-2">
<input ref={fileInputRef} type="file" accept="image/*" capture="environment" onChange={handleImageSelect} className="hidden" />
<button
onClick={() => fileInputRef.current?.click()}
disabled={disabled}
className="p-2 rounded-full text-[var(--color-text-secondary)] hover:bg-[var(--color-surface)] disabled:opacity-30 transition-colors cursor-pointer"
title="Add image"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>
</button>
<button
onClick={isRecording ? stopRecording : startRecording}
disabled={disabled}
className={`p-2 rounded-full disabled:opacity-30 transition-colors cursor-pointer ${
isRecording
? "text-[var(--color-red)] bg-red-500/10 animate-pulse"
: "text-[var(--color-text-secondary)] hover:bg-[var(--color-surface)]"
}`}
title={isRecording ? "Stop recording" : "Record audio"}
>
{isRecording ? (
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
)}
</button>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter" && hasInput && !disabled) handleSubmit(); }}
placeholder="Message Gemma 4..."
disabled={disabled}
className="flex-1 bg-[var(--color-surface)] border border-[var(--color-outline)] rounded-xl px-4 py-2.5 text-sm text-[var(--color-text)] placeholder:text-[var(--color-text-secondary)]/50 focus:border-[var(--color-blue)]/50 focus:outline-none disabled:opacity-50"
/>
<button
onClick={handleSubmit}
disabled={disabled || !hasInput}
className="px-5 py-2.5 bg-[var(--color-blue)] hover:bg-[var(--color-blue)]/90 text-white text-sm font-medium rounded-xl transition-colors disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"
>
Send
</button>
</div>
</div>
);
}