Files
calendar/apps/web/modules/ee/workflows/components/VoiceSelectionDialog.tsx
T
Eunjae LeeGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1368ffe55d refactor: move data-table hooks/contexts/provider from features to web modules (#27833)
* refactor: move data-table hooks/contexts/provider from features to web modules

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: update broken useColumnResizing import path in DataTable.tsx

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: update remaining broken import paths for moved hooks

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* docs: update GUIDE.md import paths and file references for moved modules

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-13 00:00:40 +09:00

213 lines
6.6 KiB
TypeScript

import { getCoreRowModel, getSortedRowModel, useReactTable, type ColumnDef } from "@tanstack/react-table";
import { usePathname } from "next/navigation";
import { useMemo, useState, useCallback } from "react";
import { DataTableProvider } from "~/data-table/DataTableProvider";
import { DataTableWrapper } from "~/data-table/components";
import { useSegments } from "~/data-table/hooks/useSegments";
import { useVoicePreview } from "@calcom/features/ee/workflows/hooks/useVoicePreview";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { Dialog, DialogContent, DialogHeader } from "@calcom/ui/components/dialog";
import { PauseIcon, PlayIcon, UserIcon } from "@coss/ui/icons";
import { showToast } from "@calcom/ui/components/toast";
type Voice = {
voice_id: string;
voice_name: string;
provider: "elevenlabs" | "openai" | "deepgram";
gender: "male" | "female";
accent?: string;
age?: string;
preview_audio_url?: string;
};
type VoiceSelectionDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
selectedVoiceId?: string;
onVoiceSelect: (voiceId: string) => void;
};
function VoiceSelectionTable({
selectedVoiceId,
onVoiceSelect,
}: {
selectedVoiceId?: string;
onVoiceSelect: (voiceId: string) => void;
}) {
const pathname = usePathname();
if (!pathname) return null;
return (
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments} defaultPageSize={1000}>
<VoiceSelectionContent selectedVoiceId={selectedVoiceId} onVoiceSelect={onVoiceSelect} />
</DataTableProvider>
);
}
function VoiceSelectionContent({
selectedVoiceId,
onVoiceSelect,
}: {
selectedVoiceId?: string;
onVoiceSelect: (voiceId: string) => void;
}) {
const { t } = useLocale();
const { playingVoiceId, handlePlayVoice } = useVoicePreview();
const [rowSelection, setRowSelection] = useState({});
const { data: voices, isLoading } = trpc.viewer.aiVoiceAgent.listVoices.useQuery();
const handleUseVoice = useCallback(
(voiceId: string) => {
onVoiceSelect(voiceId);
showToast("Voice selected successfully", "success");
},
[onVoiceSelect]
);
const voiceData: Voice[] = useMemo(() => {
if (!voices) return [];
return voices;
}, [voices]);
const columns = useMemo<ColumnDef<Voice>[]>(
() => [
{
id: "voice",
accessorKey: "voice_name",
header: t("voice"),
size: 200,
cell: ({ row }) => (
<div className="flex items-center gap-3">
<Button
type="button"
variant="icon"
color="secondary"
size="sm"
onClick={() => handlePlayVoice(row.original.preview_audio_url, row.original.voice_id)}
className="rounded-full">
{playingVoiceId === row.original.voice_id ? (
<PauseIcon className="text-default h-3 w-3" />
) : (
<PlayIcon className="text-default h-3 w-3" />
)}
</Button>
<div className="flex items-center gap-2">
<div className="bg-subtle flex h-10 w-10 items-center justify-center rounded-full">
<UserIcon className="text-default h-5 w-5" />
</div>
<span className="text-emphasis font-medium">{row.original.voice_name}</span>
</div>
</div>
),
},
{
id: "trait",
accessorKey: "accent",
header: t("trait"),
size: 200,
cell: ({ row }) => (
<div className="flex flex-wrap gap-2 text-sm sm:flex-nowrap">
{row.original.accent && (
<span className="bg-subtle text-default rounded-md px-2 py-1 text-xs">
{row.original.accent}
</span>
)}
{row.original.gender && (
<span className="bg-subtle text-default rounded-md px-2 py-1 text-xs">
{row.original.gender}
</span>
)}
{row.original.provider && (
<span className="bg-subtle text-default rounded-md px-2 py-1 text-xs">
{row.original.provider}
</span>
)}
{row.original.age && (
<span className="bg-subtle text-default rounded-md px-2 py-1 text-xs">{row.original.age}</span>
)}
</div>
),
},
{
id: "voice_id",
accessorKey: "voice_id",
header: t("voice_id"),
size: 200,
cell: ({ row }) => <span className="text-muted font-mono text-sm">{row.original.voice_id}</span>,
},
{
id: "actions",
header: "",
size: 120,
cell: ({ row }) => (
<Button
type="button"
size="sm"
color={selectedVoiceId === row.original.voice_id ? "primary" : "secondary"}
onClick={() => handleUseVoice(row.original.voice_id)}
className="whitespace-nowrap">
{selectedVoiceId === row.original.voice_id ? t("current_voice") : t("use_voice")}
</Button>
),
},
],
[t, playingVoiceId, selectedVoiceId, handlePlayVoice, handleUseVoice]
);
const table = useReactTable({
data: voiceData,
columns,
enableRowSelection: false,
manualPagination: false,
state: {
rowSelection,
},
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
onRowSelectionChange: setRowSelection,
getRowId: (row) => row.voice_id,
});
return (
<div className="border-subtle max-h-[500px] overflow-auto rounded-lg border">
<DataTableWrapper<Voice>
testId="voice-selection-data-table"
table={table}
isPending={isLoading}
totalRowCount={voiceData?.length || 0}
paginationMode="standard"
className="h-auto"
/>
</div>
);
}
export function VoiceSelectionDialog({
open,
onOpenChange,
selectedVoiceId,
onVoiceSelect,
}: VoiceSelectionDialogProps) {
const { t } = useLocale();
const handleVoiceSelect = (voiceId: string) => {
onVoiceSelect(voiceId);
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent enableOverflow size="md" className="flex flex-col p-6 sm:max-w-7xl">
<DialogHeader title={t("select_voice")} subtitle={t("choose_a_voice_for_your_agent")} />
<div className="mt-4 min-h-0 flex-1 overflow-hidden">
<VoiceSelectionTable selectedVoiceId={selectedVoiceId} onVoiceSelect={handleVoiceSelect} />
</div>
</DialogContent>
</Dialog>
);
}