ADZREFCOSVSO6I66BOLGTZFJI4Y7J3WJO2PNHCHE2W7Y5PZPX4OAC OQBBKXHMSDFTIYQPOAT4QTOELZP6JURH524SQPWOXWGN6DG5ZR6QC QTGZF5KRDIDHUHFMBLO4Z4AOT4EDENKF3Q3VWXE27IE4EZLZZWIAC IUHUM6OZ5KYEAYQCIYNG5Q4QLQRAQNMBWKYGV2ZDJFNY5W4DOUNQC ZMAYRGGFLPFVCHOWUI3AZQIKPJBFX7V3J6DVG3BGJFC7HBOBIAOQC DZURLJKGFFIAG4KUZTLCURERBXHFWNSNUFYYB7PXCXFUJWPIHWZQC SML4CPUO62JIWYGYWZGSWFVAMRRBNE2ECK7ATUSFPKC6XICKVIMAC YBJGFUTFTV4SBPUVC6LBETGMPMLS47LSQPL3OJAAPH7BROBQD7DQC TPANU3OQD53OXF5IGXJELSD5FFMY7KHDSVZTPSUG3VZBUAPNPK6QC I27QGYUJ66RJXCXQPY2MMSODRGL3SB7KO4UOGQSK2WRFG7LG23GAC ETAXJ5YIOUH75V6ZKHUGQWTRWIRZVJAUG5LYXYZEJDS7SXPNEZSAC 746WXWS34DYNQ4CEFWBXDFWPPDY7CNR7TJGLTCK6UEAG7ZXBYCIQC RELTAIDEZ4Y2SC2WQROQ4IEJO5BVPULTVLLJ4BA23WE5FVT5QOQAC // Load the authenticated audio stream// For authenticated requests, we need to fetch the blob first and then load itconst response = await fetch(`/api/files/${fileId}/download`, {headers: {"Authorization": `Bearer ${accessToken}`,},});
// Load the authenticated audio stream// For authenticated requests, we need to fetch the blob first and then load itconst response = await fetch(`/api/files/${fileId}/download`, {headers: {Authorization: `Bearer ${accessToken}`,},});
// Clean up the object URL when donesetTimeout(() => URL.revokeObjectURL(url), 1000);} catch (error) {console.error("Error loading online file:", error);setState(prev => ({...prev,audioError: "Failed to load audio file from server",audioLoading: false,}));}}, [state.wavesurfer, getAccessToken]);
// Clean up the object URL when donesetTimeout(() => URL.revokeObjectURL(url), 1000);} catch (error) {console.error("Error loading online file:", error);setState((prev) => ({...prev,audioError: "Failed to load audio file from server",audioLoading: false,}));}},[state.wavesurfer, getAccessToken],);
// Store cleanup function for later usesetTimeout(() => URL.revokeObjectURL(url), 60000); // Clean up after 1 minute} catch (error) {console.error("Error loading local file:", error);setState(prev => ({...prev,audioError: "Failed to load local audio file",audioLoading: false,}));}}, [state.wavesurfer]);
// Store cleanup function for later usesetTimeout(() => URL.revokeObjectURL(url), 60000); // Clean up after 1 minute} catch (error) {console.error("Error loading local file:", error);setState((prev) => ({...prev,audioError: "Failed to load local audio file",audioLoading: false,}));}},[state.wavesurfer],);
const showOpenFilePicker = (window as unknown as {showOpenFilePicker: (options: unknown) => Promise<FileSystemFileHandle[]>;}).showOpenFilePicker;
const showOpenFilePicker = (window as unknown as {showOpenFilePicker: (options: unknown,) => Promise<FileSystemFileHandle[]>;}).showOpenFilePicker;
types: [{description: "Audio files",accept: {"audio/*": [".wav", ".flac"],
types: [{description: "Audio files",accept: {"audio/*": [".wav", ".flac"],},
}}, [state.wavesurfer]);// Simple zoom controls using built-in zoom methodconst zoomIn = useCallback(() => {if (state.wavesurfer) {try {// Use a small increment for smoother zooming (25 pixels per second)state.wavesurfer.zoom(25);} catch (error) {console.error('Error zooming in:', error);}}}, [state.wavesurfer]);const zoomOut = useCallback(() => {if (state.wavesurfer) {try {// Use negative increment to zoom outstate.wavesurfer.zoom(-15);} catch (error) {console.error('Error zooming out:', error);}
const resetZoom = useCallback(() => {if (state.wavesurfer) {try {// Reset to default zoom level (0)state.wavesurfer.zoom(0);} catch (error) {console.error('Error resetting zoom:', error);}}}, [state.wavesurfer]);
const fetchSelections = useCallback(async (fileId: string) => {setState(prev => ({ ...prev, selectionsLoading: true, selectionsError: null }));
const fetchSelections = useCallback(async (fileId: string) => {setState((prev) => ({...prev,selectionsLoading: true,selectionsError: null,}));
const response = await fetch(`/api/files/${fileId}/selections`, {headers: {"Authorization": `Bearer ${accessToken}`,},});
const response = await fetch(`/api/files/${fileId}/selections`, {headers: {Authorization: `Bearer ${accessToken}`,},});
if (!response.ok) {throw new Error(`Failed to fetch selections: ${response.status}`);
if (!response.ok) {throw new Error(`Failed to fetch selections: ${response.status}`);}const data = await response.json();setState((prev) => ({...prev,selections: data.data || [],selectionsLoading: false,}));} catch (error) {console.error("Error fetching selections:", error);setState((prev) => ({...prev,selectionsError: "Failed to load selections",selectionsLoading: false,}));
const data = await response.json();setState(prev => ({...prev,selections: data.data || [],selectionsLoading: false,}));} catch (error) {console.error("Error fetching selections:", error);setState(prev => ({...prev,selectionsError: "Failed to load selections",selectionsLoading: false,}));}}, [getAccessToken]);
const fetchFileContext = useCallback(async (fileId: string) => {setState(prev => ({ ...prev, fileContextLoading: true }));
const fetchFileContext = useCallback(async (fileId: string) => {setState((prev) => ({ ...prev, fileContextLoading: true }));try {const accessToken = await getAccessToken();
const response = await fetch(`/api/files/${fileId}/context`, {headers: {"Authorization": `Bearer ${accessToken}`,},});
if (!response.ok) {throw new Error(`Failed to fetch file context: ${response.status}`);}
if (!response.ok) {throw new Error(`Failed to fetch file context: ${response.status}`);
const data = await response.json();setState((prev) => ({...prev,fileContext: data.data,fileContextLoading: false,}));} catch (error) {console.error("Error fetching file context:", error);setState((prev) => ({...prev,fileContextLoading: false,}));
const data = await response.json();setState(prev => ({...prev,fileContext: data.data,fileContextLoading: false,}));} catch (error) {console.error("Error fetching file context:", error);setState(prev => ({...prev,fileContextLoading: false,}));}}, [getAccessToken]);
},[getAccessToken],);
const speciesLabels = filter.species.map(species => {const shortName = species.name.length > 15 ? species.name.substring(0, 15) + "..." : species.name;const callTypeText = species.callTypes.length > 0? ` (${species.callTypes[0].name})` // Only show first call type: "";return `${shortName}${callTypeText}`;}).join("; ");
const speciesLabels = filter.species.map((species) => {const shortName =species.name.length > 15? species.name.substring(0, 15) + "...": species.name;const callTypeText =species.callTypes.length > 0? ` (${species.callTypes[0].name})` // Only show first call type: "";return `${shortName}${callTypeText}`;}).join("; ");
const region = (state.regionsPlugin as { addRegion: (config: unknown) => { element?: HTMLElement } }).addRegion({start: regionData.startTime,end: regionData.endTime,color: regionData.color + "25", // Slightly more opacityresize: false,drag: false,},);
const region = (state.regionsPlugin as {addRegion: (config: unknown) => { element?: HTMLElement };}).addRegion({start: regionData.startTime,end: regionData.endTime,color: regionData.color + "25", // Slightly more opacityresize: false,drag: false,});
{state.fileContext? (<>{state.fileContext.dataset && `Dataset: ${state.fileContext.dataset.name}`}{state.fileContext.dataset && state.fileContext.location && " • "}{state.fileContext.location && `Location: ${state.fileContext.location.name}`}{(state.fileContext.dataset || state.fileContext.location) && state.fileContext.cluster && " • "}{state.fileContext.cluster && `Cluster: ${state.fileContext.cluster.name}`}</>): state.fileContextLoading? ("Loading context..."): ("Audio File")}
{state.fileContext ? (<>{state.fileContext.dataset &&`Dataset: ${state.fileContext.dataset.name}`}{state.fileContext.dataset &&state.fileContext.location &&" • "}{state.fileContext.location &&`Location: ${state.fileContext.location.name}`}{(state.fileContext.dataset ||state.fileContext.location) &&state.fileContext.cluster &&" • "}{state.fileContext.cluster &&`Cluster: ${state.fileContext.cluster.name}`}</>) : state.fileContextLoading ? ("Loading context...") : ("Audio File")}
{state.wavesurfer && !state.audioLoading && !state.audioError && (<div className="flex items-center gap-4 p-4 bg-muted/20 rounded-md"><Buttonvariant="outline"size="icon"onClick={togglePlayPause}disabled={state.audioLoading}>{state.isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}</Button>{/* Zoom Controls */}<div className="flex items-center gap-1 border-l pl-4 ml-2"><Buttonvariant="outline"size="icon"onClick={zoomOut}disabled={state.audioLoading}title="Zoom Out"><Minus className="h-4 w-4" /></Button>
{state.wavesurfer &&!state.audioLoading &&!state.audioError && (<div className="flex items-center gap-4 p-4 bg-muted/20 rounded-md">
<Buttonvariant="outline"size="icon"onClick={resetZoom}disabled={state.audioLoading}title="Reset Zoom"><RotateCcw className="h-4 w-4" /></Button>
<div className="text-sm text-muted-foreground">Duration: {formatTime(file.duration)} • Sample Rate:{" "}{(file.sampleRate / 1000).toFixed(1)} kHz</div>
<div className="text-sm text-muted-foreground">Duration: {formatTime(file.duration)} • Sample Rate: {(file.sampleRate / 1000).toFixed(1)} kHz</div></div>)}
)}
{state.wavesurfer && !state.audioLoading && !state.audioError && (<div className="flex items-center gap-4 p-4 bg-muted/20 rounded-md"><Buttonvariant="outline"size="icon"onClick={togglePlayPause}disabled={state.audioLoading}>{state.isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}</Button>{/* Zoom Controls */}<div className="flex items-center gap-1 border-l pl-4 ml-2">
{state.wavesurfer &&!state.audioLoading &&!state.audioError && (<div className="flex items-center gap-4 p-4 bg-muted/20 rounded-md">
<Minus className="h-4 w-4" /></Button><Buttonvariant="outline"size="icon"onClick={zoomIn}disabled={state.audioLoading}title="Zoom In"><Plus className="h-4 w-4" />
{state.isPlaying ? (<Pause className="h-4 w-4" />) : (<Play className="h-4 w-4" />)}
<Buttonvariant="outline"size="icon"onClick={resetZoom}disabled={state.audioLoading}title="Reset Zoom"><RotateCcw className="h-4 w-4" /></Button></div><div className="text-sm text-muted-foreground">Duration: {formatTime(file.duration)} • Sample Rate: {(file.sampleRate / 1000).toFixed(1)} kHz
<div className="text-sm text-muted-foreground">Duration: {formatTime(file.duration)} • Sample Rate:{" "}{(file.sampleRate / 1000).toFixed(1)} kHz</div>
{state.selections.length > 0 && getAvailableFilters().length > 0 && (<div className="absolute bottom-4 right-4"><selectvalue={state.selectedFilterId || "all"}onChange={(e) =>setState(prev => ({...prev,selectedFilterId: e.target.value === "all" ? null : e.target.value,}))}className="text-xs rounded-md border border-gray-300 bg-white py-1 px-2 shadow-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"><option value="all">All Models</option>{getAvailableFilters().map(filter => (<option key={filter.id} value={filter.id}>{filter.name || `Filter ${filter.id}`}</option>))}</select></div>)}
{state.selections.length > 0 &&getAvailableFilters().length > 0 && (<div className="absolute bottom-4 right-4"><selectvalue={state.selectedFilterId || "all"}onChange={(e) =>setState((prev) => ({...prev,selectedFilterId:e.target.value === "all" ? null : e.target.value,}))}className="text-xs rounded-md border border-gray-300 bg-white py-1 px-2 shadow-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"><option value="all">All Models</option>{getAvailableFilters().map((filter) => (<option key={filter.id} value={filter.id}>{filter.name || `Filter ${filter.id}`}</option>))}</select></div>)}