generated from nhcarrigan/template
feat: create title for each meeting
This commit is contained in:
+21
-2
@@ -470,13 +470,26 @@ async fn get_remaining_audio(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract title from the summary text.
|
||||||
|
fn extract_title_from_summary(summary: &str) -> Option<String> {
|
||||||
|
// 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.
|
/// Generate a summary from a transcript.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn summarize(
|
async fn summarize(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
transcript: String,
|
transcript: String,
|
||||||
) -> Result<String, String> {
|
) -> Result<(String, Option<String>), String> {
|
||||||
let logs = Arc::clone(&state.logs);
|
let logs = Arc::clone(&state.logs);
|
||||||
|
|
||||||
emit_log(&app_handle, &logs, "[Summary] Generating summary...");
|
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()));
|
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.
|
/// Get backend logs.
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ impl LlamaSummarizer {
|
|||||||
"<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n\
|
"<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n\
|
||||||
You are a helpful assistant that creates structured meeting summaries. \
|
You are a helpful assistant that creates structured meeting summaries. \
|
||||||
Format your response using the following template:\n\n\
|
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\
|
**Summary:** A high level overview of the meeting.\n\n\
|
||||||
**Key decisions:** Any important resolutions that the meeting reached.\n\n\
|
**Key decisions:** Any important resolutions that the meeting reached.\n\n\
|
||||||
**Action Items:**\n\
|
**Action Items:**\n\
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ pub struct StoredRecording {
|
|||||||
pub duration: f64,
|
pub duration: f64,
|
||||||
pub transcript_segments: Vec<StoredTranscriptSegment>,
|
pub transcript_segments: Vec<StoredTranscriptSegment>,
|
||||||
pub summary: Option<String>,
|
pub summary: Option<String>,
|
||||||
|
pub title: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Storage manager for recordings.
|
/// Storage manager for recordings.
|
||||||
@@ -160,6 +161,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
summary: Some("Test summary".to_string()),
|
summary: Some("Test summary".to_string()),
|
||||||
|
title: Some("Test Meeting".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save it
|
// Save it
|
||||||
|
|||||||
+10
-2
@@ -663,9 +663,17 @@ body {
|
|||||||
color: rgba(255, 255, 255, 0.8);
|
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 {
|
.transcript-time {
|
||||||
font-weight: 500;
|
font-weight: 400;
|
||||||
font-size: 0.875rem;
|
font-size: 0.8125rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.transcript-status {
|
.transcript-status {
|
||||||
|
|||||||
+16
-5
@@ -27,6 +27,7 @@ interface StoredRecording {
|
|||||||
duration: number;
|
duration: number;
|
||||||
transcript_segments: StoredTranscriptSegment[];
|
transcript_segments: StoredTranscriptSegment[];
|
||||||
summary: string | null;
|
summary: string | null;
|
||||||
|
title: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Recording {
|
interface Recording {
|
||||||
@@ -35,6 +36,7 @@ interface Recording {
|
|||||||
duration: number;
|
duration: number;
|
||||||
transcriptSegments: TranscriptSegment[];
|
transcriptSegments: TranscriptSegment[];
|
||||||
summary: string | null;
|
summary: string | null;
|
||||||
|
title: string | null;
|
||||||
isGeneratingSummary: boolean;
|
isGeneratingSummary: boolean;
|
||||||
summaryProgress?: number;
|
summaryProgress?: number;
|
||||||
}
|
}
|
||||||
@@ -73,6 +75,7 @@ function App() {
|
|||||||
duration: stored.duration,
|
duration: stored.duration,
|
||||||
transcriptSegments: stored.transcript_segments,
|
transcriptSegments: stored.transcript_segments,
|
||||||
summary: stored.summary,
|
summary: stored.summary,
|
||||||
|
title: stored.title,
|
||||||
isGeneratingSummary: false,
|
isGeneratingSummary: false,
|
||||||
summaryProgress: undefined,
|
summaryProgress: undefined,
|
||||||
});
|
});
|
||||||
@@ -84,6 +87,7 @@ function App() {
|
|||||||
duration: recording.duration,
|
duration: recording.duration,
|
||||||
transcript_segments: recording.transcriptSegments,
|
transcript_segments: recording.transcriptSegments,
|
||||||
summary: recording.summary,
|
summary: recording.summary,
|
||||||
|
title: recording.title,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup timers and listeners on unmount
|
// Cleanup timers and listeners on unmount
|
||||||
@@ -242,6 +246,7 @@ function App() {
|
|||||||
duration: 0,
|
duration: 0,
|
||||||
transcriptSegments: [],
|
transcriptSegments: [],
|
||||||
summary: null,
|
summary: null,
|
||||||
|
title: null,
|
||||||
isGeneratingSummary: false,
|
isGeneratingSummary: false,
|
||||||
};
|
};
|
||||||
setActiveRecording(newRecording);
|
setActiveRecording(newRecording);
|
||||||
@@ -366,14 +371,15 @@ function App() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Progress will be updated via events from the backend
|
// Progress will be updated via events from the backend
|
||||||
const summaryResult = await invoke<string>("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);
|
const updatedRecording = recordings.find(r => r.id === recordingId);
|
||||||
if (updatedRecording) {
|
if (updatedRecording) {
|
||||||
const recordingWithSummary = {
|
const recordingWithSummary = {
|
||||||
...updatedRecording,
|
...updatedRecording,
|
||||||
summary: summaryResult,
|
summary: summaryResult,
|
||||||
|
title: titleResult,
|
||||||
isGeneratingSummary: false,
|
isGeneratingSummary: false,
|
||||||
summaryProgress: 100
|
summaryProgress: 100
|
||||||
};
|
};
|
||||||
@@ -598,9 +604,14 @@ function App() {
|
|||||||
className="transcript-content"
|
className="transcript-content"
|
||||||
onClick={() => setSelectedRecordingId(recording.id)}
|
onClick={() => setSelectedRecordingId(recording.id)}
|
||||||
>
|
>
|
||||||
<div className="transcript-time">
|
<div className="transcript-title">
|
||||||
{recording.timestamp.toLocaleTimeString()} - {formatDuration(recording.duration)}
|
{recording.title || `${recording.timestamp.toLocaleTimeString()} - ${formatDuration(recording.duration)}`}
|
||||||
</div>
|
</div>
|
||||||
|
{recording.title && (
|
||||||
|
<div className="transcript-time">
|
||||||
|
{recording.timestamp.toLocaleString()} • {formatDuration(recording.duration)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="transcript-status">
|
<div className="transcript-status">
|
||||||
{recording.summary ? '✓ Summary' : recording.isGeneratingSummary ? '⏳ Summarizing...' : ''}
|
{recording.summary ? '✓ Summary' : recording.isGeneratingSummary ? '⏳ Summarizing...' : ''}
|
||||||
</div>
|
</div>
|
||||||
@@ -654,7 +665,7 @@ function App() {
|
|||||||
{displayedRecording && (
|
{displayedRecording && (
|
||||||
<div className="transcript-details">
|
<div className="transcript-details">
|
||||||
<div className="transcript-header">
|
<div className="transcript-header">
|
||||||
<h2>Transcript from {displayedRecording.timestamp.toLocaleString()}</h2>
|
<h2>{displayedRecording.title || `Transcript from ${displayedRecording.timestamp.toLocaleString()}`}</h2>
|
||||||
<div className="transcript-actions">
|
<div className="transcript-actions">
|
||||||
<button
|
<button
|
||||||
className="secondary-button"
|
className="secondary-button"
|
||||||
|
|||||||
Reference in New Issue
Block a user