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:
@@ -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 { MessageThread } from "@/components/patient-connection/message-thread";
|
||||
import { DialPad } from "@/components/patient-connection/dial-pad";
|
||||
import { LiveTranslate } from "@/components/patient-connection/live-translate";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import type { Patient } from "@repo/db/types";
|
||||
@@ -427,6 +428,11 @@ export default function PatientConnectionPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Live Translation */}
|
||||
<div className="mt-6 max-w-2xl">
|
||||
<LiveTranslate />
|
||||
</div>
|
||||
|
||||
{/* Dial Pad */}
|
||||
<div className="mt-6 max-w-xs">
|
||||
<DialPad />
|
||||
|
||||
Reference in New Issue
Block a user