diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e1ffc1b..accf071 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -470,13 +470,26 @@ async fn get_remaining_audio( } } +/// Extract title from the summary text. +fn extract_title_from_summary(summary: &str) -> Option { + // Look for **Title:** pattern and extract the line + if let Some(title_start) = summary.find("**Title:**") { + let title_line_start = title_start + "**Title:**".len(); + if let Some(title_end) = summary[title_line_start..].find("\n") { + let title = summary[title_line_start..title_line_start + title_end].trim(); + return Some(title.to_string()); + } + } + None +} + /// Generate a summary from a transcript. #[tauri::command] async fn summarize( state: State<'_, AppState>, app_handle: tauri::AppHandle, transcript: String, -) -> Result { +) -> Result<(String, Option), String> { let logs = Arc::clone(&state.logs); emit_log(&app_handle, &logs, "[Summary] Generating summary..."); @@ -498,7 +511,13 @@ async fn summarize( emit_log(&app_handle, &logs, &format!("[Summary] Generated {} character summary", summary.len())); - Ok(summary) + // Extract title from the summary + let title = extract_title_from_summary(&summary); + if let Some(ref t) = title { + emit_log(&app_handle, &logs, &format!("[Summary] Extracted title: {}", t)); + } + + Ok((summary, title)) } /// Get backend logs. diff --git a/src-tauri/src/ml/summarizer.rs b/src-tauri/src/ml/summarizer.rs index 0049fec..ae23567 100644 --- a/src-tauri/src/ml/summarizer.rs +++ b/src-tauri/src/ml/summarizer.rs @@ -88,6 +88,7 @@ impl LlamaSummarizer { "<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n\ You are a helpful assistant that creates structured meeting summaries. \ Format your response using the following template:\n\n\ + **Title:** A concise, descriptive title for the meeting (5-10 words max).\n\n\ **Summary:** A high level overview of the meeting.\n\n\ **Key decisions:** Any important resolutions that the meeting reached.\n\n\ **Action Items:**\n\ diff --git a/src-tauri/src/storage.rs b/src-tauri/src/storage.rs index 4a2f4c4..38b4c98 100644 --- a/src-tauri/src/storage.rs +++ b/src-tauri/src/storage.rs @@ -33,6 +33,7 @@ pub struct StoredRecording { pub duration: f64, pub transcript_segments: Vec, pub summary: Option, + pub title: Option, } /// Storage manager for recordings. @@ -160,6 +161,7 @@ mod tests { }, ], summary: Some("Test summary".to_string()), + title: Some("Test Meeting".to_string()), }; // Save it diff --git a/src/App.css b/src/App.css index acfffb3..ffb3685 100644 --- a/src/App.css +++ b/src/App.css @@ -663,9 +663,17 @@ body { color: rgba(255, 255, 255, 0.8); } +.transcript-title { + font-weight: 600; + font-size: 0.9375rem; + line-height: 1.3; + margin-bottom: 0.25rem; +} + .transcript-time { - font-weight: 500; - font-size: 0.875rem; + font-weight: 400; + font-size: 0.8125rem; + color: var(--text-secondary); } .transcript-status { diff --git a/src/App.tsx b/src/App.tsx index 040ff5e..5ea4e88 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,6 +27,7 @@ interface StoredRecording { duration: number; transcript_segments: StoredTranscriptSegment[]; summary: string | null; + title: string | null; } interface Recording { @@ -35,6 +36,7 @@ interface Recording { duration: number; transcriptSegments: TranscriptSegment[]; summary: string | null; + title: string | null; isGeneratingSummary: boolean; summaryProgress?: number; } @@ -73,6 +75,7 @@ function App() { duration: stored.duration, transcriptSegments: stored.transcript_segments, summary: stored.summary, + title: stored.title, isGeneratingSummary: false, summaryProgress: undefined, }); @@ -84,6 +87,7 @@ function App() { duration: recording.duration, transcript_segments: recording.transcriptSegments, summary: recording.summary, + title: recording.title, }); // Cleanup timers and listeners on unmount @@ -242,6 +246,7 @@ function App() { duration: 0, transcriptSegments: [], summary: null, + title: null, isGeneratingSummary: false, }; setActiveRecording(newRecording); @@ -366,14 +371,15 @@ function App() { try { // Progress will be updated via events from the backend - const summaryResult = await invoke("summarize", { transcript: fullTranscript }); + const [summaryResult, titleResult] = await invoke<[string, string | null]>("summarize", { transcript: fullTranscript }); - // Update the recording with the summary + // Update the recording with the summary and title const updatedRecording = recordings.find(r => r.id === recordingId); if (updatedRecording) { const recordingWithSummary = { ...updatedRecording, summary: summaryResult, + title: titleResult, isGeneratingSummary: false, summaryProgress: 100 }; @@ -598,9 +604,14 @@ function App() { className="transcript-content" onClick={() => setSelectedRecordingId(recording.id)} > -
- {recording.timestamp.toLocaleTimeString()} - {formatDuration(recording.duration)} +
+ {recording.title || `${recording.timestamp.toLocaleTimeString()} - ${formatDuration(recording.duration)}`}
+ {recording.title && ( +
+ {recording.timestamp.toLocaleString()} • {formatDuration(recording.duration)} +
+ )}
{recording.summary ? '✓ Summary' : recording.isGeneratingSummary ? '⏳ Summarizing...' : ''}
@@ -654,7 +665,7 @@ function App() { {displayedRecording && (
-

Transcript from {displayedRecording.timestamp.toLocaleString()}

+

{displayedRecording.title || `Transcript from ${displayedRecording.timestamp.toLocaleString()}`}