Add project save/load and improve AI chat formatting
Project persistence: - save_project_transcript command: persists segments, speakers, words to SQLite - load_project_transcript command: loads full transcript with nested words - delete_project command: soft-delete projects - Auto-save after pipeline completes (named from filename) - Project dropdown in header to switch between saved transcripts - Projects load audio, segments, and speakers from database AI chat improvements: - Markdown rendering in assistant messages (headers, lists, bold, italic, code) - Better message spacing and visual distinction (border-left accents) - Styled markdown elements matching dark theme - Improved empty state and quick action button sizing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,57 @@ pub fn delete_project(conn: &Connection, id: &str) -> Result<(), DatabaseError>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Media Files ──────────────────────────────────────────────────
|
||||
|
||||
pub fn create_media_file(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
file_path: &str,
|
||||
) -> Result<MediaFile, DatabaseError> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let now = Utc::now().to_rfc3339();
|
||||
conn.execute(
|
||||
"INSERT INTO media_files (id, project_id, file_path, created_at) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![id, project_id, file_path, now],
|
||||
)?;
|
||||
Ok(MediaFile {
|
||||
id,
|
||||
project_id: project_id.to_string(),
|
||||
file_path: file_path.to_string(),
|
||||
file_hash: None,
|
||||
duration_ms: None,
|
||||
sample_rate: None,
|
||||
channels: None,
|
||||
format: None,
|
||||
file_size: None,
|
||||
created_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_media_files_for_project(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
) -> Result<Vec<MediaFile>, DatabaseError> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, project_id, file_path, file_hash, duration_ms, sample_rate, channels, format, file_size, created_at FROM media_files WHERE project_id = ?1 ORDER BY created_at",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![project_id], |row| {
|
||||
Ok(MediaFile {
|
||||
id: row.get(0)?,
|
||||
project_id: row.get(1)?,
|
||||
file_path: row.get(2)?,
|
||||
file_hash: row.get(3)?,
|
||||
duration_ms: row.get(4)?,
|
||||
sample_rate: row.get(5)?,
|
||||
channels: row.get(6)?,
|
||||
format: row.get(7)?,
|
||||
file_size: row.get(8)?,
|
||||
created_at: row.get(9)?,
|
||||
})
|
||||
})?;
|
||||
Ok(rows.collect::<Result<Vec<_>, _>>()?)
|
||||
}
|
||||
|
||||
// ── Speakers ──────────────────────────────────────────────────────
|
||||
|
||||
pub fn create_speaker(
|
||||
@@ -194,6 +245,39 @@ pub fn reassign_speaker(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── Segments (create) ────────────────────────────────────────────
|
||||
|
||||
pub fn create_segment(
|
||||
conn: &Connection,
|
||||
project_id: &str,
|
||||
media_file_id: &str,
|
||||
speaker_id: Option<&str>,
|
||||
start_ms: i64,
|
||||
end_ms: i64,
|
||||
text: &str,
|
||||
segment_index: i32,
|
||||
) -> Result<Segment, DatabaseError> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
conn.execute(
|
||||
"INSERT INTO segments (id, project_id, media_file_id, speaker_id, start_ms, end_ms, text, is_edited, segment_index) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 0, ?8)",
|
||||
params![id, project_id, media_file_id, speaker_id, start_ms, end_ms, text, segment_index],
|
||||
)?;
|
||||
Ok(Segment {
|
||||
id,
|
||||
project_id: project_id.to_string(),
|
||||
media_file_id: media_file_id.to_string(),
|
||||
speaker_id: speaker_id.map(String::from),
|
||||
start_ms,
|
||||
end_ms,
|
||||
text: text.to_string(),
|
||||
original_text: None,
|
||||
confidence: None,
|
||||
is_edited: false,
|
||||
edited_at: None,
|
||||
segment_index,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Words ─────────────────────────────────────────────────────────
|
||||
|
||||
pub fn get_words_for_segment(
|
||||
@@ -217,6 +301,31 @@ pub fn get_words_for_segment(
|
||||
Ok(rows.collect::<Result<Vec<_>, _>>()?)
|
||||
}
|
||||
|
||||
pub fn create_word(
|
||||
conn: &Connection,
|
||||
segment_id: &str,
|
||||
word: &str,
|
||||
start_ms: i64,
|
||||
end_ms: i64,
|
||||
confidence: Option<f64>,
|
||||
word_index: i32,
|
||||
) -> Result<Word, DatabaseError> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
conn.execute(
|
||||
"INSERT INTO words (id, segment_id, word, start_ms, end_ms, confidence, word_index) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![id, segment_id, word, start_ms, end_ms, confidence, word_index],
|
||||
)?;
|
||||
Ok(Word {
|
||||
id,
|
||||
segment_id: segment_id.to_string(),
|
||||
word: word.to_string(),
|
||||
start_ms,
|
||||
end_ms,
|
||||
confidence,
|
||||
word_index,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user