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 { 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 />
|
||||||
|
|||||||
Reference in New Issue
Block a user