//! 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<String>,
data: serde_json::Value,
config: Option<serde_json::Value>,
},
/// Image element (deferred for MVP)
Image {
src: String,
alt: Option<String>,
caption: Option<String>,
},
}
/// 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<String>,
#[sqlx(json)]
pub transcript: Vec<TranscriptEntry>,
pub location: Option<String>,
/// AI-generated summary of the transcript
pub summary: Option<String>,
/// Structured body content (headings, paragraphs, charts)
#[sqlx(json)]
pub body: Vec<BodyElement>,
/// Version number for optimistic locking
pub version: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// 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<String>,
/// Optional description
pub description: Option<String>,
/// Transcript entries
pub transcript: Vec<TranscriptEntry>,
/// Storage location (e.g., s3://bucket/path) - not used yet
pub location: Option<String>,
}
/// Request payload for updating an existing file.
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateFileRequest {
/// New name (optional)
pub name: Option<String>,
/// New description (optional)
pub description: Option<String>,
/// New transcript (optional)
pub transcript: Option<Vec<TranscriptEntry>>,
/// AI-generated summary (optional)
pub summary: Option<String>,
/// Structured body content (optional)
pub body: Option<Vec<BodyElement>>,
/// Version for optimistic locking (required for updates from frontend)
pub version: Option<i32>,
}
/// Response for file list endpoint.
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct FileListResponse {
pub files: Vec<FileSummary>,
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<String>,
pub transcript_count: usize,
/// Duration derived from last transcript end time
pub duration: Option<f32>,
/// Version number for optimistic locking
pub version: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl From<File> 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<Self, Self::Err> {
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<String>,
pub summary: Option<String>,
#[sqlx(json)]
pub body: Vec<BodyElement>,
pub source: String,
pub change_description: Option<String>,
pub created_at: DateTime<Utc>,
}
/// 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<Utc>,
pub change_description: Option<String>,
}
impl From<FileVersion> 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<FileVersionSummary>,
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,
}