File size: 4,889 Bytes
697751d f19b1aa 697751d f19b1aa 697751d f19b1aa 697751d f19b1aa 697751d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | "use client";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { useSpeechRecognition } from "@/lib/useSpeechRecognition";
import { useCallback } from "react";
export function QueryComposer({
question,
isLoading,
disabled = false,
onChange,
onSubmit,
}: {
question: string;
isLoading: boolean;
disabled?: boolean;
onChange: (value: string) => void;
onSubmit: () => void;
}) {
const submitDisabled = isLoading || disabled;
// Append speech to whatever is already typed, so voice and keyboard compose.
const handleTranscript = useCallback(
(text: string) => {
if (!text) return;
const base = question.trim();
onChange(base ? `${base} ${text}` : text);
},
[onChange, question]
);
const { supported, listening, interim, error, toggle } = useSpeechRecognition({
onTranscript: handleTranscript,
});
return (
<div className="space-y-3">
<label className="sr-only" htmlFor="question">
Question
</label>
<div className="relative">
<Textarea
id="question"
value={question}
onChange={(event) => onChange(event.target.value)}
placeholder={listening ? "Listening…" : "e.g. How do I rotate API keys?"}
className={cn(
"min-h-[84px] border-[#111111]/15 bg-white text-[15px] leading-6 focus-visible:border-[#d62839]",
listening && "border-[#d62839]/60 pr-12"
)}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
if (!submitDisabled) onSubmit();
}
}}
/>
{supported && (
<Button
type="button"
variant="ghost"
size="icon"
onClick={toggle}
aria-pressed={listening}
aria-label={listening ? "Stop voice input" : "Start voice input"}
title={
listening
? "Stop voice input"
: "Speak your question (uses your browser's speech recognition)"
}
className={cn(
"absolute bottom-2 right-2 h-9 w-9",
listening
? "bg-[#d62839] text-white hover:bg-[#bf2130]"
: "text-[#8a8a8a] hover:bg-[#111111]/6 hover:text-[#111111]"
)}
>
{listening ? (
// Stop square: clearer than a filled mic for the active state.
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className="size-4">
<rect x="7" y="7" width="10" height="10" rx="1.5" />
</svg>
) : (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="size-4"
>
<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-2M12 19v3" />
</svg>
)}
</Button>
)}
</div>
{/* Live feedback while speaking, so the user sees recognition working. */}
{listening && (
<p
className="flex items-center gap-2 text-[12px] italic text-[#6b6b6b]"
aria-live="polite"
>
<span className="inline-block size-1.5 shrink-0 animate-pulse rounded-full bg-[#d62839]" />
{interim ? interim : "Listening… speak now"}
</p>
)}
{error && (
<p className="text-[12px] text-[#a81f2d]" role="alert">
{error}
</p>
)}
<div className="flex items-center justify-between gap-4">
<span className="text-[10px] font-medium uppercase tracking-[0.14em] text-[#8a8a8a]">
{disabled
? "Upload a document first"
: supported
? "Enter to send · Shift+Enter for a new line · Mic to speak"
: "Enter to send · Shift+Enter for a new line"}
</span>
<Button
type="button"
onClick={onSubmit}
disabled={submitDisabled}
className="group min-w-[130px]"
>
{isLoading ? "Working…" : "Send"}
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="transition-transform group-hover:translate-x-0.5"
>
<path d="M5 12h13M12 5l7 7-7 7" />
</svg>
</Button>
</div>
</div>
);
}
|