initial commit

This commit is contained in:
Rami Bitar
2026-04-13 15:34:57 -04:00
commit 9778f24862
125 changed files with 39760 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
"use client";
import { useControllableState } from "@radix-ui/react-use-controllable-state";
import { cn } from "@/lib/utils";
import type { Experimental_TranscriptionResult as TranscriptionResult } from "ai";
import type { ComponentProps, ReactNode } from "react";
import { createContext, useCallback, useContext, useMemo } from "react";
type TranscriptionSegment = TranscriptionResult["segments"][number];
interface TranscriptionContextValue {
segments: TranscriptionSegment[];
currentTime: number;
onTimeUpdate: (time: number) => void;
onSeek?: (time: number) => void;
}
const TranscriptionContext = createContext<TranscriptionContextValue | null>(
null
);
const useTranscription = () => {
const context = useContext(TranscriptionContext);
if (!context) {
throw new Error(
"Transcription components must be used within Transcription"
);
}
return context;
};
export type TranscriptionProps = Omit<ComponentProps<"div">, "children"> & {
segments: TranscriptionSegment[];
currentTime?: number;
onSeek?: (time: number) => void;
children: (segment: TranscriptionSegment, index: number) => ReactNode;
};
export const Transcription = ({
segments,
currentTime: externalCurrentTime,
onSeek,
className,
children,
...props
}: TranscriptionProps) => {
const [currentTime, setCurrentTime] = useControllableState({
defaultProp: 0,
onChange: onSeek,
prop: externalCurrentTime,
});
const contextValue = useMemo(
() => ({ currentTime, onSeek, onTimeUpdate: setCurrentTime, segments }),
[currentTime, onSeek, setCurrentTime, segments]
);
return (
<TranscriptionContext.Provider value={contextValue}>
<div
className={cn(
"flex flex-wrap gap-1 text-sm leading-relaxed",
className
)}
data-slot="transcription"
{...props}
>
{segments
.filter((segment) => segment.text.trim())
.map((segment, index) => children(segment, index))}
</div>
</TranscriptionContext.Provider>
);
};
export type TranscriptionSegmentProps = ComponentProps<"button"> & {
segment: TranscriptionSegment;
index: number;
};
export const TranscriptionSegment = ({
segment,
index,
className,
onClick,
...props
}: TranscriptionSegmentProps) => {
const { currentTime, onSeek } = useTranscription();
const isActive =
currentTime >= segment.startSecond && currentTime < segment.endSecond;
const isPast = currentTime >= segment.endSecond;
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
if (onSeek) {
onSeek(segment.startSecond);
}
onClick?.(event);
},
[onSeek, segment.startSecond, onClick]
);
return (
<button
className={cn(
"inline text-left",
isActive && "text-primary",
isPast && "text-muted-foreground",
!(isActive || isPast) && "text-muted-foreground/60",
onSeek && "cursor-pointer hover:text-foreground",
!onSeek && "cursor-default",
className
)}
data-active={isActive}
data-index={index}
data-slot="transcription-segment"
onClick={handleClick}
type="button"
{...props}
>
{segment.text}
</button>
);
};