//! Database models for the files table. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; use utoipa::ToSchema; use uuid::Uuid; /// TranscriptEntry stored in JSONB - matches frontend TranscriptEntry #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TranscriptEntry { pub id: String, pub speaker: String, pub start: f32, pub end: f32, pub text: String, pub is_final: bool, } /// Chart type for visualization elements #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum ChartType { Line, Bar, Pie, Area, } /// Body element types for structured file content #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(tag = "type", rename_all = "camelCase")] pub enum BodyElement { /// Heading element (h1-h6) Heading { level: u8, text: String }, /// Paragraph text Paragraph { text: String }, /// Chart visualization Chart { #[serde(rename = "chartType")] chart_type: ChartType, title: Option, data: serde_json::Value, config: Option, }, /// Image element (deferred for MVP) Image { src: String, alt: Option, caption: Option, }, } /// File record from the database. #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct File { pub id: Uuid, pub owner_id: Uuid, pub name: String, pub description: Option, #[sqlx(json)] pub transcript: Vec, pub location: Option, /// AI-generated summary of the transcript pub summary: Option, /// Structured body content (headings, paragraphs, charts) #[sqlx(json)] pub body: Vec, /// Version number for optimistic locking pub version: i32, pub created_at: DateTime, pub updated_at: DateTime, } /// Request payload for creating a new file. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CreateFileRequest { /// Name of the file (auto-generated if not provided) pub name: Option, /// Optional description pub description: Option, /// Transcript entries pub transcript: Vec, /// Storage location (e.g., s3://bucket/path) - not used yet pub location: Option, } /// Request payload for updating an existing file. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateFileRequest { /// New name (optional) pub name: Option, /// New description (optional) pub description: Option, /// New transcript (optional) pub transcript: Option>, /// AI-generated summary (optional) pub summary: Option, /// Structured body content (optional) pub body: Option>, /// Version for optimistic locking (required for updates from frontend) pub version: Option, } /// Response for file list endpoint. #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileListResponse { pub files: Vec, pub total: i64, } /// Summary of a file for list views (excludes full transcript). #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileSummary { pub id: Uuid, pub name: String, pub description: Option, pub transcript_count: usize, /// Duration derived from last transcript end time pub duration: Option, /// Version number for optimistic locking pub version: i32, pub created_at: DateTime, pub updated_at: DateTime, } impl From for FileSummary { fn from(file: File) -> Self { let duration = file .transcript .iter() .map(|t| t.end) .fold(0.0_f32, f32::max); Self { id: file.id, name: file.name, description: file.description, transcript_count: file.transcript.len(), duration: if duration > 0.0 { Some(duration) } else { None }, version: file.version, created_at: file.created_at, updated_at: file.updated_at, } } } // ============================================================================= // Version History Types // ============================================================================= /// Source of a version change #[derive(Debug, Clone, Serialize, Deserialize, ToSchema, sqlx::Type)] #[sqlx(type_name = "varchar")] #[serde(rename_all = "lowercase")] pub enum VersionSource { #[sqlx(rename = "user")] User, #[sqlx(rename = "llm")] Llm, #[sqlx(rename = "system")] System, } impl std::fmt::Display for VersionSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { VersionSource::User => write!(f, "user"), VersionSource::Llm => write!(f, "llm"), VersionSource::System => write!(f, "system"), } } } impl std::str::FromStr for VersionSource { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "user" => Ok(VersionSource::User), "llm" => Ok(VersionSource::Llm), "system" => Ok(VersionSource::System), _ => Err(format!("Unknown version source: {}", s)), } } } /// Full version record from the database #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileVersion { pub id: Uuid, pub file_id: Uuid, pub version: i32, pub name: String, pub description: Option, pub summary: Option, #[sqlx(json)] pub body: Vec, pub source: String, pub change_description: Option, pub created_at: DateTime, } /// Summary of a version for list views #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileVersionSummary { pub version: i32, pub source: String, pub created_at: DateTime, pub change_description: Option, } impl From for FileVersionSummary { fn from(v: FileVersion) -> Self { Self { version: v.version, source: v.source, created_at: v.created_at, change_description: v.change_description, } } } /// Response for version list endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileVersionListResponse { pub versions: Vec, pub total: i64, } /// Request to restore a file to a previous version #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct RestoreVersionRequest { /// The version to restore to pub target_version: i32, /// The current version (for optimistic locking) pub current_version: i32, }