Files
calendar/packages/features/ee/workflows/components/VoiceSelectionDialog.tsx
T
Eunjae LeeGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
09ddbe886f refactor: make DataTableProvider framework-agnostic by requiring tableIdentifier (#24513)
* refactor: make DataTableProvider framework-agnostic by requiring tableIdentifier

- Remove Next.js usePathname dependency from DataTableProvider
- Make tableIdentifier a required prop instead of optional
- Update all usages to provide explicit tableIdentifier values
- This makes DataTableProvider usable in non-Next.js contexts

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

* refactor: use usePathname at usage sites instead of hardcoding tableIdentifier

- Add validation in DataTableProvider for empty/nullish tableIdentifier
- Use usePathname() in Next.js apps to pass pathname as tableIdentifier
- Use descriptive identifiers for non-Next.js package components
- This keeps DataTableProvider framework-agnostic while allowing Next.js apps to use pathname

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

* use pathname instead of hard-coded identifiers

* change type of tableIdentifier

* simplify

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-23 15:32:16 +02:00

210 lines
6.5 KiB
TypeScript

import { getCoreRowModel, getSortedRowModel, useReactTable, type ColumnDef } from "@tanstack/react-table";
import { usePathname } from "next/navigation";
import { useMemo, useState } from "react";
import { DataTableProvider, DataTableWrapper } from "@calcom/features/data-table";
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
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 { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
import { useVoicePreview } from "../hooks/useVoicePreview";
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 = (voiceId: string) => {
onVoiceSelect(voiceId);
showToast("Voice selected successfully", "success");
};
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 ? (
<Icon name="pause" className="text-default h-3 w-3" />
) : (
<Icon name="play" 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">
<Icon name="user" 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 gap-2 text-sm">
{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]
);
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 className="flex h-[80vh] 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>
);
}