feat: add live translation UI section above dial pad

Google Translate-style side-by-side layout with per-side language
pickers on the patient connection page. UI shell only for now, no
translation provider wired up yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 16:05:30 -04:00
co-authored by Claude Sonnet 5
parent 8f9fd13208
commit 616ca5f259
2 changed files with 146 additions and 0 deletions
@@ -0,0 +1,140 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Languages, ArrowLeftRight, Mic } from "lucide-react";
const LANGUAGES = [
{ code: "en", label: "English" },
{ code: "es", label: "Spanish" },
{ code: "pt", label: "Portuguese" },
{ code: "zh", label: "Chinese" },
{ code: "vi", label: "Vietnamese" },
{ code: "ht", label: "Haitian Creole" },
{ code: "fr", label: "French" },
{ code: "ar", label: "Arabic" },
{ code: "ru", label: "Russian" },
];
export function LiveTranslate() {
const [leftLang, setLeftLang] = useState("en");
const [rightLang, setRightLang] = useState("es");
const [leftText, setLeftText] = useState("");
const [rightText, setRightText] = useState("");
const handleSwap = () => {
setLeftLang(rightLang);
setRightLang(leftLang);
setLeftText(rightText);
setRightText(leftText);
};
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Languages className="h-4 w-4" />
Live Translation
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-[1fr_auto_1fr] gap-3 items-stretch">
{/* Left side */}
<div className="flex flex-col gap-2">
<Select value={leftLang} onValueChange={setLeftLang}>
<SelectTrigger data-testid="select-translate-left-language">
<SelectValue placeholder="Language" />
</SelectTrigger>
<SelectContent>
{LANGUAGES.map((lang) => (
<SelectItem key={lang.code} value={lang.code}>
{lang.label}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<Textarea
value={leftText}
onChange={(e) => setLeftText(e.target.value)}
placeholder="Type or speak..."
className="min-h-[120px] resize-none pr-10"
data-testid="textarea-translate-left"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute bottom-2 right-2 h-7 w-7 text-muted-foreground"
title="Voice input (coming soon)"
disabled
>
<Mic className="h-4 w-4" />
</Button>
</div>
</div>
{/* Swap button */}
<div className="flex md:flex-col items-center justify-center">
<Button
type="button"
variant="outline"
size="icon"
onClick={handleSwap}
className="rounded-full"
title="Swap languages"
data-testid="button-translate-swap"
>
<ArrowLeftRight className="h-4 w-4" />
</Button>
</div>
{/* Right side */}
<div className="flex flex-col gap-2">
<Select value={rightLang} onValueChange={setRightLang}>
<SelectTrigger data-testid="select-translate-right-language">
<SelectValue placeholder="Language" />
</SelectTrigger>
<SelectContent>
{LANGUAGES.map((lang) => (
<SelectItem key={lang.code} value={lang.code}>
{lang.label}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<Textarea
value={rightText}
onChange={(e) => setRightText(e.target.value)}
placeholder="Translation will appear here..."
className="min-h-[120px] resize-none pr-10"
data-testid="textarea-translate-right"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute bottom-2 right-2 h-7 w-7 text-muted-foreground"
title="Voice input (coming soon)"
disabled
>
<Mic className="h-4 w-4" />
</Button>
</div>
</div>
</div>
<p className="text-xs text-muted-foreground mt-3">
Translation isn't connected to an AI provider yet — this is the UI shell for now.
</p>
</CardContent>
</Card>
);
}
@@ -29,6 +29,7 @@ import {
import { SmsTemplateDialog } from "@/components/patient-connection/sms-template-diaog"; import { SmsTemplateDialog } from "@/components/patient-connection/sms-template-diaog";
import { MessageThread } from "@/components/patient-connection/message-thread"; import { MessageThread } from "@/components/patient-connection/message-thread";
import { DialPad } from "@/components/patient-connection/dial-pad"; import { DialPad } from "@/components/patient-connection/dial-pad";
import { LiveTranslate } from "@/components/patient-connection/live-translate";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { apiRequest } from "@/lib/queryClient"; import { apiRequest } from "@/lib/queryClient";
import type { Patient } from "@repo/db/types"; import type { Patient } from "@repo/db/types";
@@ -427,6 +428,11 @@ export default function PatientConnectionPage() {
</CardContent> </CardContent>
</Card> </Card>
{/* Live Translation */}
<div className="mt-6 max-w-2xl">
<LiveTranslate />
</div>
{/* Dial Pad */} {/* Dial Pad */}
<div className="mt-6 max-w-xs"> <div className="mt-6 max-w-xs">
<DialPad /> <DialPad />