|
|
|
@@ -1,4 +1,4 @@
|
|
|
|
|
import { useState, useEffect, useRef } from "react";
|
|
|
|
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
|
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
|
|
|
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
|
|
|
|
import { getVersion } from "@tauri-apps/api/app";
|
|
|
|
@@ -42,7 +42,14 @@ interface Recording {
|
|
|
|
|
summaryProgress?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type AppState = "checking" | "downloading_models" | "initializing" | "ready" | "recording" | "transcribing" | "error";
|
|
|
|
|
type AppState =
|
|
|
|
|
| "checking"
|
|
|
|
|
| "downloading_models"
|
|
|
|
|
| "initializing"
|
|
|
|
|
| "ready"
|
|
|
|
|
| "recording"
|
|
|
|
|
| "transcribing"
|
|
|
|
|
| "error";
|
|
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
|
const [recordings, setRecordings] = useState<Recording[]>([]);
|
|
|
|
@@ -63,17 +70,75 @@ function App() {
|
|
|
|
|
const transcriptionProgressUnlisten = useRef<UnlistenFn | null>(null);
|
|
|
|
|
const summaryProgressUnlisten = useRef<UnlistenFn | null>(null);
|
|
|
|
|
|
|
|
|
|
// Define initializeApp before using it
|
|
|
|
|
const initializeApp = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
|
setAppState("checking");
|
|
|
|
|
setStatusMessage("Checking if models are present...");
|
|
|
|
|
|
|
|
|
|
const modelsPresent = await invoke<boolean>("check_models");
|
|
|
|
|
|
|
|
|
|
if (!modelsPresent) {
|
|
|
|
|
setAppState("downloading_models");
|
|
|
|
|
setStatusMessage("Downloading AI models (~2GB)...");
|
|
|
|
|
setShowLogs(true);
|
|
|
|
|
|
|
|
|
|
await invoke("download_models");
|
|
|
|
|
|
|
|
|
|
// Check again after download
|
|
|
|
|
const modelsVerified = await invoke<boolean>("check_models");
|
|
|
|
|
if (!modelsVerified) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
"Download completed but model file not found. This might be a path or permissions issue."
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize ML models
|
|
|
|
|
setAppState("initializing");
|
|
|
|
|
setStatusMessage("Loading AI models (this may take a moment)...");
|
|
|
|
|
setShowLogs(true);
|
|
|
|
|
|
|
|
|
|
await invoke("initialize_models");
|
|
|
|
|
|
|
|
|
|
const ready = await invoke<boolean>("check_ready");
|
|
|
|
|
if (!ready) {
|
|
|
|
|
throw new Error("Models failed to initialize. Check logs for details.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAppState("ready");
|
|
|
|
|
setStatusMessage("");
|
|
|
|
|
setShowLogs(false);
|
|
|
|
|
|
|
|
|
|
// Load saved recordings
|
|
|
|
|
try {
|
|
|
|
|
const savedRecordings = await invoke<StoredRecording[]>("load_recordings");
|
|
|
|
|
const loadedRecordings = savedRecordings.map(storedToFrontend);
|
|
|
|
|
setRecordings(loadedRecordings);
|
|
|
|
|
console.log(`Loaded ${loadedRecordings.length} transcripts from storage`);
|
|
|
|
|
} catch (loadError) {
|
|
|
|
|
console.error("Failed to load transcripts:", loadError);
|
|
|
|
|
// Don't fail app init if we can't load transcripts
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Initialization failed:", error);
|
|
|
|
|
setAppState("error");
|
|
|
|
|
setErrorMessage(String(error));
|
|
|
|
|
setShowLogs(true);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (initStarted.current) return;
|
|
|
|
|
initStarted.current = true;
|
|
|
|
|
|
|
|
|
|
// Get app version
|
|
|
|
|
getVersion().then(version => {
|
|
|
|
|
getVersion().then((version) => {
|
|
|
|
|
setAppVersion(version);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
initializeApp();
|
|
|
|
|
}, []);
|
|
|
|
|
}, [initializeApp]);
|
|
|
|
|
|
|
|
|
|
// Helper function to convert stored recording to frontend format
|
|
|
|
|
const storedToFrontend = (stored: StoredRecording): Recording => ({
|
|
|
|
@@ -119,17 +184,22 @@ function App() {
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const setupListeners = async () => {
|
|
|
|
|
// Listen for transcription progress
|
|
|
|
|
transcriptionProgressUnlisten.current = await listen<number>('transcription-progress', (event) => {
|
|
|
|
|
setTranscriptionProgress(event.payload * 100); // Convert 0-1 to 0-100
|
|
|
|
|
});
|
|
|
|
|
transcriptionProgressUnlisten.current = await listen<number>(
|
|
|
|
|
"transcription-progress",
|
|
|
|
|
(event) => {
|
|
|
|
|
setTranscriptionProgress(event.payload * 100); // Convert 0-1 to 0-100
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Listen for summary progress
|
|
|
|
|
summaryProgressUnlisten.current = await listen<number>('summary-progress', (event) => {
|
|
|
|
|
summaryProgressUnlisten.current = await listen<number>("summary-progress", (event) => {
|
|
|
|
|
const recordingId = selectedRecordingId || recordings[0]?.id;
|
|
|
|
|
if (recordingId) {
|
|
|
|
|
setRecordings(prev => prev.map(r =>
|
|
|
|
|
r.id === recordingId ? { ...r, summaryProgress: event.payload * 100 } : r
|
|
|
|
|
));
|
|
|
|
|
setRecordings((prev) =>
|
|
|
|
|
prev.map((r) =>
|
|
|
|
|
r.id === recordingId ? { ...r, summaryProgress: event.payload * 100 } : r
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
@@ -137,62 +207,6 @@ function App() {
|
|
|
|
|
setupListeners();
|
|
|
|
|
}, [selectedRecordingId, recordings]);
|
|
|
|
|
|
|
|
|
|
const initializeApp = async () => {
|
|
|
|
|
try {
|
|
|
|
|
setAppState("checking");
|
|
|
|
|
setStatusMessage("Checking if models are present...");
|
|
|
|
|
|
|
|
|
|
const modelsPresent = await invoke<boolean>("check_models");
|
|
|
|
|
|
|
|
|
|
if (!modelsPresent) {
|
|
|
|
|
setAppState("downloading_models");
|
|
|
|
|
setStatusMessage("Downloading AI models (~2GB)...");
|
|
|
|
|
setShowLogs(true);
|
|
|
|
|
|
|
|
|
|
await invoke("download_models");
|
|
|
|
|
|
|
|
|
|
// Check again after download
|
|
|
|
|
const modelsVerified = await invoke<boolean>("check_models");
|
|
|
|
|
if (!modelsVerified) {
|
|
|
|
|
throw new Error("Download completed but model file not found. This might be a path or permissions issue.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize ML models
|
|
|
|
|
setAppState("initializing");
|
|
|
|
|
setStatusMessage("Loading AI models (this may take a moment)...");
|
|
|
|
|
setShowLogs(true);
|
|
|
|
|
|
|
|
|
|
await invoke("initialize_models");
|
|
|
|
|
|
|
|
|
|
// Check if models are ready
|
|
|
|
|
const ready = await invoke<boolean>("check_ready");
|
|
|
|
|
if (!ready) {
|
|
|
|
|
throw new Error("Models failed to initialize. Check logs for details.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAppState("ready");
|
|
|
|
|
setStatusMessage("");
|
|
|
|
|
setShowLogs(false);
|
|
|
|
|
|
|
|
|
|
// Load saved recordings
|
|
|
|
|
try {
|
|
|
|
|
const savedRecordings = await invoke<StoredRecording[]>("load_recordings");
|
|
|
|
|
const loadedRecordings = savedRecordings.map(storedToFrontend);
|
|
|
|
|
setRecordings(loadedRecordings);
|
|
|
|
|
console.log(`Loaded ${loadedRecordings.length} transcripts from storage`);
|
|
|
|
|
} catch (loadError) {
|
|
|
|
|
console.error("Failed to load transcripts:", loadError);
|
|
|
|
|
// Don't fail app init if we can't load transcripts
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Initialization failed:", error);
|
|
|
|
|
setAppState("error");
|
|
|
|
|
setErrorMessage(String(error));
|
|
|
|
|
setShowLogs(true);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const retrySetup = () => {
|
|
|
|
|
setErrorMessage(null);
|
|
|
|
|
initStarted.current = false;
|
|
|
|
@@ -203,7 +217,7 @@ function App() {
|
|
|
|
|
try {
|
|
|
|
|
// Get the next chunk of audio
|
|
|
|
|
const [audioChunk, newOffset] = await invoke<[number[], number]>("get_audio_chunk", {
|
|
|
|
|
lastOffset: audioOffset.current
|
|
|
|
|
lastOffset: audioOffset.current,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// If we have enough audio (at least 5 seconds worth at 16kHz)
|
|
|
|
@@ -212,22 +226,26 @@ function App() {
|
|
|
|
|
const chunkStartTime = totalProcessedSamples.current / 16000;
|
|
|
|
|
const newSegments = await invoke<TranscriptSegment[]>("transcribe_chunk", {
|
|
|
|
|
audioData: audioChunk,
|
|
|
|
|
chunkStartTime: chunkStartTime
|
|
|
|
|
chunkStartTime: chunkStartTime,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (newSegments.length > 0) {
|
|
|
|
|
// Calculate timestamps based on total processed samples
|
|
|
|
|
const baseTime = totalProcessedSamples.current / 16000;
|
|
|
|
|
const adjustedSegments = newSegments.map(seg => ({
|
|
|
|
|
const adjustedSegments = newSegments.map((seg) => ({
|
|
|
|
|
...seg,
|
|
|
|
|
start: seg.start + baseTime,
|
|
|
|
|
end: seg.end + baseTime,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
setActiveRecording(prev => prev ? {
|
|
|
|
|
...prev,
|
|
|
|
|
transcriptSegments: [...prev.transcriptSegments, ...adjustedSegments]
|
|
|
|
|
} : null);
|
|
|
|
|
setActiveRecording((prev) =>
|
|
|
|
|
prev
|
|
|
|
|
? {
|
|
|
|
|
...prev,
|
|
|
|
|
transcriptSegments: [...prev.transcriptSegments, ...adjustedSegments],
|
|
|
|
|
}
|
|
|
|
|
: null
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Track total processed samples
|
|
|
|
@@ -264,7 +282,7 @@ function App() {
|
|
|
|
|
|
|
|
|
|
// Start timer to show recording duration
|
|
|
|
|
recordingTimer.current = window.setInterval(() => {
|
|
|
|
|
setRecordingDuration(d => d + 1);
|
|
|
|
|
setRecordingDuration((d) => d + 1);
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
|
|
|
|
// Start real-time transcription timer (every 5 seconds)
|
|
|
|
@@ -297,7 +315,7 @@ function App() {
|
|
|
|
|
// First, process any audio that hasn't been processed yet
|
|
|
|
|
try {
|
|
|
|
|
const finalChunk = await invoke<number[]>("get_remaining_audio", {
|
|
|
|
|
lastOffset: audioOffset.current
|
|
|
|
|
lastOffset: audioOffset.current,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (finalChunk.length > 0) {
|
|
|
|
@@ -306,21 +324,25 @@ function App() {
|
|
|
|
|
// The progress will be updated via events from the backend
|
|
|
|
|
const finalSegments = await invoke<TranscriptSegment[]>("transcribe_chunk", {
|
|
|
|
|
audioData: finalChunk,
|
|
|
|
|
chunkStartTime: chunkStartTime
|
|
|
|
|
chunkStartTime: chunkStartTime,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (finalSegments.length > 0) {
|
|
|
|
|
const baseTime = totalProcessedSamples.current / 16000;
|
|
|
|
|
const adjustedSegments = finalSegments.map(seg => ({
|
|
|
|
|
const adjustedSegments = finalSegments.map((seg) => ({
|
|
|
|
|
...seg,
|
|
|
|
|
start: seg.start + baseTime,
|
|
|
|
|
end: seg.end + baseTime,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
setActiveRecording(prev => prev ? {
|
|
|
|
|
...prev,
|
|
|
|
|
transcriptSegments: [...prev.transcriptSegments, ...adjustedSegments]
|
|
|
|
|
} : null);
|
|
|
|
|
setActiveRecording((prev) =>
|
|
|
|
|
prev
|
|
|
|
|
? {
|
|
|
|
|
...prev,
|
|
|
|
|
transcriptSegments: [...prev.transcriptSegments, ...adjustedSegments],
|
|
|
|
|
}
|
|
|
|
|
: null
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (chunkError) {
|
|
|
|
@@ -335,16 +357,16 @@ function App() {
|
|
|
|
|
if (activeRecording) {
|
|
|
|
|
const finalRecording = {
|
|
|
|
|
...activeRecording,
|
|
|
|
|
duration: recordingDuration
|
|
|
|
|
duration: recordingDuration,
|
|
|
|
|
};
|
|
|
|
|
setRecordings(prev => [finalRecording, ...prev]);
|
|
|
|
|
setRecordings((prev) => [finalRecording, ...prev]);
|
|
|
|
|
setSelectedRecordingId(finalRecording.id);
|
|
|
|
|
setActiveRecording(null);
|
|
|
|
|
|
|
|
|
|
// Save to persistent storage
|
|
|
|
|
try {
|
|
|
|
|
await invoke("save_recording", {
|
|
|
|
|
recording: frontendToStored(finalRecording)
|
|
|
|
|
recording: frontendToStored(finalRecording),
|
|
|
|
|
});
|
|
|
|
|
console.log("Transcript saved to storage");
|
|
|
|
|
} catch (saveError) {
|
|
|
|
@@ -354,7 +376,7 @@ function App() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Brief delay to show completion
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
|
|
|
|
|
|
|
|
setAppState("ready");
|
|
|
|
|
setStatusMessage("");
|
|
|
|
@@ -368,13 +390,15 @@ function App() {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const generateSummary = async (recordingId: string) => {
|
|
|
|
|
const recording = recordings.find(r => r.id === recordingId);
|
|
|
|
|
const recording = recordings.find((r) => r.id === recordingId);
|
|
|
|
|
if (!recording || recording.transcriptSegments.length === 0) return;
|
|
|
|
|
|
|
|
|
|
// Update the recording to show it's generating
|
|
|
|
|
setRecordings(prev => prev.map(r =>
|
|
|
|
|
r.id === recordingId ? { ...r, isGeneratingSummary: true, summaryProgress: 0 } : r
|
|
|
|
|
));
|
|
|
|
|
setRecordings((prev) =>
|
|
|
|
|
prev.map((r) =>
|
|
|
|
|
r.id === recordingId ? { ...r, isGeneratingSummary: true, summaryProgress: 0 } : r
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const fullTranscript = recording.transcriptSegments
|
|
|
|
|
.map((seg) => `${seg.speaker}: ${seg.text}`)
|
|
|
|
@@ -382,27 +406,27 @@ function App() {
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Progress will be updated via events from the backend
|
|
|
|
|
const [summaryResult, titleResult] = await invoke<[string, string | null]>("summarize", { transcript: fullTranscript });
|
|
|
|
|
const [summaryResult, titleResult] = await invoke<[string, string | null]>("summarize", {
|
|
|
|
|
transcript: fullTranscript,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update the recording with the summary and title
|
|
|
|
|
const updatedRecording = recordings.find(r => r.id === recordingId);
|
|
|
|
|
const updatedRecording = recordings.find((r) => r.id === recordingId);
|
|
|
|
|
if (updatedRecording) {
|
|
|
|
|
const recordingWithSummary = {
|
|
|
|
|
...updatedRecording,
|
|
|
|
|
summary: summaryResult,
|
|
|
|
|
title: titleResult,
|
|
|
|
|
isGeneratingSummary: false,
|
|
|
|
|
summaryProgress: 100
|
|
|
|
|
summaryProgress: 100,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
setRecordings(prev => prev.map(r =>
|
|
|
|
|
r.id === recordingId ? recordingWithSummary : r
|
|
|
|
|
));
|
|
|
|
|
setRecordings((prev) => prev.map((r) => (r.id === recordingId ? recordingWithSummary : r)));
|
|
|
|
|
|
|
|
|
|
// Update in persistent storage
|
|
|
|
|
try {
|
|
|
|
|
await invoke("update_recording", {
|
|
|
|
|
recording: frontendToStored(recordingWithSummary)
|
|
|
|
|
recording: frontendToStored(recordingWithSummary),
|
|
|
|
|
});
|
|
|
|
|
console.log("Transcript updated with summary");
|
|
|
|
|
} catch (updateError) {
|
|
|
|
@@ -411,9 +435,9 @@ function App() {
|
|
|
|
|
|
|
|
|
|
// Clear progress after a brief delay
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setRecordings(prev => prev.map(r =>
|
|
|
|
|
r.id === recordingId ? { ...r, summaryProgress: undefined } : r
|
|
|
|
|
));
|
|
|
|
|
setRecordings((prev) =>
|
|
|
|
|
prev.map((r) => (r.id === recordingId ? { ...r, summaryProgress: undefined } : r))
|
|
|
|
|
);
|
|
|
|
|
}, 1000);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
@@ -421,9 +445,13 @@ function App() {
|
|
|
|
|
setErrorMessage(String(error));
|
|
|
|
|
|
|
|
|
|
// Reset generating state on error
|
|
|
|
|
setRecordings(prev => prev.map(r =>
|
|
|
|
|
r.id === recordingId ? { ...r, isGeneratingSummary: false, summaryProgress: undefined } : r
|
|
|
|
|
));
|
|
|
|
|
setRecordings((prev) =>
|
|
|
|
|
prev.map((r) =>
|
|
|
|
|
r.id === recordingId
|
|
|
|
|
? { ...r, isGeneratingSummary: false, summaryProgress: undefined }
|
|
|
|
|
: r
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@@ -438,7 +466,9 @@ function App() {
|
|
|
|
|
|
|
|
|
|
const deleteRecording = async (recordingId: string) => {
|
|
|
|
|
// Confirm deletion
|
|
|
|
|
if (!confirm("Are you sure you want to delete this transcript? This action cannot be undone.")) {
|
|
|
|
|
if (
|
|
|
|
|
!confirm("Are you sure you want to delete this transcript? This action cannot be undone.")
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -447,7 +477,7 @@ function App() {
|
|
|
|
|
await invoke("delete_recording", { recordingId });
|
|
|
|
|
|
|
|
|
|
// Remove from state
|
|
|
|
|
setRecordings(prev => prev.filter(r => r.id !== recordingId));
|
|
|
|
|
setRecordings((prev) => prev.filter((r) => r.id !== recordingId));
|
|
|
|
|
|
|
|
|
|
// Clear selection if we deleted the selected recording
|
|
|
|
|
if (selectedRecordingId === recordingId) {
|
|
|
|
@@ -462,7 +492,7 @@ function App() {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const downloadTranscript = (recordingId: string) => {
|
|
|
|
|
const recording = recordings.find(r => r.id === recordingId);
|
|
|
|
|
const recording = recordings.find((r) => r.id === recordingId);
|
|
|
|
|
if (!recording) return;
|
|
|
|
|
|
|
|
|
|
const content = recording.transcriptSegments
|
|
|
|
@@ -479,7 +509,7 @@ function App() {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const downloadSummary = (recordingId: string) => {
|
|
|
|
|
const recording = recordings.find(r => r.id === recordingId);
|
|
|
|
|
const recording = recordings.find((r) => r.id === recordingId);
|
|
|
|
|
if (!recording || !recording.summary) return;
|
|
|
|
|
|
|
|
|
|
const blob = new Blob([recording.summary], { type: "text/plain" });
|
|
|
|
@@ -504,8 +534,9 @@ function App() {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Get the currently selected recording or active recording
|
|
|
|
|
const displayedRecording = activeRecording ||
|
|
|
|
|
(selectedRecordingId ? recordings.find(r => r.id === selectedRecordingId) : null);
|
|
|
|
|
const displayedRecording =
|
|
|
|
|
activeRecording ||
|
|
|
|
|
(selectedRecordingId ? recordings.find((r) => r.id === selectedRecordingId) : null);
|
|
|
|
|
|
|
|
|
|
const renderSetupScreen = () => (
|
|
|
|
|
<div className="setup-screen">
|
|
|
|
@@ -540,9 +571,7 @@ function App() {
|
|
|
|
|
<li>Whisper - Speech recognition</li>
|
|
|
|
|
<li>VAD - Speaker detection</li>
|
|
|
|
|
</ul>
|
|
|
|
|
<p className="setup-note">
|
|
|
|
|
This is a pure Rust backend - no Python required!
|
|
|
|
|
</p>
|
|
|
|
|
<p className="setup-note">This is a pure Rust backend - no Python required!</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
@@ -555,9 +584,9 @@ function App() {
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{(appState === "checking" || appState === "initializing" || appState === "downloading_models") && (
|
|
|
|
|
<div className="loading-spinner" />
|
|
|
|
|
)}
|
|
|
|
|
{(appState === "checking" ||
|
|
|
|
|
appState === "initializing" ||
|
|
|
|
|
appState === "downloading_models") && <div className="loading-spinner" />}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<BackendLogs isVisible={showLogs} onToggle={() => setShowLogs(!showLogs)} />
|
|
|
|
@@ -589,13 +618,9 @@ function App() {
|
|
|
|
|
|
|
|
|
|
{appState === "transcribing" && (
|
|
|
|
|
<div className="transcribing-indicator">
|
|
|
|
|
<ProgressBar
|
|
|
|
|
progress={transcriptionProgress}
|
|
|
|
|
label="Processing final audio..."
|
|
|
|
|
/>
|
|
|
|
|
<ProgressBar progress={transcriptionProgress} label="Processing final audio..." />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
@@ -606,17 +631,18 @@ function App() {
|
|
|
|
|
<p className="no-transcripts">No transcripts yet</p>
|
|
|
|
|
) : (
|
|
|
|
|
<ul className="transcript-items">
|
|
|
|
|
{recordings.map(recording => (
|
|
|
|
|
{recordings.map((recording) => (
|
|
|
|
|
<li
|
|
|
|
|
key={recording.id}
|
|
|
|
|
className={`transcript-item ${selectedRecordingId === recording.id ? 'selected' : ''}`}
|
|
|
|
|
className={`transcript-item ${selectedRecordingId === recording.id ? "selected" : ""}`}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="transcript-content"
|
|
|
|
|
onClick={() => setSelectedRecordingId(recording.id)}
|
|
|
|
|
>
|
|
|
|
|
<div className="transcript-title">
|
|
|
|
|
{recording.title || `${recording.timestamp.toLocaleTimeString()} - ${formatDuration(recording.duration)}`}
|
|
|
|
|
{recording.title ||
|
|
|
|
|
`${recording.timestamp.toLocaleTimeString()} - ${formatDuration(recording.duration)}`}
|
|
|
|
|
</div>
|
|
|
|
|
{recording.title && (
|
|
|
|
|
<div className="transcript-time">
|
|
|
|
@@ -624,7 +650,11 @@ function App() {
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div className="transcript-status">
|
|
|
|
|
{recording.summary ? '✓ Summary' : recording.isGeneratingSummary ? '⏳ Summarizing...' : ''}
|
|
|
|
|
{recording.summary
|
|
|
|
|
? "✓ Summary"
|
|
|
|
|
: recording.isGeneratingSummary
|
|
|
|
|
? "⏳ Summarizing..."
|
|
|
|
|
: ""}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
@@ -645,7 +675,12 @@ function App() {
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Show setup screen for non-ready states
|
|
|
|
|
if (appState === "checking" || appState === "downloading_models" || appState === "initializing" || appState === "error") {
|
|
|
|
|
if (
|
|
|
|
|
appState === "checking" ||
|
|
|
|
|
appState === "downloading_models" ||
|
|
|
|
|
appState === "initializing" ||
|
|
|
|
|
appState === "error"
|
|
|
|
|
) {
|
|
|
|
|
return (
|
|
|
|
|
<main className="container">
|
|
|
|
|
<header className="app-header">
|
|
|
|
@@ -678,7 +713,10 @@ function App() {
|
|
|
|
|
{displayedRecording && (
|
|
|
|
|
<div className="transcript-details">
|
|
|
|
|
<div className="transcript-header">
|
|
|
|
|
<h2>{displayedRecording.title || `Transcript from ${displayedRecording.timestamp.toLocaleString()}`}</h2>
|
|
|
|
|
<h2>
|
|
|
|
|
{displayedRecording.title ||
|
|
|
|
|
`Transcript from ${displayedRecording.timestamp.toLocaleString()}`}
|
|
|
|
|
</h2>
|
|
|
|
|
<div className="transcript-actions">
|
|
|
|
|
<button
|
|
|
|
|
className="secondary-button"
|
|
|
|
|