This file is a merged representation of the entire codebase, combined into a single document by Repomix.

================================================================
File Summary
================================================================

Purpose:
--------
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.

File Format:
------------
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Multiple file entries, each consisting of:
  a. A separator line (================)
  b. The file path (File: path/to/file)
  c. Another separator line
  d. The full contents of the file
  e. A blank line

Usage Guidelines:
-----------------
- This file should be treated as read-only. Any changes should be made to the
  original repository files, not this packed version.
- When processing this file, use the file path to distinguish
  between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
  the same level of security as you would the original repository.

Notes:
------
- Some files may have been excluded based on .gitignore rules and Repomix's configuration
- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files
- Files matching patterns in .gitignore are excluded
- Files matching default ignore patterns are excluded

Additional Info:
----------------

================================================================
Directory Structure
================================================================
Assets.xcassets/
  AppIcon.appiconset/
    Contents.json
  MyAppIcon.dataset/
    Contents.json
  Contents.json
Database/
  DatabaseManager.swift
  README.md
  SQLiteManager.swift
Models/
  MeetingNotesSchema.swift
  Note.swift
  WhisperResponse.swift
Preview Content/
  Preview Assets.xcassets/
    Contents.json
ViewModels/
  MeetingsViewModel.swift
  RecordingViewModel.swift
Views/
  LicenseView.swift
  MeetingsListView.swift
  MeetingView.swift
  RecordingView.swift
  StructuredNotesView.swift
AppDelegate.swift
CLAUDE.md
ContentView.swift
DesignSystem.swift
Info.plist
KeychainManager.swift
LicenseManager.swift
MicrophoneMonitor.swift
my-custom-theme.json
OnboardingCoordinator.swift
OnboardingStepViews.swift
OnboardingView.swift
openai_whisper-small
OpenAIManager.swift
PermissionsManager.swift
RecordingManager.swift
RecordingService.swift
SessionScribe.entitlements
SessionScribeApp.swift
SettingsView.swift
ThemeManager.swift
TranscriptionService.swift
TrialManager.swift

================================================================
Files
================================================================

================
File: Assets.xcassets/AppIcon.appiconset/Contents.json
================
{
  "images" : [
    {
      "filename" : "icon_16pt.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "16x16"
    },
    {
      "filename" : "icon_16x16@2x.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "16x16"
    },
    {
      "filename" : "icon_32pt.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "32x32"
    },
    {
      "filename" : "icon_32x32@2x.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "32x32"
    },
    {
      "filename" : "icon_128pt.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "128x128"
    },
    {
      "filename" : "icon_128x128@2x.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "128x128"
    },
    {
      "filename" : "icon_256pt.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "256x256"
    },
    {
      "filename" : "icon_256x256@2x.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "256x256"
    },
    {
      "filename" : "icon_512pt.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "512x512"
    },
    {
      "filename" : "icon_512x512@2x.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "512x512"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

================
File: Assets.xcassets/MyAppIcon.dataset/Contents.json
================
{
  "data" : [
    {
      "filename" : "MyAppIcon.icns",
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

================
File: Assets.xcassets/Contents.json
================
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

================
File: Database/DatabaseManager.swift
================
import Foundation

class DatabaseManager {
  static let shared = DatabaseManager()
  private var notes: [Note] = []
  private let sqliteManager = SQLiteManager.shared
  private let userDefaults = UserDefaults.standard
  private let migrationKey = "SQLiteMigrationCompleted"
  
  private init() {
    // Check if migration is needed
    checkAndPerformMigration()
    
    // Fix any notes that might have the recording path in the userNotes field
    sqliteManager.fixUserNotesWithRecordingPath()
    
    // Load notes from SQLite database when initializing
    do {
      notes = try sqliteManager.getAllNotes()
    } catch {
      print("Error loading notes from database: \(error.localizedDescription)")
    }
  }
  
  // Check if migration is needed and perform it if necessary
  private func checkAndPerformMigration() {
    // Check if migration has already been performed
    if !userDefaults.bool(forKey: migrationKey) {
      // Check if the database is empty
      if sqliteManager.isDatabaseEmpty() && !notes.isEmpty {
        // Migrate existing in-memory notes to SQLite
        migrateToSQLite(existingNotes: notes)
        // Mark migration as completed
        userDefaults.set(true, forKey: migrationKey)
      } else {
        // No migration needed or database already has data
        userDefaults.set(true, forKey: migrationKey)
      }
    }
  }
  
  // Function to migrate existing in-memory data to SQLite
  func migrateToSQLite(existingNotes: [Note]) {
    for note in existingNotes {
      do {
        try sqliteManager.saveNote(note)
        print("Migrated note: \(note.title)")
      } catch {
        print("Error migrating note \(note.title): \(error.localizedDescription)")
      }
    }
  }
  
  func saveNote(_ note: Note) throws {
    // Save to SQLite database
    try sqliteManager.saveNote(note)
    // Update in-memory cache
    notes.append(note)
  }
  
  func updateNote(_ note: Note) throws {
    // Update in SQLite database
    try sqliteManager.updateNote(note)
    // Update in-memory cache
    if let index = notes.firstIndex(where: { $0.id == note.id }) {
      notes[index] = note
    } else {
      throw NSError(
        domain: "DatabaseManager", code: 1,
        userInfo: [NSLocalizedDescriptionKey: "Note not found"])
    }
  }
  
  func getAllNotes() throws -> [Note] {
    // Refresh from SQLite database to ensure we have the latest data
    notes = try sqliteManager.getAllNotes()
    return notes
  }
  
  func getNote(id: UUID) throws -> Note? {
    // Try to get from SQLite database
    return try sqliteManager.getNote(id: id)
  }
  
  func deleteNote(id: UUID) throws {
    // Delete from SQLite database
    try sqliteManager.deleteNote(id: id)
    // Update in-memory cache
    if let index = notes.firstIndex(where: { $0.id == id }) {
      notes.remove(at: index)
    } else {
      throw NSError(
        domain: "DatabaseManager", code: 1,
        userInfo: [NSLocalizedDescriptionKey: "Note not found"])
    }
  }
  
  // New function to update just the user notes for a note
  func updateUserNotes(id: UUID, userNotes: String) throws {
    // Get the current note
    guard var note = try getNote(id: id) else {
      throw NSError(
        domain: "DatabaseManager", code: 1,
        userInfo: [NSLocalizedDescriptionKey: "Note not found"])
    }
    
    // Update the userNotes field and updatedAt timestamp
    note.userNotes = userNotes
    note.updatedAt = Date()
    
    // Save the updated note
    try updateNote(note)
  }
  
  // Function to fix a note with swapped recordingPath and userNotes fields
  func fixSwappedFields(id: UUID) async throws {
    print("DatabaseManager: Fixing swapped fields for note \(id)")
    
    // Get the current note
    guard var note = try getNote(id: id) else {
      throw NSError(
        domain: "DatabaseManager", code: 1,
        userInfo: [NSLocalizedDescriptionKey: "Note not found"])
    }
    
    // Check if we have the issue where userNotes contains a path and recordingPath contains a timestamp
    let userNotesContainsPath = note.userNotes.contains("/Users") && note.userNotes.contains("Containers")
    let recordingPathLooksLikeTimestamp = note.recordingPath.contains("T") && note.recordingPath.contains("Z") && note.recordingPath.count < 30
    
    if userNotesContainsPath && recordingPathLooksLikeTimestamp {
      print("DatabaseManager: Confirmed swapped fields issue for note \(id)")
      
      // Save the path from userNotes
      let path = note.userNotes
      
      // Update the note with the correct values
      note.userNotes = ""
      note.recordingPath = path
      note.updatedAt = Date()
      
      // Save the updated note
      try updateNote(note)
      print("DatabaseManager: Successfully fixed swapped fields for note \(id)")
      
      // Refresh the in-memory cache
      notes = try sqliteManager.getAllNotes()
    } else {
      print("DatabaseManager: No swapped fields issue detected for note \(id)")
    }
  }
}

================
File: Database/README.md
================
# SQLite Integration for SessionScribe

This directory contains the SQLite database integration for the SessionScribe app.

## Files

- `DatabaseManager.swift`: The main database manager that provides an interface for the app to interact with the database.
- `SQLiteManager.swift`: The SQLite implementation that handles the low-level SQLite operations.

## Setup Instructions

To use SQLite in the project, follow these steps:

1. Open your project in Xcode
2. Select your project in the Project Navigator
3. Select your target
4. Go to the 'Build Phases' tab
5. Expand 'Link Binary With Libraries'
6. Click the '+' button
7. Search for 'libsqlite3.tbd'
8. Select it and click 'Add'

## Required Entitlements

To access the user's main Documents folder, the app requires the following entitlements:

```xml
<key>com.apple.security.files.home-relative-path.read-write</key>
<array>
    <string>/Documents/SessionScribe/</string>
</array>
```

These entitlements are already added to the `SessionScribe.entitlements` file.

## Database Location

The SQLite database is stored in the user's main Documents folder in a subdirectory called "SessionScribe":

```
~/Documents/SessionScribe/notes.sqlite
```

This location makes it easier for users to back up their data manually if needed and is outside the app's sandbox.

### Fallback Mechanism

If the app doesn't have permission to access the user's main Documents folder (for example, if the required entitlements are not properly set up), it will fall back to storing the database in the app's sandboxed Documents directory:

```
~/Library/Containers/[app-bundle-id]/Data/Documents/SessionScribe/notes.sqlite
```

This ensures that the app will still function correctly even if it doesn't have the necessary permissions to access the user's main Documents folder.

## Database Structure

The SQLite database contains a single table called `notes` with the following schema:

```sql
CREATE TABLE IF NOT EXISTS notes(
    id TEXT PRIMARY KEY,
    title TEXT,
    raw_transcript TEXT,
    formatted_notes TEXT,
    recording_path TEXT,
    created_at TEXT,
    updated_at TEXT
);
```

## Migration

The `DatabaseManager` includes functionality to migrate existing in-memory notes to the SQLite database. This migration happens automatically when the app is first run after the SQLite integration is added.

## Error Handling

The SQLite integration includes comprehensive error handling to provide meaningful error messages when database operations fail.

## Usage

The app continues to use the `DatabaseManager` class as before, with no changes required to the rest of the codebase. The `DatabaseManager` now uses SQLite for persistent storage instead of in-memory storage.

================
File: Database/SQLiteManager.swift
================
import Foundation
import SQLite3

class SQLiteManager {
  static let shared = SQLiteManager()

  private var db: OpaquePointer?
  private let dbPath: String
  private let schemaVersion = 2  // Increment this when schema changes

  private init() {
    // Try to use the user's main Documents folder first
    let homeDirectory = FileManager.default.homeDirectoryForCurrentUser
    let documentsDirectory = homeDirectory.appendingPathComponent("Documents")
    let directoryURL = documentsDirectory.appendingPathComponent("SessionScribe")
    let fileURL = directoryURL.appendingPathComponent("notes.sqlite")

    var useMainDocuments = true

    // Create directory if it doesn't exist
    if !FileManager.default.fileExists(atPath: directoryURL.path) {
      do {
        try FileManager.default.createDirectory(
          at: directoryURL, withIntermediateDirectories: true, attributes: nil)
      } catch {
        print("Error creating directory in main Documents folder: \(error.localizedDescription)")
        useMainDocuments = false
      }
    }

    // If we can't use the main Documents folder, fall back to the app's Documents directory
    if !useMainDocuments {
      print("Falling back to app's Documents directory")
      let appDocumentsDirectory = FileManager.default.urls(
        for: .documentDirectory, in: .userDomainMask)[0]
      let appDirectoryURL = appDocumentsDirectory.appendingPathComponent("SessionScribe")
      let appFileURL = appDirectoryURL.appendingPathComponent("notes.sqlite")

      // Create directory if it doesn't exist
      if !FileManager.default.fileExists(atPath: appDirectoryURL.path) {
        do {
          try FileManager.default.createDirectory(
            at: appDirectoryURL, withIntermediateDirectories: true, attributes: nil)
        } catch {
          print(
            "Error creating directory in app's Documents directory: \(error.localizedDescription)")
        }
      }

      dbPath = appFileURL.path
    } else {
      dbPath = fileURL.path
    }

    print("Database path: \(dbPath)")

    // Open the database
    if sqlite3_open(dbPath, &db) != SQLITE_OK {
      print("Error opening database: \(String(describing: sqlite3_errmsg(db)))")
      return
    }

    // Create tables and perform migrations if needed
    setupDatabase()
  }

  deinit {
    sqlite3_close(db)
  }

  // Helper method to handle SQLite errors
  private func handleSQLiteError(operation: String) -> Error {
    let errorCode = sqlite3_errcode(db)
    let errorMessage = String(cString: sqlite3_errmsg(db)!)

    let error = NSError(
      domain: "SQLiteManager",
      code: Int(errorCode),
      userInfo: [NSLocalizedDescriptionKey: "SQLite error during \(operation): \(errorMessage)"]
    )
    print("SQLite error: \(error.localizedDescription)")
    return error
  }

  private func setupDatabase() {
    // Check if we need to create or migrate the database
    let currentVersion = getCurrentSchemaVersion()
    print("Current database schema version: \(currentVersion)")
    
    if currentVersion == 0 {
      // New database - create tables with the latest schema
      createTablesV2()
      setSchemaVersion(version: schemaVersion)
    } else if currentVersion < schemaVersion {
      // Database exists but needs migration
      migrateDatabase(fromVersion: currentVersion, toVersion: schemaVersion)
    }
  }
  
  private func getCurrentSchemaVersion() -> Int {
    // Check if the user_version pragma exists
    var queryStatement: OpaquePointer?
    var version = 0
    
    let query = "PRAGMA user_version;"
    
    if sqlite3_prepare_v2(db, query, -1, &queryStatement, nil) == SQLITE_OK {
      if sqlite3_step(queryStatement) == SQLITE_ROW {
        version = Int(sqlite3_column_int(queryStatement, 0))
      }
    }
    
    sqlite3_finalize(queryStatement)
    
    // If version is 0 but the notes table exists, this is a v1 database
    if version == 0 && tableExists(tableName: "notes") {
      return 1
    }
    
    return version
  }
  
  private func setSchemaVersion(version: Int) {
    let query = "PRAGMA user_version = \(version);"
    var statement: OpaquePointer?
    
    if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
      if sqlite3_step(statement) != SQLITE_DONE {
        print("Error setting schema version: \(String(describing: sqlite3_errmsg(db)))")
      }
    } else {
      print("Error preparing schema version statement: \(String(describing: sqlite3_errmsg(db)))")
    }
    
    sqlite3_finalize(statement)
  }
  
  private func tableExists(tableName: String) -> Bool {
    let query = "SELECT name FROM sqlite_master WHERE type='table' AND name=?;"
    var statement: OpaquePointer?
    var exists = false
    
    if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
      sqlite3_bind_text(statement, 1, (tableName as NSString).utf8String, -1, nil)
      
      if sqlite3_step(statement) == SQLITE_ROW {
        exists = true
      }
    }
    
    sqlite3_finalize(statement)
    return exists
  }
  
  private func migrateDatabase(fromVersion: Int, toVersion: Int) {
    print("Migrating database from v\(fromVersion) to v\(toVersion)")
    
    if fromVersion == 1 && toVersion >= 2 {
      // Migrate from v1 to v2 (adding userNotes column)
      migrateV1ToV2()
    }
    
    // Set the new schema version
    setSchemaVersion(version: toVersion)
  }
  
  private func migrateV1ToV2() {
    print("Performing migration from v1 to v2 (adding userNotes field)")
    
    // Add the userNotes column to the existing table
    let alterTableSQL = "ALTER TABLE notes ADD COLUMN user_notes TEXT DEFAULT '';"
    var alterStatement: OpaquePointer?
    
    if sqlite3_prepare_v2(db, alterTableSQL, -1, &alterStatement, nil) == SQLITE_OK {
      if sqlite3_step(alterStatement) == SQLITE_DONE {
        print("Successfully added user_notes column")
      } else {
        print("Failed to add user_notes column: \(String(describing: sqlite3_errmsg(db)))")
      }
    } else {
      print("Failed to prepare alter table statement: \(String(describing: sqlite3_errmsg(db)))")
    }
    
    sqlite3_finalize(alterStatement)
  }

  private func createTablesV2() {
    // Create notes table with the latest schema including user_notes
    let createTableString = """
      CREATE TABLE IF NOT EXISTS notes(
          id TEXT PRIMARY KEY,
          title TEXT,
          raw_transcript TEXT,
          formatted_notes TEXT,
          user_notes TEXT,
          recording_path TEXT,
          created_at TEXT,
          updated_at TEXT
      );
      """

    var createTableStatement: OpaquePointer?

    // Prepare the statement
    if sqlite3_prepare_v2(db, createTableString, -1, &createTableStatement, nil) == SQLITE_OK {
      // Execute the statement
      if sqlite3_step(createTableStatement) == SQLITE_DONE {
        print("Notes table created with latest schema (v2)")
      } else {
        print("Notes table could not be created: \(String(describing: sqlite3_errmsg(db)))")
      }
    } else {
      print(
        "CREATE TABLE statement could not be prepared: \(String(describing: sqlite3_errmsg(db)))")
    }

    // Finalize the statement
    sqlite3_finalize(createTableStatement)
  }

  // MARK: - Note Operations

  func saveNote(_ note: Note) throws {
    let insertStatementString = """
      INSERT INTO notes (id, title, raw_transcript, formatted_notes, user_notes, recording_path, created_at, updated_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?);
      """

    var insertStatement: OpaquePointer?

    // Prepare the statement
    if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK {
      // Bind values
      sqlite3_bind_text(insertStatement, 1, (note.id.uuidString as NSString).utf8String, -1, nil)
      sqlite3_bind_text(insertStatement, 2, (note.title as NSString).utf8String, -1, nil)
      sqlite3_bind_text(insertStatement, 3, (note.rawTranscript as NSString).utf8String, -1, nil)
      sqlite3_bind_text(insertStatement, 4, (note.formattedNotes as NSString).utf8String, -1, nil)
      sqlite3_bind_text(insertStatement, 5, (note.userNotes as NSString).utf8String, -1, nil)
      sqlite3_bind_text(insertStatement, 6, (note.recordingPath as NSString).utf8String, -1, nil)

      let dateFormatter = ISO8601DateFormatter()
      let createdAtString = dateFormatter.string(from: note.createdAt)
      let updatedAtString = dateFormatter.string(from: note.updatedAt)

      sqlite3_bind_text(insertStatement, 7, (createdAtString as NSString).utf8String, -1, nil)
      sqlite3_bind_text(insertStatement, 8, (updatedAtString as NSString).utf8String, -1, nil)

      // Execute the statement
      if sqlite3_step(insertStatement) != SQLITE_DONE {
        throw handleSQLiteError(operation: "saving note")
      }
    } else {
      throw handleSQLiteError(operation: "preparing save statement")
    }

    // Finalize the statement
    sqlite3_finalize(insertStatement)
  }

  func updateNote(_ note: Note) throws {
    let updateStatementString = """
      UPDATE notes
      SET title = ?, raw_transcript = ?, formatted_notes = ?, user_notes = ?, recording_path = ?, updated_at = ?
      WHERE id = ?;
      """

    var updateStatement: OpaquePointer?

    // Prepare the statement
    if sqlite3_prepare_v2(db, updateStatementString, -1, &updateStatement, nil) == SQLITE_OK {
      // Bind values
      sqlite3_bind_text(updateStatement, 1, (note.title as NSString).utf8String, -1, nil)
      sqlite3_bind_text(updateStatement, 2, (note.rawTranscript as NSString).utf8String, -1, nil)
      sqlite3_bind_text(updateStatement, 3, (note.formattedNotes as NSString).utf8String, -1, nil)
      sqlite3_bind_text(updateStatement, 4, (note.userNotes as NSString).utf8String, -1, nil)
      sqlite3_bind_text(updateStatement, 5, (note.recordingPath as NSString).utf8String, -1, nil)

      let dateFormatter = ISO8601DateFormatter()
      let updatedAtString = dateFormatter.string(from: note.updatedAt)

      sqlite3_bind_text(updateStatement, 6, (updatedAtString as NSString).utf8String, -1, nil)
      sqlite3_bind_text(updateStatement, 7, (note.id.uuidString as NSString).utf8String, -1, nil)

      // Execute the statement
      if sqlite3_step(updateStatement) != SQLITE_DONE {
        throw handleSQLiteError(operation: "updating note")
      }

      // Check if any rows were affected
      if sqlite3_changes(db) == 0 {
        throw handleSQLiteError(operation: "note not found")
      }
    } else {
      throw handleSQLiteError(operation: "preparing update statement")
    }

    // Finalize the statement
    sqlite3_finalize(updateStatement)
  }

  func getAllNotes() throws -> [Note] {
    let queryStatementString = "SELECT * FROM notes ORDER BY created_at DESC;"
    var queryStatement: OpaquePointer?
    var notes: [Note] = []

    // Prepare the statement
    if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
      // Execute the statement
      while sqlite3_step(queryStatement) == SQLITE_ROW {
        let id = String(cString: sqlite3_column_text(queryStatement, 0))
        let title = String(cString: sqlite3_column_text(queryStatement, 1))
        let rawTranscript = String(cString: sqlite3_column_text(queryStatement, 2))
        let formattedNotes = String(cString: sqlite3_column_text(queryStatement, 3))
        
        // Handle the user_notes column (might not exist in older schema versions)
        var userNotes = ""
        if sqlite3_column_type(queryStatement, 4) != SQLITE_NULL {
          userNotes = String(cString: sqlite3_column_text(queryStatement, 4))
        }
        
        let recordingPath = String(cString: sqlite3_column_text(queryStatement, 5))
        let createdAtString = String(cString: sqlite3_column_text(queryStatement, 6))
        let updatedAtString = String(cString: sqlite3_column_text(queryStatement, 7))

        let dateFormatter = ISO8601DateFormatter()
        let createdAt = dateFormatter.date(from: createdAtString) ?? Date()
        let updatedAt = dateFormatter.date(from: updatedAtString) ?? Date()

        let note = Note(
          id: UUID(uuidString: id) ?? UUID(),
          title: title,
          rawTranscript: rawTranscript,
          formattedNotes: formattedNotes,
          userNotes: userNotes,
          recordingPath: recordingPath,
          createdAt: createdAt,
          updatedAt: updatedAt
        )

        notes.append(note)
      }
    } else {
      throw handleSQLiteError(operation: "preparing select statement")
    }

    // Finalize the statement
    sqlite3_finalize(queryStatement)

    return notes
  }

  func getNote(id: UUID) throws -> Note? {
    let queryStatementString = "SELECT * FROM notes WHERE id = ?;"
    var queryStatement: OpaquePointer?
    var note: Note?

    // Prepare the statement
    if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
      // Bind the id value
      sqlite3_bind_text(queryStatement, 1, (id.uuidString as NSString).utf8String, -1, nil)

      // Execute the statement
      if sqlite3_step(queryStatement) == SQLITE_ROW {
        let id = String(cString: sqlite3_column_text(queryStatement, 0))
        let title = String(cString: sqlite3_column_text(queryStatement, 1))
        let rawTranscript = String(cString: sqlite3_column_text(queryStatement, 2))
        let formattedNotes = String(cString: sqlite3_column_text(queryStatement, 3))
        
        // Handle the user_notes column (might not exist in older schema versions)
        var userNotes = ""
        if sqlite3_column_type(queryStatement, 4) != SQLITE_NULL {
          userNotes = String(cString: sqlite3_column_text(queryStatement, 4))
        }
        
        let recordingPath = String(cString: sqlite3_column_text(queryStatement, 5))
        let createdAtString = String(cString: sqlite3_column_text(queryStatement, 6))
        let updatedAtString = String(cString: sqlite3_column_text(queryStatement, 7))

        let dateFormatter = ISO8601DateFormatter()
        let createdAt = dateFormatter.date(from: createdAtString) ?? Date()
        let updatedAt = dateFormatter.date(from: updatedAtString) ?? Date()

        note = Note(
          id: UUID(uuidString: id) ?? UUID(),
          title: title,
          rawTranscript: rawTranscript,
          formattedNotes: formattedNotes,
          userNotes: userNotes,
          recordingPath: recordingPath,
          createdAt: createdAt,
          updatedAt: updatedAt
        )
      }
    } else {
      throw handleSQLiteError(operation: "preparing select statement")
    }

    // Finalize the statement
    sqlite3_finalize(queryStatement)

    return note
  }

  func deleteNote(id: UUID) throws {
    let deleteStatementString = "DELETE FROM notes WHERE id = ?;"
    var deleteStatement: OpaquePointer?

    // Prepare the statement
    if sqlite3_prepare_v2(db, deleteStatementString, -1, &deleteStatement, nil) == SQLITE_OK {
      // Bind the id value
      sqlite3_bind_text(deleteStatement, 1, (id.uuidString as NSString).utf8String, -1, nil)

      // Execute the statement
      if sqlite3_step(deleteStatement) != SQLITE_DONE {
        throw handleSQLiteError(operation: "deleting note")
      }

      // Check if any rows were affected
      if sqlite3_changes(db) == 0 {
        throw handleSQLiteError(operation: "note not found")
      }
    } else {
      throw handleSQLiteError(operation: "preparing delete statement")
    }

    // Finalize the statement
    sqlite3_finalize(deleteStatement)
  }

  // Check if the database is empty (no notes)
  func isDatabaseEmpty() -> Bool {
    let queryStatementString = "SELECT COUNT(*) FROM notes;"
    var queryStatement: OpaquePointer?
    var isEmpty = true

    // Prepare the statement
    if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
      // Execute the statement
      if sqlite3_step(queryStatement) == SQLITE_ROW {
        let count = sqlite3_column_int(queryStatement, 0)
        isEmpty = (count == 0)
      }
    }

    // Finalize the statement
    sqlite3_finalize(queryStatement)

    return isEmpty
  }
  
  // Fix any notes that might have the recording path in the userNotes field
  func fixUserNotesWithRecordingPath() {
    print("Checking for notes with recording path in userNotes field...")
    
    let queryStatementString = "SELECT id, user_notes, recording_path FROM notes;"
    var queryStatement: OpaquePointer?
    
    // Prepare the statement
    if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
      // Execute the statement
      while sqlite3_step(queryStatement) == SQLITE_ROW {
        let id = String(cString: sqlite3_column_text(queryStatement, 0))
        
        // Get userNotes and recordingPath
        var userNotes = ""
        if sqlite3_column_type(queryStatement, 1) != SQLITE_NULL {
          userNotes = String(cString: sqlite3_column_text(queryStatement, 1))
        }
        
        var recordingPath = ""
        if sqlite3_column_type(queryStatement, 2) != SQLITE_NULL {
          recordingPath = String(cString: sqlite3_column_text(queryStatement, 2))
        }
        
        print("Note ID: \(id)")
        print("  userNotes: '\(userNotes)'")
        print("  recordingPath: '\(recordingPath)'")
        
        // Check if userNotes contains a path and recordingPath looks like a timestamp
        let userNotesContainsPath = userNotes.contains("/Users") && userNotes.contains("Containers")
        let recordingPathLooksLikeTimestamp = recordingPath.contains("T") && recordingPath.contains("Z") && recordingPath.count < 30
        
        if userNotesContainsPath && recordingPathLooksLikeTimestamp {
          print("Found note with ID \(id) that has swapped userNotes and recordingPath. Fixing...")
          
          // Direct SQL update to fix the issue
          let updateSQL = """
            UPDATE notes 
            SET user_notes = '', 
                recording_path = ?, 
                updated_at = ? 
            WHERE id = ?
          """
          
          var updateStatement: OpaquePointer?
          
          if sqlite3_prepare_v2(db, updateSQL, -1, &updateStatement, nil) == SQLITE_OK {
            let dateFormatter = ISO8601DateFormatter()
            let updatedAtString = dateFormatter.string(from: Date())
            
            sqlite3_bind_text(updateStatement, 1, (userNotes as NSString).utf8String, -1, nil)
            sqlite3_bind_text(updateStatement, 2, (updatedAtString as NSString).utf8String, -1, nil)
            sqlite3_bind_text(updateStatement, 3, (id as NSString).utf8String, -1, nil)
            
            if sqlite3_step(updateStatement) != SQLITE_DONE {
              print("Error executing direct SQL update: \(String(describing: sqlite3_errmsg(db)))")
            } else {
              print("Successfully fixed swapped fields for note with ID \(id) using direct SQL update")
            }
          } else {
            print("Error preparing direct SQL update: \(String(describing: sqlite3_errmsg(db)))")
          }
          
          sqlite3_finalize(updateStatement)
        }
        // Also check the original case where userNotes equals recordingPath
        else if userNotes == recordingPath && userNotes.contains("/") {
          print("Found note with ID \(id) that has recording path in userNotes field. Fixing...")
          
          // Update the note to have an empty userNotes field
          let updateStatementString = "UPDATE notes SET user_notes = '' WHERE id = ?;"
          var updateStatement: OpaquePointer?
          
          if sqlite3_prepare_v2(db, updateStatementString, -1, &updateStatement, nil) == SQLITE_OK {
            sqlite3_bind_text(updateStatement, 1, (id as NSString).utf8String, -1, nil)
            
            if sqlite3_step(updateStatement) != SQLITE_DONE {
              print("Error fixing userNotes field: \(String(describing: sqlite3_errmsg(db)))")
            } else {
              print("Successfully fixed userNotes field for note with ID \(id)")
            }
          } else {
            print("Error preparing update statement: \(String(describing: sqlite3_errmsg(db)))")
          }
          
          sqlite3_finalize(updateStatement)
        }
        // Check for the reverse case: recordingPath contains a timestamp and userNotes is empty
        else if recordingPath.contains("T") && recordingPath.contains("Z") && recordingPath.count < 30 && userNotes.isEmpty {
          print("Found note with ID \(id) that has timestamp in recordingPath field. Fixing...")
          
          // Update the note to have an empty recordingPath field (or set to a default value)
          let updateStatementString = "UPDATE notes SET recording_path = '/Users/kverma/Library/Containers/arnavgosain.SessionScribe/Data/OngoingRecording' WHERE id = ?;"
          var updateStatement: OpaquePointer?
          
          if sqlite3_prepare_v2(db, updateStatementString, -1, &updateStatement, nil) == SQLITE_OK {
            sqlite3_bind_text(updateStatement, 1, (id as NSString).utf8String, -1, nil)
            
            if sqlite3_step(updateStatement) != SQLITE_DONE {
              print("Error fixing recordingPath field: \(String(describing: sqlite3_errmsg(db)))")
            } else {
              print("Successfully fixed recordingPath field for note with ID \(id)")
            }
          } else {
            print("Error preparing update statement: \(String(describing: sqlite3_errmsg(db)))")
          }
          
          sqlite3_finalize(updateStatement)
        }
      }
    } else {
      print("Error preparing query statement: \(String(describing: sqlite3_errmsg(db)))")
    }
    
    // Finalize the statement
    sqlite3_finalize(queryStatement)
  }
  
  // Get the total count of notes in the database
  func getNotesCount() throws -> Int {
    let queryStatementString = "SELECT COUNT(*) FROM notes;"
    var queryStatement: OpaquePointer?
    var count: Int = 0
    
    // Prepare the statement
    if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
      // Execute the statement
      if sqlite3_step(queryStatement) == SQLITE_ROW {
        count = Int(sqlite3_column_int(queryStatement, 0))
      } else {
        throw handleSQLiteError(operation: "counting notes")
      }
    } else {
      throw handleSQLiteError(operation: "preparing count statement")
    }
    
    // Finalize the statement
    sqlite3_finalize(queryStatement)
    
    return count
  }
  
  // Get the timestamp of the most recently updated note
  func getLatestUpdateTime() throws -> Date? {
    let queryStatementString = "SELECT updated_at FROM notes ORDER BY updated_at DESC LIMIT 1;"
    var queryStatement: OpaquePointer?
    var latestUpdate: Date? = nil
    
    // Prepare the statement
    if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
      // Execute the statement
      if sqlite3_step(queryStatement) == SQLITE_ROW {
        if let dateText = sqlite3_column_text(queryStatement, 0) {
          let updatedAtString = String(cString: dateText)
          let dateFormatter = ISO8601DateFormatter()
          latestUpdate = dateFormatter.date(from: updatedAtString)
        }
      }
    } else {
      throw handleSQLiteError(operation: "preparing latest update time statement")
    }
    
    // Finalize the statement
    sqlite3_finalize(queryStatement)
    
    return latestUpdate
  }
  
  // Fix a specific note by ID - useful for direct fixes of known problematic notes
  func fixSpecificNote(id: String) {
    print("Applying direct fix for note with ID: \(id)")
    
    // First, get the current values to check what's wrong
    let queryStatementString = "SELECT user_notes, recording_path FROM notes WHERE id = ?;"
    var queryStatement: OpaquePointer?
    
    if sqlite3_prepare_v2(db, queryStatementString, -1, &queryStatement, nil) == SQLITE_OK {
      sqlite3_bind_text(queryStatement, 1, (id as NSString).utf8String, -1, nil)
      
      if sqlite3_step(queryStatement) == SQLITE_ROW {
        var userNotes = ""
        if sqlite3_column_type(queryStatement, 0) != SQLITE_NULL {
          userNotes = String(cString: sqlite3_column_text(queryStatement, 0))
        }
        
        var recordingPath = ""
        if sqlite3_column_type(queryStatement, 1) != SQLITE_NULL {
          recordingPath = String(cString: sqlite3_column_text(queryStatement, 1))
        }
        
        print("Note ID \(id) current values:")
        print("  userNotes: '\(userNotes)'")
        print("  recordingPath: '\(recordingPath)'")
        
        // Check for the specific issue where recordingPath contains a timestamp
        if recordingPath.contains("T") && recordingPath.contains("Z") && recordingPath.count < 30 {
          print("Confirmed timestamp in recordingPath. Fixing...")
          
          // Update the note with fixed values
          let updateStatementString = """
            UPDATE notes 
            SET recording_path = '/Users/kverma/Library/Containers/arnavgosain.SessionScribe/Data/OngoingRecording',
                user_notes = '',
                updated_at = ?
            WHERE id = ?;
          """
          
          var updateStatement: OpaquePointer?
          
          if sqlite3_prepare_v2(db, updateStatementString, -1, &updateStatement, nil) == SQLITE_OK {
            let dateFormatter = ISO8601DateFormatter()
            let updatedAtString = dateFormatter.string(from: Date())
            
            sqlite3_bind_text(updateStatement, 1, (updatedAtString as NSString).utf8String, -1, nil)
            sqlite3_bind_text(updateStatement, 2, (id as NSString).utf8String, -1, nil)
            
            if sqlite3_step(updateStatement) != SQLITE_DONE {
              print("Error fixing specific note: \(String(describing: sqlite3_errmsg(db)))")
            } else {
              print("Successfully fixed specific note with ID \(id)")
            }
          } else {
            print("Error preparing update statement: \(String(describing: sqlite3_errmsg(db)))")
          }
          
          sqlite3_finalize(updateStatement)
        }
        // Check if userNotes contains a timestamp
        else if userNotes.contains("T") && userNotes.contains("Z") && userNotes.count < 30 {
          print("Confirmed timestamp in userNotes. Fixing...")
          
          // Update the note with fixed values
          let updateStatementString = """
            UPDATE notes 
            SET user_notes = '',
                updated_at = ?
            WHERE id = ?;
          """
          
          var updateStatement: OpaquePointer?
          
          if sqlite3_prepare_v2(db, updateStatementString, -1, &updateStatement, nil) == SQLITE_OK {
            let dateFormatter = ISO8601DateFormatter()
            let updatedAtString = dateFormatter.string(from: Date())
            
            sqlite3_bind_text(updateStatement, 1, (updatedAtString as NSString).utf8String, -1, nil)
            sqlite3_bind_text(updateStatement, 2, (id as NSString).utf8String, -1, nil)
            
            if sqlite3_step(updateStatement) != SQLITE_DONE {
              print("Error fixing specific note: \(String(describing: sqlite3_errmsg(db)))")
            } else {
              print("Successfully fixed specific note with ID \(id)")
            }
          } else {
            print("Error preparing update statement: \(String(describing: sqlite3_errmsg(db)))")
          }
          
          sqlite3_finalize(updateStatement)
        }
      } else {
        print("Note with ID \(id) not found")
      }
    } else {
      print("Error preparing query statement: \(String(describing: sqlite3_errmsg(db)))")
    }
    
    sqlite3_finalize(queryStatement)
  }
}

================
File: Models/MeetingNotesSchema.swift
================
import Foundation
import OpenAI

// This is the updated MeetingNotesSchema that matches the function definition
// in the RecordingViewModel class
struct MeetingNotesSchema: Codable {
    let meetingType: String
    let participants: [String]
    let topics: [Topic]
    let decisions: [String]?
    let actionItems: [ActionItem]?
    let nextSteps: [String]?
    let pendingDecisions: [String]?
    let technicalDetails: [String]?
    let industryTerms: [String]?
    
    struct Topic: Codable {
        let name: String
        let keyPoints: [String]
    }
    
    struct ActionItem: Codable {
        let description: String
        let assignee: String?
        let deadline: String?
    }
    
    // Convert to markdown for backward compatibility
    func toMarkdown() -> String {
        // Derive title from first topic or use default
        let title = !topics.isEmpty ? topics[0].name : "Meeting Notes"
        
        var markdown = "# \(title)\n\n"
        
        markdown += "## Meeting Type\n"
        markdown += "\(meetingType)\n\n"
        
        if !participants.isEmpty {
            markdown += "## Participants\n"
            for participant in participants {
                markdown += "• \(participant)\n"
            }
            markdown += "\n"
        }
        
        markdown += "## Discussion Topics\n"
        for topic in topics {
            markdown += "### \(topic.name)\n"
            for point in topic.keyPoints {
                markdown += "• \(point)\n"
            }
            markdown += "\n"
        }
        
        if let decisions = decisions, !decisions.isEmpty {
            markdown += "## Decisions Made\n"
            for decision in decisions {
                markdown += "• \(decision)\n"
            }
            markdown += "\n"
        }
        
        if let actionItems = actionItems, !actionItems.isEmpty {
            markdown += "## Action Items\n"
            for item in actionItems {
                var actionText = "• \(item.description)"
                if let assignee = item.assignee {
                    actionText += " - \(assignee)"
                }
                if let deadline = item.deadline {
                    actionText += " (Due: \(deadline))"
                }
                markdown += actionText + "\n"
            }
            markdown += "\n"
        }
        
        if let nextSteps = nextSteps, !nextSteps.isEmpty {
            markdown += "## Next Steps\n"
            for step in nextSteps {
                markdown += "• \(step)\n"
            }
            markdown += "\n"
        }
        
        if let pendingDecisions = pendingDecisions, !pendingDecisions.isEmpty {
            markdown += "## Pending Decisions\n"
            for decision in pendingDecisions {
                markdown += "• \(decision)\n"
            }
            markdown += "\n"
        }
        
        if let technicalDetails = technicalDetails, !technicalDetails.isEmpty {
            markdown += "## Technical Details\n"
            for detail in technicalDetails {
                markdown += "• \(detail)\n"
            }
            markdown += "\n"
        }
        
        if let industryTerms = industryTerms, !industryTerms.isEmpty {
            markdown += "## Industry Terms\n"
            for term in industryTerms {
                markdown += "• \(term)\n"
            }
        }
        
        return markdown
    }
}

// Extending with the function declaration (this part stays the same)
extension MeetingNotesSchema {
    static var functionDeclaration: FunctionDeclaration {
        .init(
            name: "generate_meeting_notes",
            description: "Generate structured meeting notes from a transcript",
            parameters: .init(
                type: .object,
                properties: [
                    "meetingType": .init(type: .string, description: "Type of meeting (discussion, one-on-one, standup, etc.)"),
                    "participants": .init(type: .array, description: "List of participants mentioned in the meeting", items: .init(type: .string)),
                    "topics": .init(
                        type: .array,
                        description: "Main discussion topics with key points",
                        items: .init(
                            type: .object,
                            properties: [
                                "name": .init(type: .string, description: "Name of the discussion topic"),
                                "keyPoints": .init(type: .array, description: "Key points discussed for this topic", items: .init(type: .string))
                            ]
                        )
                    ),
                    "decisions": .init(type: .array, description: "Decisions made during the meeting", items: .init(type: .string)),
                    "actionItems": .init(
                        type: .array,
                        description: "Action items with assignees and deadlines if mentioned",
                        items: .init(
                            type: .object,
                            properties: [
                                "description": .init(type: .string, description: "Description of the action item"),
                                "assignee": .init(type: .string, description: "Person assigned to the task (if mentioned)"),
                                "deadline": .init(type: .string, description: "Deadline for the task (if mentioned)")
                            ]
                        )
                    ),
                    "nextSteps": .init(type: .array, description: "Next steps discussed in the meeting", items: .init(type: .string)),
                    "pendingDecisions": .init(type: .array, description: "Decisions that are still pending", items: .init(type: .string)),
                    "technicalDetails": .init(type: .array, description: "Technical details or requirements mentioned", items: .init(type: .string)),
                    "industryTerms": .init(type: .array, description: "Industry-specific terms or jargon used", items: .init(type: .string))
                ]
            )
        )
    }
}

================
File: Models/Note.swift
================
import Foundation

struct Note: Codable, Hashable {
  var id: UUID
  var title: String
  var rawTranscript: String
  var formattedNotes: String  // AI-generated notes
  var userNotes: String       // User's manually entered notes
  var recordingPath: String
  var createdAt: Date
  var updatedAt: Date
  
  init(
    id: UUID = UUID(),
    title: String,
    rawTranscript: String = "",
    formattedNotes: String = "",
    userNotes: String = "",     // Added default value
    recordingPath: String,
    createdAt: Date = Date(),
    updatedAt: Date = Date()
  ) {
    self.id = id
    self.title = title
    self.rawTranscript = rawTranscript
    self.formattedNotes = formattedNotes
    self.userNotes = userNotes  // Set the new property
    self.recordingPath = recordingPath
    self.createdAt = createdAt
    self.updatedAt = updatedAt
  }
  
  // Extract a meaningful title from the formatted notes if available
  var extractedTitle: String {
    // If the formatted notes are empty, return the existing title
    if formattedNotes.isEmpty {
      return title
    }
    
    // Try to extract the title from the formatted notes
    // The AI-generated notes typically start with a # Title format
    let lines = formattedNotes.split(separator: "\n")
    if let firstLine = lines.first, firstLine.hasPrefix("# ") {
      // Remove the # prefix and any trailing whitespace
      let titleText = firstLine.dropFirst(2).trimmingCharacters(in: .whitespacesAndNewlines)
      return titleText.isEmpty ? title : titleText
    }
    
    return title
  }
  
  // Implement Hashable
  func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }
  
  static func == (lhs: Note, rhs: Note) -> Bool {
    lhs.id == rhs.id
  }
}

================
File: Models/WhisperResponse.swift
================
import Foundation

struct CustomWhisperResponse: Codable {
    let text: String
    
    enum CodingKeys: String, CodingKey {
        case text
    }
}

================
File: Preview Content/Preview Assets.xcassets/Contents.json
================
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}

================
File: ViewModels/MeetingsViewModel.swift
================
import Combine
import Foundation
import SQLite3

// DatabaseNotifier - A reactive database wrapper that directly integrates with SQLite
// and provides reactive updates when data changes
class DatabaseNotifier {
  static let shared = DatabaseNotifier()

  // Published properties for reactive updates
  @Published private(set) var notes: [Note] = []
  @Published private(set) var isLoading = false
  @Published private(set) var error: Error? = nil
  @Published private(set) var hasLoadedData = false

  // Active subscribers counter
  private var activeSubscribers = 0

  // Internal SQLite reference
  private var sqliteManager = SQLiteManager.shared

  // Database change listener
  private var dbChangeTimer: Timer? = nil

  private init() {
    // Initial data load
    Task {
      await refresh()
    }

    // Setup change detection timer
    setupDatabaseChangeTimer()
  }

  deinit {
    dbChangeTimer?.invalidate()
  }

  private func setupDatabaseChangeTimer() {
    // Poll for database changes every 2 seconds when subscribers are active
    dbChangeTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
      guard let self = self, self.activeSubscribers > 0 else { return }
      Task {
        await self.checkForDatabaseChanges()
      }
    }
    dbChangeTimer?.tolerance = 0.5
    RunLoop.main.add(dbChangeTimer!, forMode: .common)
  }

  @MainActor
  private func checkForDatabaseChanges() async {
    do {
      // Fetch the current count of notes to detect changes
      let currentCount = try getNotesCount()
      if currentCount != self.notes.count {
        await self.refresh(silent: true)
        return
      }

      // Also check if any notes have been modified recently
      let latestUpdateTime = try getLatestUpdateTime()
      if let latestUpdateTime = latestUpdateTime,
        let latestNoteTime = self.notes.first?.updatedAt,
        latestUpdateTime > latestNoteTime
      {
        await self.refresh(silent: true)
      }
    } catch {
      print("Error checking for database changes: \(error)")
    }
  }

  // Helper method to get the count of notes
  private func getNotesCount() throws -> Int {
    return try sqliteManager.getAllNotes().count
  }

  // Helper method to get the latest update time
  private func getLatestUpdateTime() throws -> Date? {
    let allNotes = try sqliteManager.getAllNotes()
    return allNotes.sorted(by: { $0.updatedAt > $1.updatedAt }).first?.updatedAt
  }

  func subscribe() {
    activeSubscribers += 1
    if activeSubscribers == 1 {
      // First subscriber, ensure data is loaded
      Task {
        await refresh()
      }
    }
  }

  func unsubscribe() {
    activeSubscribers = max(0, activeSubscribers - 1)
  }

  @MainActor
  func refresh(silent: Bool = false) async {
    // Skip if already loading
    if isLoading && !silent { return }

    // Start loading
    if !silent { isLoading = true }
    error = nil

    do {
      // Direct database access
      let fetchedNotes = try sqliteManager.getAllNotes()
      notes = fetchedNotes.sorted(by: { $0.createdAt > $1.createdAt })
      hasLoadedData = true
    } catch {
      self.error = error
      print("Error loading notes: \(error)")
    }

    if !silent { isLoading = false }
  }

  @MainActor
  func addNote(_ note: Note) async {
    do {
      // Direct database access
      try sqliteManager.saveNote(note)
      await refresh()
    } catch {
      self.error = error
      print("Error adding note: \(error)")
    }
  }

  @MainActor
  func updateNote(_ note: Note) async {
    do {
      // Direct database access
      try sqliteManager.updateNote(note)
      await refresh(silent: true)
    } catch {
      self.error = error
      print("Error updating note: \(error)")
    }
  }

  @MainActor
  func deleteNote(id: UUID) async {
    do {
      // Direct database access
      try sqliteManager.deleteNote(id: id)
      await refresh(silent: true)
    } catch {
      self.error = error
      print("Error deleting note: \(error)")
    }
  }

  @MainActor
  func getNote(id: UUID) async -> Note? {
    do {
      return try sqliteManager.getNote(id: id)
    } catch {
      self.error = error
      print("Error fetching note: \(error)")
      return nil
    }
  }
}

// React-like hook for notes data
@MainActor
class MeetingsViewModel: ObservableObject {
  // Published properties that views will observe
  @Published var meetings: [Note] = []
  @Published var selectedMeeting: Note?
  @Published var isLoading = false
  @Published var errorMessage: String?
  @Published var hasLoadedInitialData = false

  // Cancellables for reactive bindings
  private var cancellables = Set<AnyCancellable>()

  init() {
    // Subscribe to the database notifier
    DatabaseNotifier.shared.subscribe()

    // Reactive subscription to database changes
    DatabaseNotifier.shared.$notes
      .sink { [weak self] notes in
        self?.meetings = notes
      }
      .store(in: &cancellables)

    DatabaseNotifier.shared.$isLoading
      .sink { [weak self] isLoading in
        self?.isLoading = isLoading
      }
      .store(in: &cancellables)

    DatabaseNotifier.shared.$error
      .sink { [weak self] error in
        self?.errorMessage = error?.localizedDescription
      }
      .store(in: &cancellables)

    DatabaseNotifier.shared.$hasLoadedData
      .sink { [weak self] hasLoaded in
        self?.hasLoadedInitialData = hasLoaded
      }
      .store(in: &cancellables)
  }

  deinit {
    // Clean up subscription when deallocated
    DatabaseNotifier.shared.unsubscribe()
  }

  func loadMeetings() async {
    await DatabaseNotifier.shared.refresh()
  }

  func selectMeeting(_ meeting: Note) {
    selectedMeeting = meeting
  }

  func deleteMeeting(id: UUID) async {
    await DatabaseNotifier.shared.deleteNote(id: id)
    if selectedMeeting?.id == id {
      selectedMeeting = nil
    }
  }

@MainActor
func updateMeeting(_ updatedNote: Note) async {
    if let index = meetings.firstIndex(where: { $0.id == updatedNote.id }) {
        meetings[index] = updatedNote
    }
}

  // func updateMeeting(_ meeting: Note) async {
  //   await DatabaseNotifier.shared.updateNote(meeting)
  //   if selectedMeeting?.id == meeting.id {
  //     selectedMeeting = meeting
  //   }
  // }
// func updateMeeting(_ updatedNote: Note) async {
//     if let index = meetings.firstIndex(where: { $0.id == updatedNote.id }) {
//         meetings[index] = updatedNote
//     }
// }
}

================
File: ViewModels/RecordingViewModel.swift
================
import AVFoundation
import Foundation
import OpenAI
import Combine
import SwiftUI

@MainActor
class RecordingViewModel: ObservableObject {
    @Published var isRecording = false
    @Published var isProcessing = false
    @Published var processingStatus = ""
    @Published var recordingTimeString = "00:00"
    @Published var audioLevel: Float = 0
    
    // Meeting title, editable by the user.
    @Published var meetingTitle: String = "Untitled Meeting"
    
    // User's manually entered notes.
    @Published var userNotes: String = ""
    // AI-generated meeting summary.
    @Published var aiSummary: String = ""
    
    // The full transcript from recording.
    @Published var finalTranscript = ""
    @Published var isPaused = false
    @Published var meetingNotes: MeetingNotesSchema?
    
    // AI generation status properties
    @Published var isGeneratingAISummary = false
    @Published var generationProgress = "Initializing AI..."
    
    // New property to track if we're waiting for transcription
    @Published var isWaitingForTranscription = false
    @Published var isMicMuted = false
    
    private var recordingManager = RecordingManager.shared
    private var currentRecordingURL: URL?
    private var recordingStartTime: Date?
    private var timer: Timer?
    private var openAI: OpenAI?
    private var cancellables = Set<AnyCancellable>()
    private var openAIManager = OpenAIManager.shared

    init() {
        setupObservers()
        setupOpenAI()
    }
    
    func toggleMicrophoneMute() async {
        isMicMuted.toggle()
        recordingManager.toggleMicrophoneMute(muted: isMicMuted)
    }
    
    private func setupOpenAI() {
        let manager = OpenAIManager.shared
        let provider = manager.selectedProvider
        
        switch provider {
        case .openAI:
            if let apiKey = KeychainManager.getKey(for: APIProvider.openAI.keychainKey) {
                // Standard OpenAI configuration
                openAI = OpenAI(apiToken: apiKey)
                print("Configured client with OpenAI provider")
            } else {
                print("OpenAI API key not found")
            }
            
        case .openRouter:
            if let apiKey = KeychainManager.getKey(for: APIProvider.openRouter.keychainKey),
               let baseURL = URL(string: APIProvider.openRouter.baseURL) {
                // Instead of using the OpenAI client for OpenRouter,
                // we'll use curl for our API call.
                print("Configured for OpenRouter provider (endpoint: \(baseURL))")
                print("Using model: \(manager.openRouterModelName)")
                // Optionally, you could still instantiate a client if needed,
                // but our implementation will use curl.
            } else {
                print("OpenRouter API key not found")
            }
        }
    }
    
    private func setupObservers() {
        recordingManager.$isRecording
            .assign(to: &$isRecording)
        recordingManager.$audioLevel
            .assign(to: &$audioLevel)
        recordingManager.$transcriptions
            .sink { [weak self] transcriptions in
                guard let self = self else { return }
                // Combine all transcription results sorted by timestamp.
                let combinedText = transcriptions.sorted { $0.timestamp < $1.timestamp }
                    .map { $0.text }
                    .joined(separator: " ")
                self.finalTranscript = combinedText
            }
            .store(in: &cancellables)
        recordingManager.recordingFinished = { [weak self] url in
            guard let self = self, let url = url else { return }
            Task { @MainActor in
                self.currentRecordingURL = url
            }
        }
    }
    
    func startRecording() async {
        print("RecordingViewModel: Starting recording...")
        do {
            setupOpenAI()
            // Reset user and AI notes when starting a new session.
            userNotes = ""
            aiSummary = ""
            recordingTimeString = "00:00"
            try await recordingManager.startRecording()
            recordingStartTime = Date()
            startTimer()
            print("RecordingViewModel: Recording started successfully")
        } catch {
            print("RecordingViewModel: Failed to start recording: \(error.localizedDescription)")
        }
    }
    
    func stopRecording() async {
        await recordingManager.stopRecording()
        stopTimer()
    }
    
    // When pausing, finalize the current chunk and update the transcript immediately.
    func pauseRecording() async {
        guard isRecording && !isPaused else { return }
        print("RecordingViewModel: Pausing recording...")
        await recordingManager.pauseRecording()
        
        // Process the last audio chunk to get any remaining transcription
        recordingManager.processLastChunkIfNeeded()
        
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            self.isPaused = true
            self.stopTimer()
            print("RecordingViewModel: Recording paused")
        }
    }
    
    func resumeRecording() async {
        isPaused = false
        await recordingManager.resumeRecording()
        // Update start time so that the timer reflects only active recording time.
        recordingStartTime = Date().addingTimeInterval(-recordingManager.recordingDuration)
        startTimer()
        print("RecordingViewModel: Recording resumed and timer updated")
    }
    
    // New function to stop recording and then generate notes
    func stopRecordingAndGenerateNotes() async {
        print("RecordingViewModel: Stopping recording and generating notes")
        
        // Set waiting for transcription flag
        isWaitingForTranscription = true
        
        // Stop the recording
        await stopRecording()
        
        // Process the last chunk to get any remaining transcription
        recordingManager.processLastChunkIfNeeded()
        
        // Wait for transcription to complete
        let transcriptionSuccess = await waitForTranscription()
        isWaitingForTranscription = false
        
        if transcriptionSuccess {
            print("RecordingViewModel: Successfully captured final transcription")
        } else {
            print("RecordingViewModel: Failed to capture final transcription in time")
        }
        
        // Now generate the meeting notes
        await generateMeetingNotes()
    }
    
    // Generates AI meeting summary based on transcript and user notes.
    func generateMeetingNotes() async {
        print("RecordingViewModel: Enhance with AI button pressed")
        let recordingURL = currentRecordingURL ?? URL(fileURLWithPath: "OngoingRecording")
        
        // Set status as generating and initialize progress
        isGeneratingAISummary = true
        generationProgress = "Initializing AI..."
        
        await processRecordingWithTranscript(recordingURL)
        
        // Reset generation status when done
        isGeneratingAISummary = false
    }
    
    // Helper to combine all transcription results immediately.
    private func updateFinalTranscript() {
        let combinedText = recordingManager.transcriptions.sorted { $0.timestamp < $1.timestamp }
            .map { $0.text }
            .joined(separator: " ")
        self.finalTranscript = combinedText
    }
    
    private func startTimer() {
        print("RecordingViewModel: Starting timer...")
        DispatchQueue.main.async { [weak self] in
            guard let self = self else {
                print("RecordingViewModel: Self is nil when starting timer")
                return
            }
            self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
                self?.updateRecordingTime()
            }
            RunLoop.current.add(self.timer!, forMode: .common)
        }
    }
    
    private func stopTimer() {
        DispatchQueue.main.async { [weak self] in
            self?.timer?.invalidate()
            self?.timer = nil
        }
    }
    
    private func updateRecordingTime() {
        guard let startTime = recordingStartTime else {
            print("RecordingViewModel: Start time is nil when updating recording time")
            return
        }
        let duration = Int(Date().timeIntervalSince(startTime))
        let minutes = duration / 60
        let seconds = duration % 60
        DispatchQueue.main.async { [weak self] in
            self?.recordingTimeString = String(format: "%02d:%02d", minutes, seconds)
        }
    }
    
    // Function to wait for transcription to complete
    private func waitForTranscription() async -> Bool {
        print("RecordingViewModel: Waiting for transcription to complete...")
        
        // No point waiting if there are no audio chunks
        guard recordingManager.currentChunkIndex > 0 else {
            print("RecordingViewModel: No audio chunks to transcribe")
            return false
        }
        
        let initialTranscriptCount = recordingManager.transcriptions.count
        
        // Wait for transcription to complete with timeout
        let startTime = Date()
        let timeoutDuration: TimeInterval = 15.0 // Maximum 15 seconds wait
        let checkInterval: TimeInterval = 0.5 // Check every 0.5 seconds
        
        while Date().timeIntervalSince(startTime) < timeoutDuration {
            // Check if we've received any new transcription
            if recordingManager.transcriptions.count > initialTranscriptCount {
                // Update transcript with the latest data
                updateFinalTranscript()
                print("RecordingViewModel: Transcription completed successfully after \(Date().timeIntervalSince(startTime)) seconds")
                return true
            }
            
            // Wait a bit and check again
            try? await Task.sleep(nanoseconds: UInt64(checkInterval * 1_000_000_000))
            
            // Log every 2 seconds
            if Int(Date().timeIntervalSince(startTime)) % 2 == 0 {
                print("RecordingViewModel: Still waiting for transcription... (\(Int(Date().timeIntervalSince(startTime)))s elapsed)")
            }
        }
        
        print("RecordingViewModel: Transcription timeout after \(timeoutDuration) seconds")
        return false
    }
    
    // New helper function to call OpenRouter API via curl
    private func callOpenRouterUsingCurl(finalPrompt: String, modelName: String) async throws -> String {
        return try await withCheckedThrowingContinuation { continuation in
            DispatchQueue.global(qos: .background).async {
                do {
                    guard let apiKey = KeychainManager.getKey(for: APIProvider.openRouter.keychainKey) else {
                        continuation.resume(throwing: NSError(domain: "RecordingViewModel", code: 1001, userInfo: [NSLocalizedDescriptionKey: "OpenRouter API key not found"]))
                        return
                    }
                    
                    let endpoint = "https://openrouter.ai/api/v1/chat/completions"
                    
                    let messages: [[String: Any]] = [
                        [
                            "role": "system",
                            "content": finalPrompt
                        ],
                        [
                            "role": "user",
                            "content": "Please generate an AI Meeting Summary in Markdown."
                        ]
                    ]
                    
                    let payload: [String: Any] = [
                        "model": modelName,
                        "messages": messages
                    ]
                    
                    let jsonData = try JSONSerialization.data(withJSONObject: payload, options: [])
                    guard let jsonString = String(data: jsonData, encoding: .utf8) else {
                        continuation.resume(throwing: NSError(domain: "RecordingViewModel", code: 1002, userInfo: [NSLocalizedDescriptionKey: "Failed to encode JSON payload"]))
                        return
                    }
                    
                    let curlArgs = [
                        "curl",
                        "--silent",
                        "--show-error",
                        "-X", "POST",
                        endpoint,
                        "-H", "Content-Type: application/json",
                        "-H", "Authorization: Bearer \(apiKey)",
                        "-d", jsonString
                    ]
                    
                    print("RecordingViewModel: Using OpenRouter model \(modelName)")
                    print("Executing curl command:")
                    
                    let process = Process()
                    process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
                    process.arguments = curlArgs
                    
                    let pipe = Pipe()
                    process.standardOutput = pipe
                    process.standardError = pipe
                    
                    try process.run()
                    
                    var didTimeout = false
                    let group = DispatchGroup()
                    group.enter() // timer
                    group.enter() // process wait
                    
                    let timer = DispatchSource.makeTimerSource()
                    timer.schedule(deadline: .now() + 60.0)
                    timer.setEventHandler {
                        print("RecordingViewModel: Curl request timed out")
                        if process.isRunning {
                            process.terminate()
                        }
                        didTimeout = true
                        group.leave()
                    }
                    timer.resume()
                    
                    DispatchQueue.global(qos: .background).async {
                        process.waitUntilExit()
                        timer.cancel()
                        group.leave()
                    }
                    
                    _ = group.wait(timeout: .now() + 31.0)
                    
                    if didTimeout {
                        throw NSError(domain: "RecordingViewModel", code: 1004, userInfo: [NSLocalizedDescriptionKey: "Curl request timed out"])
                    }
                    
                    let data = pipe.fileHandleForReading.readDataToEndOfFile()
                    guard let output = String(data: data, encoding: .utf8) else {
                        throw NSError(domain: "RecordingViewModel", code: 1003, userInfo: [NSLocalizedDescriptionKey: "Failed to read curl output"])
                    }
                    
                    print("RecordingViewModel: API request completed successfully")
                    continuation.resume(returning: output)
                } catch {
                    print("RecordingViewModel: Error executing curl: \(error.localizedDescription)")
                    continuation.resume(throwing: error)
                }
            }
        }
    }
    
    private func processRecordingWithTranscript(_ folderURL: URL) async {
        // Check for transcript text
        let transcriptText = finalTranscript
        if transcriptText.isEmpty {
            processingStatus = "No transcript available"
            isProcessing = false
            isGeneratingAISummary = false
            return
        }
        
        // Use the user's manually entered notes.
        let userNotesText = userNotes
        
        // Update generation progress
        generationProgress = "Analyzing transcript..."
        
        print("RecordingViewModel: Processing recording with transcript and user notes.")
        
        // Revised prompt template.
        let promptTemplate = """
        You are an AI meeting notes assistant specialized in generating concise and structured meeting summaries.
        You are provided with two inputs:
        1. A meeting transcript:
        <transcript>
        {{transcript}}
        </transcript>
        
        2. The user's manually entered meeting notes in Markdown:
        <user_notes>
        {{userNotes}}
        </user_notes>
        
        Generate an **AI Meeting Summary** in Markdown format that highlights key discussion points, decisions, action items, and next steps.
        Do not alter or replace the user's notes; instead, produce a separate summary that takes them into account.
        The output should be a valid Markdown document under a heading "AI Meeting Summary".
        """
        
        // Replace placeholders with actual transcript and user notes.
        let finalPrompt = promptTemplate
            .replacingOccurrences(of: "{{transcript}}", with: transcriptText)
            .replacingOccurrences(of: "{{userNotes}}", with: userNotesText)
        
        print("RecordingViewModel: Final prompt: \(finalPrompt)")
        
        // Select model based on provider.
        let modelName: String
        if openAIManager.selectedProvider == .openRouter && !openAIManager.openRouterModelName.isEmpty {
            modelName = openAIManager.openRouterModelName
            print("RecordingViewModel: Using OpenRouter model \(modelName)")
        } else {
            modelName = openAIManager.selectedModel.model
            print("RecordingViewModel: Using OpenAI model \(modelName)")
        }
        
        // If provider is OpenRouter, use curl; otherwise, use the OpenAI client.
        if openAIManager.selectedProvider == .openRouter {
            generationProgress = "Generating summary with OpenRouter..."
            do {
                let rawResponse = try await callOpenRouterUsingCurl(finalPrompt: finalPrompt, modelName: modelName)
                
                // Attempt to decode the JSON response.
                if let data = rawResponse.data(using: .utf8) {
                    do {
                        if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
                            print("RecordingViewModel: Successfully received OpenRouter response")
                            
                            // Handle both function call responses and plain text responses
                            if let choices = json["choices"] as? [[String: Any]],
                               let firstChoice = choices.first,
                               let message = firstChoice["message"] as? [String: Any] {
                                
                                // First try to extract content directly (most common case)
                                if let content = message["content"] as? String {
                                    print("RecordingViewModel: Extracted content from OpenRouter response")
                                    self.aiSummary = content
                                    print("RecordingViewModel: Generated AI meeting summary successfully")
                                }
                                // If no direct content, check for tool_calls/function_call
                                else if let toolCalls = message["tool_calls"] as? [[String: Any]],
                                      let firstToolCall = toolCalls.first,
                                      let functionData = firstToolCall["function"] as? [String: Any],
                                      let arguments = functionData["arguments"] as? String,
                                      let jsonData = arguments.data(using: .utf8) {
                                    
                                    print("RecordingViewModel: Processing function call from OpenRouter")
                                    
                                    do {
                                        // Try to decode as a dictionary first
                                        if let decodedResponse = try? JSONDecoder().decode([String: String].self, from: jsonData),
                                           let summary = decodedResponse["aiSummary"] {
                                            self.aiSummary = summary
                                            print("RecordingViewModel: Generated AI meeting summary from function call")
                                        } 
                                        // If that fails, use the arguments string directly
                                        else {
                                            print("RecordingViewModel: Using function arguments as summary")
                                            self.aiSummary = arguments
                                        }
                                    } catch {
                                        print("RecordingViewModel: Error processing function arguments: \(error.localizedDescription)")
                                        self.aiSummary = "Error processing AI response. Please try again."
                                    }
                                }
                                // Handle function_call format (alternative format some models use)
                                else if let functionCall = message["function_call"] as? [String: Any],
                                       let arguments = functionCall["arguments"] as? String,
                                       let jsonData = arguments.data(using: .utf8) {
                                    
                                    print("RecordingViewModel: Processing function_call from OpenRouter")
                                    
                                    do {
                                        if let decodedResponse = try? JSONDecoder().decode([String: String].self, from: jsonData),
                                           let summary = decodedResponse["aiSummary"] {
                                            self.aiSummary = summary
                                            print("RecordingViewModel: Generated AI meeting summary from function_call")
                                        } else {
                                            print("RecordingViewModel: Using function_call arguments as summary")
                                            self.aiSummary = arguments
                                        }
                                    } catch {
                                        print("RecordingViewModel: Error processing function_call arguments: \(error.localizedDescription)")
                                        self.aiSummary = "Error processing AI response. Please try again."
                                    }
                                }
                                else {
                                    print("RecordingViewModel: Could not extract content from message: \(message)")
                                    self.aiSummary = "Error processing AI response. Please try again."
                                }
                            } else if let error = json["error"] as? [String: Any], 
                                      let message = error["message"] as? String {
                                print("RecordingViewModel: OpenRouter API error: \(message)")
                                self.aiSummary = "Error from API: \(message)"
                            } else {
                                print("RecordingViewModel: Unexpected JSON structure from OpenRouter")
                                self.aiSummary = "Error: Unexpected response format from API"
                            }
                        }
                    } catch {
                        print("RecordingViewModel: JSON parsing error: \(error.localizedDescription)")
                        self.aiSummary = "Error parsing API response. Please try again."
                    }
                } else {
                    print("RecordingViewModel: Could not convert API response to data")
                    self.aiSummary = "Error processing API response. Please try again."
                }
            } catch {
                print("RecordingViewModel: Failed to call OpenRouter via curl with error: \(error)")
            }
        } else {
            // If using OpenAI client, continue with the existing implementation.
            generationProgress = "Generating summary with OpenAI..."
            guard let openAI = openAI else {
                processingStatus = "Error: OpenAI API key not configured"
                isGeneratingAISummary = false
                return
            }
            do {
                let systemMessage = ChatQuery.ChatCompletionMessageParam.system(.init(content: finalPrompt))
                let userMessage = ChatQuery.ChatCompletionMessageParam.user(.init(content: .init(string: "Please generate an AI Meeting Summary in Markdown.")))
                let messages = [systemMessage, userMessage]
                
                let chatQuery = ChatQuery(
                    messages: messages,
                    model: .init(stringLiteral: modelName)
                    
                )
                
                generationProgress = "Model thinking..."
                let completion = try await openAI.chats(query: chatQuery)
                generationProgress = "Formatting results..."
                print("RecordingViewModel: Received completion response: \(completion)")
                
                if let toolCall = completion.choices.first?.message.toolCalls?.first {
                    print("RecordingViewModel: Tool call found with function name: \(toolCall.function.name)")
                    print("RecordingViewModel: Raw function arguments: \(toolCall.function.arguments)")
                    if toolCall.function.name == MeetingNotesSchema.functionDeclaration.name,
                       let jsonData = toolCall.function.arguments.data(using: .utf8, allowLossyConversion: false) {
                        let decoder = JSONDecoder()
                        do {
                            let decodedResponse = try decoder.decode([String: String].self, from: jsonData)
                            if let summary = decodedResponse["aiSummary"] {
                                self.aiSummary = summary
                                print("RecordingViewModel: Generated AI meeting summary successfully")
                            } else {
                                print("RecordingViewModel: JSON does not contain 'aiSummary' key. Decoded response: \(decodedResponse)")
                            }
                        } catch {
                            print("RecordingViewModel: Failed to decode JSON. Error: \(error)")
                            print("RecordingViewModel: Raw JSON data: \(String(data: jsonData, encoding: .utf8) ?? "nil")")
                        }
                    } else {
                        print("RecordingViewModel: Function call name does not match expected or arguments could not be converted to JSON")
                    }
                }
                else if let content = completion.choices.first?.message.content,
                        let formattedContent = content.string {
                    print("RecordingViewModel: Fallback processed content: \(formattedContent)")
                    self.aiSummary = formattedContent
                }
                else {
                    print("RecordingViewModel: No valid response received from AI")
                }
            } catch {
                print("RecordingViewModel: Failed to process recording with error: \(error)")
                let nsError = error as NSError
                print("RecordingViewModel: Error domain: \(nsError.domain), code: \(nsError.code)")
                print("RecordingViewModel: Error userInfo: \(nsError.userInfo)")
                processingStatus = "Error processing recording: \(error.localizedDescription)"
            }
        }
        isProcessing = false
        
        await createNote(folderURL: folderURL, transcriptText: transcriptText, aiSummary: aiSummary, userNotes: userNotesText)
    }
    
    private func createNote(folderURL: URL, transcriptText: String, aiSummary: String, userNotes: String = "") async {
        // Ensure the userNotes doesn't contain a path or timestamp
        var safeUserNotes = userNotes
        
        // Check if userNotes contains a path or timestamp
        let userNotesContainsPath = userNotes.contains("/Users") && userNotes.contains("Containers")
        let userNotesLooksLikeTimestamp = userNotes.contains("T") && userNotes.contains("Z") && userNotes.count < 30
        
        if userNotesContainsPath || userNotesLooksLikeTimestamp {
            print("WARNING: userNotes contains a path or timestamp. Setting to empty string.")
            safeUserNotes = ""
        }
        
        let noteTitle = meetingTitle.isEmpty ? "Untitled Meeting" : meetingTitle
        
        // Create a new note with the transcript and AI-generated summary
        let note = Note(
            title: noteTitle,
            rawTranscript: transcriptText,
            formattedNotes: aiSummary, // Storing the AI-generated summary
            userNotes: safeUserNotes, // Include user's notes (safely)
            recordingPath: folderURL.path
        )
        
        // Print debug info
        print("Creating new note:")
        print("  Title: \(noteTitle)")
        print("  User Notes: \(safeUserNotes)")
        print("  Recording Path: \(folderURL.path)")
        
        await DatabaseNotifier.shared.addNote(note)
    }
    
    func saveNote() async -> Bool {
        print("RecordingViewModel: Starting saveNote process")
        
        // If a recording is still active, stop it first
        if isRecording {
            print("RecordingViewModel: Recording is active, pausing...")
            await pauseRecording()
            
            // Process the last chunk
            recordingManager.processLastChunkIfNeeded()
            print("RecordingViewModel: Last chunk processing triggered")
            
            // Check if we already have transcriptions
            if !recordingManager.transcriptions.isEmpty {
                updateFinalTranscript()
                print("RecordingViewModel: Using existing transcriptions")
            } else {
                // Need to wait for transcription to complete
                print("RecordingViewModel: No transcriptions yet, waiting for callback...")
                
                // Set a flag to track that we're waiting
                isWaitingForTranscription = true
                
                // Wait for transcription to complete
                let transcriptionSuccess = await waitForTranscription()
                isWaitingForTranscription = false
                
                if transcriptionSuccess {
                    print("RecordingViewModel: Successfully captured final transcription")
                } else {
                    print("RecordingViewModel: Failed to capture final transcription in time")
                }
            }
        }
        
        // Use the current recording URL if available or a default placeholder
        let recordingURL = currentRecordingURL ?? URL(fileURLWithPath: "OngoingRecording")
        
        // Create and save the note in the database asynchronously
        print("RecordingViewModel: Saving note with transcript length: \(finalTranscript.count) characters")
        await createNote(folderURL: recordingURL,
                         transcriptText: finalTranscript,
                         aiSummary: aiSummary,
                         userNotes: userNotes)
        
        return !finalTranscript.isEmpty
    }
}

================
File: Views/LicenseView.swift
================
import SwiftUI

struct LicenseView: View {
  @ObservedObject private var licenseManager = LicenseManager.shared
  @ObservedObject private var trialManager = TrialManager.shared
  @State private var licenseKey: String = ""
  @State private var email: String = ""
  @State private var showSuccessAlert: Bool = false
  @State private var showErrorAlert: Bool = false
  @Environment(\.dismiss) private var dismiss

  var body: some View {
    ZStack(alignment: .topTrailing) {
      content
      closeButton
    }
    // MARK: - Alerts
    .alert("License Verified", isPresented: $showSuccessAlert) {
      Button("OK", role: .cancel) {}
    } message: {
      Text("Your license has been successfully verified. Thank you for purchasing SessionScribe!")
    }
    .alert("Verification Failed", isPresented: $showErrorAlert) {
      Button("OK", role: .cancel) {}
    } message: {
      Text(licenseManager.errorMessage)
    }
  }
}

// MARK: - Main Content + Close Button
extension LicenseView {
  private var content: some View {
    VStack(spacing: 0) {
      ScrollView(.vertical, showsIndicators: true) {
        VStack(spacing: 24) {
          licenseStatusCard
          
          if licenseManager.licenseState != .registered {
            enterLicenseSection
            
            if licenseManager.licenseState == .expired && !trialManager.trialActive {
              trialSection
            }
            
            purchaseSection
          } else {
            deactivateSection
          }
        }
        .padding(.bottom, 24)
      }
    }
    .frame(width: 500)
    .background(AppColors.primaryBackground)
    .cornerRadius(12)
  }
  
  private var closeButton: some View {
    Button(action: { dismiss() }) {
      Image(systemName: "xmark.circle.fill")
        .font(.system(size: 20))
        .foregroundColor(AppColors.textSecondary.opacity(0.7))
    }
    .buttonStyle(PlainButtonStyle())
    .padding(12)
  }
}

// MARK: - License Status Card
extension LicenseView {
  private var licenseStatusCard: some View {
    VStack(alignment: .leading, spacing: 16) {
      HStack {
        Image(systemName: "checkmark.seal.fill")
          .font(.system(size: 18, weight: .semibold))
          .foregroundColor(statusColor)

        Text("License Status")
          .font(.system(size: 16, weight: .bold))
          .foregroundColor(AppColors.textPrimary)
      }

      VStack(spacing: 16) {
        // Status
        HStack(spacing: 12) {
          Text("Status:")
            .font(.system(size: 14))
            .foregroundColor(AppColors.textSecondary)
          
          Spacer()
          StatusBadge(status: licenseManager.licenseState)
        }
        
        if licenseManager.licenseState == .registered {
          Divider()
          licenseKeyRow
          Divider()
          expirationRow
        }
        
        if licenseManager.licenseState == .trial {
          Divider()
          daysRemainingRow
          trialProgressView
        }
      }
      .padding(16)
      .background(AppColors.secondaryBackground)
      .cornerRadius(12)
    }
    .padding(.horizontal, 24)
    .padding(.top, 24)
  }
  
  // MARK: - Sub-chunks within License Status Card
  private var licenseKeyRow: some View {
    HStack(spacing: 12) {
      Text("License Key:")
        .font(.system(size: 14))
        .foregroundColor(AppColors.textSecondary)
      
      Spacer()
      Text(maskedLicenseKey)
        .font(.system(size: 14, weight: .medium))
        .foregroundColor(AppColors.textPrimary)
    }
  }
  
  private var expirationRow: some View {
    HStack(spacing: 12) {
      Text("Expiration:")
        .font(.system(size: 14))
        .foregroundColor(AppColors.textSecondary)
      
      Spacer()
      Text(
        licenseManager.expirationDate != nil
          ? formattedDate(licenseManager.expirationDate!)
          : "Lifetime"
      )
      .font(.system(size: 14, weight: .medium))
      .foregroundColor(AppColors.textPrimary)
    }
  }
  
  private var daysRemainingRow: some View {
    HStack(spacing: 12) {
      Text("Days Remaining:")
        .font(.system(size: 14))
        .foregroundColor(AppColors.textSecondary)
      
      Spacer()
      Text("\(trialManager.daysRemaining)")
        .font(.system(size: 14, weight: .medium))
        .padding(.horizontal, 12)
        .padding(.vertical, 6)
        .background(Capsule().fill(Color.green.opacity(0.15)))
        .foregroundColor(Color.green)
    }
  }
  
  private var trialProgressView: some View {
    VStack(spacing: 4) {
      GeometryReader { geometry in
        ZStack(alignment: .leading) {
          RoundedRectangle(cornerRadius: 2)
            .fill(Color.green.opacity(0.2))
            .frame(width: geometry.size.width, height: 4)
          
          RoundedRectangle(cornerRadius: 2)
            .fill(Color.green)
            .frame(
              width: geometry.size.width * CGFloat(trialManager.trialProgress),
              height: 4
            )
        }
      }
      .frame(height: 4)
      
      Text("Trial period: \(trialManager.daysUsed) of 7 days used")
        .font(.system(size: 12))
        .foregroundColor(AppColors.textSecondary)
    }
  }
}

// MARK: - Enter License Section
extension LicenseView {
  private var enterLicenseSection: some View {
    VStack(alignment: .leading, spacing: 16) {
      HStack {
        Image(systemName: "key.fill")
          .font(.system(size: 18, weight: .semibold))
          .foregroundColor(.blue)
        
        Text("Enter License")
          .font(.system(size: 16, weight: .bold))
          .foregroundColor(AppColors.textPrimary)
      }
      
      VStack(spacing: 16) {
        // License Key Field
        VStack(alignment: .leading, spacing: 6) {
          Text("License Key")
            .font(.system(size: 12, weight: .medium))
            .foregroundColor(AppColors.textSecondary)
          
          TextField("Enter your license key", text: $licenseKey)
            .textFieldStyle(ModernTextFieldStyle())
            .disableAutocorrection(true)
        }
        
        // Email Field
        VStack(alignment: .leading, spacing: 6) {
          Text("Email")
            .font(.system(size: 12, weight: .medium))
            .foregroundColor(AppColors.textSecondary)
          
          TextField("Enter your email address", text: $email)
            .textFieldStyle(ModernTextFieldStyle())
            .disableAutocorrection(true)
        }
        
        // Verify Button
        Button(action: { verifyLicense() }) {
          Text("Verify License")
            .font(.system(size: 14, weight: .bold))
            .frame(maxWidth: .infinity)
            .padding(.vertical, 12)
            .background(
              RoundedRectangle(cornerRadius: 10)
                .fill(
                  licenseKey.isEmpty || email.isEmpty
                  ? Color.blue.opacity(0.5)
                  : Color.blue
                )
            )
            .foregroundColor(.white)
        }
        .disabled(licenseKey.isEmpty || email.isEmpty)
        .buttonStyle(PlainButtonStyle())
      }
      .padding(16)
      .background(AppColors.secondaryBackground)
      .cornerRadius(12)
    }
    .padding(.horizontal, 24)
  }
}

// MARK: - Trial Section
extension LicenseView {
  private var trialSection: some View {
    VStack(alignment: .leading, spacing: 16) {
      HStack {
        Image(systemName: "clock.fill")
          .font(.system(size: 18, weight: .semibold))
          .foregroundColor(.orange)
        
        Text("Start Free Trial")
          .font(.system(size: 16, weight: .bold))
          .foregroundColor(AppColors.textPrimary)
      }
      
      VStack(spacing: 16) {
        Text("Try SessionScribe free for 7 days with full access to all features.")
          .font(.system(size: 14))
          .foregroundColor(AppColors.textSecondary)
          .multilineTextAlignment(.center)
        
        // Email Field
        VStack(alignment: .leading, spacing: 6) {
          Text("Email")
            .font(.system(size: 12, weight: .medium))
            .foregroundColor(AppColors.textSecondary)
          
          TextField("Enter your email address", text: $email)
            .textFieldStyle(ModernTextFieldStyle())
            .disableAutocorrection(true)
        }
        
        // Start Trial Button
        Button(action: { startTrial() }) {
          Text("Start 7-Day Trial")
            .font(.system(size: 14, weight: .bold))
            .frame(maxWidth: .infinity)
            .padding(.vertical, 12)
            .background(
              RoundedRectangle(cornerRadius: 10)
                .fill(email.isEmpty ? .orange.opacity(0.5) : .orange)
            )
            .foregroundColor(.white)
        }
        .disabled(email.isEmpty)
        .buttonStyle(PlainButtonStyle())
      }
      .padding(16)
      .background(AppColors.secondaryBackground)
      .cornerRadius(12)
    }
    .padding(.horizontal, 24)
  }
}

// MARK: - Purchase Section
extension LicenseView {
  private var purchaseSection: some View {
    VStack(alignment: .leading, spacing: 16) {
      HStack {
        Image(systemName: "cart.fill")
          .font(.system(size: 18, weight: .semibold))
          .foregroundColor(.blue)
        
        Text("Purchase License")
          .font(.system(size: 16, weight: .bold))
          .foregroundColor(AppColors.textPrimary)
      }
      
      VStack(spacing: 16) {
        Text("Get unlimited access to all features with a one-time purchase.")
          .font(.system(size: 14))
          .foregroundColor(AppColors.textSecondary)
          .multilineTextAlignment(.center)
        
        // Purchase Button
        Button(action: {
          if let url = URL(string: "https://gum.co/sessionscribe") {
            NSWorkspace.shared.open(url)
          }
        }) {
          HStack {
            Image(systemName: "link")
              .font(.system(size: 14))
            Text("Purchase License")
              .font(.system(size: 14, weight: .bold))
          }
          .frame(maxWidth: .infinity)
          .padding(.vertical, 12)
          .background(
            RoundedRectangle(cornerRadius: 10)
              .fill(Color.blue)
          )
          .foregroundColor(.white)
        }
        .buttonStyle(PlainButtonStyle())
      }
      .padding(16)
      .background(AppColors.secondaryBackground)
      .cornerRadius(12)
    }
    .padding(.horizontal, 24)
  }
}

// MARK: - Deactivate Section
extension LicenseView {
  private var deactivateSection: some View {
    VStack(alignment: .leading, spacing: 16) {
      HStack {
        Image(systemName: "power")
          .font(.system(size: 18, weight: .semibold))
          .foregroundColor(AppColors.error)
        
        Text("License Management")
          .font(.system(size: 16, weight: .bold))
          .foregroundColor(AppColors.textPrimary)
      }
      
      VStack(spacing: 16) {
        Text("If you need to transfer your license to another device, you can deactivate it here.")
          .font(.system(size: 14))
          .foregroundColor(AppColors.textSecondary)
          .multilineTextAlignment(.center)
        
        // Deactivate Button
        Button(action: {
          licenseManager.deactivate()
        }) {
          HStack {
            Image(systemName: "xmark.circle")
              .font(.system(size: 14))
            Text("Deactivate License")
              .font(.system(size: 14, weight: .bold))
          }
          .frame(maxWidth: .infinity)
          .padding(.vertical, 12)
          .background(
            RoundedRectangle(cornerRadius: 10)
              .stroke(AppColors.error, lineWidth: 1)
          )
          .foregroundColor(AppColors.error)
        }
        .buttonStyle(PlainButtonStyle())
      }
      .padding(16)
      .background(AppColors.secondaryBackground)
      .cornerRadius(12)
    }
    .padding(.horizontal, 24)
  }
}

// MARK: - Helper Methods/Properties
extension LicenseView {
  private var statusColor: Color {
    switch licenseManager.licenseState {
    case .registered:
      return AppColors.success
    case .trial:
      return .orange
    case .expired:
      return AppColors.error
    }
  }

  private var maskedLicenseKey: String {
    guard !licenseManager.licenseKey.isEmpty else { return "" }
    let key = licenseManager.licenseKey
    if key.count <= 8 {
      return key
    }
    let prefix = key.prefix(4)
    let suffix = key.suffix(4)
    return "\(prefix)...\(suffix)"
  }

  private func formattedDate(_ date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    formatter.timeStyle = .none
    return formatter.string(from: date)
  }

  private func verifyLicense() {
    licenseManager.verifyLicense(licenseKey: licenseKey, email: email) { result in
      switch result {
      case .success:
        showSuccessAlert = true
      case .failure:
        showErrorAlert = true
      }
    }
  }

  private func startTrial() {
    if licenseManager.tryStartTrial(withEmail: email) {
      // Successfully started trial
    } else {
      licenseManager.errorMessage = "Unable to start trial. You may have already used your trial period."
      showErrorAlert = true
    }
  }
}

// MARK: - Preview
struct LicenseView_Previews: PreviewProvider {
  static var previews: some View {
    LicenseView()
      .preferredColorScheme(.dark)
  }
}

// MARK: - StatusBadge + ModernTextFieldStyle
// If needed, place these in the same file or in separate files that are included.
struct StatusBadge: View {
  let status: LicenseState

  var body: some View {
    Text(statusText)
      .font(.system(size: 12, weight: .bold))
      .padding(.horizontal, 12)
      .padding(.vertical, 6)
      .background(
        Capsule().fill(statusColor.opacity(0.15))
      )
      .foregroundColor(statusColor)
  }

  private var statusText: String {
    switch status {
    case .registered: return "Registered"
    case .trial:      return "Trial"
    case .expired:    return "Expired"
    }
  }

  private var statusColor: Color {
    switch status {
    case .registered: return AppColors.success
    case .trial:      return .orange
    case .expired:    return AppColors.error
    }
  }
}

struct ModernTextFieldStyle: TextFieldStyle {
  func _body(configuration: TextField<Self._Label>) -> some View {
    configuration
      .padding(12)
      .background(
        RoundedRectangle(cornerRadius: 8)
          .fill(AppColors.secondaryBackground.opacity(0.5))
      )
      .overlay(
        RoundedRectangle(cornerRadius: 8)
          .stroke(AppColors.textSecondary.opacity(0.2), lineWidth: 1)
      )
  }
}

================
File: Views/MeetingsListView.swift
================
import SimpleToast
import SwiftUI

struct MeetingsListView: View {
  @ObservedObject var viewModel: MeetingsViewModel
  @ObservedObject var recordingManager: RecordingManager
  @Binding var showRecordingError: Bool
  @Binding var navigateToRecording: Bool
  @State private var selectedMeetingID: UUID?
  @State private var showDeleteConfirmation = false
  @State private var meetingToDelete: UUID?
  @State private var showToast = false  // SimpleToast flag
  @State private var toastMessage = ""  // SimpleToast message
  @StateObject private var openAIManager = OpenAIManager.shared

  // SimpleToast configuration
  private let toastOptions = SimpleToastOptions(
    alignment: .bottom,
    hideAfter: 3,
    animation: .default,
    modifierType: .slide
  )

  var body: some View {
    VStack(spacing: 0) {
      // Header with title and create button
      HStack {
          Text("Meetings")
              .font(.system(size: 28, weight: .semibold))
              .foregroundColor(AppColors.textPrimary)
              .frame(maxWidth: .infinity, alignment: .leading)
              .accessibility(identifier: "meetingsHeaderTitle")
              
          Button(action: {
              if !OpenAIManager.hasValidAPIKey() {
                  // Show toast message when API key is missing
                  toastMessage = "OpenAI API key is required to start a new meeting"
                  showToast = true
              } else {
                  navigateToRecording = true
              }
          }) {
              Label("New Meeting", systemImage: "plus")
                  .symbolRenderingMode(.hierarchical)
          }
          .buttonStyle(AppButton(size: .small))
          .accessibility(identifier: "newMeetingButton")
      }
      .padding(.horizontal, 24)
      .padding(.top, 22) 
      .padding(.bottom, 16)
      .frame(maxWidth: .infinity)
      .background(
          Rectangle()
              .fill(AppColors.systemGray6)
              .shadow(color: AppColors.shadowMedium, radius: 1, x: 0, y: 1)
              .opacity(0.8)
      )

      if viewModel.isLoading && !viewModel.hasLoadedInitialData {
        ProgressView()
          .frame(maxWidth: .infinity, maxHeight: .infinity)
      } else if viewModel.meetings.isEmpty && viewModel.hasLoadedInitialData {
        emptyStateView
      } else if !viewModel.meetings.isEmpty {
        meetingsList
      } else {
        // Show a placeholder while loading initial data
        Color.clear
          .frame(maxWidth: .infinity, maxHeight: .infinity)
      }
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .navigationDestination(for: Note.self) { meeting in
      MeetingView(meeting: meeting)
        .environmentObject(viewModel)
    }
    .alert("Delete Meeting", isPresented: $showDeleteConfirmation) {
      Button("Cancel", role: .cancel) {}
      Button("Delete", role: .destructive) {
        if let id = meetingToDelete {
          Task {
            await viewModel.deleteMeeting(id: id)
          }
        }
      }
    } message: {
      Text("Are you sure you want to delete this meeting? This action cannot be undone.")
    }
    .onAppear {
      // The view model is already subscribed to NotesStore changes
      // We'll still call loadMeetings for an immediate refresh
 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        Task {
            await viewModel.loadMeetings()
        }
    }
    }
    .onChange(of: recordingManager.isRecording) { oldValue, isRecording in
      if !isRecording {
        // Refresh meetings list when recording finishes
        Task {
          await viewModel.loadMeetings()
        }
      }
    }
    .simpleToast(isPresented: $showToast, options: toastOptions) {
      // Customize SimpleToast appearance
      HStack {
        Image(systemName: "exclamationmark.circle.fill")
          .foregroundColor(.white)
        Text(toastMessage)
          .foregroundColor(.white)
          .font(AppFont.body())
      }
      .padding(.horizontal, 16)
      .padding(.vertical, 12)
      .background(AppColors.error.opacity(0.9))
      .cornerRadius(8)
      .padding(.bottom, 16)
      .padding(.horizontal, 8)
    }
  }

  // Empty state view when no meetings exist
  private var emptyStateView: some View {
    VStack(spacing: 20) {
      Image(systemName: "waveform")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 100, height: 100)
        .foregroundColor(AppColors.primaryAccent.opacity(0.6))

      Text("No Meetings Yet")
        .font(AppFont.heading2())

      Text("Start recording your first meeting to get transcription and AI-generated notes")
        .font(AppFont.body())
        .foregroundColor(AppColors.textSecondary)
        .multilineTextAlignment(.center)
        .frame(maxWidth: 400)

      Button("Start New Meeting") {
        if !OpenAIManager.hasValidAPIKey() {
          // Show toast message when API key is missing
          toastMessage = "OpenAI API key is required to start a new meeting"
          showToast = true
        } else {
          navigateToRecording = true
        }
      }
      .buttonStyle(AppButton())
      .padding(.top, 16)
    }
    .padding()
    .frame(maxWidth: .infinity, maxHeight: .infinity)
  }

  // List of meetings
  private var meetingsList: some View {
    List {
      ForEach(viewModel.meetings, id: \.id) { meeting in
        NavigationLink(value: meeting) {
          MeetingRow(meeting: meeting)
        }
        .contextMenu {
          Button(action: {
            meetingToDelete = meeting.id
            showDeleteConfirmation = true
          }) {
            Label("Delete", systemImage: "trash")
          }
        }
      }
    }
    .overlay(
      Group {
        if viewModel.isLoading && viewModel.hasLoadedInitialData {
          ProgressView()
            .frame(width: 50, height: 50)
            .background(Color.secondary.opacity(0.1))
            .cornerRadius(8)
        }
      }
    )
  }
}

// Meeting row component
struct MeetingRow: View {
  let meeting: Note

  var body: some View {
    VStack(alignment: .leading, spacing: 4) {
      Text(meeting.extractedTitle)
        .font(AppFont.bodyBold())
        .foregroundColor(AppColors.textPrimary)

      Text(meeting.createdAt.formatted(date: .abbreviated, time: .shortened))
        .font(AppFont.caption())
        .foregroundColor(AppColors.textSecondary)

      if !meeting.formattedNotes.isEmpty {
        Label("Notes available", systemImage: "doc.text.fill")
          .font(AppFont.caption())
          .foregroundColor(AppColors.primaryAccent)
          .padding(.top, 4)
      }
    }
    .padding(.vertical, 4)
  }
}

#Preview {
  MeetingsListView(
    viewModel: MeetingsViewModel(),
    recordingManager: RecordingManager.shared,
    showRecordingError: .constant(false),
    navigateToRecording: .constant(false)
  )
}

================
File: Views/MeetingView.swift
================
import SwiftUI
import AppKit
import SwiftDown  // Import SwiftDown for the Markdown editor

struct MeetingView: View {
    let meeting: Note
    @State private var markdownText: String
    @State private var aiMarkdownText: String
    @State private var selectedNotesTab: NotesTab = .aiSummary // Default to AI Summary
    @State private var showTranscript = false // Toggle for transcript sheet
    @State private var isEditingTitle = false // Toggle for editing meeting title
    @State private var meetingTitle: String // Title field
    @FocusState private var titleFieldIsFocused: Bool // Focus state for the title field
    @EnvironmentObject var viewModel: MeetingsViewModel // Environment object for meetings
    
    // Custom theme for SwiftDown
    private var customTheme: Theme {
        let themePath = Bundle.main.path(forResource: "my-custom-theme", ofType: "json")
        return Theme(themePath: themePath ?? "")
    }
    
    init(meeting: Note) {
        self.meeting = meeting
        self._meetingTitle = State(initialValue: meeting.title)
        
        // Debug prints
        print("MeetingView init - userNotes: '\(meeting.userNotes)'")
        print("MeetingView init - recordingPath: '\(meeting.recordingPath)'")
        
        // Check for issues where userNotes contains a file path and recordingPath contains a timestamp
        let userNotesContainsPath = meeting.userNotes.contains("/Users") &&
                                    meeting.userNotes.contains("Containers") &&
                                    meeting.userNotes.contains("SessionScribe")
        let timestampPattern = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"
        let recordingPathLooksLikeTimestamp = (meeting.recordingPath.range(of: timestampPattern, options: .regularExpression) != nil) &&
                                               meeting.recordingPath.count < 30
        let userNotesLooksLikeTimestamp = (meeting.userNotes.range(of: timestampPattern, options: .regularExpression) != nil) &&
                                          meeting.userNotes.count < 30
        
        if userNotesContainsPath && recordingPathLooksLikeTimestamp {
            print("Detected swapped fields issue - fixing immediately")
            Task {
                do {
                    try await DatabaseManager.shared.fixSwappedFields(id: meeting.id)
                    print("Successfully triggered fix for swapped fields")
                } catch {
                    print("Error fixing swapped fields: \(error.localizedDescription)")
                    print("Falling back to local fix for display")
                }
            }
            self._markdownText = State(initialValue: "")
        }
        else if userNotesLooksLikeTimestamp {
            print("Detected timestamp in userNotes - fixing immediately")
            Task {
                do {
                    try DatabaseManager.shared.updateUserNotes(id: meeting.id, userNotes: "")
                    print("Successfully cleared timestamp from userNotes")
                } catch {
                    print("Error clearing timestamp from userNotes: \(error.localizedDescription)")
                    print("Falling back to local fix for display")
                }
            }
            self._markdownText = State(initialValue: "")
        }
        else {
            print("Using existing userNotes: '\(meeting.userNotes)'")
            self._markdownText = State(initialValue: meeting.userNotes)
        }
        
        self._aiMarkdownText = State(initialValue: meeting.formattedNotes)
    }
    
    // Function to save user notes
    private func saveUserNotes(_ notes: String) {
        print("Saving user notes: '\(notes)'")
        let notesContainPath = notes.contains("/Users") &&
                               notes.contains("Containers") &&
                               notes.contains("SessionScribe")
        let timestampPattern = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"
        let notesLookLikeTimestamp = (notes.range(of: timestampPattern, options: .regularExpression) != nil) && notes.count < 30
        
        if notesContainPath || notesLookLikeTimestamp {
            print("Not saving notes because they contain a path or timestamp")
            return
        }
        
        print("Notes passed validation, saving to database...")
        Task {
            do {
                try DatabaseManager.shared.updateUserNotes(id: meeting.id, userNotes: notes)
                print("Successfully saved user notes to database")
            } catch {
                print("Error saving user notes: \(error.localizedDescription)")
                DispatchQueue.main.async {
                    self.markdownText = notes
                }
            }
        }
    }
    
    // Function to save the meeting title
    private func saveTitle(_ title: String, completion: @escaping () -> Void = {}) {
        var updatedNote = meeting
        updatedNote.title = title
        updatedNote.updatedAt = Date()
        Task {
            do {
                try await DatabaseManager.shared.updateNote(updatedNote)
                await viewModel.updateMeeting(updatedNote)
                await viewModel.loadMeetings()
                completion()
            } catch {
                print("Error saving title: \(error.localizedDescription)")
            }
        }
    }
    
    // Meeting title view: tapping anywhere outside the TextField will remove focus and trigger a save.
    private var meetingTitleView: some View {
        HStack {
            if isEditingTitle {
                TextField("Meeting Title", text: $meetingTitle, onCommit: {
                    isEditingTitle = false
                    saveTitle(meetingTitle)
                })
                .font(.largeTitle)
                .fontWeight(.bold)
                .textFieldStyle(PlainTextFieldStyle())
                .focused($titleFieldIsFocused)
                .onSubmit {
                    isEditingTitle = false
                    saveTitle(meetingTitle)
                }
                .onChange(of: titleFieldIsFocused) { newValue in
                    if !newValue {
                        isEditingTitle = false
                        saveTitle(meetingTitle)
                    }
                }
            } else {
                Text(meetingTitle)
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .onTapGesture {
                        withAnimation {
                            isEditingTitle = true
                            titleFieldIsFocused = true
                        }
                    }
            }
            Spacer()
        }
    }
    
    var body: some View {
        // Outer container with a tap gesture to dismiss focus
        VStack(alignment: .leading, spacing: 16) {
            // Header
            VStack(alignment: .leading, spacing: 8) {
                meetingTitleView
                
                HStack {
                    Text("Created: \(meeting.createdAt.formatted(date: .abbreviated, time: .shortened))")
                        .font(.subheadline)
                        .foregroundColor(AppColors.textSecondary)
                    
                    Spacer()
                    
                    Button(action: {
                        showTranscript.toggle()
                    }) {
                        Label("View Transcript", systemImage: "doc.plaintext")
                    }
                    .buttonStyle(AppButton(size: .small))
                }
            }
            
            Divider()
            
            // Tabs for switching between user notes and AI summary
            Picker("Notes", selection: $selectedNotesTab) {
                ForEach(NotesTab.allCases) { tab in
                    Text(tab.rawValue).tag(tab)
                }
            }
            .pickerStyle(SegmentedPickerStyle())
            
            if selectedNotesTab == .myNotes {
                SwiftDownEditor(text: $markdownText)
                    .theme(customTheme)
                    .overlay(
                        RoundedRectangle(cornerRadius: 8)
                            .stroke(Color.secondary, lineWidth: 1)
                    )
                    .onAppear {
                        print("My Notes tab selected - displaying userNotes: '\(markdownText)'")
                    }
                    .onChange(of: markdownText) { oldValue, newValue in
                        print("User notes changed from '\(oldValue)' to: '\(newValue)'")
                        saveUserNotes(newValue)
                    }
            } else {
                SwiftDownEditor(text: $aiMarkdownText)
                    .theme(customTheme)
                    .font(.body)
                    .cornerRadius(8)
                    .overlay(
                        RoundedRectangle(cornerRadius: 8)
                            .stroke(Color.secondary, lineWidth: 1)
                    )
                    .onChange(of: aiMarkdownText) { oldValue, newValue in
                        print("AI summary changed")
                    }
            }
        }
        .padding()
        // Add a global tap gesture that dismisses the title field's focus
        .contentShape(Rectangle())
        .onTapGesture {
            // Dismiss focus from the title field if it's active.
            if titleFieldIsFocused {
                titleFieldIsFocused = false
            }
        }
        // Transcript sheet
        .sheet(isPresented: $showTranscript) {
            VStack {
                HStack {
                    Text("Meeting Transcript")
                        .font(.title2)
                        .fontWeight(.bold)
                    Spacer()
                    Button("Done") {
                        showTranscript = false
                    }
                    .buttonStyle(AppButton(size: .small))
                    .padding(.trailing)
                }
                .padding()
                
                ScrollView {
                    Text(meeting.rawTranscript.isEmpty ? "No transcript available" : meeting.rawTranscript)
                        .padding()
                        .frame(maxWidth: .infinity, alignment: .leading)
                }
            }
            .frame(minWidth: 600, minHeight: 400)
        }
        .onAppear {
            print("MeetingView appeared")
        }
        // Save changes on disappear
        .onDisappear {
            print("MeetingView disappearing - saving final user notes: '\(markdownText)'")
            var updatedNote = meeting
            if meetingTitle != meeting.title {
                print("Title changed from '\(meeting.title)' to '\(meetingTitle)'")
                updatedNote.title = meetingTitle
                updatedNote.updatedAt = Date()
            }
            let isRecordingPath = markdownText.contains("/Users") &&
                                  markdownText.contains("Containers") &&
                                  markdownText.contains("SessionScribe")
            let timestampPattern = "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"
            let isTimestamp = (markdownText.range(of: timestampPattern, options: .regularExpression) != nil) &&
                              markdownText.count < 30
            
            if !isRecordingPath && !isTimestamp && !markdownText.isEmpty {
                updatedNote.userNotes = markdownText
                updatedNote.updatedAt = Date()
            } else {
                print("Not updating notes on disappear because they contain a path or timestamp or are empty")
            }
            
            if updatedNote.title != meeting.title ||
               (updatedNote.userNotes != meeting.userNotes && !isRecordingPath && !isTimestamp && !markdownText.isEmpty) {
                Task {
                    do {
                        try DatabaseManager.shared.updateNote(updatedNote)
                        print("Successfully saved changes on disappear")
                        await viewModel.updateMeeting(updatedNote)
                        await viewModel.loadMeetings()
                    } catch {
                        print("Error saving changes on disappear: \(error.localizedDescription)")
                    }
                }
            }
        }
    }
}

// A preview requires proper sample data
#Preview {
    let sampleNote = Note(
        title: "Product Planning Meeting",
        rawTranscript: "This is a sample transcript of the meeting where we discussed product roadmap...",
        formattedNotes: "# Product Planning Meeting\n\n## Key Discussion Points\n\n- Reviewed Q2 timeline\n- Discussed feature prioritization\n- Assigned tasks to team members",
        recordingPath: "/path/to/recording"
    )
    
    return MeetingView(meeting: sampleNote)
        .environmentObject(MeetingsViewModel())
}

================
File: Views/RecordingView.swift
================
import AVFoundation
import SwiftUI
import SimpleToast
import SwiftDown

// MARK: - Enum for Note Types

enum NotesTab: String, CaseIterable, Identifiable {
    case myNotes = "My Notes"
    case aiSummary = "AI Summary"
    
    var id: String { self.rawValue }
}

// MARK: - AI Generation Status View

struct AIGenerationStatusView: View {
    let progress: String
    
    var body: some View {
        HStack(spacing: 12) {
            ProgressView()
                .scaleEffect(0.8)
            Text(progress)
                .font(.footnote)
                .foregroundColor(AppColors.textSecondary)
            Spacer()
        }
        .padding(.horizontal, 16)
        .padding(.vertical, 10)
        .background(
            RoundedRectangle(cornerRadius: 8)
                .fill(Color.secondary.opacity(0.1))
                .shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1)
        )
        .padding(.horizontal)
        .padding(.bottom, 8)
        .transition(.move(edge: .bottom).combined(with: .opacity))
    }
}

// MARK: - Recording View

struct RecordingView: View {
    @StateObject private var viewModel = RecordingViewModel()
    @EnvironmentObject private var navigationState: NavigationState
    @Environment(\.dismiss) private var dismiss
    @EnvironmentObject private var themeManager: ThemeManager
    @State private var showToast = false
    @State private var toastMessage = ""
    @State private var showTranscript = false
    @State private var transcriptPosition: CGPoint = .zero
    @State private var selectedNotesTab: NotesTab = .myNotes
    @State private var isEditingTitle = false
    @FocusState private var titleFieldIsFocused: Bool
    
    // Define your custom theme for the markdown editor
    private var customTheme: Theme {
        let themePath = Bundle.main.path(forResource: "my-custom-theme", ofType: "json")
        return Theme(themePath: themePath ?? "")
    }
    
    // SimpleToast configuration
    private let toastOptions = SimpleToastOptions(
        alignment: .bottom,
        hideAfter: 3,
        animation: .default,
        modifierType: .slide
    )
    
    // Computed binding for the markdown editor
    private var notesEditorBinding: Binding<String> {
        Binding(
            get: {
                selectedNotesTab == .myNotes ? viewModel.userNotes : viewModel.aiSummary
            },
            set: { newValue in
                if selectedNotesTab == .myNotes {
                    viewModel.userNotes = newValue
                } else {
                    viewModel.aiSummary = newValue
                }
            }
        )
    }
    
    // MARK: - Meeting Title View
    private var meetingTitleView: some View {
        HStack {
            if isEditingTitle {
                TextField("Meeting Title", text: $viewModel.meetingTitle, onCommit: {
                    isEditingTitle = false
                })
                .font(.title)
                .textFieldStyle(PlainTextFieldStyle())
                .foregroundColor(AppColors.textPrimary)
                .focused($titleFieldIsFocused)
                .onSubmit { isEditingTitle = false }
                .onChange(of: titleFieldIsFocused) { newValue in
                    if !newValue { isEditingTitle = false }
                }
            } else {
                Text(viewModel.meetingTitle.isEmpty ? "Untitled Meeting" : viewModel.meetingTitle)
                    .font(.title)
                    .foregroundColor(AppColors.textPrimary)
                    .onTapGesture {
                        withAnimation {
                            isEditingTitle = true
                            titleFieldIsFocused = true
                        }
                    }
            }
            Spacer()
        }
        .padding(.horizontal, 16)
        .padding(.vertical, 12)
    }
    
    // MARK: - Markdown Editor View
    private var notesEditorView: some View {
        ZStack {
            SwiftDownEditor(text: notesEditorBinding)
                .theme(customTheme)
                .font(.body)
                .padding(.horizontal, 11)
        }
    }
    
    var body: some View {
        ZStack {
            // Background for the entire view
            AppColors.background
                .ignoresSafeArea(edges: .all)
                .simultaneousGesture(
                    TapGesture().onEnded {
                        titleFieldIsFocused = false
                        #if canImport(UIKit)
                        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
                                                        to: nil, from: nil, for: nil)
                        #elseif canImport(AppKit)
                        NSApp.keyWindow?.makeFirstResponder(nil)
                        #endif
                    }
                )
            
            // Main content stack
            VStack(spacing: 0) {
                
                // Title (centered, but no background "box")
                meetingTitleView
                    .frame(maxWidth: 700)              // Constrain width
                    .padding(.horizontal, 16)
                    .padding(.top, 16)
                    .frame(maxWidth: .infinity)        // Center horizontally
                
                // AI generation / transcript status
                if viewModel.isGeneratingAISummary {
                    AIGenerationStatusView(progress: viewModel.generationProgress)
                        .animation(.easeInOut, value: viewModel.generationProgress)
                }
                if viewModel.isWaitingForTranscription {
                    AIGenerationStatusView(progress: "Finalizing transcript...")
                        .animation(.easeInOut, value: viewModel.isWaitingForTranscription)
                }
                
                // Editor (centered, but no background "box")
                notesEditorView
                    .frame(maxWidth: 700, minHeight: 400)    // Constrain width & height
                    .padding(.horizontal, 16)
                    .padding(.top, 16)
                    .frame(maxWidth: .infinity, alignment: .center)
                    .edgesIgnoringSafeArea(.bottom)
            }
            .background(AppColors.background)
            
            // Floating recording pill at the bottom
            VStack {
                Spacer()
                recordingPill
                    .padding(.bottom, 20)
            }
        }
        .toolbar {
            // Close button
            ToolbarItem(placement: .cancellationAction) {
                Button("Close") {
                    titleFieldIsFocused = false
                    Task { await handleClose() }
                }
                .disabled(viewModel.isProcessing || viewModel.isWaitingForTranscription)
                .foregroundColor(AppColors.textPrimary)
            }
        }
        .onAppear {
            // Hide sidebar when recording view appears and disable sidebar toggle
            navigationState.columnVisibility = .detailOnly
            
            // Adjust the NSWindow appearance on macOS for a seamless toolbar look
            if let window = NSApplication.shared.windows.first {
                window.titlebarAppearsTransparent = true
                window.titleVisibility = .hidden
                window.backgroundColor = NSColor(red: 38/255, green: 38/255, blue: 39/255, alpha: 1.0)
                window.toolbar?.showsBaselineSeparator = false
                if let titlebarView = window.standardWindowButton(.closeButton)?.superview {
                    titlebarView.wantsLayer = true
                    titlebarView.layer?.backgroundColor =
                        NSColor(red: 38/255, green: 38/255, blue: 39/255, alpha: 1.0).cgColor
                }
                
                // Hide sidebar toggle functionality
                NSApp.mainMenu?.items.forEach { menuItem in
                    if menuItem.title == "View" {
                        menuItem.submenu?.items.forEach { subMenuItem in
                            if subMenuItem.title == "Toggle Sidebar" || 
                               subMenuItem.title.contains("Sidebar") ||
                               subMenuItem.action == #selector(NSSplitViewController.toggleSidebar(_:)) {
                                subMenuItem.isHidden = true
                            }
                        }
                    }
                }
            }
            
            if OpenAIManager.hasValidAPIKey() {
                Task { await viewModel.startRecording() }
            } else {
                toastMessage = "OpenAI API key is required to start a new meeting"
                showToast = true
                DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
                    dismiss()
                }
            }
        }
        .onDisappear {
            // Restore sidebar visibility when recording view is dismissed
            navigationState.columnVisibility = .all
            
            // Fully restore window appearance with theme awareness
            DispatchQueue.main.async {
                if let window = NSApplication.shared.windows.first {
                    window.titlebarAppearsTransparent = false
                    window.titleVisibility = .visible
                    window.toolbar?.showsBaselineSeparator = true
                    
                    // Reset background color based on theme
                    let isDarkTheme = themeManager.currentTheme == .dark ||
                        (themeManager.currentTheme == .system && NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua)
                    
                    if isDarkTheme {
                        window.backgroundColor = NSColor(red: 0.15, green: 0.15, blue: 0.15, alpha: 1.0)
                    } else {
                        window.backgroundColor = NSColor(red: 0.95, green: 0.95, blue: 0.97, alpha: 1.0)
                    }
                    
                    // Ensure titlebar matches theme
                    if let titlebarView = window.standardWindowButton(.closeButton)?.superview {
                        titlebarView.wantsLayer = true
                        titlebarView.layer?.backgroundColor = isDarkTheme ?
                            NSColor(red: 0.15, green: 0.15, blue: 0.15, alpha: 1.0).cgColor :
                            NSColor(red: 0.95, green: 0.95, blue: 0.97, alpha: 1.0).cgColor
                    }
                }
                
                // Show sidebar toggle functionality
                NSApp.mainMenu?.items.forEach { menuItem in
                    if menuItem.title == "View" {
                        menuItem.submenu?.items.forEach { subMenuItem in
                            if subMenuItem.title == "Toggle Sidebar" ||
                               subMenuItem.title.contains("Sidebar") ||
                               subMenuItem.action == #selector(NSSplitViewController.toggleSidebar(_:)) {
                                subMenuItem.isHidden = false
                            }
                        }
                    }
                }
            }
            
            if viewModel.isRecording && !viewModel.isWaitingForTranscription {
                Task {
                    await viewModel.stopRecording()
                    await viewModel.saveNote()
                }
            }
        }
        .simpleToast(isPresented: $showToast, options: toastOptions) {
            HStack {
                if toastMessage.contains("Error") || toastMessage.contains("required") {
                    Image(systemName: "exclamationmark.circle.fill")
                        .foregroundColor(.white)
                    Text(toastMessage)
                        .foregroundColor(.white)
                        .font(.body)
                } else {
                    Image(systemName: "checkmark.circle.fill")
                        .foregroundColor(.white)
                    Text(toastMessage)
                        .foregroundColor(.white)
                        .font(.body)
                }
            }
            .padding(.horizontal, 16)
            .padding(.vertical, 12)
            .background(
                toastMessage.contains("Error") || toastMessage.contains("required")
                ? Color.red.opacity(0.9)
                : Color.green.opacity(0.9)
            )
            .cornerRadius(8)
            .padding(.bottom, 16)
            .padding(.horizontal, 8)
        }
        .overlay(
            ZStack {
                if showTranscript {
                    TranscriptOverlayView(
                        transcript: viewModel.finalTranscript.isEmpty ? "No transcript available" : viewModel.finalTranscript,
                        isVisible: $showTranscript,
                        recordingTime: viewModel.recordingTimeString
                    )
                    .transition(.opacity)
                    .zIndex(100)
                }
            }
        )
        .onChange(of: navigationState.columnVisibility) { newVisibility in
            if newVisibility != .detailOnly {
                navigationState.columnVisibility = .detailOnly
            }
        }
    }
    
    // MARK: - Floating Recording Pill
    
    private var recordingPill: some View {
        VStack(spacing: 8) {
            HStack(spacing: 15) {
                // Recording indicator
                Circle()
                    .fill(viewModel.isRecording ? (viewModel.isPaused ? Color.orange : Color.red) : Color.secondary)
                    .frame(width: 12, height: 12)
                    .opacity(viewModel.isRecording ? 1 : 0.5)
                
                // Waveform view
                WaveformView(audioLevel: viewModel.isPaused ? 0 : viewModel.audioLevel)
                    .frame(width: 60, height: 30)
                
                // Recording time
                Text(viewModel.recordingTimeString)
                    .monospacedDigit()
                    .foregroundColor(AppColors.textSecondary)
                
                // Pause/Resume or Record button
                if viewModel.isRecording {
                    Button(action: {
                        titleFieldIsFocused = false
                        Task {
                            if viewModel.isPaused {
                                await viewModel.resumeRecording()
                            } else {
                                await viewModel.pauseRecording()
                            }
                        }
                    }) {
                        Image(systemName: viewModel.isPaused ? "play.circle.fill" : "pause.circle.fill")
                            .font(.title)
                            .foregroundColor(viewModel.isPaused ? .green : .orange)
                    }
                } else {
                    Button(action: {
                        titleFieldIsFocused = false
                        Task {
                            if !OpenAIManager.hasValidAPIKey() {
                                toastMessage = "OpenAI API key is required to start recording"
                                showToast = true
                            } else {
                                await viewModel.startRecording()
                            }
                        }
                    }) {
                        Image(systemName: "record.circle")
                            .font(.title)
                            .foregroundColor(AppColors.accentBlue)
                    }
                }
                
                // Transcript button
                Button(action: {
                    titleFieldIsFocused = false
                    withAnimation(.easeInOut(duration: 0.2)) {
                        showTranscript.toggle()
                    }
                }) {
                    Image(systemName: "captions.bubble.fill")
                        .font(.title3)
                        .foregroundColor(AppColors.accentBlue)
                }
                // Microphone mute button
                Button(action: {
                    titleFieldIsFocused = false
                    Task {
                        await viewModel.toggleMicrophoneMute()
                    }
                }) {
                    Image(systemName: viewModel.isMicMuted ? "mic.slash.fill" : "mic.fill")
                        .font(.title3)
                        .foregroundColor(viewModel.isMicMuted ? .red : AppColors.accentBlue)
                        .padding(8)
                        .background(
                            Circle()
                                .fill(viewModel.isMicMuted ? Color.red.opacity(0.2) : AppColors.accentBlue.opacity(0.1))
                        )
                }
                .buttonStyle(PlainButtonStyle())
                .disabled(!viewModel.isRecording)
                .help(viewModel.isMicMuted ? "Unmute Microphone" : "Mute Microphone")
                
                // Generate notes button
                Button(action: {
                    titleFieldIsFocused = false
                    Task { 
                        if viewModel.isRecording {
                            await viewModel.stopRecordingAndGenerateNotes()
                        } else {
                            await viewModel.generateMeetingNotes()
                        }
                    }
                }) {
                    HStack(spacing: 4) {
                        Image(systemName: "wand.and.stars")
                        if viewModel.isGeneratingAISummary || viewModel.isWaitingForTranscription {
                            ProgressView()
                                .scaleEffect(0.7)
                        } else {
                            Text("Generate")
                                .font(.footnote)
                        }
                    }
                    .padding(.horizontal, 10)
                    .padding(.vertical, 6)
                    .background(Capsule().fill(Color.blue.opacity(0.2)))
                    .foregroundColor(.blue)
                }
                .disabled(viewModel.isGeneratingAISummary || viewModel.isWaitingForTranscription)
            }
            
            // Toggle for switching between My Notes and AI Summary
            if !viewModel.aiSummary.isEmpty {
                HStack(spacing: 8) {
                    Button(action: { selectedNotesTab = .myNotes }) {
                        Text("My Notes")
                            .padding(8)
                            .background(
                                selectedNotesTab == .myNotes
                                ? AppColors.accentBlue.opacity(0.2)
                                : Color.clear
                            )
                            .cornerRadius(8)
                            .foregroundColor(AppColors.textPrimary)
                    }
                    Button(action: { selectedNotesTab = .aiSummary }) {
                        Text("AI Summary")
                            .padding(8)
                            .background(
                                selectedNotesTab == .aiSummary
                                ? AppColors.accentBlue.opacity(0.2)
                                : Color.clear
                            )
                            .cornerRadius(8)
                            .foregroundColor(AppColors.textPrimary)
                    }
                }
            }
        }
        .padding(.horizontal, 20)
        .padding(.vertical, 12)
        .background(
            Capsule()
                .fill(AppColors.systemGray5)
                .shadow(radius: 8)
        )
    }
    
    // MARK: - Helper Functions
    
    private func handleClose() async {
        await MainActor.run {
            toastMessage = "Finishing recording and saving..."
            showToast = true
        }
        
        let success = await viewModel.saveNote()
        
        if viewModel.isRecording {
            await viewModel.stopRecording()
        }
        
        await MainActor.run {
            toastMessage = success ? "Meeting saved with transcript" : "Meeting saved"
            showToast = true
            Task {
                try? await Task.sleep(nanoseconds: 1_500_000_000)
                dismiss()
            }
        }
    }
}

// MARK: - Waveform View

struct WaveformView: View {
    let audioLevel: Float
    
    var body: some View {
        GeometryReader { geometry in
            HStack(spacing: 3) {
                ForEach(0..<10) { i in
                    RoundedRectangle(cornerRadius: 2)
                        .fill(AppColors.accentBlue)
                        .frame(width: 4)
                        .frame(height: geometry.size.height
                               * CGFloat(min(1, audioLevel * Float(i + 1) / 5)))
                }
            }
            .frame(maxHeight: .infinity, alignment: .center)
        }
    }
}

// MARK: - Transcript Overlay View

struct TranscriptOverlayView: View {
    let transcript: String
    @Binding var isVisible: Bool
    let recordingTime: String
    
    var body: some View {
        ZStack {
            // Semi-transparent background
            Color.black.opacity(0.5)
                .ignoresSafeArea()
                .onTapGesture {
                    withAnimation(.easeOut(duration: 0.2)) {
                        isVisible = false
                    }
                }
            
            // Transcript container
            VStack(spacing: 0) {
                // Header
                HStack {
                    Text("Transcript")
                        .font(.headline)
                        .foregroundColor(AppColors.textPrimary)
                    
                    Spacer()
                    
                    Button(action: {
                        withAnimation(.easeOut(duration: 0.2)) {
                            isVisible = false
                        }
                    }) {
                        Image(systemName: "xmark.circle.fill")
                            .font(.title3)
                            .foregroundColor(AppColors.textSecondary)
                    }
                    .buttonStyle(PlainButtonStyle())
                }
                .padding(.horizontal, 16)
                .padding(.vertical, 12)
                .background(AppColors.systemGray6)
                
                Divider()
                
                // Transcript content
                ScrollView {
                    VStack(alignment: .leading, spacing: 16) {
                        if transcript == "No transcript available" {
                            HStack {
                                Spacer()
                                Text(transcript)
                                    .foregroundColor(AppColors.textSecondary)
                                    .padding(.vertical, 40)
                                Spacer()
                            }
                        } else {
                            // Time marker
                            HStack {
                                Text(recordingTime)
                                    .font(.caption)
                                    .foregroundColor(AppColors.textSecondary)
                                    .padding(.vertical, 4)
                                    .padding(.horizontal, 8)
                                    .background(
                                        Capsule()
                                            .fill(AppColors.systemGray5)
                                    )
                                Spacer()
                            }
                            .padding(.top, 8)
                            
                            // Continuous transcript text
                            Text(LocalizedStringKey(transcript))
                                .foregroundColor(AppColors.textPrimary)
                                .padding(.top, 8)
                                .textSelection(.enabled)
                        }
                    }
                    .padding(16)
                }
                
                // Bottom controls
                HStack {
                    Spacer()
                    
                    Button(action: {
                        // Copy transcript to clipboard
                        #if os(macOS)
                        let pasteboard = NSPasteboard.general
                        pasteboard.clearContents()
                        pasteboard.setString(transcript, forType: .string)
                        #else
                        UIPasteboard.general.string = transcript
                        #endif
                    }) {
                        Label("Copy", systemImage: "doc.on.doc")
                            .font(.footnote)
                            .padding(.horizontal, 12)
                            .padding(.vertical, 6)
                            .background(
                                Capsule()
                                    .fill(AppColors.systemGray5)
                            )
                            .foregroundColor(AppColors.textPrimary)
                    }
                    .buttonStyle(PlainButtonStyle())
                    .disabled(transcript == "No transcript available")
                }
                .padding(12)
                .background(AppColors.systemGray6)
            }
            .frame(width: 500, height: 400)
            .background(Color(red: 0.15, green: 0.15, blue: 0.15))
            .cornerRadius(12)
            .shadow(color: Color.black.opacity(0.3), radius: 20, x: 0, y: 10)
        }
    }
}

================
File: Views/StructuredNotesView.swift
================
// import SwiftUI
// import Foundation
//
// struct StructuredNotesView: View {
//     let meetingNotes: MeetingNotesSchema
//
//     var body: some View {
//         ScrollView {
//             VStack(alignment: .leading, spacing: 16) {
//                 // Header
//                 VStack(alignment: .leading, spacing: 4) {
//                     // Get title from first topic or use default
//                     Text(meetingNotes.topics.first?.name ?? "Meeting Notes")
//                         .font(.largeTitle)
//                         .fontWeight(.bold)
//
//                     // Meeting type
//                     HStack {
//                         Label(meetingNotes.meetingType.capitalized, systemImage: "calendar")
//                             .padding(.horizontal, 8)
//                             .padding(.vertical, 4)
//                             .background(Color.blue.opacity(0.1))
//                             .cornerRadius(8)
//                     }
//                     .padding(.top, 4)
//                 }
//                 .padding(.bottom, 8)
//
//                 Divider()
//
//                 // Participants
//                 if !meetingNotes.participants.isEmpty {
//                     Section(header: NoteSectionHeader(title: "Participants", icon: "person.2")) {
//                         LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], spacing: 8) {
//                             ForEach(meetingNotes.participants, id: \.self) { participant in
//                                 Text(participant)
//                                     .padding(.horizontal, 10)
//                                     .padding(.vertical, 5)
//                                     .background(Color.gray.opacity(0.1))
//                                     .cornerRadius(12)
//                             }
//                         }
//                     }
//
//                     Divider()
//                 }
//
//                 // Discussion Topics
//                 Section(header: NoteSectionHeader(title: "Key Discussion Points", icon: "list.bullet")) {
//                     ForEach(Array(meetingNotes.topics.enumerated()), id: \.offset) { index, topic in
//                         TopicView(topic: topic)
//
//                         if index < meetingNotes.topics.count - 1 {
//                             Divider()
//                                 .padding(.vertical, 8)
//                         }
//                     }
//                 }
//
//                 // Decisions
//                 if let decisions = meetingNotes.decisions, !decisions.isEmpty {
//                     Divider()
//
//                     Section(header: NoteSectionHeader(title: "Decisions Made", icon: "checkmark.circle")) {
//                         ForEach(decisions, id: \.self) { decision in
//                             HStack(alignment: .top) {
//                                 Image(systemName: "checkmark.circle.fill")
//                                     .foregroundColor(.green)
//                                     .padding(.top, 2)
//
//                                 Text(decision)
//                             }
//                             .padding(.bottom, 4)
//                         }
//                     }
//                 }
//
//                 // Action Items
//                 if let actionItems = meetingNotes.actionItems, !actionItems.isEmpty {
//                     Divider()
//
//                     Section(header: NoteSectionHeader(title: "Action Items", icon: "arrow.right.circle")) {
//                         ForEach(Array(actionItems.enumerated()), id: \.offset) { index, item in
//                             ActionItemView(item: item)
//
//                             if index < actionItems.count - 1 {
//                                 Divider()
//                                     .padding(.vertical, 4)
//                             }
//                         }
//                     }
//                 }
//
//                 // Next Steps
//                 if let nextSteps = meetingNotes.nextSteps, !nextSteps.isEmpty {
//                     Divider()
//
//                     Section(header: NoteSectionHeader(title: "Next Steps", icon: "arrow.forward.circle")) {
//                         ForEach(nextSteps, id: \.self) { step in
//                             HStack(alignment: .top) {
//                                 Image(systemName: "arrow.right")
//                                     .foregroundColor(.blue)
//                                     .padding(.top, 2)
//
//                                 Text(step)
//                             }
//                             .padding(.bottom, 4)
//                         }
//                     }
//                 }
//
//                 // Pending Decisions
//                 if let pendingDecisions = meetingNotes.pendingDecisions, !pendingDecisions.isEmpty {
//                     Divider()
//
//                     Section(header: NoteSectionHeader(title: "Pending Decisions", icon: "questionmark.circle")) {
//                         ForEach(pendingDecisions, id: \.self) { decision in
//                             HStack(alignment: .top) {
//                                 Image(systemName: "questionmark.circle.fill")
//                                     .foregroundColor(.orange)
//                                     .padding(.top, 2)
//
//                                 Text(decision)
//                             }
//                             .padding(.bottom, 4)
//                         }
//                     }
//                 }
//
//                 // Technical Details
//                 if let technicalDetails = meetingNotes.technicalDetails, !technicalDetails.isEmpty {
//                     Divider()
//
//                     Section(header: NoteSectionHeader(title: "Technical Details", icon: "gear")) {
//                         ForEach(technicalDetails, id: \.self) { detail in
//                             HStack(alignment: .top) {
//                                 Image(systemName: "wrench.fill")
//                                     .foregroundColor(.gray)
//                                     .padding(.top, 2)
//
//                                 Text(detail)
//                             }
//                             .padding(.bottom, 4)
//                         }
//                     }
//                 }
//
//                 // Industry Terms
//                 if let industryTerms = meetingNotes.industryTerms, !industryTerms.isEmpty {
//                     Divider()
//
//                     Section(header: NoteSectionHeader(title: "Industry Terms", icon: "book")) {
//                         ForEach(industryTerms, id: \.self) { term in
//                             HStack(alignment: .top) {
//                                 Image(systemName: "book.fill")
//                                     .foregroundColor(.purple)
//                                     .padding(.top, 2)
//
//                                 Text(term)
//                             }
//                             .padding(.bottom, 4)
//                         }
//                     }
//                 }
//             }
//             .padding()
//         }
//     }
// }
//
// // Helper Views
// struct NoteSectionHeader: View {
//     let title: String
//     let icon: String
//
//     var body: some View {
//         HStack {
//             Label(title, systemImage: icon)
//                 .font(.title2)
//                 .fontWeight(.bold)
//         }
//         .padding(.vertical, 8)
//     }
// }
//
// struct TopicView: View {
//     let topic: MeetingNotesSchema.Topic
//
//     var body: some View {
//         VStack(alignment: .leading, spacing: 8) {
//             Text(topic.name)
//                 .font(.title3)
//                 .fontWeight(.semibold)
//                 .padding(.bottom, 4)
//
//             ForEach(Array(topic.keyPoints.enumerated()), id: \.offset) { index, point in
//                 HStack(alignment: .top) {
//                     Image(systemName: "circle.fill")
//                         .font(.system(size: 8))
//                         .padding(.top, 6)
//
//                     Text(point)
//                         .fontWeight(.medium)
//                 }
//
//                 if index < topic.keyPoints.count - 1 {
//                     Spacer()
//                         .frame(height: 8)
//                 }
//             }
//         }
//     }
// }
//
// struct ActionItemView: View {
//     let item: MeetingNotesSchema.ActionItem
//
//     var body: some View {
//         HStack(alignment: .top) {
//             Image(systemName: "checkmark.square")
//                 .foregroundColor(.blue)
//                 .padding(.top, 2)
//
//             VStack(alignment: .leading, spacing: 4) {
//                 Text(item.description)
//                     .fontWeight(.medium)
//
//                 if let assignee = item.assignee {
//                     HStack {
//                         Image(systemName: "person.fill")
//                             .font(.system(size: 12))
//
//                         Text(assignee)
//                             .font(.subheadline)
//                             .foregroundColor(.secondary)
//                     }
//                 }
//
//                 if let deadline = item.deadline {
//                     HStack {
//                         Image(systemName: "calendar")
//                             .font(.system(size: 12))
//
//                         Text("Due: \(deadline)")
//                             .font(.subheadline)
//                             .foregroundColor(.secondary)
//                     }
//                 }
//             }
//         }
//     }
// }
//
// #Preview {
//     // Sample data for preview
//     let sampleNotes = MeetingNotesSchema(
//         meetingType: "planning",
//         participants: ["John Smith", "Sarah Johnson", "Mike Lee", "Emma Davis", "Alex Wong"],
//         topics: [
//             MeetingNotesSchema.Topic(
//                 name: "Q2 Feature Priorities",
//                 keyPoints: [
//                     "Mobile app redesign is the top priority",
//                     "Need to improve user engagement metrics",
//                     "API performance improvements needed"
//                 ]
//             ),
//             MeetingNotesSchema.Topic(
//                 name: "Resource Allocation",
//                 keyPoints: [
//                     "Need to hire two more frontend developers",
//                     "Current team is overloaded with maintenance tasks"
//                 ]
//             )
//         ],
//         decisions: [
//             "Mobile redesign will start in April",
//             "API performance improvements will be tackled in parallel"
//         ],
//         actionItems: [
//             MeetingNotesSchema.ActionItem(
//                 description: "Create detailed specs for mobile redesign",
//                 assignee: "Sarah Johnson",
//                 deadline: "March 10, 2025"
//             ),
//             MeetingNotesSchema.ActionItem(
//                 description: "Benchmark current API performance",
//                 assignee: "Mike Lee",
//                 deadline: "March 5, 2025"
//             )
//         ],
//         nextSteps: [
//             "Schedule follow-up meeting to review mobile design concepts",
//             "Share resource allocation plan with executive team"
//         ],
//         pendingDecisions: [
//             "Final decision on third-party analytics integration"
//         ],
//         technicalDetails: [
//             "Mobile redesign will require updates to the design system",
//             "API performance improvements will focus on caching and query optimization"
//         ],
//         industryTerms: [
//             "Design System - A collection of reusable components and standards for product design",
//             "API Response Time - The time it takes for an API to respond to a request"
//         ]
//     )
//
//     return StructuredNotesView(meetingNotes: sampleNotes)
// }

================
File: AppDelegate.swift
================
import Cocoa
import SwiftUI
import Combine // Import Combine
import UserNotifications

class AppDelegate: NSObject, NSApplicationDelegate {
    private var dockMenu: NSMenu?
    private let microphoneMonitor = MicrophoneMonitor.shared // Instantiate MicrophoneMonitor
    private var cancellables = Set<AnyCancellable>() // Cancellables for Combine
    private var lastMeetingStatus: String = ""

    func applicationDidFinishLaunching(_ notification: Notification) {
        // Force the application to have a dock icon
        NSApplication.shared.setActivationPolicy(.regular)

        // Ensure the dock icon is visible
        DispatchQueue.main.async {
            // Make sure the app is active
            NSApplication.shared.activate(ignoringOtherApps: true)

            // Force refresh the dock tile
            NSApp.dockTile.display()
        }
        
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
            if let error = error {
                print("Notification permission error: \(error)")
            }
        }

        // Listen for custom dock menu setup
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(setupDockMenu(_:)),
            name: Notification.Name("SetupDockMenu"),
            object: nil
        )

        // Observe microphone activity and update dock badge
        microphoneMonitor.$isMicrophoneActive
            .dropFirst()
            .sink { isActive in
                print("Microphone Active: \(isActive)")
                self.updateDockBadge() // Call updateDockBadge here
            }
            .store(in: &cancellables)

        microphoneMonitor.$meetingStatusText
            .dropFirst()
            .sink { statusText in
                print("Dock Badge Status: \(statusText)")
                self.updateDockBadge() // Call updateDockBadge here as well (though the logic is in updateDockBadge)
            }
            .store(in: &cancellables)
    }

    @objc func setupDockMenu(_ notification: Notification) {
        if let menu = notification.object as? NSMenu {
            self.dockMenu = menu
        }
    }

    // Return the custom dock menu when requested
    func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
        return dockMenu
    }

    // Ensure app stays in dock when windows are closed
    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        return false
    }

    private func updateDockBadge() { // Add this function
        DispatchQueue.main.async {
            NSApp.dockTile.badgeLabel = self.microphoneMonitor.meetingStatusText
            if self.microphoneMonitor.meetingStatusText != self.lastMeetingStatus {
                self.lastMeetingStatus = self.microphoneMonitor.meetingStatusText
                if !self.lastMeetingStatus.isEmpty {
                    let content = UNMutableNotificationContent()
                    content.title = "Session Scribe"
                    content.body = "Status: \(self.lastMeetingStatus)"
                    content.sound = UNNotificationSound.default

                    let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)

                    UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
                }
            }
        }
    }
}

================
File: CLAUDE.md
================
# SessionScribe Development Guide

## Build Commands
- Build/Run: Open in Xcode and press ⌘+R
- Archive: Product > Archive in Xcode
- Clean: Product > Clean Build Folder (⇧⌘K)

## Code Structure
- MVVM Architecture (Models/, ViewModels/, Views/)
- Services: RecordingService, TranscriptionService
- Managers: DatabaseManager, OpenAIManager, PermissionsManager

## Style Guidelines
- Indentation: 2 spaces
- Naming: camelCase for variables/functions, PascalCase for types
- Types: Use explicit type declarations when not obvious
- SwiftUI Views: Extract subviews for reuse and readability
- Extensions: Use for organizing functionality
- Comments: Document complex logic and public interfaces
- Error handling: Use descriptive try/catch with specific error types

## UI Components
- Use AppColors for color palette
- Follow DesignSystem for consistent styling
- Reuse common components (AppButton, CardView, etc.)

## API Integration
- API keys stored securely in Keychain
- Model responses parsed through dedicated model types

================
File: ContentView.swift
================
//
//  ContentView.swift
//  SessionScribe
//
//  Created by Arnav Gosain on 25/02/25.
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

================
File: DesignSystem.swift
================
import SwiftUI

enum AppColors {
  // Base colors
  static let primaryBackground = Color(NSColor.windowBackgroundColor)
  static let secondaryBackground = Color(NSColor.controlBackgroundColor)
  static let tertiaryBackground = Color(NSColor.underPageBackgroundColor)
   static let systemGray6 = Color(NSColor.windowBackgroundColor)
    // static let systemGray5 = Color(NSColor.controlBackgroundColor)

  static let background = Color(red: 38/255, green: 38/255, blue: 39/255)
// static let background = Color(red: 28/255, green: 28/255, blue: 30/255) // #1C1C1E
    static let textPrimary = Color(red: 202/255, green: 202/255, blue: 202/255) // #CACACA
    static let textSecondary = Color(red: 180/255, green: 180/255, blue: 180/255)
    static let systemGray5 = Color(red: 44/255, green: 44/255, blue: 46/255) // Slightly lighter than background

  // Accent colors
  static let primaryAccent = Color.blue
  static let accentBlue = Color.blue
  static let secondaryAccent = Color.blue.opacity(0.8)

  // Text colors
//  static let textPrimary = Color(NSColor.labelColor)
//  static let textSecondary = Color(NSColor.secondaryLabelColor)
  static let textTertiary = Color(NSColor.tertiaryLabelColor)

  // Status colors
  static let success = Color.green
  static let warning = Color.orange
  static let error = Color.red

  // UI element colors
  static let cardBackground = Color(NSColor.controlBackgroundColor)
  static let disabledBackground = Color(NSColor.disabledControlTextColor).opacity(0.3)
  static let textFieldBackground = Color(NSColor.textBackgroundColor)
  static let textFieldBorder = Color(NSColor.separatorColor)
  static let divider = Color(NSColor.separatorColor)

  // Shadows
  static let shadowLight = Color.black.opacity(0.05)
  static let shadowMedium = Color.black.opacity(0.1)
}

enum AppFont {
  static func heading1() -> Font {
    return Font.system(size: 28, weight: .bold, design: .default)
  }

  static func heading2() -> Font {
    return Font.system(size: 22, weight: .semibold, design: .default)
  }

  static func heading3() -> Font {
    return Font.system(size: 18, weight: .semibold, design: .default)
  }

  static func body() -> Font {
    return Font.system(size: 14, weight: .regular, design: .default)
  }

  static func bodyBold() -> Font {
    return Font.system(size: 14, weight: .bold, design: .default)
  }

  static func caption() -> Font {
    return Font.system(size: 12, weight: .regular, design: .default)
  }

  static func captionBold() -> Font {
    return Font.system(size: 12, weight: .bold, design: .default)
  }

  static func small() -> Font {
    return Font.system(size: 10, weight: .regular, design: .default)
  }
}

struct AppButton: ButtonStyle {
  enum ButtonType {
    case primary
    case secondary
    case destructive
    case outline
    case ghost
  }

  let type: ButtonType
  let isFullWidth: Bool
  let isDisabled: Bool
  let size: ButtonSize

  enum ButtonSize {
    case small
    case medium
    case large

    var verticalPadding: CGFloat {
      switch self {
      case .small: return 6
      case .medium: return 10
      case .large: return 14
      }
    }

    var horizontalPadding: CGFloat {
      switch self {
      case .small: return 12
      case .medium: return 20
      case .large: return 28
      }
    }

    var font: Font {
      switch self {
      case .small: return AppFont.caption()
      case .medium: return AppFont.bodyBold()
      case .large: return AppFont.heading3()
      }
    }
  }

  init(
    type: ButtonType = .primary,
    size: ButtonSize = .medium,
    isFullWidth: Bool = false,
    isDisabled: Bool = false
  ) {
    self.type = type
    self.size = size
    self.isFullWidth = isFullWidth
    self.isDisabled = isDisabled
  }

  func makeBody(configuration: Configuration) -> some View {
    let background: Color
    let foreground: Color
    let borderColor: Color

    if isDisabled {
      background = AppColors.disabledBackground
      foreground = AppColors.textSecondary
      borderColor = .clear
    } else {
      switch type {
      case .primary:
        background = AppColors.primaryAccent
        foreground = .white
        borderColor = .clear
      case .secondary:
        background = AppColors.secondaryAccent
        foreground = .white
        borderColor = .clear
      case .destructive:
        background = AppColors.error
        foreground = .white
        borderColor = .clear
      case .outline:
        background = .clear
        foreground = AppColors.primaryAccent
        borderColor = AppColors.primaryAccent
      case .ghost:
        background = .clear
        foreground = AppColors.textPrimary
        borderColor = .clear
      }
    }

    return configuration.label
      .font(size.font)
      .padding(.vertical, size.verticalPadding)
      .padding(.horizontal, size.horizontalPadding)
      .frame(maxWidth: isFullWidth ? .infinity : nil)
      .background(background)
      .foregroundColor(foreground)
      .cornerRadius(8)
      .overlay(
        RoundedRectangle(cornerRadius: 8)
          .stroke(borderColor, lineWidth: type == .outline ? 1 : 0)
      )
      .opacity(configuration.isPressed ? 0.8 : 1.0)
      .scaleEffect(configuration.isPressed ? 0.98 : 1.0)
      .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
  }
}

struct CardView<Content: View>: View {
  let content: Content
  let padding: CGFloat
  let cornerRadius: CGFloat
  let hasShadow: Bool

  init(
    padding: CGFloat = 16,
    cornerRadius: CGFloat = 12,
    hasShadow: Bool = true,
    @ViewBuilder content: () -> Content
  ) {
    self.content = content()
    self.padding = padding
    self.cornerRadius = cornerRadius
    self.hasShadow = hasShadow
  }

  var body: some View {
    content
      .padding(padding)
      .background(AppColors.cardBackground)
      .cornerRadius(cornerRadius)
      .if(hasShadow) { view in
        view.shadow(color: AppColors.shadowLight, radius: 5, x: 0, y: 2)
      }
  }
}

struct AppTextField: View {
  let title: String
  let placeholder: String
  @Binding var text: String
  var isSecure: Bool = false
  var showValidation: Bool = false
  var validationMessage: String = ""
  var isValid: Bool = true
  var icon: String? = nil

  var body: some View {
    VStack(alignment: .leading, spacing: 6) {
      Text(title)
        .font(AppFont.caption())
        .foregroundColor(AppColors.textSecondary)

      HStack(spacing: 8) {
        if let icon = icon {
          Image(systemName: icon)
            .foregroundColor(AppColors.textSecondary)
            .frame(width: 16, height: 16)
        }

        Group {
          if isSecure {
            SecureField(placeholder, text: $text)
          } else {
            TextField(placeholder, text: $text)
          }
        }
        .textFieldStyle(PlainTextFieldStyle())

        if showValidation {
          Image(systemName: isValid ? "checkmark.circle.fill" : "xmark.circle.fill")
            .foregroundColor(isValid ? AppColors.success : AppColors.error)
            .frame(width: 16, height: 16)
        }
      }
      .padding(10)
      .background(AppColors.textFieldBackground)
      .cornerRadius(8)
      .overlay(
        RoundedRectangle(cornerRadius: 8)
          .stroke(
            showValidation
              ? (isValid ? AppColors.success : AppColors.error) : AppColors.textFieldBorder,
            lineWidth: 1)
      )

      if showValidation && !isValid {
        HStack(spacing: 4) {
          Image(systemName: "exclamationmark.triangle.fill")
            .foregroundColor(AppColors.error)
            .font(.system(size: 10))

          Text(validationMessage)
            .font(AppFont.caption())
            .foregroundColor(AppColors.error)
        }
        .padding(.top, 4)
      }
    }
  }
}

// MARK: - View Extensions
extension View {
  @ViewBuilder
  func `if`<Transform: View>(_ condition: Bool, transform: (Self) -> Transform) -> some View {
    if condition {
      transform(self)
    } else {
      self
    }
  }
}

// Custom window style not needed - we'll use the standard SwiftUI window customization instead

================
File: Info.plist
================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>NSMicrophoneUsageDescription</key>
	<string>SessionScribe uses your microphone to transcribe what you say during meetings</string>
	<key>NSScreenCaptureUsageDescription</key>
	<string>SessionScribe needs screen recording permission to capture system audio from your meeting applications</string>
	<key>LSUIElement</key>
	<false/>
	<key>CFBundleIconFile</key>
	<string>MyAppIcon.icns</string>
	<key>NSRequiresAquaSystemAppearance</key>
	<false/>
	<key>LSBackgroundOnly</key>
	<false/>
</dict>
</plist>

================
File: KeychainManager.swift
================
import Foundation
import Security

class KeychainManager {
    enum KeychainError: Error {
        case duplicateEntry
        case unknown(OSStatus)
        case dataConversionError
        case itemNotFound
    }
    
    // Generic key saving function
    static func saveKey(_ key: String, for account: String) -> Bool {
        guard let data = key.data(using: .utf8) else { return false }
        
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: "SessionScribe",
            kSecAttrAccount as String: account,
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
        ]
        
        // First attempt to delete any existing item
        SecItemDelete(query as CFDictionary)
        
        // Add the new item
        let status = SecItemAdd(query as CFDictionary, nil)
        return status == errSecSuccess
    }
    
    // Generic key retrieval function
    static func getKey(for account: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: "SessionScribe",
            kSecAttrAccount as String: account,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        
        var dataTypeRef: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
        
        if status == errSecSuccess, let data = dataTypeRef as? Data {
            return String(data: data, encoding: .utf8)
        } else {
            return nil
        }
    }
    
    // Generic key deletion function
    static func deleteKey(for account: String) -> Bool {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: "SessionScribe",
            kSecAttrAccount as String: account
        ]
        
        let status = SecItemDelete(query as CFDictionary)
        return status == errSecSuccess
    }
    
    // Legacy functions for backward compatibility
    static func saveAPIKey(_ apiKey: String) -> Bool {
        return saveKey(apiKey, for: "OpenAIAPIKey")
    }
    
    static func getAPIKey() -> String? {
        return getKey(for: "OpenAIAPIKey")
    }
    
    static func deleteAPIKey() -> Bool {
        return deleteKey(for: "OpenAIAPIKey")
    }
}

================
File: LicenseManager.swift
================
import CryptoKit
import Foundation
import Security
import Combine

// Define the LicenseState enum
enum LicenseState {
    case registered
    case trial
    case expired
}

// Define the LicenseManager class
class LicenseManager: ObservableObject {
    static let shared = LicenseManager()
    
    @Published var licenseState: LicenseState = .expired
    @Published var licenseKey: String = ""
    @Published var errorMessage: String = ""
    @Published var expirationDate: Date? = nil
    
    private let trialManager = TrialManager.shared
    
    init() {
        // Check if there's a valid license
        loadLicenseState()
    }
    
    private func loadLicenseState() {
        // Make sure we're on the main thread when updating @Published properties
        guard Thread.isMainThread else {
            DispatchQueue.main.async { [weak self] in
                self?.loadLicenseState()
            }
            return
        }
        
        // Check if there's a valid license key
        if let storedKey = UserDefaults.standard.string(forKey: "licenseKey"), !storedKey.isEmpty {
            licenseKey = storedKey
            licenseState = .registered
            
            // Load expiration date if available
            if let expirationTimeInterval = UserDefaults.standard.object(forKey: "licenseExpirationDate") as? TimeInterval {
                expirationDate = Date(timeIntervalSince1970: expirationTimeInterval)
            }
        } else if trialManager.trialActive {
            licenseState = .trial
        } else {
            licenseState = .expired
        }
    }
    
    func verifyLicense(licenseKey: String, email: String, completion: @escaping (Result<Void, Error>) -> Void) {
        // In a real app, you would make an API call to validate the license
        // For demo purposes, we'll just check if the license key is valid format
        
        guard !licenseKey.isEmpty, !email.isEmpty else {
            errorMessage = "License key and email are required"
            completion(.failure(NSError(domain: "LicenseError", code: 1, userInfo: [NSLocalizedDescriptionKey: errorMessage])))
            return
        }
        
        // Simple validation for demo purposes
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            // Check if license key looks like a valid format (just for demo)
            if licenseKey.count >= 16 && licenseKey.contains("-") {
                self.licenseKey = licenseKey
                self.licenseState = .registered
                
                // Save the license key
                UserDefaults.standard.set(licenseKey, forKey: "licenseKey")
                
                // Set expiration date (1 year from now for demo)
                let oneYearFromNow = Date().addingTimeInterval(365 * 24 * 60 * 60)
                self.expirationDate = oneYearFromNow
                UserDefaults.standard.set(oneYearFromNow.timeIntervalSince1970, forKey: "licenseExpirationDate")
                
                // Record purchase in trial manager
                self.trialManager.recordPurchase()
                
                completion(.success(()))
            } else {
                self.errorMessage = "Invalid license key format. Please enter a valid license key."
                completion(.failure(NSError(domain: "LicenseError", code: 2, userInfo: [NSLocalizedDescriptionKey: self.errorMessage])))
            }
        }
    }
    
    func deactivate() {
        // Make sure we're on the main thread when updating @Published properties
        guard Thread.isMainThread else {
            DispatchQueue.main.async { [weak self] in
                self?.deactivate()
            }
            return
        }
        
        // Remove license key
        licenseKey = ""
        UserDefaults.standard.removeObject(forKey: "licenseKey")
        UserDefaults.standard.removeObject(forKey: "licenseExpirationDate")
        
        // Update license state
        if trialManager.trialActive {
            licenseState = .trial
        } else {
            licenseState = .expired
        }
    }
    
    func tryStartTrial(withEmail email: String) -> Bool {
        let result = trialManager.activateTrial(withEmail: email)
        if result {
            // Update license state if trial was successfully activated
            // Ensure we're on the main thread
            if Thread.isMainThread {
                loadLicenseState()
            } else {
                DispatchQueue.main.async { [weak self] in
                    self?.loadLicenseState()
                }
            }
        }
        return result
    }
    
    func restoreLicense() async {
        // Reload the license state from UserDefaults on the main thread
        await MainActor.run {
            loadLicenseState()
        }
    }
}

================
File: MicrophoneMonitor.swift
================
import AVFoundation
import Combine
import AppKit

class MicrophoneMonitor: NSObject, ObservableObject {
    static let shared = MicrophoneMonitor()

    @Published var isMicrophoneActive: Bool = false
    @Published var isRecordingSessionActive: Bool = false // Example for recording app specific activity
    @Published var meetingStatusText: String = ""

    private var audioEngine = AVAudioEngine()
    private var inputNode: AVAudioInputNode!
    private var mixerNode: AVAudioMixerNode!
    private var cancellables = Set<AnyCancellable>()

    override private init() {
        super.init()
        setupAudioEngine()
        startMonitoring()
    }

    deinit {
        stopMonitoring()
    }

private func setupAudioSession() throws {
    print("RecordingService: Setting up audio session with echo cancellation...")
    
    let audioSession = AVAudioSession.sharedInstance()
    
    // Configure the audio session for recording with echo cancellation
    try audioSession.setCategory(.playAndRecord, 
                                mode: .videoChat, // This mode activates echo cancellation
                                options: [.allowBluetoothA2DP, .defaultToSpeaker])
    
    // Set preferred sample rate and I/O buffer duration
    try audioSession.setPreferredSampleRate(48000)
    try audioSession.setPreferredIOBufferDuration(0.005) // 5ms buffer for lower latency
    
    // Activate the audio session
    try audioSession.setActive(true)
    
    print("RecordingService: Audio session configured with echo cancellation")
}

    private func startMonitoring() {
    }

    private func stopMonitoring() {
        audioEngine.stop()
        mixerNode.removeTap(onBus: 0)
        cancellables.forEach { $0.cancel() }
        cancellables.removeAll()
    }

    private func audioLevel(from buffer: AVAudioPCMBuffer) -> Float {
        guard let channelData = buffer.floatChannelData else { return -100 } // Return a very low level if no data

        let channelDataRange = 0..<Int(buffer.frameLength)
        let rms = channelDataRange.reduce(0.0) { sum, frame in
            let sampleValue = channelData[0][frame]
            return sum + Double(sampleValue) * Double(sampleValue)
        }

        let rootMeanSquare = Float(sqrt(rms / Double(buffer.frameLength)))
        let decibels = 20 * log10f(rootMeanSquare)
        return decibels
    }

    private func updateDockBadge() {
        if isMicrophoneActive {
            if isRecordingSessionActive {
                meetingStatusText = "REC"
            } else {
                meetingStatusText = "MIC"
            }
        } else {
            meetingStatusText = ""
        }
        DispatchQueue.main.async {
            NSApp.dockTile.badgeLabel = self.meetingStatusText
        }
    }

    // Example function to set recording session active status (you'll integrate your recording logic here)
    func setRecordingActive(_ isActive: Bool) {
        isRecordingSessionActive = isActive
        updateDockBadge()
    }
}

================
File: my-custom-theme.json
================
{
    "name": "Default Dark",
    "author": {
        "name": "Quentin Eude",
        "email": "quentineude@gmail.com"
    },
    "version": "1.0",
    "editor": {
        "backgroundColor": "#262627",
        "tintColor": "#A1A8B5",
        "cursorColor": "#A1A8B5"
    },
    "styles": {
        "h1": {
            "color": "#D36770",
            "size": 23
        },
        "h2": {
            "color": "#D36770",
            "size": 20
        },
        "h3": {
            "color": "#D36770",
            "size": 18
        },
        "h4": {
            "color": "#D36770"
        },
        "h5": {
            "color": "#D36770"
        },
        "h6": {
            "color": "#D36770"
        },
        "body": {
            "font": "Menlo-Regular",
            "size": 15,
            "color": "#A1A8B5"
        },
        "bold": {
            "font": "Menlo-Bold",
            "color": "#D19A66"
        },
        "italic": {
            "font": "Menlo-Italic",
            "color": "#C678DD"
        },
        "link": {
            "color": "#A66BFF"
        },
        "image": {
            "color": "#A66BFF"
        },
        "inlineCode": {
            "color": "#A7E3A6"
        },
        "codeBlock": {
            "color": "#A7E3A6"
        },
        "blockQuote": {
            "color": "#1CBD47"
        },
        "list": {
            "color": "#A7E3A6"
        }
    }
}

================
File: OnboardingCoordinator.swift
================
import SwiftUI

enum OnboardingStep: Int, CaseIterable {
  case welcome
  case permissions
  case apiKey
  case license
  case completion

  var title: String {
    switch self {
    case .welcome: return "Welcome to SessionScribe"
    case .permissions: return "Required Permissions"
    case .apiKey: return "OpenAI API Key"
    case .license: return "License Information"
    case .completion: return "You're All Set!"
    }
  }
}

class OnboardingCoordinator: ObservableObject {
  @Published var currentStep: OnboardingStep = .welcome
  @Published var onboardingComplete: Bool = false

  // User preferences
  @Published var apiKey: String = ""
  @Published var dataStoragePath: URL? = nil

  // Permission states from managers
  @Published var permissionsManager = PermissionsManager()
  @Published var trialManager = TrialManager.shared

  // Validation tracking
  @Published var apiKeyValidated: Bool = false
  @Published var isValidatingAPIKey: Bool = false
  @Published var apiKeyErrorMessage: String = ""

  // Storage selection
  @Published var showStorageLocationPicker: Bool = false

  // License and trial properties
  @Published var licenseKey: String = ""
  @Published var trialEmail: String = ""
  @Published var hasAcceptedTerms: Bool = false
  @Published var showTrialForm: Bool = false
  @Published var trialErrorMessage: String = ""

  // Add isOnboardingComplete property if it doesn't exist
  @Published var isOnboardingComplete: Bool = false

  init() {
    // Load API key if it exists
    if let savedKey = KeychainManager.getAPIKey() {
      self.apiKey = savedKey
      self.apiKeyValidated = true
    }

    // Load license key if exists
    self.licenseKey = trialManager.licenseKey

    // Load trial email if exists
    self.trialEmail = trialManager.trialEmail

    // Set up default storage path
    loadStoragePath()

    // Check permissions status
    permissionsManager.checkPermissionStatus()
  }

  func moveToNextStep() {
    let currentIndex = OnboardingStep.allCases.firstIndex(of: currentStep) ?? 0
    let nextIndex = currentIndex + 1

    if nextIndex < OnboardingStep.allCases.count {
      currentStep = OnboardingStep.allCases[nextIndex]
    } else {
      // Onboarding is complete
      completeOnboarding()
    }
  }

  func moveToPreviousStep() {
    let allSteps = OnboardingStep.allCases
    if let currentIndex = allSteps.firstIndex(of: currentStep),
      currentIndex > 0
    {
      currentStep = allSteps[currentIndex - 1]
    }
  }

  func skipToStep(_ step: OnboardingStep) {
    currentStep = step
  }

  func requestPermissions(completion: @escaping (Bool) -> Void) {
    var micPermissionGranted = false
    var screenCapturePermissionGranted = false

    let group = DispatchGroup()

    group.enter()
    permissionsManager.requestMicrophonePermission { granted in
      micPermissionGranted = granted
      group.leave()
    }

    group.enter()
    permissionsManager.requestScreenCapturePermission { granted in
      screenCapturePermissionGranted = granted
      // Mark screen recording permission as previously granted if successful
      if granted {
        UserDefaults.standard.set(true, forKey: "screenPermissionPreviouslyGranted")
      }
      group.leave()
    }

    group.notify(queue: .main) {
      let allPermissionsGranted = micPermissionGranted && screenCapturePermissionGranted
      completion(allPermissionsGranted)
    }
  }

  func validateAPIKey(completion: @escaping (Bool) -> Void) {
    guard !apiKey.isEmpty else {
      self.apiKeyErrorMessage = "API key cannot be empty"
      self.apiKeyValidated = false
      completion(false)
      return
    }

    self.isValidatingAPIKey = true

    // Simple validation for now - just check if it looks like an OpenAI key
    // In a real app, you'd make an API call to validate
    if apiKey.hasPrefix("sk-") && apiKey.count > 40 {
      DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        self.apiKeyValidated = true
        self.isValidatingAPIKey = false
        self.apiKeyErrorMessage = ""
        completion(true)
      }
    } else {
      DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        self.apiKeyValidated = false
        self.isValidatingAPIKey = false
        self.apiKeyErrorMessage =
          "Invalid OpenAI API key format. Must start with 'sk-' and be at least 40 characters."
        completion(false)
      }
    }
  }

  func selectStorageLocation() {
    let openPanel = NSOpenPanel()
    openPanel.title = "Select Storage Location"
    openPanel.showsHiddenFiles = false
    openPanel.canChooseDirectories = true
    openPanel.canCreateDirectories = true
    openPanel.allowsMultipleSelection = false
    openPanel.canChooseFiles = false

    if openPanel.runModal() == .OK {
      self.dataStoragePath = openPanel.url
    }
  }

  func validateLicenseKey(completion: @escaping (Bool) -> Void) {
    trialManager.licenseKey = self.licenseKey
    trialManager.validateLicenseKey(completion: completion)
  }

  func activateTrial(completion: @escaping (Bool) -> Void) {
    guard isValidEmail(trialEmail) else {
      trialErrorMessage = "Please enter a valid email address"
      completion(false)
      return
    }

    if trialManager.activateTrial(withEmail: trialEmail) {
      trialErrorMessage = ""
      completion(true)
    } else {
      trialErrorMessage = "Failed to activate trial. You may have already used your trial."
      completion(false)
    }
  }

  // Helper to validate email
  private func isValidEmail(_ email: String) -> Bool {
    let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
    let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx)
    return emailPred.evaluate(with: email)
  }

  func startTrial() {
    // This old method is now replaced with activateTrial(completion:)
    // Keep it for backwards compatibility
    _ = trialManager.activateTrial(withEmail: trialEmail)
  }

  func completeOnboarding() {
    // Save API key to keychain if provided
    if !apiKey.isEmpty {
      _ = KeychainManager.saveAPIKey(apiKey)
    }

    // Ensure we have a storage path set
    if dataStoragePath == nil {
      loadStoragePath()
    }

    // Save the storage path to UserDefaults
    if let storagePath = dataStoragePath {
      UserDefaults.standard.set(storagePath.path, forKey: "dataStoragePath")
    }

    // Mark onboarding as complete
    UserDefaults.standard.set(true, forKey: "onboardingComplete")

    // Set our published property to trigger UI updates
    isOnboardingComplete = true
    onboardingComplete = true
  }

  // Check if user should see onboarding
  static func shouldShowOnboarding() -> Bool {
    return !UserDefaults.standard.bool(forKey: "onboardingComplete")
  }

  // MARK: - Public API

  func reset() {
    currentStep = .welcome
    apiKey = ""
    loadStoragePath()
    apiKeyValidated = false
    onboardingComplete = false
    UserDefaults.standard.set(false, forKey: "onboardingComplete")
  }

  func prepareForReturningUser() {
    // Load previous settings if available
    if let apiKey = KeychainManager.getAPIKey() {
      self.apiKey = apiKey
      self.apiKeyValidated = true
    }

    loadStoragePath()
  }

  private func loadStoragePath() {
    // Set default storage path to Documents/SessionScribe
    if let documentsDirectory = try? FileManager.default.url(
      for: .documentDirectory,
      in: .userDomainMask,
      appropriateFor: nil,
      create: true
    ) {
      let defaultPath = documentsDirectory.appendingPathComponent("SessionScribe")
      try? FileManager.default.createDirectory(at: defaultPath, withIntermediateDirectories: true)
      UserDefaults.standard.set(defaultPath.path, forKey: "dataStoragePath")
      self.dataStoragePath = defaultPath
    }

    // Load from user defaults if available
    if let storagePath = UserDefaults.standard.string(forKey: "dataStoragePath") {
      self.dataStoragePath = URL(fileURLWithPath: storagePath)
    }
  }
}

================
File: OnboardingStepViews.swift
================
import SwiftUI
import SimpleToast

// Step 1: Welcome Step
struct WelcomeStepView: View {
  @ObservedObject var coordinator: OnboardingCoordinator

  var body: some View {
    VStack(spacing: 30) {
      Image(systemName: "waveform.circle.fill")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 100, height: 100)
        .foregroundColor(AppColors.primaryAccent)

      Text("Welcome to SessionScribe")
        .font(AppFont.heading1())

      Text(
        "Your AI-powered meeting assistant that helps you capture, transcribe, and summarize your meetings."
      )
      .font(AppFont.body())
      .multilineTextAlignment(.center)
      .foregroundColor(AppColors.textSecondary)
      .frame(maxWidth: 450)

      Button("Get Started") {
        coordinator.moveToNextStep()
      }
      .buttonStyle(AppButton())
      .padding(.top, 20)
    }
    .padding(40)
    .frame(maxWidth: .infinity, maxHeight: .infinity)
  }
}

// Helper for Welcome Step
struct FeatureItem: View {
  let icon: String
  let title: String
  let description: String

  var body: some View {
    HStack(alignment: .top, spacing: 16) {
      Image(systemName: icon)
        .font(.system(size: 20))
        .frame(width: 24, height: 24)
        .foregroundStyle(.blue)

      VStack(alignment: .leading, spacing: 4) {
        Text(title)
          .font(.system(size: 15, weight: .semibold))

        Text(description)
          .font(.system(size: 13))
          .foregroundColor(.secondary)
      }
    }
  }
}

// Step 2: Permissions Step
struct PermissionsStepView: View {
  @ObservedObject var permissionsManager: PermissionsManager
  let requestPermissions: (@escaping (Bool) -> Void) -> Void
  @State private var isRequestingMic = false
  @State private var isRequestingScreen = false

  var body: some View {
    VStack(spacing: 24) {
      Image(systemName: "checkmark.shield")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 48, height: 48)
        .foregroundStyle(.blue)

      Text(
        "SessionScribe needs access to your microphone and system audio to record and transcribe meetings."
      )
      .font(.system(size: 13))
      .foregroundColor(.secondary)
      .multilineTextAlignment(.center)
      .padding(.bottom)

      VStack(spacing: 16) {
        PermissionItem(
          icon: "mic",
          title: "Microphone Access",
          description: "Required to capture your voice during meetings",
          isGranted: permissionsManager.microphonePermissionGranted,
          isRequesting: isRequestingMic,
          onRequest: {
            isRequestingMic = true
            permissionsManager.requestMicrophonePermission { granted in
              isRequestingMic = false
            }
          }
        )

        PermissionItem(
          icon: "display",
          title: "Screen Recording Permission",
          description: "Required to capture system audio from meeting apps",
          isGranted: permissionsManager.screenCapturePermissionGranted,
          isRequesting: isRequestingScreen,
          onRequest: {
            isRequestingScreen = true
            permissionsManager.requestScreenCapturePermission { granted in
              isRequestingScreen = false
            }
          }
        )
      }
      .padding()

      if permissionsManager.microphonePermissionGranted
        && permissionsManager.screenCapturePermissionGranted
      {
        Label("All permissions granted", systemImage: "checkmark.circle.fill")
          .foregroundStyle(.green)
          .font(.system(size: 13))
      } else if isRequestingMic || isRequestingScreen {
        Text("Requesting permissions...")
          .font(.system(size: 13))
          .foregroundColor(.secondary)
      } else if permissionsManager.microphonePermissionGranted
        || permissionsManager.screenCapturePermissionGranted
      {
        Text("Click on each permission above to grant access")
          .font(.system(size: 13))
          .foregroundColor(.orange)
      }

      if !permissionsManager.microphonePermissionGranted
        || !permissionsManager.screenCapturePermissionGranted
      {
        Button("Open System Settings") {
          permissionsManager.openSystemPreferences()
        }
        .buttonStyle(.plain)
        .font(.system(size: 13))
        .padding(.top)
      }
    }
  }
}

// Helper for Permissions Step
struct PermissionItem: View {
  let icon: String
  let title: String
  let description: String
  let isGranted: Bool
  let isRequesting: Bool
  let onRequest: () -> Void

  var body: some View {
    Button(action: {
      if !isGranted && !isRequesting {
        onRequest()
      }
    }) {
      HStack(spacing: 16) {
        Image(systemName: icon)
          .font(.system(size: 20))
          .frame(width: 32, height: 32)
          .foregroundStyle(isGranted ? .green : .blue)

        VStack(alignment: .leading, spacing: 4) {
          Text(title)
            .font(.system(size: 13, weight: .semibold))

          Text(description)
            .font(.system(size: 12))
            .foregroundColor(.secondary)
        }

        Spacer()

        if isRequesting {
          ProgressView()
            .scaleEffect(0.8)
            .controlSize(.small)
        } else {
          Image(systemName: isGranted ? "checkmark.circle.fill" : "xmark.circle.fill")
            .foregroundStyle(isGranted ? .green : .red)
        }
      }
      .padding()
      .background(Color(NSColor.controlBackgroundColor))
      .cornerRadius(8)
    }
    .buttonStyle(.plain)
    .disabled(isGranted || isRequesting)
  }
}

// Step 3: API Key Step
struct APIKeyStepView: View {
  @Binding var apiKey: String
  let isValidating: Bool
  let isValidated: Bool
  let errorMessage: String
  let validateAPIKey: (@escaping (Bool) -> Void) -> Void
  @State private var showToast = false
  @State private var toastMessage = ""
  @ObservedObject var coordinator: OnboardingCoordinator

  // SimpleToast configuration
  private let toastOptions = SimpleToastOptions(
    alignment: .bottom,
    hideAfter: 2,
    animation: .default,
    modifierType: .slide
  )

  var body: some View {
    VStack(spacing: 24) {
      // Icon and title are handled by parent view

      Text("SessionScribe uses OpenAI's GPT models for transcription and note generation.")
        .font(AppFont.body())
        .foregroundColor(AppColors.textSecondary)
        .multilineTextAlignment(.center)
        .frame(maxWidth: 400)

      // API Key Input Section
      VStack(spacing: 16) {
        AppTextField(
          title: "OpenAI API Key",
          placeholder: "sk-...",
          text: $apiKey,
          isSecure: true,
          showValidation: isValidated || !errorMessage.isEmpty,
          validationMessage: errorMessage,
          isValid: isValidated
        )
        .frame(maxWidth: 400)

        Button(action: {
          validateAPIKey { success in
            if success {
              toastMessage = "API key validated successfully!"
              showToast = true
              // Automatically move to the next step after a short delay
              DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
                coordinator.moveToNextStep()
              }
            }
          }
        }) {
          HStack {
            if isValidating {
              ProgressView()
                .scaleEffect(0.8)
                .padding(.horizontal, 4)
            } else {
              Text("Validate API Key")
            }
          }
          .frame(width: 150)
        }
        .buttonStyle(AppButton())
        .disabled(apiKey.isEmpty || isValidating)
      }

      // Info Card
      CardView {
        VStack(alignment: .leading, spacing: 16) {
          HStack {
            Image(systemName: "info.circle.fill")
              .foregroundColor(AppColors.primaryAccent)
            Text("Why we need your API key")
              .font(AppFont.bodyBold())
          }

          VStack(alignment: .leading, spacing: 12) {
            InfoItem(
              icon: "dollarsign.circle.fill",
              text: "You only pay for what you use directly to OpenAI"
            )
            InfoItem(
              icon: "shield.fill",
              text: "Your data never passes through our servers"
            )
            InfoItem(
              icon: "lock.fill",
              text: "Complete privacy and control over your API usage"
            )
          }
        }
      }
      .frame(maxWidth: 400)

      VStack(spacing: 8) {
        Link(
          "Don't have an API key? Get one from OpenAI",
          destination: URL(string: "https://platform.openai.com/api-keys")!
        )
        .font(AppFont.caption())
        .foregroundColor(AppColors.primaryAccent)

        Text("You can skip this step and add your API key later")
          .font(AppFont.caption())
          .foregroundColor(AppColors.textSecondary)
      }

      if isValidated {
        HStack(spacing: 8) {
          Image(systemName: "checkmark.circle.fill")
            .foregroundColor(AppColors.success)
          Text("API key validated successfully")
            .font(AppFont.caption())
            .foregroundColor(AppColors.success)
        }
        .padding(.top, 8)
      }
    }
    .simpleToast(isPresented: $showToast, options: toastOptions) {
      HStack {
        Image(systemName: "checkmark.circle.fill")
        Text(toastMessage)
      }
      .padding()
      .background(Color.green.opacity(0.8))
      .foregroundColor(Color.white)
      .cornerRadius(10)
    }
  }
}

// Helper view for info items
private struct InfoItem: View {
  let icon: String
  let text: String

  var body: some View {
    HStack(spacing: 12) {
      Image(systemName: icon)
        .foregroundColor(AppColors.primaryAccent)
        .frame(width: 20)
      Text(text)
        .font(AppFont.body())
        .foregroundColor(AppColors.textSecondary)
    }
  }
}

// Step 4: Storage Step
// Removed StorageStepView as it's no longer needed - using default storage location in Documents/SessionScribe

// Step 5: License Step
struct LicenseStepView: View {
  @ObservedObject var trialManager: TrialManager
  @Binding var licenseKey: String
  @Binding var trialEmail: String
  @Binding var hasAcceptedTerms: Bool
  @Binding var showTrialForm: Bool
  var trialErrorMessage: String
  let onViewTerms: () -> Void
  let validateLicenseKey: (@escaping (Bool) -> Void) -> Void
  let activateTrial: (@escaping (Bool) -> Void) -> Void

  @State private var isValidatingLicense = false
  @State private var isActivatingTrial = false

  var body: some View {
    VStack(spacing: 30) {
      Image(systemName: "key.fill")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 60, height: 60)
        .foregroundColor(AppColors.primaryAccent)

      Text("License Information")
        .font(AppFont.heading2())

      if !showTrialForm {
        // License Key Form
        VStack(spacing: 20) {
          Text("Enter your license key to activate SessionScribe")
            .font(AppFont.body())
            .foregroundColor(AppColors.textSecondary)
            .multilineTextAlignment(.center)

          CardView {
            VStack(spacing: 20) {
              TextField("License Key", text: $licenseKey)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .disabled(isValidatingLicense)

              if !trialManager.licenseErrorMessage.isEmpty {
                Text(trialManager.licenseErrorMessage)
                  .font(AppFont.caption())
                  .foregroundColor(AppColors.warning)
              }

              Button(action: {
                isValidatingLicense = true
                validateLicenseKey { success in
                  isValidatingLicense = false
                }
              }) {
                if isValidatingLicense {
                  ProgressView()
                    .progressViewStyle(CircularProgressViewStyle())
                } else {
                  Text("Validate License")
                }
              }
              .buttonStyle(AppButton())
              .disabled(licenseKey.isEmpty || isValidatingLicense)

              Divider()

              VStack(alignment: .center, spacing: 10) {
                Text("Don't have a license key?")
                  .font(AppFont.caption())
                  .foregroundColor(AppColors.textSecondary)

                Button("Start 7-Day Trial") {
                  showTrialForm = true
                }
                .buttonStyle(AppButton(type: .outline))
              }
            }
            .padding()
          }
        }
      } else {
        // Trial Form
        VStack(spacing: 20) {
          Text("Start your 7-Day free trial")
            .font(AppFont.body())
            .foregroundColor(AppColors.textSecondary)
            .multilineTextAlignment(.center)

          CardView {
            VStack(spacing: 20) {
              Text("Enter your email to begin your trial")
                .font(AppFont.caption())
                .foregroundColor(AppColors.textSecondary)

              TextField("Email Address", text: $trialEmail)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .disableAutocorrection(true)
                .disabled(isActivatingTrial)

              if !trialErrorMessage.isEmpty {
                Text(trialErrorMessage)
                  .font(AppFont.caption())
                  .foregroundColor(AppColors.warning)
              }

              Button(action: {
                isActivatingTrial = true
                activateTrial { success in
                  isActivatingTrial = false
                  if success {
                    showTrialForm = false
                  }
                }
              }) {
                if isActivatingTrial {
                  ProgressView()
                    .progressViewStyle(CircularProgressViewStyle())
                } else {
                  Text("Start Trial")
                }
              }
              .buttonStyle(AppButton())
              .disabled(trialEmail.isEmpty || isActivatingTrial)

              Divider()

              Button("Back to License Key") {
                showTrialForm = false
              }
              .buttonStyle(.plain)
            }
            .padding()
          }
        }
      }

      // Terms & Conditions section
      VStack(alignment: .leading, spacing: 6) {
        Toggle("I accept the Terms and Conditions", isOn: $hasAcceptedTerms)
          .toggleStyle(CheckboxToggleStyle())

        Button("View Terms and Conditions") {
          onViewTerms()
        }
        .buttonStyle(.plain)
        .font(AppFont.caption())
        .foregroundColor(AppColors.primaryAccent)
        .padding(.leading, 24)
      }
      .padding()

      if trialManager.trialActive {
        Text("Trial already activated - \(trialManager.daysRemaining) days remaining")
          .font(AppFont.caption())
          .foregroundColor(AppColors.warning)
      }
    }
  }
}

// Helper for License Step
struct CheckboxToggleStyle: ToggleStyle {
  func makeBody(configuration: Configuration) -> some View {
    HStack {
      Image(systemName: configuration.isOn ? "checkmark.square.fill" : "square")
        .foregroundColor(configuration.isOn ? AppColors.primaryAccent : .gray)
        .font(.system(size: 16))
        .onTapGesture {
          configuration.isOn.toggle()
        }

      configuration.label
    }
  }
}

// Step 6: Completion Step
struct CompletionStepView: View {
  var body: some View {
    VStack(spacing: 30) {
      Image(systemName: "checkmark.circle")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 80, height: 80)
        .foregroundColor(AppColors.success)

      Text("You're All Set!")
        .font(AppFont.heading1())

      Text("SessionScribe is ready to help you capture and enhance your meetings.")
        .font(AppFont.body())
        .foregroundColor(AppColors.textSecondary)
        .multilineTextAlignment(.center)

      VStack(alignment: .leading, spacing: 16) {
        CompletionItem(
          number: 1,
          title: "Join a meeting",
          description: "Connect to any meeting platform like Zoom, Teams, or Google Meet"
        )

        CompletionItem(
          number: 2,
          title: "Start recording",
          description: "Click the record button to capture audio and begin transcription"
        )

        CompletionItem(
          number: 3,
          title: "Review and enhance",
          description: "After the meeting, review the transcript and generate AI-powered notes"
        )
      }
      .padding()

      Text("Click 'Get Started' to begin using SessionScribe")
        .font(AppFont.caption())
        .foregroundColor(AppColors.textSecondary)
    }
  }
}

// Helper for Completion Step
struct CompletionItem: View {
  let number: Int
  let title: String
  let description: String

  var body: some View {
    HStack(alignment: .top, spacing: 16) {
      Text("\(number)")
        .font(AppFont.bodyBold())
        .foregroundColor(.white)
        .frame(width: 30, height: 30)
        .background(AppColors.primaryAccent)
        .clipShape(Circle())

      VStack(alignment: .leading, spacing: 4) {
        Text(title)
          .font(AppFont.bodyBold())
          .foregroundColor(AppColors.textPrimary)

        Text(description)
          .font(AppFont.body())
          .foregroundColor(AppColors.textSecondary)
      }
    }
  }
}

================
File: OnboardingView.swift
================
import SwiftUI

struct OnboardingView: View {
  @StateObject private var coordinator = OnboardingCoordinator()
  @State private var showingTermsSheet = false

  // Add a state to detect if this is a reset (returning user)
  @AppStorage("hasCompletedOnboardingBefore") private var hasCompletedOnboardingBefore: Bool = false

  var body: some View {
    VStack(spacing: 0) {
      // Header
      HStack {
        if coordinator.currentStep != .welcome {
          Button(action: { coordinator.moveToPreviousStep() }) {
            Image(systemName: "chevron.left")
              .foregroundColor(.secondary)
          }
          .buttonStyle(.plain)
          .keyboardShortcut(.leftArrow, modifiers: [])
        }

        Spacer()

        Text("Setup \(coordinator.currentStep.stepNumber) of \(OnboardingStep.allCases.count)")
          .foregroundColor(.secondary)
          .font(.system(size: 13))

        Spacer()

        if coordinator.currentStep == .apiKey {
          Button("Skip") {
            coordinator.skipToStep(.license)
          }
          .buttonStyle(.plain)
          .keyboardShortcut(.rightArrow, modifiers: [.command])
        }
      }
      .padding(.horizontal)
      .padding(.vertical, 12)
      .background(VisualEffectView())

      // Main content
      ScrollView {
        VStack(spacing: 32) {
          Text(coordinator.currentStep.title)
            .font(.system(size: 24, weight: .semibold))
            .padding(.top, 32)

          Group {
            // Modify the welcome step to show a different message for returning users
            if coordinator.currentStep == .welcome && hasCompletedOnboardingBefore {
              WelcomeBackStepView(coordinator: coordinator)
            } else {
              switch coordinator.currentStep {
              case .welcome:
                WelcomeStepView(coordinator: coordinator)
              case .permissions:
                PermissionsStepView(
                  permissionsManager: coordinator.permissionsManager,
                  requestPermissions: coordinator.requestPermissions
                )
              case .apiKey:
                APIKeyStepView(
                  apiKey: $coordinator.apiKey,
                  isValidating: coordinator.isValidatingAPIKey,
                  isValidated: coordinator.apiKeyValidated,
                  errorMessage: coordinator.apiKeyErrorMessage,
                  validateAPIKey: coordinator.validateAPIKey,
                  coordinator: coordinator
                )
              case .license:
                LicenseStepView(
                  trialManager: coordinator.trialManager,
                  licenseKey: $coordinator.licenseKey,
                  trialEmail: $coordinator.trialEmail,
                  hasAcceptedTerms: $coordinator.hasAcceptedTerms,
                  showTrialForm: $coordinator.showTrialForm,
                  trialErrorMessage: coordinator.trialErrorMessage,
                  onViewTerms: { showingTermsSheet = true },
                  validateLicenseKey: coordinator.validateLicenseKey,
                  activateTrial: coordinator.activateTrial
                )
              case .completion:
                CompletionStepView()
              }
            }
          }
          .frame(maxWidth: 500)
          .padding(.bottom, 32)
        }
        .frame(maxWidth: .infinity)
      }

      // Footer with action button
      HStack {
        Spacer()

        // Only show Continue button if not on API Key step
        if coordinator.currentStep != .apiKey {
          Button(actionText) {
            coordinator.moveToNextStep()
          }
          .buttonStyle(.borderedProminent)
          .controlSize(.large)
          .disabled(!canProceedToNextStep())
          .keyboardShortcut(.return, modifiers: [])
        }
      }
      .padding()
      .background(VisualEffectView())
    }
    .background(Color(NSColor.windowBackgroundColor))
    .sheet(isPresented: $showingTermsSheet) {
      TermsAndConditionsView(isPresented: $showingTermsSheet)
    }
    .onAppear {
      // If this is a reset, we'll show a special welcome back message
      if !hasCompletedOnboardingBefore {
        // First time user
        coordinator.reset()
      } else {
        // Returning user (reset from settings)
        coordinator.prepareForReturningUser()
      }
    }
    .onChange(of: coordinator.isOnboardingComplete) { oldValue, complete in
      if complete {
        // Mark that the user has completed onboarding at least once
        hasCompletedOnboardingBefore = true
      }
    }
  }

  private var actionText: String {
    switch coordinator.currentStep {
    case .completion:
      return "Get Started"
    default:
      return "Continue"
    }
  }

  private func canProceedToNextStep() -> Bool {
    switch coordinator.currentStep {
    case .welcome:
      return true
    case .permissions:
      return coordinator.permissionsManager.microphonePermissionGranted
        && coordinator.permissionsManager.screenCapturePermissionGranted
    case .apiKey:
      return coordinator.apiKeyValidated || coordinator.apiKey.isEmpty
    case .license:
      return coordinator.hasAcceptedTerms
    case .completion:
      return true
    }
  }
}

extension OnboardingStep {
  var stepNumber: Int {
    OnboardingStep.allCases.firstIndex(of: self)! + 1
  }
}

struct TermsAndConditionsView: View {
  @Binding var isPresented: Bool

  var body: some View {
    VStack(spacing: 20) {
      Text("Terms and Conditions")
        .font(AppFont.heading1())

      ScrollView {
        Text(
          """
          # SessionScribe Terms of Service

          **Effective Date: June 1, 2023**

          ## 1. Introduction

          Welcome to SessionScribe ("we," "our," or "us"). By downloading, accessing, or using our application, you agree to be bound by these Terms of Service.

          ## 2. License

          Subject to your compliance with these Terms, we grant you a limited, non-exclusive, non-transferable, non-sublicensable license to download and use the Application for your personal, non-commercial purposes.

          ## 3. Trial Period

          SessionScribe offers a 14-day trial period. After this period expires, you will need to purchase a license to continue using the application.

          ## 4. Privacy

          Your privacy is important to us. Our Privacy Policy explains how we collect, use, and protect your information.

          ## 5. Subscription and Billing

          After the trial period, SessionScribe requires a one-time payment to continue using the service.

          ## 6. Data Storage and Security

          SessionScribe stores your data locally on your device, with optional iCloud synchronization if enabled. We implement reasonable security measures to protect your data.

          ## 7. Disclaimer of Warranties

          THE APPLICATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.

          ## 8. Limitation of Liability

          IN NO EVENT SHALL WE BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES.

          ## 9. Changes to Terms

          We may update these Terms from time to time. It is your responsibility to review these Terms periodically.

          ## 10. Contact Information

          If you have any questions about these Terms, please contact us at: support@sessionscribe.com
          """
        )
        .padding()
      }
      .frame(height: 400)
      .background(Color.gray.opacity(0.1))
      .cornerRadius(8)

      Button("Close") {
        isPresented = false
      }
      .buttonStyle(AppButton())
    }
    .frame(width: 600, height: 600)
    .padding(30)
  }
}

// Add a new view for returning users
struct WelcomeBackStepView: View {
  @ObservedObject var coordinator: OnboardingCoordinator

  var body: some View {
    VStack(spacing: 30) {
      Image(systemName: "arrow.triangle.2.circlepath")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 80, height: 80)
        .foregroundColor(AppColors.primaryAccent)

      Text("Welcome Back!")
        .font(AppFont.heading1())

      Text(
        "You've reset the app to go through the onboarding process again. Your meetings and settings have been preserved."
      )
      .font(AppFont.body())
      .multilineTextAlignment(.center)
      .foregroundColor(AppColors.textSecondary)
      .frame(maxWidth: 450)

      Button("Continue") {
        coordinator.moveToNextStep()
      }
      .buttonStyle(AppButton())
      .padding(.top, 20)
    }
    .padding(40)
    .frame(maxWidth: .infinity, maxHeight: .infinity)
  }
}

================
File: openai_whisper-small
================
<!doctype html>
<html class="">
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
		<meta name="description" content="We’re on a journey to advance and democratize artificial intelligence through open source and open science." />
		<meta property="fb:app_id" content="1321688464574422" />
		<meta name="twitter:card" content="summary_large_image" />
		<meta name="twitter:site" content="@huggingface" />
		<meta name="twitter:image" content="https://cdn-thumbnails.huggingface.co/social-thumbnails/models/argmaxinc/whisperkit-coreml.png" />
		<meta property="og:title" content="argmaxinc/whisperkit-coreml at main" />
		<meta property="og:type" content="website" />
		<meta property="og:url" content="https://huggingface.co/argmaxinc/whisperkit-coreml/tree/main/openai_whisper-small" />
		<meta property="og:image" content="https://cdn-thumbnails.huggingface.co/social-thumbnails/models/argmaxinc/whisperkit-coreml.png" />

		<link rel="stylesheet" href="/front/build/kube-272d363/style.css" />

		<link rel="preconnect" href="https://fonts.gstatic.com" />
		<link
			href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700;1,900&display=swap"
			rel="stylesheet"
		/>
		<link
			href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600;700&display=swap"
			rel="stylesheet"
		/>

		<link
			rel="preload"
			href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.12.0/katex.min.css"
			as="style"
			onload="this.onload=null;this.rel='stylesheet'"
		/>
		<noscript>
			<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.12.0/katex.min.css" />
		</noscript>

		<script>const guestTheme = document.cookie.match(/theme=(\w+)/)?.[1]; document.documentElement.classList.toggle('dark', guestTheme === 'dark' || ( (!guestTheme || guestTheme === 'system') && window.matchMedia('(prefers-color-scheme: dark)').matches));</script>
<link rel="canonical" href="https://huggingface.co/argmaxinc/whisperkit-coreml/tree/main/openai_whisper-small">  

		<title>argmaxinc/whisperkit-coreml at main</title>

		<script
			defer
			data-domain="huggingface.co"
			event-loggedIn="false"
			src="/js/script.pageview-props.js"
		></script>
		<script>
			window.plausible =
				window.plausible ||
				function () {
					(window.plausible.q = window.plausible.q || []).push(arguments);
				};
		</script>
		<script>
			window.hubConfig = {"features":{"signupDisabled":false},"sshGitUrl":"git@hf.co","moonHttpUrl":"https:\/\/huggingface.co","captchaApiKey":"bd5f2066-93dc-4bdd-a64b-a24646ca3859","captchaDisabledOnSignup":true,"datasetViewerPublicUrl":"https:\/\/datasets-server.huggingface.co","stripePublicKey":"pk_live_x2tdjFXBCvXo2FFmMybezpeM00J6gPCAAc","environment":"production","userAgent":"HuggingFace (production)","spacesIframeDomain":"hf.space","spacesApiUrl":"https:\/\/api.hf.space","docSearchKey":"ece5e02e57300e17d152c08056145326e90c4bff3dd07d7d1ae40cf1c8d39cb6","logoDev":{"apiUrl":"https:\/\/img.logo.dev\/","apiKey":"pk_UHS2HZOeRnaSOdDp7jbd5w"}};
		</script>
		<script type="text/javascript" src="https://de5282c3ca0c.edge.sdk.awswaf.com/de5282c3ca0c/526cf06acb0d/challenge.js" defer></script>
	</head>
	<body class="flex flex-col min-h-dvh bg-white dark:bg-gray-950 text-black ViewerIndexTreePage">
		<div class="flex min-h-dvh flex-col"><div class="SVELTE_HYDRATER contents" data-target="SystemThemeMonitor" data-props="{&quot;isLoggedIn&quot;:false}"></div>

	<div class="SVELTE_HYDRATER contents" data-target="MainHeader" data-props="{&quot;classNames&quot;:&quot;&quot;,&quot;isWide&quot;:false,&quot;isZh&quot;:false,&quot;isPro&quot;:false}"><header class="border-b border-gray-100 "><div class="w-full px-4 container flex h-16 items-center"><div class="flex flex-1 items-center"><a class="mr-5 flex flex-none items-center lg:mr-6" href="/"><img alt="Hugging Face's logo" class="w-7 md:mr-2" src="/front/assets/huggingface_logo-noborder.svg">
				<span class="hidden whitespace-nowrap text-lg font-bold md:block">Hugging Face</span></a>
			<div class="relative flex-1 lg:max-w-sm mr-2 sm:mr-4 md:mr-3 xl:mr-6"><input autocomplete="off" class="w-full dark:bg-gray-950 pl-8 form-input-alt h-9 pr-3 focus:shadow-xl " name="" placeholder="Search models, datasets, users..."   spellcheck="false" type="text" value="">
	<svg class="absolute left-2.5 text-gray-400 top-1/2 transform -translate-y-1/2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M30 28.59L22.45 21A11 11 0 1 0 21 22.45L28.59 30zM5 14a9 9 0 1 1 9 9a9 9 0 0 1-9-9z" fill="currentColor"></path></svg>
	</div>
			<div class="flex flex-none items-center justify-center p-0.5 place-self-stretch lg:hidden"><button class="relative z-40 flex h-6 w-8 items-center justify-center" type="button"><svg width="1em" height="1em" viewBox="0 0 10 10" class="text-xl" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" preserveAspectRatio="xMidYMid meet" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.65039 2.9999C1.65039 2.8066 1.80709 2.6499 2.00039 2.6499H8.00039C8.19369 2.6499 8.35039 2.8066 8.35039 2.9999C8.35039 3.1932 8.19369 3.3499 8.00039 3.3499H2.00039C1.80709 3.3499 1.65039 3.1932 1.65039 2.9999ZM1.65039 4.9999C1.65039 4.8066 1.80709 4.6499 2.00039 4.6499H8.00039C8.19369 4.6499 8.35039 4.8066 8.35039 4.9999C8.35039 5.1932 8.19369 5.3499 8.00039 5.3499H2.00039C1.80709 5.3499 1.65039 5.1932 1.65039 4.9999ZM2.00039 6.6499C1.80709 6.6499 1.65039 6.8066 1.65039 6.9999C1.65039 7.1932 1.80709 7.3499 2.00039 7.3499H8.00039C8.19369 7.3499 8.35039 7.1932 8.35039 6.9999C8.35039 6.8066 8.19369 6.6499 8.00039 6.6499H2.00039Z"></path></svg>
		</button>

	</div></div>
		<nav aria-label="Main" class="ml-auto hidden lg:block"><ul class="flex items-center space-x-1.5 2xl:space-x-2"><li class="hover:text-indigo-700"><a class="group flex items-center px-2 py-0.5 dark:text-gray-300 dark:hover:text-gray-100" href="/models"><svg class="mr-1.5 text-gray-400 group-hover:text-indigo-500" style="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path class="uim-quaternary" d="M20.23 7.24L12 12L3.77 7.24a1.98 1.98 0 0 1 .7-.71L11 2.76c.62-.35 1.38-.35 2 0l6.53 3.77c.29.173.531.418.7.71z" opacity=".25" fill="currentColor"></path><path class="uim-tertiary" d="M12 12v9.5a2.09 2.09 0 0 1-.91-.21L4.5 17.48a2.003 2.003 0 0 1-1-1.73v-7.5a2.06 2.06 0 0 1 .27-1.01L12 12z" opacity=".5" fill="currentColor"></path><path class="uim-primary" d="M20.5 8.25v7.5a2.003 2.003 0 0 1-1 1.73l-6.62 3.82c-.275.13-.576.198-.88.2V12l8.23-4.76c.175.308.268.656.27 1.01z" fill="currentColor"></path></svg>
					Models</a>
			</li><li class="hover:text-red-700"><a class="group flex items-center px-2 py-0.5 dark:text-gray-300 dark:hover:text-gray-100" href="/datasets"><svg class="mr-1.5 text-gray-400 group-hover:text-red-500" style="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 25 25"><ellipse cx="12.5" cy="5" fill="currentColor" fill-opacity="0.25" rx="7.5" ry="2"></ellipse><path d="M12.5 15C16.6421 15 20 14.1046 20 13V20C20 21.1046 16.6421 22 12.5 22C8.35786 22 5 21.1046 5 20V13C5 14.1046 8.35786 15 12.5 15Z" fill="currentColor" opacity="0.5"></path><path d="M12.5 7C16.6421 7 20 6.10457 20 5V11.5C20 12.6046 16.6421 13.5 12.5 13.5C8.35786 13.5 5 12.6046 5 11.5V5C5 6.10457 8.35786 7 12.5 7Z" fill="currentColor" opacity="0.5"></path><path d="M5.23628 12C5.08204 12.1598 5 12.8273 5 13C5 14.1046 8.35786 15 12.5 15C16.6421 15 20 14.1046 20 13C20 12.8273 19.918 12.1598 19.7637 12C18.9311 12.8626 15.9947 13.5 12.5 13.5C9.0053 13.5 6.06886 12.8626 5.23628 12Z" fill="currentColor"></path></svg>
					Datasets</a>
			</li><li class="hover:text-blue-700"><a class="group flex items-center px-2 py-0.5 dark:text-gray-300 dark:hover:text-gray-100" href="/spaces"><svg class="mr-1.5 text-gray-400 group-hover:text-blue-500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" viewBox="0 0 25 25"><path opacity=".5" d="M6.016 14.674v4.31h4.31v-4.31h-4.31ZM14.674 14.674v4.31h4.31v-4.31h-4.31ZM6.016 6.016v4.31h4.31v-4.31h-4.31Z" fill="currentColor"></path><path opacity=".75" fill-rule="evenodd" clip-rule="evenodd" d="M3 4.914C3 3.857 3.857 3 4.914 3h6.514c.884 0 1.628.6 1.848 1.414a5.171 5.171 0 0 1 7.31 7.31c.815.22 1.414.964 1.414 1.848v6.514A1.914 1.914 0 0 1 20.086 22H4.914A1.914 1.914 0 0 1 3 20.086V4.914Zm3.016 1.102v4.31h4.31v-4.31h-4.31Zm0 12.968v-4.31h4.31v4.31h-4.31Zm8.658 0v-4.31h4.31v4.31h-4.31Zm0-10.813a2.155 2.155 0 1 1 4.31 0 2.155 2.155 0 0 1-4.31 0Z" fill="currentColor"></path><path opacity=".25" d="M16.829 6.016a2.155 2.155 0 1 0 0 4.31 2.155 2.155 0 0 0 0-4.31Z" fill="currentColor"></path></svg>
					Spaces</a>
			</li><li class="hover:text-yellow-700 max-xl:hidden"><a class="group flex items-center px-2 py-0.5 dark:text-gray-300 dark:hover:text-gray-100" href="/posts"><svg class="mr-1.5 text-gray-400 group-hover:text-yellow-500 text-yellow-500!" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" viewBox="0 0 12 12" preserveAspectRatio="xMidYMid meet"><path fill="currentColor" fill-rule="evenodd" d="M3.73 2.4A4.25 4.25 0 1 1 6 10.26H2.17l-.13-.02a.43.43 0 0 1-.3-.43l.01-.06a.43.43 0 0 1 .12-.22l.84-.84A4.26 4.26 0 0 1 3.73 2.4Z" clip-rule="evenodd"></path></svg>
					Posts</a>
			</li><li class="hover:text-yellow-700"><a class="group flex items-center px-2 py-0.5 dark:text-gray-300 dark:hover:text-gray-100" href="/docs"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="mr-1.5 text-gray-400 group-hover:text-yellow-500" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16"><path d="m2.28 3.7-.3.16a.67.67 0 0 0-.34.58v8.73l.01.04.02.07.01.04.03.06.02.04.02.03.04.06.05.05.04.04.06.04.06.04.08.04.08.02h.05l.07.02h.11l.04-.01.07-.02.03-.01.07-.03.22-.12a5.33 5.33 0 0 1 5.15.1.67.67 0 0 0 .66 0 5.33 5.33 0 0 1 5.33 0 .67.67 0 0 0 1-.58V4.36a.67.67 0 0 0-.34-.5l-.3-.17v7.78a.63.63 0 0 1-.87.59 4.9 4.9 0 0 0-4.35.35l-.65.39a.29.29 0 0 1-.15.04.29.29 0 0 1-.16-.04l-.65-.4a4.9 4.9 0 0 0-4.34-.34.63.63 0 0 1-.87-.59V3.7Z" fill="currentColor" class="dark:opacity-40"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M8 3.1a5.99 5.99 0 0 0-5.3-.43.66.66 0 0 0-.42.62v8.18c0 .45.46.76.87.59a4.9 4.9 0 0 1 4.34.35l.65.39c.05.03.1.04.16.04.05 0 .1-.01.15-.04l.65-.4a4.9 4.9 0 0 1 4.35-.34.63.63 0 0 0 .86-.59V3.3a.67.67 0 0 0-.41-.62 5.99 5.99 0 0 0-5.3.43l-.3.17L8 3.1Zm.73 1.87a.43.43 0 1 0-.86 0v5.48a.43.43 0 0 0 .86 0V4.97Z" fill="currentColor" class="opacity-40 dark:opacity-100"></path><path d="M8.73 4.97a.43.43 0 1 0-.86 0v5.48a.43.43 0 1 0 .86 0V4.96Z" fill="currentColor" class="dark:opacity-40"></path></svg>
					Docs</a>
			</li><li class="hover:text-green-700"><a class="group flex items-center px-2 py-0.5 dark:text-gray-300 dark:hover:text-gray-100" href="/enterprise"><svg class="mr-1.5 text-gray-400 group-hover:text-green-500" xmlns="http://www.w3.org/2000/svg" fill="none" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 33 27"><path fill="currentColor" fill-rule="evenodd" d="M13.5.7a8.7 8.7 0 0 0-7.7 5.7L1 20.6c-1 3.1.9 5.7 4.1 5.7h15c3.3 0 6.8-2.6 7.8-5.7l4.6-14.2c1-3.1-.8-5.7-4-5.7h-15Zm1.1 5.7L9.8 20.3h9.8l1-3.1h-5.8l.8-2.5h4.8l1.1-3h-4.8l.8-2.3H23l1-3h-9.5Z" clip-rule="evenodd"></path></svg>
					Enterprise</a>
			</li>

		<li><a class="group flex items-center px-2 py-0.5 dark:text-gray-300 dark:hover:text-gray-100" href="/pricing">Pricing
			</a></li>

		<li><div class="relative group">
	<button class="px-2 py-0.5 hover:text-gray-500 dark:hover:text-gray-600 flex items-center " type="button">
		<svg class=" text-gray-500 w-5 group-hover:text-gray-400 dark:text-gray-300 dark:group-hover:text-gray-100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" viewBox="0 0 32 18" preserveAspectRatio="xMidYMid meet"><path fill-rule="evenodd" clip-rule="evenodd" d="M14.4504 3.30221C14.4504 2.836 14.8284 2.45807 15.2946 2.45807H28.4933C28.9595 2.45807 29.3374 2.836 29.3374 3.30221C29.3374 3.76842 28.9595 4.14635 28.4933 4.14635H15.2946C14.8284 4.14635 14.4504 3.76842 14.4504 3.30221Z" fill="currentColor"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M14.4504 9.00002C14.4504 8.53382 14.8284 8.15588 15.2946 8.15588H28.4933C28.9595 8.15588 29.3374 8.53382 29.3374 9.00002C29.3374 9.46623 28.9595 9.84417 28.4933 9.84417H15.2946C14.8284 9.84417 14.4504 9.46623 14.4504 9.00002Z" fill="currentColor"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M14.4504 14.6978C14.4504 14.2316 14.8284 13.8537 15.2946 13.8537H28.4933C28.9595 13.8537 29.3374 14.2316 29.3374 14.6978C29.3374 15.164 28.9595 15.542 28.4933 15.542H15.2946C14.8284 15.542 14.4504 15.164 14.4504 14.6978Z" fill="currentColor"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M1.94549 6.87377C2.27514 6.54411 2.80962 6.54411 3.13928 6.87377L6.23458 9.96907L9.32988 6.87377C9.65954 6.54411 10.194 6.54411 10.5237 6.87377C10.8533 7.20343 10.8533 7.73791 10.5237 8.06756L6.23458 12.3567L1.94549 8.06756C1.61583 7.73791 1.61583 7.20343 1.94549 6.87377Z" fill="currentColor"></path></svg>
			
		</button>
	
	
	</div></li>
		<li><hr class="h-5 w-0.5 border-none bg-gray-100 dark:bg-gray-800"></li>
		<li><a class="block cursor-pointer whitespace-nowrap px-2 py-0.5 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-100" href="/login">Log In
				</a></li>
			<li><a class="whitespace-nowrap rounded-full border border-transparent bg-gray-900 px-3 py-1 leading-none text-white hover:border-black hover:bg-white hover:text-black" href="/join">Sign Up
					</a></li></ul></nav></div></header></div>
	
	
	
	<div class="SVELTE_HYDRATER contents" data-target="SSOBanner" data-props="{}"></div>
	
	

	<main class="flex flex-1 flex-col"><div class="SVELTE_HYDRATER contents" data-target="ModelHeader" data-props="{&quot;activeTab&quot;:&quot;files&quot;,&quot;author&quot;:{&quot;avatarUrl&quot;:&quot;https://cdn-avatars.huggingface.co/v1/production/uploads/64c612d618bc1b4e81023e7b/Ga9AJAiQrVDBGlmt9lMmO.png&quot;,&quot;fullname&quot;:&quot;Argmax&quot;,&quot;name&quot;:&quot;argmaxinc&quot;,&quot;type&quot;:&quot;org&quot;,&quot;isHf&quot;:false,&quot;isMod&quot;:false,&quot;isEnterprise&quot;:true,&quot;followerCount&quot;:99},&quot;canReadRepoSettings&quot;:false,&quot;canWriteRepoContent&quot;:false,&quot;canDisable&quot;:false,&quot;model&quot;:{&quot;author&quot;:&quot;argmaxinc&quot;,&quot;cardData&quot;:{&quot;pretty_name&quot;:&quot;WhisperKit&quot;,&quot;viewer&quot;:false,&quot;library_name&quot;:&quot;whisperkit&quot;,&quot;tags&quot;:[&quot;whisper&quot;,&quot;whisperkit&quot;,&quot;coreml&quot;,&quot;asr&quot;,&quot;quantized&quot;,&quot;automatic-speech-recognition&quot;]},&quot;cardExists&quot;:true,&quot;config&quot;:{},&quot;createdAt&quot;:&quot;2024-02-28T08:05:21.000Z&quot;,&quot;discussionsDisabled&quot;:false,&quot;downloads&quot;:250218,&quot;downloadsAllTime&quot;:821954,&quot;id&quot;:&quot;argmaxinc/whisperkit-coreml&quot;,&quot;isLikedByUser&quot;:false,&quot;availableInferenceProviders&quot;:[],&quot;inference&quot;:&quot;&quot;,&quot;lastModified&quot;:&quot;2025-02-03T16:52:29.000Z&quot;,&quot;likes&quot;:112,&quot;pipeline_tag&quot;:&quot;automatic-speech-recognition&quot;,&quot;library_name&quot;:&quot;whisperkit&quot;,&quot;librariesOther&quot;:[],&quot;trackDownloads&quot;:true,&quot;model-index&quot;:null,&quot;private&quot;:false,&quot;repoType&quot;:&quot;model&quot;,&quot;gated&quot;:false,&quot;pwcLink&quot;:{&quot;error&quot;:&quot;Unknown error, can't generate link to Papers With Code.&quot;},&quot;tags&quot;:[&quot;whisperkit&quot;,&quot;coreml&quot;,&quot;whisper&quot;,&quot;asr&quot;,&quot;quantized&quot;,&quot;automatic-speech-recognition&quot;,&quot;region:us&quot;],&quot;tag_objs&quot;:[{&quot;id&quot;:&quot;automatic-speech-recognition&quot;,&quot;label&quot;:&quot;Automatic Speech Recognition&quot;,&quot;type&quot;:&quot;pipeline_tag&quot;,&quot;subType&quot;:&quot;audio&quot;},{&quot;id&quot;:&quot;whisperkit&quot;,&quot;label&quot;:&quot;WhisperKit&quot;,&quot;type&quot;:&quot;library&quot;},{&quot;id&quot;:&quot;coreml&quot;,&quot;label&quot;:&quot;Core ML&quot;,&quot;type&quot;:&quot;library&quot;},{&quot;id&quot;:&quot;whisper&quot;,&quot;label&quot;:&quot;whisper&quot;,&quot;type&quot;:&quot;other&quot;},{&quot;id&quot;:&quot;asr&quot;,&quot;label&quot;:&quot;asr&quot;,&quot;type&quot;:&quot;other&quot;},{&quot;id&quot;:&quot;quantized&quot;,&quot;label&quot;:&quot;quantized&quot;,&quot;type&quot;:&quot;other&quot;},{&quot;type&quot;:&quot;region&quot;,&quot;label&quot;:&quot;🇺🇸 Region: US&quot;,&quot;id&quot;:&quot;region:us&quot;}],&quot;hasBlockedOids&quot;:false,&quot;region&quot;:&quot;us&quot;,&quot;isQuantized&quot;:false,&quot;inferenceStatic&quot;:&quot;pipeline-library-pair-not-supported&quot;},&quot;discussionsStats&quot;:{&quot;closed&quot;:11,&quot;open&quot;:1,&quot;total&quot;:12},&quot;query&quot;:{},&quot;inferenceProviders&quot;:[{&quot;name&quot;:&quot;sambanova&quot;,&quot;enabled&quot;:true,&quot;position&quot;:0,&quot;lastUsedAt&quot;:&quot;2025-03-04T21:05:44.991Z&quot;,&quot;isReleased&quot;:true},{&quot;name&quot;:&quot;hyperbolic&quot;,&quot;enabled&quot;:true,&quot;position&quot;:1,&quot;lastUsedAt&quot;:&quot;2025-03-04T21:05:44.991Z&quot;,&quot;isReleased&quot;:true},{&quot;name&quot;:&quot;novita&quot;,&quot;enabled&quot;:true,&quot;position&quot;:2,&quot;lastUsedAt&quot;:&quot;2025-03-04T21:05:44.991Z&quot;,&quot;isReleased&quot;:true},{&quot;name&quot;:&quot;together&quot;,&quot;enabled&quot;:true,&quot;position&quot;:3,&quot;lastUsedAt&quot;:&quot;2025-03-04T21:05:44.991Z&quot;,&quot;isReleased&quot;:true},{&quot;name&quot;:&quot;nebius&quot;,&quot;enabled&quot;:true,&quot;position&quot;:4,&quot;lastUsedAt&quot;:&quot;2025-03-04T21:05:44.991Z&quot;,&quot;isReleased&quot;:true},{&quot;name&quot;:&quot;fal-ai&quot;,&quot;enabled&quot;:true,&quot;position&quot;:5,&quot;lastUsedAt&quot;:&quot;2025-03-04T21:05:44.991Z&quot;,&quot;isReleased&quot;:true},{&quot;name&quot;:&quot;replicate&quot;,&quot;enabled&quot;:true,&quot;position&quot;:6,&quot;lastUsedAt&quot;:&quot;2025-03-04T21:05:44.991Z&quot;,&quot;isReleased&quot;:true},{&quot;name&quot;:&quot;fireworks-ai&quot;,&quot;enabled&quot;:true,&quot;position&quot;:7,&quot;lastUsedAt&quot;:&quot;2025-03-04T21:05:44.991Z&quot;,&quot;isReleased&quot;:true},{&quot;name&quot;:&quot;hf-inference&quot;,&quot;enabled&quot;:true,&quot;position&quot;:8,&quot;lastUsedAt&quot;:&quot;2025-03-04T21:05:44.991Z&quot;,&quot;isReleased&quot;:true}]}"><header class="from-gray-50-to-white bg-linear-to-t border-b border-gray-100 via-white dark:via-gray-950 pt-6 sm:pt-9"><div class="container relative "><h1 class="flex flex-wrap items-center max-md:leading-tight mb-3 text-lg max-sm:gap-y-1.5 md:text-xl">
			<div class="group flex flex-none items-center"><div class="relative mr-1 flex items-center">

			

<span class="inline-block "><span class="contents"><a href="/argmaxinc" class="text-gray-400 hover:text-blue-600"><img alt="" class="w-3.5 h-3.5 rounded-sm  flex-none" src="https://cdn-avatars.huggingface.co/v1/production/uploads/64c612d618bc1b4e81023e7b/Ga9AJAiQrVDBGlmt9lMmO.png" crossorigin="anonymous"></a></span>
	</span></div>
		

<span class="inline-block "><span class="contents"><a href="/argmaxinc" class="text-gray-400 hover:text-blue-600">argmaxinc</a></span>
	</span>
		<div class="mx-0.5 text-gray-300">/</div></div>

<div class="max-w-full "><a class="break-words font-mono font-semibold hover:text-blue-600 " href="/argmaxinc/whisperkit-coreml">whisperkit-coreml</a>
	<button class="relative text-sm mr-4 focus:outline-hidden inline-flex cursor-pointer items-center text-sm  mx-0.5   text-gray-600 " title="Copy model name to clipboard" type="button"><svg class="" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z" transform="translate(0)"></path><path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)"></path><rect fill="none" width="32" height="32"></rect></svg>
	
	</button></div>
			<div class="inline-flex items-center overflow-hidden whitespace-nowrap rounded-md border bg-white text-sm leading-none text-gray-500  mr-2"><button class="relative flex items-center overflow-hidden from-red-50 to-transparent dark:from-red-900 px-1.5 py-1 hover:bg-linear-to-t focus:outline-hidden"  title="Like"><svg class="left-1.5 absolute" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32" fill="currentColor"><path d="M22.45,6a5.47,5.47,0,0,1,3.91,1.64,5.7,5.7,0,0,1,0,8L16,26.13,5.64,15.64a5.7,5.7,0,0,1,0-8,5.48,5.48,0,0,1,7.82,0L16,10.24l2.53-2.58A5.44,5.44,0,0,1,22.45,6m0-2a7.47,7.47,0,0,0-5.34,2.24L16,7.36,14.89,6.24a7.49,7.49,0,0,0-10.68,0,7.72,7.72,0,0,0,0,10.82L16,29,27.79,17.06a7.72,7.72,0,0,0,0-10.82A7.49,7.49,0,0,0,22.45,4Z"></path></svg>

		
		<span class="ml-4 pl-0.5 ">like</span></button>
	<button class="focus:outline-hidden flex items-center border-l px-1.5 py-1 text-gray-400 hover:bg-gray-50 focus:bg-gray-100 dark:hover:bg-gray-900 dark:focus:bg-gray-800" title="See users who liked this repository">112</button></div>




			<div class="relative flex items-center gap-1.5  "><div class="mr-2 inline-flex h-6 items-center overflow-hidden whitespace-nowrap rounded-md border text-sm text-gray-500"><button class="focus:outline-hidden relative flex h-full max-w-56 items-center gap-1.5 overflow-hidden px-1.5 hover:bg-gray-50 focus:bg-gray-100 dark:hover:bg-gray-900 dark:focus:bg-gray-800" type="button" ><div class="flex h-full flex-1 items-center justify-center ">Follow</div>
		<img alt="" class="rounded-xs size-3 flex-none" src="https://cdn-avatars.huggingface.co/v1/production/uploads/64c612d618bc1b4e81023e7b/Ga9AJAiQrVDBGlmt9lMmO.png">
		<span class="truncate">Argmax</span></button>
	<button class="focus:outline-hidden flex h-full items-center border-l pl-1.5 pr-1.5 text-gray-400 hover:bg-gray-50 focus:bg-gray-100 dark:hover:bg-gray-900 dark:focus:bg-gray-800" title="Show Argmax's followers" type="button">99</button></div>

		</div>
			
	</h1>
		<div class="mb-3 flex flex-wrap md:mb-4"><a class="mb-1 mr-1 md:mb-1.5 md:mr-1.5 rounded-lg" href="/models?pipeline_tag=automatic-speech-recognition"><div class="tag tag-white   "><div class="tag-ico -ml-2 tag-ico-yellow"><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 18 18"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.38893 3.42133C7.9778 3.14662 7.49446 3 7 3C6.33696 3 5.70108 3.26339 5.23223 3.73223C4.76339 4.20107 4.5 4.83696 4.5 5.5C4.5 5.99445 4.64662 6.4778 4.92133 6.88893C5.19603 7.30005 5.58648 7.62048 6.04329 7.8097C6.50011 7.99892 7.00278 8.04843 7.48773 7.95196C7.97268 7.8555 8.41814 7.6174 8.76777 7.26777C9.1174 6.91814 9.3555 6.47268 9.45197 5.98773C9.54843 5.50277 9.49892 5.00011 9.3097 4.54329C9.12048 4.08648 8.80005 3.69603 8.38893 3.42133ZM5.05551 2.58986C5.63108 2.20527 6.30777 2 7 2C7.92826 2 8.8185 2.36875 9.47488 3.02513C10.1313 3.6815 10.5 4.57174 10.5 5.5C10.5 6.19223 10.2947 6.86892 9.91015 7.4445C9.52556 8.02007 8.97894 8.46867 8.33939 8.73358C7.69985 8.99849 6.99612 9.0678 6.31719 8.93275C5.63825 8.7977 5.01461 8.46436 4.52513 7.97487C4.03564 7.48539 3.7023 6.86175 3.56725 6.18282C3.4322 5.50388 3.50152 4.80015 3.76642 4.16061C4.03133 3.52107 4.47993 2.97444 5.05551 2.58986ZM14.85 9.6425L15.7075 10.5C15.8005 10.5927 15.8743 10.7029 15.9245 10.8242C15.9747 10.9456 16.0004 11.0757 16 11.207V16H2V13.5C2.00106 12.5721 2.37015 11.6824 3.0263 11.0263C3.68244 10.3701 4.57207 10.0011 5.5 10H8.5C9.42793 10.0011 10.3176 10.3701 10.9737 11.0263C11.6299 11.6824 11.9989 12.5721 12 13.5V15H15V11.207L14.143 10.35C13.9426 10.4476 13.7229 10.4989 13.5 10.5C13.2033 10.5 12.9133 10.412 12.6666 10.2472C12.42 10.0824 12.2277 9.84811 12.1142 9.57403C12.0006 9.29994 11.9709 8.99834 12.0288 8.70737C12.0867 8.41639 12.2296 8.14912 12.4393 7.93934C12.6491 7.72956 12.9164 7.5867 13.2074 7.52882C13.4983 7.47094 13.7999 7.50065 14.074 7.61418C14.3481 7.72771 14.5824 7.91997 14.7472 8.16665C14.912 8.41332 15 8.70333 15 9C14.9988 9.22271 14.9475 9.44229 14.85 9.6425ZM3.73311 11.7331C3.26444 12.2018 3.00079 12.8372 3 13.5V15H11V13.5C10.9992 12.8372 10.7356 12.2018 10.2669 11.7331C9.79822 11.2644 9.1628 11.0008 8.5 11H5.5C4.8372 11.0008 4.20178 11.2644 3.73311 11.7331Z" fill="currentColor"></path></svg></div>

	

	<span>Automatic Speech Recognition</span>
	

	</div></a><a class="mb-1 mr-1 md:mb-1.5 md:mr-1.5 rounded-lg" href="/models?library=whisperkit"><div class="tag tag-white   ">

	

	<span>WhisperKit</span>
	

	</div></a><a class="mb-1 mr-1 md:mb-1.5 md:mr-1.5 rounded-lg" href="/models?library=coreml"><div class="tag tag-white   "><svg class="text-black inline-block text-sm" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 734 734"><path d="M476.841 682.161L685.922 532.632C707.042 517.527 718.051 495.675 718.941 473.535V259.914H16.1792V473.535C17.0788 495.915 28.3135 518.001 49.8743 533.109L263.968 683.123C326.853 727.186 414.437 726.791 476.841 682.161Z" fill="#02C0A8"></path><path d="M476.841 581.103L685.922 431.573C707.042 416.468 718.051 394.616 718.941 372.476V268.5H16.1792V372.476C17.0788 394.856 28.3135 416.942 49.8743 432.05L263.968 582.065C326.853 626.128 414.437 625.732 476.841 581.103Z" fill="url(#paint0_linear_333_277)"></path><path d="M49.8739 326.114C4.93902 294.628 4.85527 232.827 49.7047 201.241L263.624 50.5795C326.643 6.19495 414.642 6.59392 477.176 51.5461L686.09 201.722C730.039 233.314 729.957 294.144 685.922 325.637L476.841 475.166C414.437 519.796 326.853 520.192 263.968 476.128L49.8739 326.114Z" fill="url(#paint1_linear_333_277)"></path><path d="M527.914 280.62L349.792 152.852L381.876 129.116L534.552 238.199L616.975 178.607L643.527 198.303L527.914 280.62Z" fill="white"></path><path d="M353.111 407.378L320.474 433.134L140.139 301.326L178.861 274.055L371.366 330.111L288.943 194.263L328.218 166.992L508 295.265L478.128 317.486L353.111 224.059L425.024 354.352L404.556 371.522L222.562 317.486L353.111 407.378Z" fill="white"></path><defs><linearGradient id="paint0_linear_333_277" x1="367.56" y1="325.566" x2="367.56" y2="748.767" gradientUnits="userSpaceOnUse"><stop stop-color="#CBF3FF"></stop><stop offset="1" stop-color="#CBF3FF" stop-opacity="0"></stop></linearGradient><linearGradient id="paint1_linear_333_277" x1="156.734" y1="113.461" x2="488.495" y2="473.957" gradientUnits="userSpaceOnUse"><stop stop-color="#02C5A8"></stop><stop offset="1" stop-color="#0186A7"></stop></linearGradient></defs></svg>

	

	<span>Core ML</span>
	

	</div></a><a class="mb-1 mr-1 md:mb-1.5 md:mr-1.5 rounded-lg" href="/models?other=whisper"><div class="tag tag-white   ">

	

	<span>whisper</span>
	

	</div></a><a class="mb-1 mr-1 md:mb-1.5 md:mr-1.5 rounded-lg" href="/models?other=asr"><div class="tag tag-white   ">

	

	<span>asr</span>
	

	</div></a><a class="mb-1 mr-1 md:mb-1.5 md:mr-1.5 rounded-lg" href="/models?other=quantized"><div class="tag tag-white   ">

	

	<span>quantized</span>
	

	</div></a></div>

		<div class="flex flex-col-reverse lg:flex-row lg:items-center lg:justify-between"><div class="-mb-px flex h-12 items-center overflow-x-auto overflow-y-hidden "><a class="tab-alternate " href="/argmaxinc/whisperkit-coreml"><svg class="mr-1.5 text-gray-400 flex-none" style="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path class="uim-quaternary" d="M20.23 7.24L12 12L3.77 7.24a1.98 1.98 0 0 1 .7-.71L11 2.76c.62-.35 1.38-.35 2 0l6.53 3.77c.29.173.531.418.7.71z" opacity=".25" fill="currentColor"></path><path class="uim-tertiary" d="M12 12v9.5a2.09 2.09 0 0 1-.91-.21L4.5 17.48a2.003 2.003 0 0 1-1-1.73v-7.5a2.06 2.06 0 0 1 .27-1.01L12 12z" opacity=".5" fill="currentColor"></path><path class="uim-primary" d="M20.5 8.25v7.5a2.003 2.003 0 0 1-1 1.73l-6.62 3.82c-.275.13-.576.198-.88.2V12l8.23-4.76c.175.308.268.656.27 1.01z" fill="currentColor"></path></svg>
			Model card
			
			
		</a><a class="tab-alternate active" href="/argmaxinc/whisperkit-coreml/tree/main"><svg class="mr-1.5 text-gray-400 flex-none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path class="uim-tertiary" d="M21 19h-8a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2zm0-4h-8a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2zm0-8h-8a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2zm0 4h-8a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2z" opacity=".5" fill="currentColor"></path><path class="uim-primary" d="M9 19a1 1 0 0 1-1-1V6a1 1 0 0 1 2 0v12a1 1 0 0 1-1 1zm-6-4.333a1 1 0 0 1-.64-1.769L3.438 12l-1.078-.898a1 1 0 0 1 1.28-1.538l2 1.667a1 1 0 0 1 0 1.538l-2 1.667a.999.999 0 0 1-.64.231z" fill="currentColor"></path></svg>
			<span class="xl:hidden">Files</span>
				<span class="hidden xl:inline">Files and versions</span>
			
			
		</a><a class="tab-alternate " href="/argmaxinc/whisperkit-coreml/discussions"><svg class="mr-1.5 text-gray-400 flex-none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M20.6081 3C21.7684 3 22.8053 3.49196 23.5284 4.38415C23.9756 4.93678 24.4428 5.82749 24.4808 7.16133C24.9674 7.01707 25.4353 6.93643 25.8725 6.93643C26.9833 6.93643 27.9865 7.37587 28.696 8.17411C29.6075 9.19872 30.0124 10.4579 29.8361 11.7177C29.7523 12.3177 29.5581 12.8555 29.2678 13.3534C29.8798 13.8646 30.3306 14.5763 30.5485 15.4322C30.719 16.1032 30.8939 17.5006 29.9808 18.9403C30.0389 19.0342 30.0934 19.1319 30.1442 19.2318C30.6932 20.3074 30.7283 21.5229 30.2439 22.6548C29.5093 24.3704 27.6841 25.7219 24.1397 27.1727C21.9347 28.0753 19.9174 28.6523 19.8994 28.6575C16.9842 29.4379 14.3477 29.8345 12.0653 29.8345C7.87017 29.8345 4.8668 28.508 3.13831 25.8921C0.356375 21.6797 0.754104 17.8269 4.35369 14.1131C6.34591 12.058 7.67023 9.02782 7.94613 8.36275C8.50224 6.39343 9.97271 4.20438 12.4172 4.20438H12.4179C12.6236 4.20438 12.8314 4.2214 13.0364 4.25468C14.107 4.42854 15.0428 5.06476 15.7115 6.02205C16.4331 5.09583 17.134 4.359 17.7682 3.94323C18.7242 3.31737 19.6794 3 20.6081 3ZM20.6081 5.95917C20.2427 5.95917 19.7963 6.1197 19.3039 6.44225C17.7754 7.44319 14.8258 12.6772 13.7458 14.7131C13.3839 15.3952 12.7655 15.6837 12.2086 15.6837C11.1036 15.6837 10.2408 14.5497 12.1076 13.1085C14.9146 10.9402 13.9299 7.39584 12.5898 7.1776C12.5311 7.16799 12.4731 7.16355 12.4172 7.16355C11.1989 7.16355 10.6615 9.33114 10.6615 9.33114C10.6615 9.33114 9.0863 13.4148 6.38031 16.206C3.67434 18.998 3.5346 21.2388 5.50675 24.2246C6.85185 26.2606 9.42666 26.8753 12.0653 26.8753C14.8021 26.8753 17.6077 26.2139 19.1799 25.793C19.2574 25.7723 28.8193 22.984 27.6081 20.6107C27.4046 20.212 27.0693 20.0522 26.6471 20.0522C24.9416 20.0522 21.8393 22.6726 20.5057 22.6726C20.2076 22.6726 19.9976 22.5416 19.9116 22.222C19.3433 20.1173 28.552 19.2325 27.7758 16.1839C27.639 15.6445 27.2677 15.4256 26.746 15.4263C24.4923 15.4263 19.4358 19.5181 18.3759 19.5181C18.2949 19.5181 18.2368 19.4937 18.2053 19.4419C17.6743 18.557 17.9653 17.9394 21.7082 15.6009C25.4511 13.2617 28.0783 11.8545 26.5841 10.1752C26.4121 9.98141 26.1684 9.8956 25.8725 9.8956C23.6001 9.89634 18.2311 14.9403 18.2311 14.9403C18.2311 14.9403 16.7821 16.496 15.9057 16.496C15.7043 16.496 15.533 16.4139 15.4169 16.2112C14.7956 15.1296 21.1879 10.1286 21.5484 8.06535C21.7928 6.66715 21.3771 5.95917 20.6081 5.95917Z" fill="#FF9D00"></path><path d="M5.50686 24.2246C3.53472 21.2387 3.67446 18.9979 6.38043 16.206C9.08641 13.4147 10.6615 9.33111 10.6615 9.33111C10.6615 9.33111 11.2499 6.95933 12.59 7.17757C13.93 7.39581 14.9139 10.9401 12.1069 13.1084C9.29997 15.276 12.6659 16.7489 13.7459 14.713C14.8258 12.6772 17.7747 7.44316 19.304 6.44221C20.8326 5.44128 21.9089 6.00204 21.5484 8.06532C21.188 10.1286 14.795 15.1295 15.4171 16.2118C16.0391 17.2934 18.2312 14.9402 18.2312 14.9402C18.2312 14.9402 25.0907 8.49588 26.5842 10.1752C28.0776 11.8545 25.4512 13.2616 21.7082 15.6008C17.9646 17.9393 17.6744 18.557 18.2054 19.4418C18.7372 20.3266 26.9998 13.1351 27.7759 16.1838C28.5513 19.2324 19.3434 20.1173 19.9117 22.2219C20.48 24.3274 26.3979 18.2382 27.6082 20.6107C28.8193 22.9839 19.2574 25.7722 19.18 25.7929C16.0914 26.62 8.24723 28.3726 5.50686 24.2246Z" fill="#FFD21E"></path></svg>
			Community
			<div class="ml-1.5 flex h-4 min-w-[1rem] items-center justify-center rounded px-1 text-xs leading-none shadow-sm bg-black text-white dark:bg-gray-800 dark:text-gray-200">12
				</div>
			
		</a>
	</div>
	
			


<div class="relative mb-1.5 flex flex-wrap gap-1.5 sm:flex-nowrap lg:mb-0"><div class="order-last sm:order-first"><div class="relative ">
	<button class="btn px-1.5 py-1.5 " type="button">
		
			<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="p-0.5" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><circle cx="16" cy="7" r="3" fill="currentColor"></circle><circle cx="16" cy="16" r="3" fill="currentColor"></circle><circle cx="16" cy="25" r="3" fill="currentColor"></circle></svg>
		
		</button>
	
	
	</div></div>














	
		
		

<div class="relative flex-auto sm:flex-none">
	<button class="from-gray-800! to-black! text-white! gap-1! border-gray-800! dark:border-gray-900!  btn w-full cursor-pointer text-sm" type="button">
		<svg class="mr-1.5 mr-0.5! " xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path fill="currentColor" d="M28 4H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h8v4H8v2h16v-2h-4v-4h8a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2ZM18 28h-4v-4h4Zm10-6H4V6h24Z"></path></svg>
			Use this model
		<svg class="-mr-1 text-gray-500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M16.293 9.293L12 13.586L7.707 9.293l-1.414 1.414L12 16.414l5.707-5.707z" fill="currentColor"></path></svg></button>
	
	
	</div>

</div>
	</div></div></header>


</div>

	
<div class="container relative flex flex-col md:grid md:space-y-0 w-full md:grid-cols-12  space-y-4 md:gap-6 mb-16"><section class="pt-8 border-gray-100 col-span-full"><header class="flex flex-wrap items-center justify-start pb-2 md:justify-end lg:flex-nowrap"><div class="grow max-md:flex max-md:w-full max-md:items-start max-md:justify-between"><div class="relative mr-4 flex min-w-0 basis-auto flex-wrap items-center md:grow md:basis-full lg:basis-auto lg:flex-nowrap"><div class="SVELTE_HYDRATER contents" data-target="BranchSelector" data-props="{&quot;path&quot;:&quot;openai_whisper-small&quot;,&quot;repoName&quot;:&quot;argmaxinc/whisperkit-coreml&quot;,&quot;repoType&quot;:&quot;model&quot;,&quot;rev&quot;:&quot;main&quot;,&quot;refs&quot;:{&quot;branches&quot;:[{&quot;name&quot;:&quot;main&quot;,&quot;ref&quot;:&quot;refs/heads/main&quot;,&quot;targetCommit&quot;:&quot;e0c1da7687d5de5d9b9067f8941c3068558a21ab&quot;}],&quot;tags&quot;:[],&quot;converts&quot;:[]},&quot;view&quot;:&quot;tree&quot;}"><div class="relative mr-4 mb-2">
	<button class="text-sm md:text-base btn w-full cursor-pointer text-sm" type="button">
		<svg class="mr-1.5 text-gray-700 dark:text-gray-400" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" style="transform: rotate(360deg);"><path d="M13 14c-3.36 0-4.46 1.35-4.82 2.24C9.25 16.7 10 17.76 10 19a3 3 0 0 1-3 3a3 3 0 0 1-3-3c0-1.31.83-2.42 2-2.83V7.83A2.99 2.99 0 0 1 4 5a3 3 0 0 1 3-3a3 3 0 0 1 3 3c0 1.31-.83 2.42-2 2.83v5.29c.88-.65 2.16-1.12 4-1.12c2.67 0 3.56-1.34 3.85-2.23A3.006 3.006 0 0 1 14 7a3 3 0 0 1 3-3a3 3 0 0 1 3 3c0 1.34-.88 2.5-2.09 2.86C17.65 11.29 16.68 14 13 14m-6 4a1 1 0 0 0-1 1a1 1 0 0 0 1 1a1 1 0 0 0 1-1a1 1 0 0 0-1-1M7 4a1 1 0 0 0-1 1a1 1 0 0 0 1 1a1 1 0 0 0 1-1a1 1 0 0 0-1-1m10 2a1 1 0 0 0-1 1a1 1 0 0 0 1 1a1 1 0 0 0 1-1a1 1 0 0 0-1-1z" fill="currentColor"></path></svg>
			main
		<svg class="-mr-1 text-gray-500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M16.293 9.293L12 13.586L7.707 9.293l-1.414 1.414L12 16.414l5.707-5.707z" fill="currentColor"></path></svg></button>
	
	
	</div></div>
			<div class="relative mb-2 flex flex-wrap items-center"><a class="truncate text-gray-800 hover:underline" href="/argmaxinc/whisperkit-coreml/tree/main">whisperkit-coreml</a>
				<span class="mx-1 text-gray-300">/</span>
					<span class="dark:text-gray-300">openai_whisper-small</span>
					<div class="SVELTE_HYDRATER contents" data-target="CopyButton" data-props="{&quot;value&quot;:&quot;openai_whisper-small&quot;,&quot;classNames&quot;:&quot;text-xs ml-2&quot;,&quot;title&quot;:&quot;Copy path&quot;}"><button class="relative text-xs ml-2 focus:outline-hidden inline-flex cursor-pointer items-center text-sm  mx-0.5   text-gray-600 " title="Copy path" type="button"><svg class="" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z" transform="translate(0)"></path><path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)"></path><rect fill="none" width="32" height="32"></rect></svg>
	
	</button></div></div></div>
		<div class="SVELTE_HYDRATER contents" data-target="ViewerIndexTreeGoTo" data-props="{&quot;rev&quot;:&quot;main&quot;,&quot;repo&quot;:{&quot;_id&quot;:&quot;65dee9413aec0530562b4f09&quot;,&quot;gitalyUid&quot;:&quot;8ffc19694b8dfd29ebaafed41040596f15c2a6ee94d3e9f8a0bf0f1523bade3c&quot;,&quot;type&quot;:&quot;model&quot;,&quot;name&quot;:&quot;argmaxinc/whisperkit-coreml&quot;,&quot;config&quot;:{&quot;private&quot;:false,&quot;gated&quot;:false,&quot;discussionsDisabled&quot;:false,&quot;duplicationDisabled&quot;:false,&quot;region&quot;:&quot;us&quot;,&quot;gitaly&quot;:{&quot;storage&quot;:&quot;default&quot;,&quot;repoUid&quot;:&quot;8ffc19694b8dfd29ebaafed41040596f15c2a6ee94d3e9f8a0bf0f1523bade3c&quot;,&quot;region&quot;:&quot;us&quot;},&quot;lfs&quot;:{&quot;bucket&quot;:&quot;hf-hub-lfs-us-east-1&quot;,&quot;prefix&quot;:&quot;repos/8f/fc/8ffc19694b8dfd29ebaafed41040596f15c2a6ee94d3e9f8a0bf0f1523bade3c&quot;,&quot;usedStorage&quot;:36524184158},&quot;xet&quot;:{&quot;enabled&quot;:false,&quot;bucket&quot;:&quot;xet-bridge-us&quot;,&quot;service&quot;:&quot;cas&quot;},&quot;lastDiscussion&quot;:12},&quot;updatedAt&quot;:&quot;2024-03-01T18:23:49.045Z&quot;,&quot;authorId&quot;:&quot;65679f2aa704f991da67ae34&quot;,&quot;creatorId&quot;:&quot;64c612d618bc1b4e81023e7b&quot;},&quot;classNames&quot;:&quot;md:hidden&quot;}">

<div class="md:relative md:hidden"><div class="hidden md:flex cursor-not-allowed opacity-60"><input autocomplete="off" disabled class="form-input-alt text-sm! h-8 w-full pl-8 pr-7 focus:shadow-xl dark:bg-gray-950 xl:w-48 [&::-webkit-search-cancel-button]:hidden" name="go-to-file" placeholder="Go to file" spellcheck="false" type="search" value="">
		<svg class="absolute left-2.5 text-gray-400 top-1/2 transform -translate-y-1/2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M30 28.59L22.45 21A11 11 0 1 0 21 22.45L28.59 30zM5 14a9 9 0 1 1 9 9a9 9 0 0 1-9-9z" fill="currentColor"></path></svg>
		<div class="absolute right-2.5 top-1/2 -translate-y-1/2"><svg class="animate-spin text-xs" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" fill="none" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 12 12"><path class="opacity-75" fill-rule="evenodd" clip-rule="evenodd" d="M6 0C2.6862 0 0 2.6862 0 6H1.8C1.8 4.88609 2.2425 3.8178 3.03015 3.03015C3.8178 2.2425 4.88609 1.8 6 1.8V0ZM12 6C12 9.3138 9.3138 12 6 12V10.2C7.11391 10.2 8.1822 9.7575 8.96985 8.96985C9.7575 8.1822 10.2 7.11391 10.2 6H12Z" fill="currentColor"></path><path class="opacity-25" fill-rule="evenodd" clip-rule="evenodd" d="M3.03015 8.96985C3.8178 9.7575 4.88609 10.2 6 10.2V12C2.6862 12 0 9.3138 0 6H1.8C1.8 7.11391 2.2425 8.1822 3.03015 8.96985ZM7.60727 2.11971C7.0977 1.90864 6.55155 1.8 6 1.8V0C9.3138 0 12 2.6862 12 6H10.2C10.2 5.44845 10.0914 4.9023 9.88029 4.39273C9.66922 3.88316 9.35985 3.42016 8.96985 3.03015C8.57984 2.64015 8.11684 2.33078 7.60727 2.11971Z" fill="currentColor"></path></svg></div></div>
	<button class="btn px-1 md:hidden" disabled><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M30 28.59L22.45 21A11 11 0 1 0 21 22.45L28.59 30zM5 14a9 9 0 1 1 9 9a9 9 0 0 1-9-9z" fill="currentColor"></path></svg></button>
	</div></div></div>
	<div class="mb-2 flex w-full flex-wrap items-center justify-between md:w-auto md:justify-end"><div class="SVELTE_HYDRATER contents" data-target="ViewerIndexTreeGoTo" data-props="{&quot;rev&quot;:&quot;main&quot;,&quot;repo&quot;:{&quot;_id&quot;:&quot;65dee9413aec0530562b4f09&quot;,&quot;gitalyUid&quot;:&quot;8ffc19694b8dfd29ebaafed41040596f15c2a6ee94d3e9f8a0bf0f1523bade3c&quot;,&quot;type&quot;:&quot;model&quot;,&quot;name&quot;:&quot;argmaxinc/whisperkit-coreml&quot;,&quot;config&quot;:{&quot;private&quot;:false,&quot;gated&quot;:false,&quot;discussionsDisabled&quot;:false,&quot;duplicationDisabled&quot;:false,&quot;region&quot;:&quot;us&quot;,&quot;gitaly&quot;:{&quot;storage&quot;:&quot;default&quot;,&quot;repoUid&quot;:&quot;8ffc19694b8dfd29ebaafed41040596f15c2a6ee94d3e9f8a0bf0f1523bade3c&quot;,&quot;region&quot;:&quot;us&quot;},&quot;lfs&quot;:{&quot;bucket&quot;:&quot;hf-hub-lfs-us-east-1&quot;,&quot;prefix&quot;:&quot;repos/8f/fc/8ffc19694b8dfd29ebaafed41040596f15c2a6ee94d3e9f8a0bf0f1523bade3c&quot;,&quot;usedStorage&quot;:36524184158},&quot;xet&quot;:{&quot;enabled&quot;:false,&quot;bucket&quot;:&quot;xet-bridge-us&quot;,&quot;service&quot;:&quot;cas&quot;},&quot;lastDiscussion&quot;:12},&quot;updatedAt&quot;:&quot;2024-03-01T18:23:49.045Z&quot;,&quot;authorId&quot;:&quot;65679f2aa704f991da67ae34&quot;,&quot;creatorId&quot;:&quot;64c612d618bc1b4e81023e7b&quot;},&quot;classNames&quot;:&quot;md:mr-4 max-md:hidden&quot;}">

<div class="md:relative md:mr-4 max-md:hidden"><div class="hidden md:flex cursor-not-allowed opacity-60"><input autocomplete="off" disabled class="form-input-alt text-sm! h-8 w-full pl-8 pr-7 focus:shadow-xl dark:bg-gray-950 xl:w-48 [&::-webkit-search-cancel-button]:hidden" name="go-to-file" placeholder="Go to file" spellcheck="false" type="search" value="">
		<svg class="absolute left-2.5 text-gray-400 top-1/2 transform -translate-y-1/2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M30 28.59L22.45 21A11 11 0 1 0 21 22.45L28.59 30zM5 14a9 9 0 1 1 9 9a9 9 0 0 1-9-9z" fill="currentColor"></path></svg>
		<div class="absolute right-2.5 top-1/2 -translate-y-1/2"><svg class="animate-spin text-xs" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" fill="none" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 12 12"><path class="opacity-75" fill-rule="evenodd" clip-rule="evenodd" d="M6 0C2.6862 0 0 2.6862 0 6H1.8C1.8 4.88609 2.2425 3.8178 3.03015 3.03015C3.8178 2.2425 4.88609 1.8 6 1.8V0ZM12 6C12 9.3138 9.3138 12 6 12V10.2C7.11391 10.2 8.1822 9.7575 8.96985 8.96985C9.7575 8.1822 10.2 7.11391 10.2 6H12Z" fill="currentColor"></path><path class="opacity-25" fill-rule="evenodd" clip-rule="evenodd" d="M3.03015 8.96985C3.8178 9.7575 4.88609 10.2 6 10.2V12C2.6862 12 0 9.3138 0 6H1.8C1.8 7.11391 2.2425 8.1822 3.03015 8.96985ZM7.60727 2.11971C7.0977 1.90864 6.55155 1.8 6 1.8V0C9.3138 0 12 2.6862 12 6H10.2C10.2 5.44845 10.0914 4.9023 9.88029 4.39273C9.66922 3.88316 9.35985 3.42016 8.96985 3.03015C8.57984 2.64015 8.11684 2.33078 7.60727 2.11971Z" fill="currentColor"></path></svg></div></div>
	<button class="btn px-1 md:hidden" disabled><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M30 28.59L22.45 21A11 11 0 1 0 21 22.45L28.59 30zM5 14a9 9 0 1 1 9 9a9 9 0 0 1-9-9z" fill="currentColor"></path></svg></button>
	</div></div>
			<a class="mr-2 overflow-hidden md:mr-6 md:grow" href="/argmaxinc/whisperkit-coreml/commits/main/openai_whisper-small"><ul class="flex items-center overflow-hidden justify-start md:justify-end flex-row-reverse   text-sm  "><li class="  -mr-2 h-4 w-4 md:h-5 md:w-5  bg-linear-to-br block flex-none rounded-full border-2 border-white from-gray-300 to-gray-100 dark:border-gray-900 dark:from-gray-600 dark:to-gray-800" title="aotrih" style="content-visibility:auto;"><img class="overflow-hidden rounded-full" alt="" src="https://cdn-avatars.huggingface.co/v1/production/uploads/64c612d618bc1b4e81023e7b/ikt13t_OxCt9AmzbYyAFJ.jpeg">
			</li><li class="  -mr-2 h-4 w-4 md:h-5 md:w-5  bg-linear-to-br block flex-none rounded-full border-2 border-white from-gray-300 to-gray-100 dark:border-gray-900 dark:from-gray-600 dark:to-gray-800" title="iandundas" style="content-visibility:auto;"><img class="overflow-hidden rounded-full" alt="" src="https://cdn-avatars.huggingface.co/v1/production/uploads/655cacc5e24787e8c5a1178c/MI8F2mpLaJ-MSObKCwZ65.jpeg">
			</li><li class="  -mr-2 h-4 w-4 md:h-5 md:w-5  bg-linear-to-br block flex-none rounded-full border-2 border-white from-gray-300 to-gray-100 dark:border-gray-900 dark:from-gray-600 dark:to-gray-800" title="arda-argmax" style="content-visibility:auto;"><img class="overflow-hidden rounded-full" alt="" src="/avatars/6945a0cbe489edd345983bc7294d4d98.svg">
			</li><li class="  -mr-2 h-4 w-4 md:h-5 md:w-5  bg-linear-to-br block flex-none rounded-full border-2 border-white from-gray-300 to-gray-100 dark:border-gray-900 dark:from-gray-600 dark:to-gray-800" title="ardaibis" style="content-visibility:auto;"><img class="overflow-hidden rounded-full" alt="" src="https://cdn-avatars.huggingface.co/v1/production/uploads/667077a1ddd91a91965f95ef/DA391Oo4Ima8vE1JlCQDH.jpeg">
			</li><li class="  -mr-2 h-4 w-4 md:h-5 md:w-5  bg-linear-to-br block flex-none rounded-full border-2 border-white from-gray-300 to-gray-100 dark:border-gray-900 dark:from-gray-600 dark:to-gray-800" title="bpkeene" style="content-visibility:auto;"><img class="overflow-hidden rounded-full" alt="" src="/avatars/e6fba5bc447a0d3bb52cc2acc77a9bb9.svg">
			</li>

		<li class="text-gray-600 hover:text-gray-700 order-first ml-3"><span>8 contributors</span></li></ul></a>
			<a class="btn group mr-0 grow-0 cursor-pointer rounded-full text-sm md:px-4 md:text-base" href="/argmaxinc/whisperkit-coreml/commits/main/openai_whisper-small"><svg class="mr-1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32" style="transform: rotate(360deg);"><path d="M16 4C9.383 4 4 9.383 4 16s5.383 12 12 12s12-5.383 12-12S22.617 4 16 4zm0 2c5.535 0 10 4.465 10 10s-4.465 10-10 10S6 21.535 6 16S10.465 6 16 6zm-1 2v9h7v-2h-5V8z" fill="currentColor"></path></svg>
				<span class="mr-1 text-gray-600">History:</span>
					<span class="group-hover:underline">5 commits</span></a></div>
	</header>

				<div class="SVELTE_HYDRATER contents" data-target="UnsafeBanner" data-props="{&quot;classNames&quot;:&quot;mb-3&quot;,&quot;repoId&quot;:&quot;argmaxinc/whisperkit-coreml&quot;,&quot;repoType&quot;:&quot;model&quot;,&quot;revision&quot;:&quot;main&quot;,&quot;securityRepoStatus&quot;:{&quot;scansDone&quot;:true,&quot;filesWithIssues&quot;:[]}}"></div>

				<div class="SVELTE_HYDRATER contents" data-target="LastCommit" data-props="{&quot;commitLast&quot;:{&quot;date&quot;:&quot;2024-10-12T02:12:51.000Z&quot;,&quot;verified&quot;:&quot;verified&quot;,&quot;subject&quot;:&quot;update protobufs - noncompressed (#7)&quot;,&quot;authors&quot;:[{&quot;_id&quot;:&quot;655cef564660c63af80a70f4&quot;,&quot;avatar&quot;:&quot;/avatars/2600eacf600b10701528ee2d4fa178af.svg&quot;,&quot;isHf&quot;:false,&quot;user&quot;:&quot;b-argmax&quot;}],&quot;commit&quot;:{&quot;id&quot;:&quot;3388c46a312ab1e9f46fe4c9f6697f948d59e03e&quot;,&quot;parentIds&quot;:[&quot;48fa831f590bf98c1b2580c531bb5eedae4c56d5&quot;]},&quot;title&quot;:&quot;update protobufs - noncompressed (<a href=\&quot;/argmaxinc/whisperkit-coreml/discussions/7\&quot;>#7</a>)&quot;},&quot;repo&quot;:{&quot;name&quot;:&quot;argmaxinc/whisperkit-coreml&quot;,&quot;type&quot;:&quot;model&quot;}}"><div class="from-gray-100-to-white bg-linear-to-t flex flex-wrap items-baseline rounded-t-lg border border-b-0 px-3 py-2 dark:border-gray-800"><img class="mr-2.5 mt-0.5 h-4 w-4 self-center rounded-full" alt="b-argmax's picture" src="/avatars/2600eacf600b10701528ee2d4fa178af.svg">
			<div class="mr-4 flex flex-none items-center truncate"><a class="hover:underline" href="/b-argmax">b-argmax
					</a>
				
			</div>
		<div class="mr-4 truncate font-mono text-sm text-gray-500 hover:prose-a:underline"><!-- HTML_TAG_START -->update protobufs - noncompressed (<a href="/argmaxinc/whisperkit-coreml/discussions/7">#7</a>)<!-- HTML_TAG_END --></div>
		<a class="rounded-sm border bg-gray-50 px-1.5 text-sm hover:underline dark:border-gray-800 dark:bg-gray-900" href="/argmaxinc/whisperkit-coreml/commit/3388c46a312ab1e9f46fe4c9f6697f948d59e03e">3388c46</a>
		<span class="mx-2 text-green-500 dark:text-green-600 px-1.5 border-green-100 dark:border-green-800 rounded-full border text-xs uppercase" title="This commit is signed and the signature is verified">verified</span>
		<time class="ml-auto hidden flex-none truncate pl-2 text-gray-500 dark:text-gray-400 lg:block" datetime="2024-10-12T02:12:51" title="Sat, 12 Oct 2024 02:12:51 GMT">5 months ago</time></div></div>
				<div class="SVELTE_HYDRATER contents" data-target="ViewerIndexTreeList" data-props="{&quot;context&quot;:{&quot;rev&quot;:&quot;main&quot;,&quot;path&quot;:&quot;openai_whisper-small&quot;,&quot;repo&quot;:{&quot;_id&quot;:&quot;65dee9413aec0530562b4f09&quot;,&quot;gitalyUid&quot;:&quot;8ffc19694b8dfd29ebaafed41040596f15c2a6ee94d3e9f8a0bf0f1523bade3c&quot;,&quot;type&quot;:&quot;model&quot;,&quot;name&quot;:&quot;argmaxinc/whisperkit-coreml&quot;,&quot;config&quot;:{&quot;private&quot;:false,&quot;gated&quot;:false,&quot;discussionsDisabled&quot;:false,&quot;duplicationDisabled&quot;:false,&quot;region&quot;:&quot;us&quot;,&quot;gitaly&quot;:{&quot;storage&quot;:&quot;default&quot;,&quot;repoUid&quot;:&quot;8ffc19694b8dfd29ebaafed41040596f15c2a6ee94d3e9f8a0bf0f1523bade3c&quot;,&quot;region&quot;:&quot;us&quot;},&quot;lfs&quot;:{&quot;bucket&quot;:&quot;hf-hub-lfs-us-east-1&quot;,&quot;prefix&quot;:&quot;repos/8f/fc/8ffc19694b8dfd29ebaafed41040596f15c2a6ee94d3e9f8a0bf0f1523bade3c&quot;,&quot;usedStorage&quot;:36524184158},&quot;xet&quot;:{&quot;enabled&quot;:false,&quot;bucket&quot;:&quot;xet-bridge-us&quot;,&quot;service&quot;:&quot;cas&quot;},&quot;lastDiscussion&quot;:12},&quot;updatedAt&quot;:&quot;2024-03-01T18:23:49.045Z&quot;,&quot;authorId&quot;:&quot;65679f2aa704f991da67ae34&quot;,&quot;creatorId&quot;:&quot;64c612d618bc1b4e81023e7b&quot;},&quot;commit&quot;:{&quot;id&quot;:&quot;e0c1da7687d5de5d9b9067f8941c3068558a21ab&quot;,&quot;subject&quot;:&quot;For M1-series macs: remove `openai_whisper-large-v3-v20240930` from supported models, and change default to `openai_whisper-large-v3-v20240930_626MB` (#12)&quot;,&quot;body&quot;:&quot;For M1-series macs: remove `openai_whisper-large-v3-v20240930` from supported models, and change default to `openai_whisper-large-v3-v20240930_626MB` (#12)\n\n\n- For M1-series macs: remove `openai_whisper-large-v3-v20240930` from supported models, and change default to `openai_whisper-large-v3-v20240930_626MB` (707247e689e23f68dcf0e7c81dbdc870de0890d4)\n\n\nCo-authored-by: Ian Dundas <iandundas@users.noreply.huggingface.co>\n&quot;,&quot;author&quot;:{&quot;name&quot;:&quot;Atila&quot;,&quot;date&quot;:&quot;2025-02-03T16:52:29.000Z&quot;,&quot;email&quot;:&quot;aotrih@users.noreply.huggingface.co&quot;,&quot;timezone&quot;:&quot;+0000&quot;},&quot;committer&quot;:{&quot;name&quot;:&quot;system&quot;,&quot;date&quot;:&quot;2025-02-03T16:52:29.000Z&quot;,&quot;email&quot;:&quot;system@huggingface.co&quot;,&quot;timezone&quot;:&quot;+0000&quot;},&quot;parentIds&quot;:[&quot;f92303ce21b6b2db139d6100dd47de333d562dfe&quot;],&quot;bodySize&quot;:423,&quot;signatureType&quot;:1,&quot;treeId&quot;:&quot;b3a5c0bd29707d56ad3777d83ce03332b9bdf0c6&quot;,&quot;trailers&quot;:[{&quot;key&quot;:&quot;Co-authored-by&quot;,&quot;value&quot;:&quot;Ian Dundas <iandundas@users.noreply.huggingface.co>&quot;}],&quot;referencedBy&quot;:[],&quot;encoding&quot;:&quot;&quot;}},&quot;entries&quot;:[{&quot;type&quot;:&quot;directory&quot;,&quot;oid&quot;:&quot;e6bd6e3c2b130d9ee2fac18e4724a2363f9fc8f9&quot;,&quot;size&quot;:0,&quot;path&quot;:&quot;openai_whisper-small/AudioEncoder.mlmodelc&quot;,&quot;lastCommit&quot;:{&quot;id&quot;:&quot;326f1ec08a45f8c4dbe069d88d1f7acdb3a3556f&quot;,&quot;title&quot;:&quot;mlmodel_loading (#1)&quot;,&quot;date&quot;:&quot;2024-09-16T15:41:54.000Z&quot;}},{&quot;type&quot;:&quot;directory&quot;,&quot;oid&quot;:&quot;fd0ba736b12bd226602d26e767507c9045c60d02&quot;,&quot;size&quot;:0,&quot;path&quot;:&quot;openai_whisper-small/MelSpectrogram.mlmodelc&quot;,&quot;lastCommit&quot;:{&quot;id&quot;:&quot;e8139cbd7328eac3e88291f7c756f7df7572a70b&quot;,&quot;title&quot;:&quot;whisperkittools-81ba3338e9e50affc6da63b97dd26b9a9d34b2ff generated files: openai_whisper-small&quot;,&quot;date&quot;:&quot;2024-03-23T02:19:52.000Z&quot;}},{&quot;type&quot;:&quot;directory&quot;,&quot;oid&quot;:&quot;8eb6b6b0bfa11810aecca1aadd1163156540f6d6&quot;,&quot;size&quot;:0,&quot;path&quot;:&quot;openai_whisper-small/TextDecoder.mlmodelc&quot;,&quot;lastCommit&quot;:{&quot;id&quot;:&quot;3388c46a312ab1e9f46fe4c9f6697f948d59e03e&quot;,&quot;title&quot;:&quot;update protobufs - noncompressed (#7)&quot;,&quot;date&quot;:&quot;2024-10-12T02:12:51.000Z&quot;}},{&quot;type&quot;:&quot;file&quot;,&quot;oid&quot;:&quot;9dee569cf0c20925208ec84fecbb95e873f8bf24&quot;,&quot;size&quot;:1456,&quot;path&quot;:&quot;openai_whisper-small/config.json&quot;,&quot;lastCommit&quot;:{&quot;id&quot;:&quot;703d187d049785a9b1b9c9d16b859002569ea273&quot;,&quot;title&quot;:&quot;whisperkittools-192190cedeefc4d317d13c2196dc29f9a9f99628 generated files: openai_whisper-small&quot;,&quot;date&quot;:&quot;2024-02-29T02:57:17.000Z&quot;},&quot;securityFileStatus&quot;:{&quot;status&quot;:&quot;safe&quot;,&quot;protectAiScan&quot;:{&quot;status&quot;:&quot;safe&quot;,&quot;message&quot;:&quot;This file has no security findings.&quot;,&quot;reportLink&quot;:&quot;https://protectai.com/insights/models/argmaxinc/whisperkit-coreml/e0c1da7687d5de5d9b9067f8941c3068558a21ab/files?blob-id=9dee569cf0c20925208ec84fecbb95e873f8bf24&amp;utm_source=huggingface&quot;},&quot;avScan&quot;:{&quot;status&quot;:&quot;safe&quot;},&quot;pickleImportScan&quot;:{&quot;status&quot;:&quot;unscanned&quot;,&quot;pickleImports&quot;:[]},&quot;jFrogScan&quot;:{&quot;status&quot;:&quot;unscanned&quot;,&quot;message&quot;:&quot;Not a machine-learning model&quot;,&quot;reportLink&quot;:&quot;&quot;,&quot;reportLabel&quot;:&quot;&quot;}}},{&quot;type&quot;:&quot;file&quot;,&quot;oid&quot;:&quot;cdd26273f9cd1ab8ecda49f5b8c033134c61cb4a&quot;,&quot;size&quot;:2779,&quot;path&quot;:&quot;openai_whisper-small/generation_config.json&quot;,&quot;lastCommit&quot;:{&quot;id&quot;:&quot;703d187d049785a9b1b9c9d16b859002569ea273&quot;,&quot;title&quot;:&quot;whisperkittools-192190cedeefc4d317d13c2196dc29f9a9f99628 generated files: openai_whisper-small&quot;,&quot;date&quot;:&quot;2024-02-29T02:57:17.000Z&quot;},&quot;securityFileStatus&quot;:{&quot;status&quot;:&quot;safe&quot;,&quot;protectAiScan&quot;:{&quot;status&quot;:&quot;safe&quot;,&quot;message&quot;:&quot;This file has no security findings.&quot;,&quot;reportLink&quot;:&quot;https://protectai.com/insights/models/argmaxinc/whisperkit-coreml/e0c1da7687d5de5d9b9067f8941c3068558a21ab/files?blob-id=cdd26273f9cd1ab8ecda49f5b8c033134c61cb4a&amp;utm_source=huggingface&quot;},&quot;avScan&quot;:{&quot;status&quot;:&quot;safe&quot;},&quot;pickleImportScan&quot;:{&quot;status&quot;:&quot;unscanned&quot;,&quot;pickleImports&quot;:[]},&quot;jFrogScan&quot;:{&quot;status&quot;:&quot;unscanned&quot;,&quot;message&quot;:&quot;Not a machine-learning model&quot;,&quot;reportLink&quot;:&quot;&quot;,&quot;reportLabel&quot;:&quot;&quot;}}}],&quot;nextURL&quot;:null,&quot;query&quot;:{},&quot;objectInfo&quot;:{&quot;author&quot;:&quot;argmaxinc&quot;,&quot;cardData&quot;:{&quot;pretty_name&quot;:&quot;WhisperKit&quot;,&quot;viewer&quot;:false,&quot;library_name&quot;:&quot;whisperkit&quot;,&quot;tags&quot;:[&quot;whisper&quot;,&quot;whisperkit&quot;,&quot;coreml&quot;,&quot;asr&quot;,&quot;quantized&quot;,&quot;automatic-speech-recognition&quot;]},&quot;cardExists&quot;:true,&quot;config&quot;:{},&quot;createdAt&quot;:&quot;2024-02-28T08:05:21.000Z&quot;,&quot;discussionsDisabled&quot;:false,&quot;downloads&quot;:250218,&quot;downloadsAllTime&quot;:821954,&quot;id&quot;:&quot;argmaxinc/whisperkit-coreml&quot;,&quot;isLikedByUser&quot;:false,&quot;availableInferenceProviders&quot;:[],&quot;inference&quot;:&quot;&quot;,&quot;lastModified&quot;:&quot;2025-02-03T16:52:29.000Z&quot;,&quot;likes&quot;:112,&quot;pipeline_tag&quot;:&quot;automatic-speech-recognition&quot;,&quot;library_name&quot;:&quot;whisperkit&quot;,&quot;librariesOther&quot;:[],&quot;trackDownloads&quot;:true,&quot;model-index&quot;:null,&quot;private&quot;:false,&quot;repoType&quot;:&quot;model&quot;,&quot;gated&quot;:false,&quot;pwcLink&quot;:{&quot;error&quot;:&quot;Unknown error, can't generate link to Papers With Code.&quot;},&quot;tags&quot;:[&quot;whisperkit&quot;,&quot;coreml&quot;,&quot;whisper&quot;,&quot;asr&quot;,&quot;quantized&quot;,&quot;automatic-speech-recognition&quot;,&quot;region:us&quot;],&quot;tag_objs&quot;:[{&quot;id&quot;:&quot;automatic-speech-recognition&quot;,&quot;label&quot;:&quot;Automatic Speech Recognition&quot;,&quot;type&quot;:&quot;pipeline_tag&quot;,&quot;subType&quot;:&quot;audio&quot;},{&quot;id&quot;:&quot;whisperkit&quot;,&quot;label&quot;:&quot;WhisperKit&quot;,&quot;type&quot;:&quot;library&quot;},{&quot;id&quot;:&quot;coreml&quot;,&quot;label&quot;:&quot;Core ML&quot;,&quot;type&quot;:&quot;library&quot;},{&quot;id&quot;:&quot;whisper&quot;,&quot;label&quot;:&quot;whisper&quot;,&quot;type&quot;:&quot;other&quot;},{&quot;id&quot;:&quot;asr&quot;,&quot;label&quot;:&quot;asr&quot;,&quot;type&quot;:&quot;other&quot;},{&quot;id&quot;:&quot;quantized&quot;,&quot;label&quot;:&quot;quantized&quot;,&quot;type&quot;:&quot;other&quot;},{&quot;type&quot;:&quot;region&quot;,&quot;label&quot;:&quot;🇺🇸 Region: US&quot;,&quot;id&quot;:&quot;region:us&quot;}],&quot;hasBlockedOids&quot;:false,&quot;region&quot;:&quot;us&quot;,&quot;isQuantized&quot;:false,&quot;inferenceStatic&quot;:&quot;pipeline-library-pair-not-supported&quot;}}">

<ul class="mb-8 rounded-b-lg border border-t-0 dark:border-gray-800 dark:bg-gray-900"><li class="grid h-10 grid-cols-12 place-content-center gap-x-3 border-t px-3 dark:border-gray-800"><a class="col-span-8 flex items-center hover:underline md:col-span-5 lg:col-span-4" href="/argmaxinc/whisperkit-coreml/tree/main/openai_whisper-small/AudioEncoder.mlmodelc"><svg class="flex-none mr-2 text-blue-400 fill-current" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M10 4H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="currentColor"></path></svg>
				<span class="truncate">AudioEncoder.mlmodelc</span></a>
			<div class="col-span-4 md:col-span-2"></div>
			<a class="col-span-4 hidden font-mono text-sm text-gray-400 hover:underline md:col-span-3 md:flex lg:col-span-4" href="/argmaxinc/whisperkit-coreml/commit/326f1ec08a45f8c4dbe069d88d1f7acdb3a3556f"><span class="truncate">mlmodel_loading (#1)</span></a>
				<a class="col-span-2 hidden truncate text-right text-gray-400 md:block" href="/argmaxinc/whisperkit-coreml/commit/326f1ec08a45f8c4dbe069d88d1f7acdb3a3556f"><time datetime="2024-09-16T15:41:54" title="Mon, 16 Sep 2024 15:41:54 GMT">6 months ago</time>
				</a>
		</li><li class="grid h-10 grid-cols-12 place-content-center gap-x-3 border-t px-3 dark:border-gray-800"><a class="col-span-8 flex items-center hover:underline md:col-span-5 lg:col-span-4" href="/argmaxinc/whisperkit-coreml/tree/main/openai_whisper-small/MelSpectrogram.mlmodelc"><svg class="flex-none mr-2 text-blue-400 fill-current" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M10 4H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="currentColor"></path></svg>
				<span class="truncate">MelSpectrogram.mlmodelc</span></a>
			<div class="col-span-4 md:col-span-2"></div>
			<a class="col-span-4 hidden font-mono text-sm text-gray-400 hover:underline md:col-span-3 md:flex lg:col-span-4" href="/argmaxinc/whisperkit-coreml/commit/e8139cbd7328eac3e88291f7c756f7df7572a70b"><span class="truncate">whisperkittools-81ba3338e9e50affc6da63b97dd26b9a9d34b2ff generated files: openai_whisper-small</span></a>
				<a class="col-span-2 hidden truncate text-right text-gray-400 md:block" href="/argmaxinc/whisperkit-coreml/commit/e8139cbd7328eac3e88291f7c756f7df7572a70b"><time datetime="2024-03-23T02:19:52" title="Sat, 23 Mar 2024 02:19:52 GMT">12 months ago</time>
				</a>
		</li><li class="grid h-10 grid-cols-12 place-content-center gap-x-3 border-t px-3 dark:border-gray-800"><a class="col-span-8 flex items-center hover:underline md:col-span-5 lg:col-span-4" href="/argmaxinc/whisperkit-coreml/tree/main/openai_whisper-small/TextDecoder.mlmodelc"><svg class="flex-none mr-2 text-blue-400 fill-current" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path d="M10 4H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="currentColor"></path></svg>
				<span class="truncate">TextDecoder.mlmodelc</span></a>
			<div class="col-span-4 md:col-span-2"></div>
			<a class="col-span-4 hidden font-mono text-sm text-gray-400 hover:underline md:col-span-3 md:flex lg:col-span-4" href="/argmaxinc/whisperkit-coreml/commit/3388c46a312ab1e9f46fe4c9f6697f948d59e03e"><span class="truncate">update protobufs - noncompressed (#7)</span></a>
				<a class="col-span-2 hidden truncate text-right text-gray-400 md:block" href="/argmaxinc/whisperkit-coreml/commit/3388c46a312ab1e9f46fe4c9f6697f948d59e03e"><time datetime="2024-10-12T02:12:51" title="Sat, 12 Oct 2024 02:12:51 GMT">5 months ago</time>
				</a>
		</li>

	
		<li class="relative grid h-10 grid-cols-12 place-content-center gap-x-3 border-t px-3 dark:border-gray-800"><div class="col-span-8 flex items-center md:col-span-4"><a class="group flex items-center truncate" href="/argmaxinc/whisperkit-coreml/blob/main/openai_whisper-small/config.json"><svg class="flex-none mr-2 text-gray-300 fill-current" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M25.7 9.3l-7-7A.908.908 0 0 0 18 2H8a2.006 2.006 0 0 0-2 2v24a2.006 2.006 0 0 0 2 2h16a2.006 2.006 0 0 0 2-2V10a.908.908 0 0 0-.3-.7zM18 4.4l5.6 5.6H18zM24 28H8V4h8v6a2.006 2.006 0 0 0 2 2h6z" fill="currentColor"></path></svg>
					<span class="truncate group-hover:underline">config.json</span></a>
				<div class="sm:relative ml-1.5"><button class="flex h-[1.125rem] select-none items-center gap-0.5 rounded border pl-0.5 pr-0.5 text-xs leading-tight text-gray-400 hover:cursor-pointer text-gray-400 hover:border-gray-200 hover:bg-gray-50 hover:text-gray-500 dark:border-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200 translate-y-px"><svg class="flex-none" width="1em" height="1em" viewBox="0 0 22 28" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.3634 10.3639C15.8486 10.8491 15.8486 11.6357 15.3634 12.1209L10.9292 16.5551C10.6058 16.8785 10.0814 16.8785 9.7579 16.5551L7.03051 13.8277C6.54532 13.3425 6.54532 12.5558 7.03051 12.0707C7.51569 11.5855 8.30234 11.5855 8.78752 12.0707L9.7579 13.041C10.0814 13.3645 10.6058 13.3645 10.9292 13.041L13.6064 10.3639C14.0916 9.8787 14.8782 9.8787 15.3634 10.3639Z" fill="currentColor"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M10.6666 27.12C4.93329 25.28 0 19.2267 0 12.7867V6.52001C0 5.40001 0.693334 4.41334 1.73333 4.01334L9.73333 1.01334C10.3333 0.786673 11 0.786673 11.6 1.02667L19.6 4.02667C20.1083 4.21658 20.5465 4.55701 20.8562 5.00252C21.1659 5.44803 21.3324 5.97742 21.3333 6.52001V12.7867C21.3333 19.24 16.4 25.28 10.6666 27.12Z" fill="currentColor" fill-opacity="0.22"></path><path d="M10.0845 1.94967L10.0867 1.94881C10.4587 1.8083 10.8666 1.81036 11.2286 1.95515L11.2387 1.95919L11.2489 1.963L19.2489 4.963L19.25 4.96342C19.5677 5.08211 19.8416 5.29488 20.0351 5.57333C20.2285 5.85151 20.3326 6.18203 20.3333 6.52082C20.3333 6.52113 20.3333 6.52144 20.3333 6.52176L20.3333 12.7867C20.3333 18.6535 15.8922 24.2319 10.6666 26.0652C5.44153 24.2316 1 18.6409 1 12.7867V6.52001C1 5.82357 1.42893 5.20343 2.08883 4.94803L10.0845 1.94967Z" stroke="currentColor" stroke-opacity="0.30" stroke-width="2"></path></svg>

			<span class="mr-0.5 max-sm:hidden">Safe</span></button>

	</div>
					

				</div>
			<a class="group col-span-4 flex items-center justify-self-end truncate text-right font-mono text-[0.8rem] leading-6 text-gray-400 md:col-span-3 lg:col-span-2 xl:pr-10" title="Download file" download href="/argmaxinc/whisperkit-coreml/resolve/main/openai_whisper-small/config.json?download=true"><span class="truncate max-sm:text-xs">1.46 kB</span>

				

				<div class="group-hover:shadow-xs ml-2 flex h-5 w-5 items-center justify-center rounded-sm border text-gray-500 group-hover:bg-gray-50 group-hover:text-gray-800 dark:border-gray-800 dark:group-hover:bg-gray-800 dark:group-hover:text-gray-300 xl:ml-4"><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" viewBox="0 0 32 32"><path fill="currentColor" d="M26 24v4H6v-4H4v4a2 2 0 0 0 2 2h20a2 2 0 0 0 2-2v-4zm0-10l-1.41-1.41L17 20.17V2h-2v18.17l-7.59-7.58L6 14l10 10l10-10z"></path></svg>
				</div></a>
			<a class="col-span-4 hidden items-center font-mono text-sm text-gray-400 hover:underline md:col-span-3 md:flex lg:col-span-4" href="/argmaxinc/whisperkit-coreml/commit/703d187d049785a9b1b9c9d16b859002569ea273"><span class="truncate">whisperkittools-192190cedeefc4d317d13c2196dc29f9a9f99628 generated files: openai_whisper-small</span></a>
				<a class="col-span-2 hidden truncate text-right text-gray-400 md:block" href="/argmaxinc/whisperkit-coreml/commit/703d187d049785a9b1b9c9d16b859002569ea273"><time datetime="2024-02-29T02:57:17" title="Thu, 29 Feb 2024 02:57:17 GMT">about 1 year ago</time>
				</a>
		</li>
		<li class="relative grid h-10 grid-cols-12 place-content-center gap-x-3 border-t px-3 dark:border-gray-800"><div class="col-span-8 flex items-center md:col-span-4"><a class="group flex items-center truncate" href="/argmaxinc/whisperkit-coreml/blob/main/openai_whisper-small/generation_config.json"><svg class="flex-none mr-2 text-gray-300 fill-current" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M25.7 9.3l-7-7A.908.908 0 0 0 18 2H8a2.006 2.006 0 0 0-2 2v24a2.006 2.006 0 0 0 2 2h16a2.006 2.006 0 0 0 2-2V10a.908.908 0 0 0-.3-.7zM18 4.4l5.6 5.6H18zM24 28H8V4h8v6a2.006 2.006 0 0 0 2 2h6z" fill="currentColor"></path></svg>
					<span class="truncate group-hover:underline">generation_config.json</span></a>
				<div class="sm:relative ml-1.5"><button class="flex h-[1.125rem] select-none items-center gap-0.5 rounded border pl-0.5 pr-0.5 text-xs leading-tight text-gray-400 hover:cursor-pointer text-gray-400 hover:border-gray-200 hover:bg-gray-50 hover:text-gray-500 dark:border-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200 translate-y-px"><svg class="flex-none" width="1em" height="1em" viewBox="0 0 22 28" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.3634 10.3639C15.8486 10.8491 15.8486 11.6357 15.3634 12.1209L10.9292 16.5551C10.6058 16.8785 10.0814 16.8785 9.7579 16.5551L7.03051 13.8277C6.54532 13.3425 6.54532 12.5558 7.03051 12.0707C7.51569 11.5855 8.30234 11.5855 8.78752 12.0707L9.7579 13.041C10.0814 13.3645 10.6058 13.3645 10.9292 13.041L13.6064 10.3639C14.0916 9.8787 14.8782 9.8787 15.3634 10.3639Z" fill="currentColor"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M10.6666 27.12C4.93329 25.28 0 19.2267 0 12.7867V6.52001C0 5.40001 0.693334 4.41334 1.73333 4.01334L9.73333 1.01334C10.3333 0.786673 11 0.786673 11.6 1.02667L19.6 4.02667C20.1083 4.21658 20.5465 4.55701 20.8562 5.00252C21.1659 5.44803 21.3324 5.97742 21.3333 6.52001V12.7867C21.3333 19.24 16.4 25.28 10.6666 27.12Z" fill="currentColor" fill-opacity="0.22"></path><path d="M10.0845 1.94967L10.0867 1.94881C10.4587 1.8083 10.8666 1.81036 11.2286 1.95515L11.2387 1.95919L11.2489 1.963L19.2489 4.963L19.25 4.96342C19.5677 5.08211 19.8416 5.29488 20.0351 5.57333C20.2285 5.85151 20.3326 6.18203 20.3333 6.52082C20.3333 6.52113 20.3333 6.52144 20.3333 6.52176L20.3333 12.7867C20.3333 18.6535 15.8922 24.2319 10.6666 26.0652C5.44153 24.2316 1 18.6409 1 12.7867V6.52001C1 5.82357 1.42893 5.20343 2.08883 4.94803L10.0845 1.94967Z" stroke="currentColor" stroke-opacity="0.30" stroke-width="2"></path></svg>

			<span class="mr-0.5 max-sm:hidden">Safe</span></button>

	</div>
					

				</div>
			<a class="group col-span-4 flex items-center justify-self-end truncate text-right font-mono text-[0.8rem] leading-6 text-gray-400 md:col-span-3 lg:col-span-2 xl:pr-10" title="Download file" download href="/argmaxinc/whisperkit-coreml/resolve/main/openai_whisper-small/generation_config.json?download=true"><span class="truncate max-sm:text-xs">2.78 kB</span>

				

				<div class="group-hover:shadow-xs ml-2 flex h-5 w-5 items-center justify-center rounded-sm border text-gray-500 group-hover:bg-gray-50 group-hover:text-gray-800 dark:border-gray-800 dark:group-hover:bg-gray-800 dark:group-hover:text-gray-300 xl:ml-4"><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" viewBox="0 0 32 32"><path fill="currentColor" d="M26 24v4H6v-4H4v4a2 2 0 0 0 2 2h20a2 2 0 0 0 2-2v-4zm0-10l-1.41-1.41L17 20.17V2h-2v18.17l-7.59-7.58L6 14l10 10l10-10z"></path></svg>
				</div></a>
			<a class="col-span-4 hidden items-center font-mono text-sm text-gray-400 hover:underline md:col-span-3 md:flex lg:col-span-4" href="/argmaxinc/whisperkit-coreml/commit/703d187d049785a9b1b9c9d16b859002569ea273"><span class="truncate">whisperkittools-192190cedeefc4d317d13c2196dc29f9a9f99628 generated files: openai_whisper-small</span></a>
				<a class="col-span-2 hidden truncate text-right text-gray-400 md:block" href="/argmaxinc/whisperkit-coreml/commit/703d187d049785a9b1b9c9d16b859002569ea273"><time datetime="2024-02-29T02:57:17" title="Thu, 29 Feb 2024 02:57:17 GMT">about 1 year ago</time>
				</a>
		</li>
	</ul></div></section></div></main>

	</div>

		<script>
			import("\/front\/build\/kube-272d363\/index.js");
			window.moonSha = "kube-272d363\/";
			window.__hf_deferred = {};
		</script>

		<!-- Stripe -->
		<script>
			if (["hf.co", "huggingface.co"].includes(window.location.hostname)) {
				const script = document.createElement("script");
				script.src = "https://js.stripe.com/v3/";
				script.async = true;
				document.head.appendChild(script);
			}
		</script>
	</body>
</html>

================
File: OpenAIManager.swift
================
import Combine
import Foundation

enum APIProvider: String, CaseIterable, Identifiable {
  case openAI = "OpenAI"
  case openRouter = "OpenRouter"
  
  var id: String { self.rawValue }
  
  var baseURL: String {
    switch self {
    case .openAI: return "https://api.openai.com/v1"
    case .openRouter: return "https://openrouter.ai/api/v1"
    }
  }
  
  var displayName: String {
    return self.rawValue
  }
  
  var apiKeyPrefix: String {
    switch self {
    case .openAI: return "sk-"
    case .openRouter: return "sk-"
    }
  }
  
  var apiKeyMinLength: Int {
    switch self {
    case .openAI: return 20
    case .openRouter: return 20
    }
  }
  
  var keychainKey: String {
    switch self {
    case .openAI: return "OpenAIAPIKey"
    case .openRouter: return "OpenRouterAPIKey"
    }
  }
}

enum OpenAIModel: String, CaseIterable, Identifiable {
  case gpt4o = "gpt-4o"
  case gpt4oMini = "gpt-4o-mini"
  case o3Mini = "o3-mini"
  case o1Mini = "o1-mini"
  case o1 = "o1"

  var id: String { self.rawValue }

  var displayName: String {
    switch self {
    case .gpt4o: return "GPT-4o"
    case .gpt4oMini: return "GPT-4o Mini"
    case .o3Mini: return "O3 Mini"
    case .o1: return "O1"
    case .o1Mini: return "O1 Mini"
    }
  }

  var model: String {
    switch self {
    case .gpt4o: return "gpt-4o"
    case .gpt4oMini: return "gpt-4o-mini"
    case .o3Mini: return "o3-mini"
    case .o1Mini: return "o1-mini"
    case .o1: return "o1"
    }
  }
}

class OpenAIManager: ObservableObject {
  static let shared = OpenAIManager()

  @Published var apiKey: String = ""
  @Published var openRouterApiKey: String = ""
  @Published var selectedModel: OpenAIModel = .gpt4o
  @Published var selectedProvider: APIProvider = .openAI
  @Published var openRouterModelName: String = ""
  @Published var openRouterSiteUrl: String = "https://sessionscribe.app"
  @Published var openRouterSiteName: String = "SessionScribe"

  private var cancellables = Set<AnyCancellable>()
  
  // Static function to check if API key exists and is valid
  static func hasValidAPIKey() -> Bool {
    let provider = UserDefaults.standard.string(forKey: "selectedProvider") ?? APIProvider.openAI.rawValue
    let apiProvider = APIProvider(rawValue: provider) ?? .openAI
    
    guard let apiKey = KeychainManager.getKey(for: apiProvider.keychainKey) else {
      return false
    }
    
    let trimmedKey = apiKey.trimmingCharacters(in: .whitespacesAndNewlines)
    return !trimmedKey.isEmpty && trimmedKey.hasPrefix(apiProvider.apiKeyPrefix) && trimmedKey.count >= apiProvider.apiKeyMinLength
  }
  
  var currentApiKey: String {
    switch selectedProvider {
    case .openAI: return apiKey
    case .openRouter: return openRouterApiKey
    }
  }

  init() {
    // Load provider from UserDefaults
    if let providerString = UserDefaults.standard.string(forKey: "selectedProvider"),
       let provider = APIProvider(rawValue: providerString) {
      self.selectedProvider = provider
    }
    
    // Load API keys from keychain
    if let savedOpenAIKey = KeychainManager.getKey(for: APIProvider.openAI.keychainKey) {
      self.apiKey = savedOpenAIKey
    }
    
    if let savedOpenRouterKey = KeychainManager.getKey(for: APIProvider.openRouter.keychainKey) {
      self.openRouterApiKey = savedOpenRouterKey
    }

    // Load selected model from UserDefaults
    if let modelString = UserDefaults.standard.string(forKey: "openAIModel"),
      let model = OpenAIModel(rawValue: modelString)
    {
      self.selectedModel = model
    }
    
    // Load OpenRouter model name from UserDefaults
    if let openRouterModel = UserDefaults.standard.string(forKey: "openRouterModelName") {
      self.openRouterModelName = openRouterModel
    }
    
    // Load OpenRouter site information from UserDefaults
    if let siteUrl = UserDefaults.standard.string(forKey: "openRouterSiteUrl") {
      self.openRouterSiteUrl = siteUrl
    }
    
    if let siteName = UserDefaults.standard.string(forKey: "openRouterSiteName") {
      self.openRouterSiteName = siteName
    }

    // Save OpenAI API key to keychain when it changes
    $apiKey
      .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
      .sink { [weak self] newValue in
        if !newValue.isEmpty {
          _ = KeychainManager.saveKey(newValue, for: APIProvider.openAI.keychainKey)
        }
      }
      .store(in: &cancellables)
      
    // Save OpenRouter API key to keychain when it changes
    $openRouterApiKey
      .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
      .sink { [weak self] newValue in
        if !newValue.isEmpty {
          _ = KeychainManager.saveKey(newValue, for: APIProvider.openRouter.keychainKey)
        }
      }
      .store(in: &cancellables)

    // Save selected model to UserDefaults when it changes
    $selectedModel
      .sink { [weak self] newValue in
        UserDefaults.standard.set(newValue.rawValue, forKey: "openAIModel")
      }
      .store(in: &cancellables)
      
    // Save selected provider to UserDefaults when it changes
    $selectedProvider
      .sink { [weak self] newValue in
        UserDefaults.standard.set(newValue.rawValue, forKey: "selectedProvider")
      }
      .store(in: &cancellables)
      
    // Save OpenRouter model name to UserDefaults when it changes
    $openRouterModelName
      .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
      .sink { [weak self] newValue in
        UserDefaults.standard.set(newValue, forKey: "openRouterModelName")
      }
      .store(in: &cancellables)
      
    // Save OpenRouter site URL to UserDefaults when it changes
    $openRouterSiteUrl
      .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
      .sink { [weak self] newValue in
        UserDefaults.standard.set(newValue, forKey: "openRouterSiteUrl")
      }
      .store(in: &cancellables)
      
    // Save OpenRouter site name to UserDefaults when it changes
    $openRouterSiteName
      .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
      .sink { [weak self] newValue in
        UserDefaults.standard.set(newValue, forKey: "openRouterSiteName")
      }
      .store(in: &cancellables)
  }

  func clearAPIKey() {
    switch selectedProvider {
    case .openAI:
      apiKey = ""
      _ = KeychainManager.deleteKey(for: APIProvider.openAI.keychainKey)
    case .openRouter:
      openRouterApiKey = ""
      _ = KeychainManager.deleteKey(for: APIProvider.openRouter.keychainKey)
    }
  }
}

================
File: PermissionsManager.swift
================
import AVFoundation
import Foundation
import ScreenCaptureKit

class PermissionsManager: ObservableObject, @unchecked Sendable {
  @Published var microphonePermissionGranted = false
  @Published var screenCapturePermissionGranted = false

  func requestMicrophonePermission(completion: @escaping (Bool) -> Void) {
    switch AVCaptureDevice.authorizationStatus(for: .audio) {
    case .authorized:
      DispatchQueue.main.async {
        self.microphonePermissionGranted = true
        completion(true)
      }
    case .notDetermined:
      AVCaptureDevice.requestAccess(for: .audio) { granted in
        DispatchQueue.main.async {
          self.microphonePermissionGranted = granted
          completion(granted)
        }
      }
    case .denied, .restricted:
      DispatchQueue.main.async {
        self.microphonePermissionGranted = false
        completion(false)
      }
    @unknown default:
      DispatchQueue.main.async {
        self.microphonePermissionGranted = false
        completion(false)
      }
    }
  }

  func requestScreenCapturePermission(completion: @escaping (Bool) -> Void) {
    // Open System Settings to Screen Recording preferences
    if let url = URL(
      string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture")
    {
      NSWorkspace.shared.open(url)

      // We can't programmatically check if permission was granted immediately
      // The user needs to restart the app after granting permission
      // So we'll just return false and let them know they need to restart
      DispatchQueue.main.async {
        self.screenCapturePermissionGranted = false
        completion(false)
      }
    }
  }

  // This method checks screen capture permission status without triggering a permission prompt
  func checkScreenCapturePermissionStatusSilently(completion: @escaping (Bool) -> Void) {
    Task {
      do {
        // Try to get shareable content
        let availableContent = try await SCShareableContent.current
        let hasDisplays = !availableContent.displays.isEmpty

        DispatchQueue.main.async {
          self.screenCapturePermissionGranted = hasDisplays
          completion(hasDisplays)
        }
      } catch {
        DispatchQueue.main.async {
          self.screenCapturePermissionGranted = false
          completion(false)
        }
      }
    }
  }

  func checkPermissionStatus() {
    // Check microphone permission status
    self.microphonePermissionGranted =
      AVCaptureDevice.authorizationStatus(for: .audio) == .authorized

    // Check screen capture permission (silently, without showing a prompt)
    checkScreenCapturePermissionStatusSilently { granted in
      self.screenCapturePermissionGranted = granted
    }
  }

  func openSystemPreferences() {
    if let url = URL(
      string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture")
    {
      NSWorkspace.shared.open(url)
    }
  }
}

================
File: RecordingManager.swift
================
import AVFoundation
import Combine
import Foundation
import ScreenCaptureKit
import WhisperKit
import SwiftUI

class RecordingManager: ObservableObject {
    static let shared = RecordingManager()
    
    @Published var isRecording = false
    @Published var isPaused = false
    @Published var recordingDuration: TimeInterval = 0
    @Published var audioLevel: Float = 0
    @Published var error: Error?
    @Published var transcriptions: [TranscriptionResult] = []
    @Published var latestSummary: String = ""
    
    // Transcription state
    @Published var isTranscriptionModelLoading: Bool = false
    @Published var transcriptionModelLoaded: Bool = false
    
    // Transcription method
    @Published var transcriptionMethod: TranscriptionMethod = .openAI {
        didSet {
            // Only store if it actually changed
            if oldValue != transcriptionMethod {
                UserDefaults.standard.set(transcriptionMethod.rawValue, forKey: "transcriptionMethod")
                setupTranscriptionService()
            }
        }
    }
    
    // WhisperKit model settings
    @Published var whisperKitModelSize: String = "small" {
        didSet {
            if oldValue != whisperKitModelSize {
                UserDefaults.standard.set(whisperKitModelSize, forKey: "whisperKitModelSize")
                if let transcriptionService = transcriptionService as? CombinedTranscriptionService {
                    transcriptionService.setWhisperKitModelSize(whisperKitModelSize)
                }
            }
        }
    }
    
    // Callback when recording finishes with the URL of the recording folder
    var recordingFinished: ((URL?) -> Void)?
    // Callback for recording errors
    var recordingError: ((Error) -> Void)?
    
    private var recordingService: RecordingService?
    private var transcriptionService: CombinedTranscriptionService?
    private var timer: Timer?
    private var startTime: Date?
    var currentChunkIndex = 0
    
    private init() {
        // Load stored preferences
        if let storedMethod = UserDefaults.standard.string(forKey: "transcriptionMethod"),
           let method = TranscriptionMethod(rawValue: storedMethod) {
            transcriptionMethod = method
        }
        
        if let storedSize = UserDefaults.standard.string(forKey: "whisperKitModelSize") {
            whisperKitModelSize = storedSize
        }
        
        setupTranscriptionService()
    }
    
    private func setupTranscriptionService() {
        print("RecordingManager: Setting up CombinedTranscriptionService with method: \(transcriptionMethod)")
        
        // Set loading state
        isTranscriptionModelLoading = transcriptionMethod == .whisperKit
        transcriptionModelLoaded = false
        
        // Get the model folder from user defaults if available
        let modelFolder = UserDefaults.standard.string(forKey: "whisperKitModelFolder")
        
        if let folder = modelFolder, transcriptionMethod == .whisperKit {
            print("RecordingManager: Found WhisperKit model folder: \(folder)")
        }
        
        transcriptionService = CombinedTranscriptionService(
            transcriptionCompletedCallback: { [weak self] result in
                guard let self = self else { return }
                DispatchQueue.main.async {
                    print("RecordingManager: Received transcription result for chunk \(result.chunkIndex)")
                    
                    // Update WhisperKit state if using it
                    if self.transcriptionMethod == .whisperKit {
                        self.transcriptionModelLoaded = true
                        self.isTranscriptionModelLoading = false
                    }
                    
                    self.transcriptions.append(result)
                    
                    // If this was a partial (e.g., "What's Going On?"), generate summary
                    if result.isPartial {
                        self.generateSummary()
                    }
                }
            },
            transcriptionErrorCallback: { [weak self] error in
                guard let self = self else { return }
                DispatchQueue.main.async {
                    print("RecordingManager: Transcription error: \(error.localizedDescription)")
                    
                    // Update state
                    if self.transcriptionMethod == .whisperKit {
                        if let transcriptionError = error as? TranscriptionError,
                           case .modelNotLoaded = transcriptionError {
                            self.isTranscriptionModelLoading = true
                            self.transcriptionModelLoaded = false
                        }
                    }
                    
                    self.error = error
                }
            },
            transcriptionMethod: transcriptionMethod,
            whisperKitModelSize: whisperKitModelSize,
            modelFolder: modelFolder
        )
        
        // Track progress if using WhisperKit
        if transcriptionMethod == .whisperKit {
            DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
                guard let self = self else { return }
                if self.isTranscriptionModelLoading {
                    print("RecordingManager: WhisperKit model still loading after 5 seconds")
                }
            }
        }
    }
    
    // MARK: - Transcription Method Controls
    
    func setTranscriptionMethod(_ method: TranscriptionMethod) {
        if method != transcriptionMethod {
            print("RecordingManager: Changing transcription method from \(transcriptionMethod) to \(method)")
            transcriptionMethod = method
            
            // Update the transcription service
            if let transcriptionService = transcriptionService {
                transcriptionService.setTranscriptionMethod(method)
            } else {
                setupTranscriptionService()
            }
            
            // Update state
            isTranscriptionModelLoading = method == .whisperKit
            transcriptionModelLoaded = method == .openAI
        }
    }
    
    func getCurrentTranscriptionMethod() -> TranscriptionMethod {
        return transcriptionMethod
    }
    
    func getTranscriptionModelStatus() -> (isLoaded: Bool, isLoading: Bool) {
        // Just return the current state without modifying it
        // State updates should happen in refreshModelStatus() instead
        return (transcriptionModelLoaded, isTranscriptionModelLoading)
    }
    
    func refreshModelStatus() {
        // Check the actual model status and update our state variables
        if transcriptionMethod == .whisperKit {
            if let transcriptionService = transcriptionService {
                // Check if WhisperKit is actually loaded
                let isLoaded = transcriptionService.isWhisperKitModelLoaded(modelSize: whisperKitModelSize)
                
                // Update state on the main thread to avoid threading issues
                DispatchQueue.main.async {
                    if isLoaded && !self.transcriptionModelLoaded {
                        // Model is loaded but our state doesn't reflect that
                        self.transcriptionModelLoaded = true
                        self.isTranscriptionModelLoading = false
                    } else if !isLoaded && self.isTranscriptionModelLoading {
                        // If model is still loading, check again after a delay
                        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
                            self.refreshModelStatus()
                        }
                    }
                }
            }
        }
    }
    
    // MARK: - Recording Control Methods
    
    func startRecording() async throws {
        print("RecordingManager: Starting recording...")
        guard !isRecording else {
            print("RecordingManager: Already recording, ignoring start request")
            return
        }
        
        // Check if transcription service is ready based on method
        if transcriptionMethod == .whisperKit && !transcriptionModelLoaded && !isTranscriptionModelLoading {
            // Reinitialize transcription service if needed
            setupTranscriptionService()
        }
        
        do {
            await MainActor.run {
                recordingService = nil
                currentChunkIndex = 0
                transcriptions = []
                latestSummary = ""
            }
            print("RecordingManager: Creating new RecordingService...")
            // Get user's chunk duration preference from UserDefaults
            let chunkDurationSeconds = UserDefaults.standard.integer(forKey: "chunkDurationSeconds")
            let effectiveDuration = chunkDurationSeconds > 0 ? chunkDurationSeconds : 10 // Default to 10s if not set
            
            recordingService = try await RecordingService.create(
                chunkDurationSeconds: effectiveDuration
            ) { [weak self] chunk in
                guard let self = self else { return }
                let chunkIndex = self.currentChunkIndex
                print("RecordingManager: Received audio chunk callback, processing chunk \(chunkIndex)")
                self.transcriptionService?.processAudioChunk(chunk, chunkIndex: chunkIndex)
                DispatchQueue.main.async {
                    self.currentChunkIndex += 1
                    print("RecordingManager: Incremented chunk index to \(self.currentChunkIndex)")
                }
            }
            print("RecordingManager: RecordingService created successfully")
            print("RecordingManager: Calling RecordingService.startRecording()...")
            try await recordingService?.startRecording()
            print("RecordingManager: RecordingService.startRecording() completed successfully")
            DispatchQueue.main.async { [weak self] in
                guard let self = self else { return }
                print("RecordingManager: Setting isRecording = true")
                self.isRecording = true
                print("RecordingManager: Setting startTime = \(Date())")
                self.startTime = Date()
                print("RecordingManager: Starting timer...")
                self.startTimer()
                self.error = nil
                print("RecordingManager: Recording started successfully")
            }
        } catch {
            print("RecordingManager: Error in startRecording: \(error.localizedDescription)")
            DispatchQueue.main.async { [weak self] in
                guard let self = self else { return }
                self.error = error
                self.isRecording = false
                print("RecordingManager: Failed to start recording: \(error.localizedDescription)")
            }
            throw error
        }
    }
    
    func pauseRecording() async {
        guard isRecording && !isPaused else { return }
        print("RecordingManager: Pausing recording...")
        await recordingService?.pauseRecording()
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            self.isPaused = true
            self.stopTimer()
            print("RecordingManager: Recording paused")
        }
    }
    
    func resumeRecording() async {
        guard isRecording && isPaused else { return }
        print("RecordingManager: Resuming recording...")
        await recordingService?.resumeRecording()
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            self.isPaused = false
            // Update start time to account only for active recording duration
            if let oldStartTime = self.startTime {
                let pausedDuration = self.recordingDuration
                self.startTime = Date().addingTimeInterval(-pausedDuration)
            } else {
                self.startTime = Date()
            }
            self.startTimer()
            print("RecordingManager: Recording resumed")
        }
    }
    
    func stopRecording() async {
        guard isRecording else { return }
        print("RecordingManager: Stopping recording...")
        let recordingFolderURL = await recordingService?.stopRecording()
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            self.isRecording = false
            self.isPaused = false
            self.stopTimer()
            self.recordingDuration = 0
            self.startTime = nil
            self.error = nil
            if let url = recordingFolderURL {
                if self.isDirectory(url: url) {
                    if let files = self.processRecordingFolder(url) {
                        print("RecordingManager: Successfully processed recording folder with \(files.count) files")
                        if let result = self.createRecordingResult(from: url) {
                            print("RecordingManager: Successfully created recording result with \(result.files.count) files")
                        }
                    }
                    print("RecordingManager: Recording stopped, folder URL is valid directory: \(url.absoluteString)")
                    self.recordingFinished?(url)
                } else {
                    print("RecordingManager: Recording stopped, but folder URL is not a directory: \(url.absoluteString)")
                    self.recordingFinished?(nil)
                }
            } else {
                print("RecordingManager: Recording stopped, but no folder URL was returned")
                self.recordingFinished?(nil)
            }
        }
    }
    
    func whatIsGoingOn() {
        guard isRecording, let recordingService = recordingService else {
            print("RecordingManager: Cannot process 'What's Going On?' - not recording or service is nil")
            return
        }
        print("RecordingManager: 'What's Going On?' button pressed")
        do {
            try recordingService.whatIsGoingOn()
        } catch let error {
            print("RecordingManager: Error processing 'What's Going On?' request: \(error)")
        }
    }
    
    func toggleMicrophoneMute(muted: Bool) {
        recordingService?.toggleMicrophoneMute(muted: muted)
    }
    
    func getRecordingFiles() -> [URL]? {
        guard !isRecording else {
            print("RecordingManager: Cannot get recording files while recording is in progress")
            return nil
        }
        guard let recordingService = recordingService else {
            return nil
        }
        return nil
    }
    
    func processRecordingFolder(_ folderURL: URL) -> [URL]? {
        guard isDirectory(url: folderURL) else {
            print("RecordingManager: Cannot process recording - not a directory: \(folderURL.path)")
            return nil
        }
        do {
            let fileURLs = try FileManager.default.contentsOfDirectory(at: folderURL, includingPropertiesForKeys: nil)
            let files = fileURLs.filter { url in
                var isDir: ObjCBool = false
                let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir)
                return exists && !isDir.boolValue
            }
            print("RecordingManager: Found \(files.count) files in recording directory:")
            files.forEach { file in
                print("  - \(file.lastPathComponent)")
            }
            return files
        } catch {
            print("RecordingManager: Failed to process recording folder: \(error.localizedDescription)")
            return nil
        }
    }
    
    func createRecordingResult(from folderURL: URL) -> RecordingResult? {
        guard isDirectory(url: folderURL) else {
            print("RecordingManager: Cannot create recording result - not a directory: \(folderURL.path)")
            return nil
        }
        let metadataURL = folderURL.appendingPathComponent("recording_metadata.json")
        do {
            let metadataData = try Data(contentsOf: metadataURL)
            let metadata = try JSONSerialization.jsonObject(with: metadataData) as? [String: Any]
            let files = processRecordingFolder(folderURL) ?? []
            let result = RecordingResult(
                folderURL: folderURL,
                files: files,
                timestamp: metadata?["sessionTimestamp"] as? String ?? "",
                totalChunks: metadata?["totalChunks"] as? Int ?? 0
            )
            print("RecordingManager: Successfully created recording result from folder")
            processAudioFiles(result)
            return result
        } catch {
            print("RecordingManager: Error creating recording result: \(error.localizedDescription)")
            return nil
        }
    }
    
    private func processAudioFiles(_ result: RecordingResult) {
        print("RecordingManager: Processing \(result.audioFiles.count) audio files")
        for (index, file) in result.audioFiles.enumerated() {
            print("RecordingManager: Audio file \(index): \(file.lastPathComponent)")
        }
    }
    
    func isDirectory(url: URL) -> Bool {
        var isDir: ObjCBool = false
        let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir)
        let result = exists && isDir.boolValue
        print("RecordingManager: Checking if path is directory: \(url.path) - Result: \(result)")
        return result
    }
    
    private func startTimer() {
        print("RecordingManager: Starting timer...")
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            guard let self = self, let startTime = self.startTime else {
                print("RecordingManager: Timer fired but self or startTime is nil")
                return
            }
            self.recordingDuration = Date().timeIntervalSince(startTime)
        }
        RunLoop.current.add(timer!, forMode: .common)
        print("RecordingManager: Timer added to run loop")
    }
    
    private func stopTimer() {
        print("RecordingManager: Stopping timer...")
        DispatchQueue.main.async { [weak self] in
            guard let self = self else {
                print("RecordingManager: Self is nil when stopping timer")
                return
            }
            if let timer = self.timer {
                timer.invalidate()
                print("RecordingManager: Timer invalidated")
            } else {
                print("RecordingManager: No timer to invalidate")
            }
            self.timer = nil
        }
    }
    
    private func generateSummary() {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            let allText = self.transcriptions
                .sorted { $0.timestamp < $1.timestamp }
                .map { $0.text }
                .joined(separator: " ")
            self.latestSummary = "Summary of \(self.transcriptions.count) transcriptions: \(allText.prefix(100))..."
            print("RecordingManager: Generated summary: \"\(self.latestSummary.prefix(50))...\"")
        }
    }
    
    func generateFinalSummary() {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            self.processLastChunkIfNeeded()
            self.latestSummary = "Final summary of \(self.transcriptions.count) transcriptions completed."
            print("RecordingManager: Generated final summary")
        }
    }
    
    func processLastChunkIfNeeded() {
        if let recordingService = recordingService {
            do {
                print("RecordingManager: Processing last audio chunk before summary")
                try recordingService.finalizeCurrentChunk(isPartial: true)
            } catch {
                print("RecordingManager: Error processing last chunk: \(error)")
            }
        }
    }
    
    // MARK: - WhisperKit Model Management
    
    func downloadWhisperKitModel() async {
        // Show download progress to user
        DispatchQueue.main.async {
            self.isTranscriptionModelLoading = true
        }
        
        // Create a directory for models if it doesn't exist
        let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let modelsDirectory = documentsPath.appendingPathComponent("WhisperKitModels")
        
        do {
            if !FileManager.default.fileExists(atPath: modelsDirectory.path) {
                try FileManager.default.createDirectory(at: modelsDirectory, withIntermediateDirectories: true)
            }
            
            // Download model (simplified example)
            let modelSize = self.whisperKitModelSize
            let modelURL = URL(string: "https://huggingface.co/argmaxinc/whisperkit-coreml/resolve/main/openai_whisper-\(modelSize).tar")!
            
            print("RecordingManager: Downloading WhisperKit model: \(modelSize) from \(modelURL)")
            
            let downloadTask = URLSession.shared.downloadTask(with: modelURL) { tempURL, _, error in
                // Handle download completion
                if let error = error {
                    print("Error downloading model: \(error.localizedDescription)")
                    DispatchQueue.main.async {
                        self.isTranscriptionModelLoading = false
                        self.error = error
                    }
                    return
                }
                
                guard let tempURL = tempURL else {
                    print("No temporary URL provided")
                    DispatchQueue.main.async {
                        self.isTranscriptionModelLoading = false
                        self.error = NSError(domain: "RecordingManager", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to download model"])
                    }
                    return
                }
                
                let destinationURL = modelsDirectory.appendingPathComponent("openai_whisper-\(modelSize).tar")
                
                do {
                    // Remove existing file if it exists
                    if FileManager.default.fileExists(atPath: destinationURL.path) {
                        try FileManager.default.removeItem(at: destinationURL)
                    }
                    
                    // Move downloaded file to destination
                    try FileManager.default.moveItem(at: tempURL, to: destinationURL)
                    
                    // Extract the tar file (would need actual implementation)
                    print("Model downloaded, extraction would happen here")
                    
                    // Save the path to user defaults
                    UserDefaults.standard.set(modelsDirectory.path, forKey: "whisperKitModelFolder")
                    
                    // Reinitialize the transcription service
                    DispatchQueue.main.async { [weak self] in
                        guard let self = self else { return }
                        self.isTranscriptionModelLoading = false
                        self.transcriptionModelLoaded = true
                        self.setupTranscriptionService()
                    }
                } catch {
                    print("Error moving downloaded file: \(error.localizedDescription)")
                    DispatchQueue.main.async {
                        self.isTranscriptionModelLoading = false
                        self.error = error
                    }
                }
            }
            
            downloadTask.resume()
        } catch {
            print("Error creating models directory: \(error.localizedDescription)")
            DispatchQueue.main.async {
                self.isTranscriptionModelLoading = false
                self.error = error
            }
        }
    }
}

// Structure to represent a recording result
struct RecordingResult {
    let folderURL: URL
    let files: [URL]
    let timestamp: String
    let totalChunks: Int
    
    var audioFiles: [URL] {
        return files.filter { $0.pathExtension == "wav" }
    }
    
    var systemAudioFiles: [URL] {
        return files.filter { $0.lastPathComponent.hasPrefix("system_audio_") }
    }
    
    var micAudioFiles: [URL] {
        return files.filter { $0.lastPathComponent.hasPrefix("mic_audio_") }
    }
    
    var metadataFile: URL? {
        return files.first { $0.lastPathComponent == "recording_metadata.json" }
    }
}

================
File: RecordingService.swift
================
import AVFoundation
import Combine
import Foundation
import ScreenCaptureKit

// Recording-related errors
enum RecordingError: LocalizedError {
    case failedToCreateAudioEngine
    case failedToStartAudioEngine
    case failedToGetScreenContent
    case failedToCreateScreenRecorder
    case failedToStartScreenRecorder
    case noScreenCapturePermission
    case noMicrophonePermission

    var errorDescription: String? {
        switch self {
        case .failedToCreateAudioEngine:
            return "Failed to create audio engine"
        case .failedToStartAudioEngine:
            return "Failed to start audio engine"
        case .failedToGetScreenContent:
            return "Failed to get screen content"
        case .failedToCreateScreenRecorder:
            return "Failed to create screen recorder"
        case .failedToStartScreenRecorder:
            return "Failed to start screen recorder"
        case .noScreenCapturePermission:
            return "Screen recording permission not granted"
        case .noMicrophonePermission:
            return "Microphone permission not granted"
        }
    }
}

// Define a struct for an audio chunk
struct AudioChunk {
    let systemAudioURL: URL
    let micAudioURL: URL
    let startTime: Date
    let endTime: Date
    let isPartial: Bool
}

// Callback for completed chunks
typealias ChunkCompletedCallback = (AudioChunk) -> Void

class RecordingService: NSObject {
    // MARK: - Properties

    // Screen capture components
    private var stream: SCStream?
    private var availableContent: SCShareableContent?

    // Audio components
    private var audioEngine: AVAudioEngine?
    private var mixerNode: AVAudioMixerNode?
    private var inputNode: AVAudioInputNode?

    // File management
    private let fileManager = FileManager.default
    private let documentsPath: URL
    private let recordingFolderURL: URL
    private var sessionTimestamp: String

    // Current chunk info
    private var currentChunkIndex = 0
    private var chunkDuration: TimeInterval = 10.0  // Default to 10 seconds per chunk
    private var systemAudioFile: AVAudioFile?
    private var micAudioFile: AVAudioFile?
    private var chunkTimer: Timer?

    // State tracking
    private var isCapturing = false
    private var currentChunkStartTime: Date?

    // Callback for completed chunks
    private var chunkCompletedCallback: ChunkCompletedCallback?
    // Microphone mute state
    private var isMicrophoneMuted = false

    // MARK: - Initialization

    private override init() {
        // Set up file paths
        documentsPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
        sessionTimestamp = DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .medium)
            .replacingOccurrences(of: "/", with: "-")
            .replacingOccurrences(of: ":", with: "-")
            .replacingOccurrences(of: ",", with: "")
        recordingFolderURL = documentsPath.appendingPathComponent("Recording_\(sessionTimestamp)")
        super.init()
        print("RecordingService: Initialized with recording folder at \(recordingFolderURL.path)")
    }

    // Factory method to create and configure RecordingService
    static func create(chunkDurationSeconds: Int = 10, chunkCompletedCallback: ChunkCompletedCallback? = nil) async throws -> RecordingService {
        print("RecordingService: Creating new instance with chunk duration: \(chunkDurationSeconds)s, callback: \(chunkCompletedCallback != nil ? "provided" : "nil")")
        // Check screen capture permission
        do {
            _ = try await SCShareableContent.current
            print("RecordingService: Screen recording permission granted")
        } catch {
            print("RecordingService: Screen recording permission not granted: \(error)")
            throw RecordingError.noScreenCapturePermission
        }
        // Check microphone permission
        let micPermission = AVCaptureDevice.authorizationStatus(for: .audio)
        if micPermission == .authorized {
            // OK
        } else if micPermission == .notDetermined {
            let granted = await withCheckedContinuation { continuation in
                AVCaptureDevice.requestAccess(for: .audio) { granted in
                    continuation.resume(returning: granted)
                }
            }
            if !granted {
                print("RecordingService: Microphone permission denied")
                throw RecordingError.noMicrophonePermission
            }
        } else {
            print("RecordingService: Microphone not authorized: \(micPermission)")
            throw RecordingError.noMicrophonePermission
        }
        // Get available screen content
        guard let content = try? await SCShareableContent.current else {
            print("RecordingService: Failed to get screen content")
            throw RecordingError.failedToGetScreenContent
        }
        print("RecordingService: Permissions verified, creating instance...")
        let service = RecordingService()
        service.chunkDuration = TimeInterval(chunkDurationSeconds)
        service.chunkCompletedCallback = chunkCompletedCallback

        do {
            try service.fileManager.createDirectory(at: service.recordingFolderURL, withIntermediateDirectories: true)
        } catch {
            print("RecordingService: Failed to create recording directory: \(error)")
            throw error
        }

        service.availableContent = content

        // Set up audio engine
        try service.setupAudioEngine()

        return service
    }

    // MARK: - Setup Methods

    private func setupAudioEngine() throws {
        print("RecordingService: Setting up audio engine...")
        audioEngine = AVAudioEngine()
        guard let audioEngine = audioEngine else {
            print("RecordingService: Failed to create audio engine")
            throw RecordingError.failedToCreateAudioEngine
        }
        // Get input node for microphone
        inputNode = audioEngine.inputNode
        // Create mixer node
        mixerNode = AVAudioMixerNode()
        guard mixerNode != nil else {
            print("RecordingService: Failed to create mixer node")
            throw RecordingError.failedToCreateAudioEngine
        }
        // Attach mixer node to engine
        audioEngine.attach(mixerNode!)
        print("RecordingService: Audio engine setup complete")
    }

    // MARK: - Recording Control

    func startRecording() async throws {
        print("RecordingService: Starting recording...")
        guard !isCapturing else {
            print("RecordingService: Already capturing, ignoring start request")
            return
        }
        // Configure for first chunk
        currentChunkIndex = 0
        try createNewChunkFiles()
        try startAudioCapture()
        try await startScreenCapture()
        startChunkTimer()
        currentChunkStartTime = Date()
        isCapturing = true
        print("RecordingService: Recording started successfully")
    }

    func pauseRecording() async {
        print("RecordingService: Pausing recording...")
        // Stop the chunk timer to prevent creating new chunks while paused
        stopChunkTimer()
        // Finalize current chunk as partial
        finalizeCurrentChunk(isPartial: true)
        // Pause the audio engine if running
        if let audioEngine = audioEngine, audioEngine.isRunning {
            audioEngine.pause()
            print("RecordingService: Audio engine paused")
        }
        // Stop screen capture so macOS screen sharing icon goes away
        await stopScreenCapture()
        print("RecordingService: Screen capture stopped for pause")
    }

    func resumeRecording() async {
        print("RecordingService: Resuming recording...")
        do {
            // Increment chunk index and create new chunk files for resumed recording
            currentChunkIndex += 1
            try createNewChunkFiles()
            currentChunkStartTime = Date()
            print("RecordingService: Created new chunk files for resumed recording (chunk \(currentChunkIndex))")
        } catch {
            print("RecordingService: Error creating new chunk files for resumed recording: \(error.localizedDescription)")
        }
        // Resume audio engine if needed
        if let audioEngine = audioEngine, !audioEngine.isRunning {
            do {
                try audioEngine.start()
                print("RecordingService: Audio engine resumed")
            } catch {
                print("RecordingService: Failed to resume audio engine: \(error)")
            }
        }
        // Restart screen capture stream
        do {
            try await startScreenCapture()
            print("RecordingService: Screen capture stream resumed")
        } catch {
            print("RecordingService: Failed to resume screen capture: \(error)")
        }
        // Restart the chunk timer
        startChunkTimer()
        print("RecordingService: Chunk timer restarted")
    }

    func stopRecording() async -> URL? {
        print("RecordingService: Stopping recording...")
        guard isCapturing else {
            print("RecordingService: Not currently recording, ignoring stop request")
            return nil
        }
        // Stop the chunk timer
        stopChunkTimer()
        // Finalize the current chunk as partial
        finalizeCurrentChunk(isPartial: true)
        // Stop screen capture
        await stopScreenCapture()
        // Stop audio capture
        stopAudioCapture()
        // Clear file references
        systemAudioFile = nil
        micAudioFile = nil
        currentChunkStartTime = nil
        isCapturing = false
        print("RecordingService: Recording stopped, saved to \(recordingFolderURL.path)")
        // Create metadata file
        createRecordingMetadata()
        var isDir: ObjCBool = false
        if FileManager.default.fileExists(atPath: recordingFolderURL.path, isDirectory: &isDir), isDir.boolValue {
            print("RecordingService: Verified recording folder exists as a directory")
            return recordingFolderURL
        } else {
            print("RecordingService: Warning - recording folder does not exist or is not a directory")
            return nil
        }
    }

    // MARK: - Chunk Management

    private func startChunkTimer() {
        print("RecordingService: Starting chunk timer...")
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            // Use a shorter effective interval to create 5-second overlaps
            let effectiveInterval = max(self.chunkDuration - 5.0, 5.0) // At least 5 seconds between chunks
            self.chunkTimer = Timer.scheduledTimer(withTimeInterval: effectiveInterval, repeats: true) { [weak self] _ in
                guard let self = self else { return }
                print("RecordingService: Chunk timer fired at \(Date()), creating new chunk...")
                
                // Store the current chunk info without finalizing it yet
                let previousSystemFile = self.systemAudioFile
                let previousMicFile = self.micAudioFile
                let previousStartTime = self.currentChunkStartTime
                
                // Increment chunk and create new files, but keep recording to the previous ones too
                self.currentChunkIndex += 1
                do {
                    try self.createNewChunkFiles()
                    // Set start time to 5 seconds ago so chunks overlap
                    self.currentChunkStartTime = Date().addingTimeInterval(-5.0)
                    print("RecordingService: Created overlapping chunk files for chunk \(self.currentChunkIndex)")
                    
                    // Now finalize the previous chunk (asynchronously to avoid blocking the timer)
                    DispatchQueue.global().async {
                        if let systemFile = previousSystemFile,
                           let micFile = previousMicFile,
                           let startTime = previousStartTime {
                            
                            let systemURL = systemFile.url
                            let micURL = micFile.url
                            
                            let chunk = AudioChunk(
                                systemAudioURL: systemURL,
                                micAudioURL: micURL,
                                startTime: startTime,
                                endTime: Date(),
                                isPartial: false
                            )
                            
                            self.chunkCompletedCallback?(chunk)
                        }
                    }
                } catch {
                    print("RecordingService: Error creating new chunk files: \(error.localizedDescription)")
                }
            }
            RunLoop.main.add(self.chunkTimer!, forMode: .common)
            print("RecordingService: Chunk timer scheduled to fire every \(effectiveInterval) seconds (with 5s overlap)")
        }
    }

    private func stopChunkTimer() {
        print("RecordingService: Stopping chunk timer...")
        chunkTimer?.invalidate()
        chunkTimer = nil
    }

    private func createNewChunkFiles() throws {
        print("RecordingService: Creating new chunk files for chunk \(currentChunkIndex)...")
        let audioSettings: [String: Any] = [
            AVFormatIDKey: kAudioFormatLinearPCM,
            AVSampleRateKey: 48000,
            AVNumberOfChannelsKey: 1,
            AVLinearPCMBitDepthKey: 32,
            AVLinearPCMIsFloatKey: true,
            AVLinearPCMIsNonInterleaved: false,
        ]
        let systemFileURL = recordingFolderURL.appendingPathComponent("system_audio_\(currentChunkIndex).wav")
        systemAudioFile = try AVAudioFile(forWriting: systemFileURL, settings: audioSettings)
        let micFileURL = recordingFolderURL.appendingPathComponent("mic_audio_\(currentChunkIndex).wav")
        micAudioFile = try AVAudioFile(forWriting: micFileURL, settings: audioSettings)
        print("RecordingService: Created files at:\n- System: \(systemFileURL.path)\n- Mic: \(micFileURL.path)")
    }

     func finalizeCurrentChunk(isPartial: Bool) {
        print("RecordingService: Finalizing current chunk (isPartial: \(isPartial))...")
        guard let systemAudioFile = systemAudioFile,
              let micAudioFile = micAudioFile,
              let chunkStartTime = currentChunkStartTime else {
            print("RecordingService: No active chunk to finalize")
            return
        }
        do {
            let systemAttributes = try FileManager.default.attributesOfItem(atPath: systemAudioFile.url.path)
            let micAttributes = try FileManager.default.attributesOfItem(atPath: micAudioFile.url.path)
            if let systemSize = systemAttributes[.size] as? UInt64, let micSize = micAttributes[.size] as? UInt64 {
                print("DEBUG: Finalized chunk file sizes - System audio: \(systemSize) bytes, Mic audio: \(micSize) bytes")
            }
        } catch {
            print("DEBUG: Error retrieving file sizes: \(error)")
        }
        let systemFileURL = systemAudioFile.url
        let micFileURL = micAudioFile.url
        
        // Only nil out the audio files if this is a partial chunk from pause/stop
        // This allows us to keep recording to the current files while starting a new chunk
        if isPartial {
            self.systemAudioFile = nil
            self.micAudioFile = nil
        }
        
        let chunk = AudioChunk(systemAudioURL: systemFileURL,
                               micAudioURL: micFileURL,
                               startTime: chunkStartTime,
                               endTime: Date(),
                               isPartial: isPartial)
        if let callback = chunkCompletedCallback {
            print("RecordingService: Notifying callback of completed chunk (system: \(systemFileURL.lastPathComponent), mic: \(micFileURL.lastPathComponent), duration: \(chunk.endTime.timeIntervalSince(chunk.startTime))s, isPartial: \(isPartial))")
            callback(chunk)
        } else {
            print("RecordingService: No callback registered for chunk processing")
        }
    }

    // MARK: - Screen Capture

    private func startScreenCapture() async throws {
        print("RecordingService: Starting screen capture...")
        guard let availableContent = availableContent,
              let display = availableContent.displays.first else {
            print("RecordingService: No available display")
            throw RecordingError.failedToGetScreenContent
        }
        let filter = SCContentFilter(display: display, excludingWindows: [])
        let configuration = SCStreamConfiguration()
        configuration.width = 320
        configuration.height = 180
        configuration.minimumFrameInterval = CMTime(value: 1, timescale: 5)
        configuration.capturesAudio = true
        configuration.excludesCurrentProcessAudio = true
        configuration.sampleRate = 48000
        configuration.channelCount = 1
        stream = SCStream(filter: filter, configuration: configuration, delegate: nil)
        guard let stream = stream else {
            print("RecordingService: Failed to create stream")
            throw RecordingError.failedToCreateScreenRecorder
        }
        try stream.addStreamOutput(self, type: .audio, sampleHandlerQueue: .global())
        try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: .global())
        try await stream.startCapture()
        print("RecordingService: Screen capture started successfully")
    }

    private func stopScreenCapture() async {
        print("RecordingService: Stopping screen capture...")
        if let stream = stream {
            do {
                try await stream.stopCapture()
                self.stream = nil
            } catch {
                print("RecordingService: Error stopping stream capture: \(error)")
            }
        }
    }

    // MARK: - Audio Capture

    private func startAudioCapture() throws {
        print("RecordingService: Starting audio capture...")
        guard let audioEngine = audioEngine,
              let inputNode = inputNode,
              let mixerNode = mixerNode else {
            print("RecordingService: Audio engine components not initialized")
            throw RecordingError.failedToCreateAudioEngine
        }
        let inputFormat = inputNode.outputFormat(forBus: 0)
        audioEngine.connect(inputNode, to: mixerNode, format: inputFormat)
        let tapFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 48000, channels: 1, interleaved: false)!
        mixerNode.installTap(onBus: 0, bufferSize: 4096, format: tapFormat) { [weak self] buffer, time in
            guard let self = self, let micAudioFile = self.micAudioFile else { return }
            if !self.isMicrophoneMuted {
                do {
                    try micAudioFile.write(from: buffer)
                } catch {
                    print("RecordingService: Error writing to mic audio file: \(error.localizedDescription)")
                }
            } else {
                let silenceBuffer = AVAudioPCMBuffer(pcmFormat: micAudioFile.processingFormat, frameCapacity: buffer.frameLength)!
                silenceBuffer.frameLength = buffer.frameLength
                for i in 0..<Int(buffer.format.channelCount) {
                    if let channel = silenceBuffer.floatChannelData?[i] {
                        memset(channel, 0, Int(buffer.frameLength) * MemoryLayout<Float>.size)
                    }
                }
                do {
                    try micAudioFile.write(from: silenceBuffer)
                } catch {
                    print("RecordingService: Error writing silence to mic audio file: \(error.localizedDescription)")
                }
            }
        }
        audioEngine.prepare()
        do {
            try audioEngine.start()
            print("RecordingService: Audio engine started successfully")
        } catch {
            print("RecordingService: Failed to start audio engine: \(error)")
            throw RecordingError.failedToStartAudioEngine
        }
    }

    private func stopAudioCapture() {
        print("RecordingService: Stopping audio capture...")
        if let mixerNode = mixerNode {
            print("RecordingService: Removing tap on mixer node")
            mixerNode.removeTap(onBus: 0)
        }
        if let audioEngine = audioEngine {
            if audioEngine.isRunning {
                print("RecordingService: Stopping audio engine")
                audioEngine.stop()
            }
            print("RecordingService: Resetting audio engine")
            audioEngine.reset()
        }
        print("RecordingService: Audio capture fully stopped")
    }

    // MARK: - Metadata

    private func createRecordingMetadata() {
        let metadataURL = recordingFolderURL.appendingPathComponent("recording_metadata.json")
        let metadata: [String: Any] = [
            "sessionTimestamp": sessionTimestamp,
            "totalChunks": currentChunkIndex + 1,
            "recordingPath": recordingFolderURL.path,
            "isDirectory": true,
            "chunks": (0...currentChunkIndex).map { index in
                [
                    "index": index,
                    "systemAudio": "system_audio_\(index).wav",
                    "micAudio": "mic_audio_\(index).wav",
                ]
            },
            "recordingDate": Date().timeIntervalSince1970,
        ]
        if let jsonData = try? JSONSerialization.data(withJSONObject: metadata, options: .prettyPrinted) {
            try? jsonData.write(to: metadataURL)
            print("RecordingService: Created metadata file at \(metadataURL.path)")
        } else {
            print("RecordingService: Failed to create metadata file")
        }
    }

    // MARK: - Text Processing
    
    private func removeDuplicateSentences(_ text: String) -> String {
        // Split text into sentences (roughly)
        let sentenceDelimiters = CharacterSet(charactersIn: ".!?")
        let sentences = text.components(separatedBy: sentenceDelimiters)
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }
        
        // Remove duplicates while preserving order
        var uniqueSentences: [String] = []
        var seenSentences: Set<String> = []
        
        for sentence in sentences {
            // Use a more aggressive normalization for better duplicate detection
            let normalized = sentence.lowercased()
                .trimmingCharacters(in: .whitespacesAndNewlines)
                .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
            
            // Check for similarity with already seen sentences (to handle small differences)
            let isDuplicate = seenSentences.contains { existing in
                // For long sentences, allow more variation
                if normalized.count > 20 && existing.count > 20 {
                    // Check if either contains most of the other
                    return normalized.contains(existing.prefix(existing.count/2)) || 
                           existing.contains(normalized.prefix(normalized.count/2))
                } else {
                    // For short sentences, require exact match
                    return existing == normalized
                }
            }
            
            if !isDuplicate && normalized.count > 3 {
                uniqueSentences.append(sentence)
                seenSentences.insert(normalized)
            }
        }
        
        // Rejoin with proper punctuation
        return uniqueSentences.joined(separator: ". ") + "."
    }
    
    // MARK: - Microphone Mute
    func toggleMicrophoneMute(muted: Bool) {
        isMicrophoneMuted = muted
        print("RecordingService: Microphone \(muted ? "muted" : "unmuted")")
    }
    
    // MARK: - "What's Going On?" Functionality

    func whatIsGoingOn() throws {
        guard isCapturing else {
            print("RecordingService: Not recording, can't process 'What's Going On?' request")
            return
        }
        print("RecordingService: 'What's Going On?' button pressed, cutting current chunk short...")
        stopChunkTimer()
        
        // Store the current chunk info
        let previousSystemFile = self.systemAudioFile
        let previousMicFile = self.micAudioFile
        let previousStartTime = self.currentChunkStartTime
        
        // Increment chunk and create new files with overlap
        currentChunkIndex += 1
        try createNewChunkFiles()
        // Set start time to 5 seconds ago to create overlap
        currentChunkStartTime = Date().addingTimeInterval(-5.0)
        
        // Finalize the previous chunk as partial (for "What's Going On?" feature)
        if let systemFile = previousSystemFile,
           let micFile = previousMicFile,
           let startTime = previousStartTime {
            
            let systemURL = systemFile.url
            let micURL = micFile.url
            
            let chunk = AudioChunk(
                systemAudioURL: systemURL,
                micAudioURL: micURL,
                startTime: startTime,
                endTime: Date(),
                isPartial: true  // Mark as partial for "What's Going On?"
            )
            
            chunkCompletedCallback?(chunk)
        }
        
        startChunkTimer()
        print("RecordingService: Started new chunk after 'What's Going On?' request")
    }
}

// MARK: - SCStreamOutput Extension

extension RecordingService: SCStreamOutput {
    func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
        switch type {
        case .audio:
            processAudioSampleBuffer(sampleBuffer)
        case .screen:
            // Video frames are received but not processed
            break
        @unknown default:
            break
        }
    }

    private func processAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
        guard let systemAudioFile = systemAudioFile else { return }
        let numSamples = CMSampleBufferGetNumSamples(sampleBuffer)
        guard numSamples > 0,
              let pcmBuffer = AVAudioPCMBuffer(pcmFormat: systemAudioFile.processingFormat, frameCapacity: AVAudioFrameCount(numSamples)) else { return }
        pcmBuffer.frameLength = AVAudioFrameCount(numSamples)
        let mutableAudioBufferList = UnsafeMutableAudioBufferListPointer(UnsafeMutablePointer(mutating: pcmBuffer.audioBufferList))
        let status = CMSampleBufferCopyPCMDataIntoAudioBufferList(sampleBuffer, at: 0, frameCount: Int32(numSamples), into: mutableAudioBufferList.unsafeMutablePointer)
        if status == noErr {
            do {
                try systemAudioFile.write(from: pcmBuffer)
            } catch {
                print("RecordingService: Error writing system audio to file: \(error.localizedDescription)")
            }
        } else {
            print("RecordingService: Error copying PCM data from system audio: \(status)")
        }
    }
}

================
File: SessionScribe.entitlements
================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.device.audio-input</key>
	<true/>
	<key>com.apple.security.files.bookmarks.app-scope</key>
	<true/>
	<key>com.apple.security.files.downloads.read-write</key>
	<true/>
	<key>com.apple.security.files.home-relative-path.read-write</key>
	<array>
		<string>/Documents/SessionScribe/</string>
	</array>
	<key>com.apple.security.files.user-selected.read-write</key>
	<true/>
	<key>com.apple.security.network.client</key>
	<true/>
	<key>com.apple.security.screen-recording</key>
	<true/>
</dict>
</plist>

================
File: SessionScribeApp.swift
================
//
//  SessionScribeApp.swift
//  SessionScribe
//
//  Created by Arnav Gosain on 25/02/25.
//

import SimpleToast
import SwiftUI

@main
struct SessionScribeApp: App {

  @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

  @StateObject private var trialManager = TrialManager.shared
  @StateObject private var licenseManager = LicenseManager.shared
  @StateObject private var themeManager = ThemeManager.shared
  @StateObject private var recordingManager = RecordingManager.shared
  @StateObject private var meetingsViewModel = MeetingsViewModel()
  @StateObject private var navigationState = NavigationState()
  @AppStorage("onboardingComplete") private var onboardingComplete: Bool = false

  // Add a state to track when the app is reset
  @State private var appWasReset: Bool = false
  @State private var showRecordingError = false
  @State private var showLicenseView = false
  
  // For dock menu
  @State private var dockMenuDelegate = DockMenuDelegate()

init() {
    // Fix any notes with swapped fields
    print("SessionScribeApp: Checking for notes with swapped fields...")
    SQLiteManager.shared.fixUserNotesWithRecordingPath()
    
    // Direct fix for the specific note with timestamp in recordingPath
    print("SessionScribeApp: Applying direct fix for note with timestamp in recordingPath...")
    SQLiteManager.shared.fixSpecificNote(id: "6329F663-BE91-4754-A478-AC996A90C9BB")
    
    // Keep the existing activation policy setting
    NSApplication.shared.setActivationPolicy(.regular)
    
    // Set application icon (if missing from Assets)
    if NSImage(named: NSImage.applicationIconName) == nil {
        // If your icon is in the Assets catalog, use this line instead:
        // let appIcon = NSImage(named: "AppIcon")
        
        // If your icon is in the bundle resources, use this:
        if let appIconPath = Bundle.main.path(forResource: "AppIcon", ofType: "icns") {
            let appIcon = NSImage(byReferencingFile: appIconPath)
            NSApplication.shared.applicationIconImage = appIcon
        }
    }
}

// Add this method to your DockMenuDelegate class
func applicationDidFinishLaunching(_ notification: Notification) {
    // Update the dock icon badge if needed
    let dockTile = NSApp.dockTile
    // For showing a badge with a number (like unread count)
    // dockTile.badgeLabel = "1"
    
    // For a custom dock icon overlay (if needed)
    // let overlayImage = NSImage(named: "DockOverlay")
    // let contentView = NSImageView(image: overlayImage!)
    // dockTile.contentView = contentView
    
    dockTile.display()
}

  var body: some Scene {
    WindowGroup {
      if !onboardingComplete {
        OnboardingView()
          .frame(minWidth: 640, minHeight: 480)
          .onDisappear {
            // Don't auto-activate trial anymore since we now use email-based activation
            // or license key validation
          }
          // Add an onAppear to handle the reset case
          .onAppear {
            if appWasReset {
              // Reset any necessary state when showing onboarding after a reset
              appWasReset = false
            }
          }
      } else {
        NavigationSplitView(columnVisibility: $navigationState.columnVisibility) {
          Sidebar(recordingManager: recordingManager, showRecordingError: $showRecordingError)
            .frame(minWidth: 220)
            .navigationSplitViewColumnWidth(min: 200, ideal: 220)
            .background(AppColors.systemGray6)
        } detail: {
          MeetingsView(
            recordingManager: recordingManager,
            showRecordingError: $showRecordingError,
            viewModel: meetingsViewModel
          )
          .frame(minWidth: 550)
        }
        .navigationSplitViewStyle(.balanced) // Use balanced style for normal view
        .preferredColorScheme(themeManager.currentTheme.colorScheme)
        .frame(minWidth: 800, minHeight: 500)
        .onAppear {
            setupApp()
            
            // Configure window appearance consistently with theme support
            DispatchQueue.main.async {
                if let window = NSApp.windows.first {
                    // Set consistent window size
                    window.setContentSize(NSSize(width: 900, height: 600))
                    window.minSize = NSSize(width: 800, height: 500)
                    
                    // Apply beautiful transitions
                    NSAnimationContext.runAnimationGroup { context in
                        context.duration = 0.3
                        context.allowsImplicitAnimation = true
                        
                        // Set toolbar appearance for better aesthetics
                        window.toolbar?.displayMode = .iconOnly
                        window.toolbar?.showsBaselineSeparator = false
                        
                        // Apply theme-appropriate style
                        applyThemeToWindow(window, theme: themeManager.currentTheme)
                    }
                }
            }
            
            // Check license state on startup
            Task {
              await licenseManager.restoreLicense()
            }
            
            // Check if trial is expired and no valid license
            if trialManager.trialExpired && licenseManager.licenseState != .registered {
              DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                showLicenseView = true
              }
            }
        }
        // Add an onChange to detect when the app is reset from settings
        .onChange(of: onboardingComplete) { oldValue, newValue in
          if !newValue {
            // The app was reset from settings
            appWasReset = true
          }
        }
        .sheet(isPresented: $showLicenseView) {
          LicenseView()
            .frame(width: 500, height: 400)
        }
        .onChange(of: themeManager.currentTheme) { oldTheme, newTheme in
          // Force refresh the window appearance when theme changes
          DispatchQueue.main.async {
            NSApp.windows.forEach { window in
              if newTheme == .system {
                // For system theme, reset to nil to use system appearance
                window.appearance = nil
              } else {
                // For explicit themes, apply the specified appearance
                window.appearance = newTheme == .dark ? NSAppearance(named: .darkAqua) : NSAppearance(named: .aqua)
              }
              
              // Apply theme-appropriate window background
              let isDarkTheme = newTheme == .dark || 
                (newTheme == .system && NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua)
              
              window.backgroundColor = isDarkTheme ? 
                NSColor(red: 0.15, green: 0.15, blue: 0.15, alpha: 1.0) : 
                NSColor(red: 0.95, green: 0.95, blue: 0.97, alpha: 1.0)
                
              // Update titlebar appearance
              if let titlebarView = window.standardWindowButton(.closeButton)?.superview {
                titlebarView.wantsLayer = true
                titlebarView.layer?.backgroundColor = isDarkTheme ?
                  NSColor(red: 0.15, green: 0.15, blue: 0.15, alpha: 1.0).cgColor :
                  NSColor(red: 0.95, green: 0.95, blue: 0.97, alpha: 1.0).cgColor
              }
              
              // Force redraw the content and toolbar
              window.contentView?.needsDisplay = true
              window.toolbar?.displayMode = .iconOnly
              window.toolbar?.showsBaselineSeparator = false
            }
          }
        }
      }
    }
    .windowStyle(HiddenTitleBarWindowStyle())
    .environmentObject(themeManager)
    .environmentObject(navigationState)
    .commands {
      CommandGroup(replacing: .newItem) {
        Button("New Recording") {
          // Check for API key and license before posting notification
          if !OpenAIManager.hasValidAPIKey() {
            // Post notification for missing API key
            NotificationCenter.default.post(
              name: Notification.Name("MissingAPIKey"),
              object: nil
            )
          } else if !(LicenseManager.shared.licenseState == .registered
            || TrialManager.shared.trialActive)
          {
            // Post notification for license required
            NotificationCenter.default.post(
              name: Notification.Name("LicenseRequired"),
              object: nil
            )
          } else {
            // Create a notification to trigger navigation to recording view
            NotificationCenter.default.post(
              name: Notification.Name("StartNewRecording"),
              object: nil
            )
          }
        }
        .keyboardShortcut("n")
      }

      CommandGroup(after: .appInfo) {
        Button("Check for Updates...") {
          // Handle updates check
        }

        Button("License Management...") {
          NotificationCenter.default.post(
            name: Notification.Name("ShowLicenseView"),
            object: nil
          )
        }
      }

      CommandMenu("Recording") {
        Button("Start") {
          // Check for API key and license before posting notification
          if !OpenAIManager.hasValidAPIKey() {
            // Post notification for missing API key
            NotificationCenter.default.post(
              name: Notification.Name("MissingAPIKey"),
              object: nil
            )
          } else if !(LicenseManager.shared.licenseState == .registered
            || TrialManager.shared.trialActive)
          {
            // Post notification for license required
            NotificationCenter.default.post(
              name: Notification.Name("LicenseRequired"),
              object: nil
            )
          } else {
            // Handle start recording
            NotificationCenter.default.post(
              name: Notification.Name("StartRecordingCommand"),
              object: nil
            )
          }
        }
        .keyboardShortcut("r")

        Button("Stop") {
          // Handle stop recording
          NotificationCenter.default.post(
            name: Notification.Name("StopRecordingCommand"),
            object: nil
          )
        }
        .keyboardShortcut("s")

        Divider()

        Button("Generate Notes") {
          // Handle note generation
        }
        .keyboardShortcut("g")
      }
    }
  }

  private func setupApp() {
    // This is where we can add any initial setup code that should run on app launch
    // For example, setting up logging, checking for first run, etc.

    // Make sure the application has a proper directory for storing data
    if !onboardingComplete {
      return  // We'll set this up during onboarding
    }

    // If we're using a custom storage location, make sure it exists
    if let storagePath = UserDefaults.standard.string(forKey: "dataStoragePath") {
      let url = URL(fileURLWithPath: storagePath)
      do {
        try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
      } catch {
        print("Failed to create storage directory: \(error.localizedDescription)")
      }
    }
    
    // Set up the dock menu on the main thread
    DispatchQueue.main.async {
      NSApp.dockTile.contentView = nil // Reset any existing content
      
      // Register the custom dock menu with the app delegate
      // We need to add this to AppDelegate instead of setting it directly
      NotificationCenter.default.post(
        name: Notification.Name("SetupDockMenu"),
        object: self.dockMenuDelegate.createDockMenu()
      )
      
      // Force refresh the dock tile
      NSApp.dockTile.display()
    }
  }
}

// Native macOS visual effect background
private func applyThemeToWindow(_ window: NSWindow, theme: AppTheme) {
    let isDarkTheme = theme == .dark ||
        (theme == .system && NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua)
    
    // Apply proper appearance
    window.appearance = isDarkTheme ? NSAppearance(named: .darkAqua) : NSAppearance(named: .aqua)
    
    // Set window background appropriate for theme
    if isDarkTheme {
        window.backgroundColor = NSColor(red: 0.15, green: 0.15, blue: 0.15, alpha: 1.0)
    } else {
        window.backgroundColor = NSColor(red: 0.95, green: 0.95, blue: 0.97, alpha: 1.0)
    }
    
    // Ensure titlebar matches theme
    if let titlebarView = window.standardWindowButton(.closeButton)?.superview {
        titlebarView.wantsLayer = true
        titlebarView.layer?.backgroundColor = isDarkTheme ?
            NSColor(red: 0.15, green: 0.15, blue: 0.15, alpha: 1.0).cgColor :
            NSColor(red: 0.95, green: 0.95, blue: 0.97, alpha: 1.0).cgColor
    }
    
    // Make sure the toolbar appearance matches the theme
    window.toolbar?.displayMode = .iconOnly
    window.toolbar?.showsBaselineSeparator = false
    
    // Refresh window to apply changes
    window.contentView?.needsDisplay = true
}

struct VisualEffectView: NSViewRepresentable {
  func makeNSView(context: Context) -> NSVisualEffectView {
    let view = NSVisualEffectView()
    view.blendingMode = .behindWindow
    view.state = .active
    view.material = .windowBackground
    return view
  }

  func updateNSView(_ nsView: NSVisualEffectView, context: Context) {}
}

// Main view with proper macOS styling
struct MainView: View {
  @StateObject private var trialManager = TrialManager.shared
  @StateObject private var licenseManager = LicenseManager.shared
  @StateObject private var permissionsManager = PermissionsManager()
  @StateObject private var recordingManager = RecordingManager.shared
  @StateObject private var meetingsViewModel = MeetingsViewModel()
  @State private var showPaywall = false
  @State private var showPermissionsAlert = false
  @State private var showRecordingError = false
  @State private var showLicenseView = false

  var body: some View {
    NavigationSplitView(columnVisibility: .constant(.all)) {
      Sidebar(recordingManager: recordingManager, showRecordingError: $showRecordingError)
        .navigationSplitViewColumnWidth(min: 200, ideal: 220)
    } detail: {
      MeetingsView(
        recordingManager: recordingManager,
        showRecordingError: $showRecordingError,
        viewModel: meetingsViewModel
      )
    }
    .alert("Screen Recording Permission Required", isPresented: $showPermissionsAlert) {
      Button("Open System Settings") {
        permissionsManager.requestScreenCapturePermission { granted in
          if granted {
            UserDefaults.standard.set(true, forKey: "screenPermissionPreviouslyGranted")
          }
        }
      }
      Button("Later", role: .cancel) {}
    } message: {
      Text(
        "SessionScribe needs screen recording permission to capture system audio during your sessions. After granting permission in System Settings, please restart the app."
      )
    }
    .alert("Recording Error", isPresented: $showRecordingError) {
      Button("OK", role: .cancel) {}
    } message: {
      if let error = recordingManager.error {
        Text(error.localizedDescription)
      }
    }
    .sheet(isPresented: $showPaywall) {
      PaywallView(isModal: true)
    }
    .sheet(isPresented: $showLicenseView) {
      LicenseView()
        .frame(width: 500, height: 400)
    }
    .toolbar {
      // Only include the record button in the primary action area
      ToolbarItem(placement: .primaryAction) {
        RecordButton(recordingManager: recordingManager, showRecordingError: $showRecordingError)
      }

      // Add license management button
      ToolbarItem(placement: .automatic) {
        Button(action: {
          showLicenseView = true
        }) {
          Image(systemName: "key.fill")
            .font(.system(size: 14))
        }
        .help("License Management")
      }

      if (trialManager.trialActive || licenseManager.licenseState == .trial)
        && !trialManager.hasValidPurchase
      {
        ToolbarItem(placement: .automatic) {
          HStack(spacing: 8) {
            Text("\(trialManager.daysRemaining) days remaining")
              .font(.system(size: 11))
              .foregroundColor(.secondary)

            Button("Upgrade") {
              showPaywall = true
            }
            .buttonStyle(.borderedProminent)
            .controlSize(.small)
          }
        }
      }
    }
    .onAppear {
      // Check license and trial status
      Task {
        await licenseManager.restoreLicense()
      }
      trialManager.checkTrialStatus()
      permissionsManager.checkPermissionStatus()

      // Setup notification observer for license management
      NotificationCenter.default.addObserver(
        forName: Notification.Name("ShowLicenseView"),
        object: nil,
        queue: .main
      ) { _ in
        self.showLicenseView = true
      }

      // Setup notification observer for showing paywall
      NotificationCenter.default.addObserver(
        forName: Notification.Name("ShowPaywall"),
        object: nil,
        queue: .main
      ) { _ in
        self.showPaywall = true
      }

      // Show license prompt if trial expired
      if trialManager.trialExpired && licenseManager.licenseState != .registered {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
          showPaywall = true
        }
      }

      DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        if !permissionsManager.screenCapturePermissionGranted {
          if UserDefaults.standard.bool(forKey: "screenPermissionPreviouslyGranted") {
            showPermissionsAlert = true
          }
        } else {
          UserDefaults.standard.set(true, forKey: "screenPermissionPreviouslyGranted")
        }
      }
    }
    .onDisappear {
      // Clean up notification observers
      NotificationCenter.default.removeObserver(
        self,
        name: Notification.Name("ShowLicenseView"),
        object: nil
      )

      NotificationCenter.default.removeObserver(
        self,
        name: Notification.Name("ShowPaywall"),
        object: nil
      )
    }
  }

  private func toggleSidebar() {
    #if os(macOS)
      NSApp.keyWindow?.firstResponder?.tryToPerform(
        #selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
    #endif
  }
}

// Add TimeInterval extension for formatting duration
extension TimeInterval {
  var formattedDuration: String {
    let hours = Int(self) / 3600
    let minutes = Int(self) / 60 % 60
    let seconds = Int(self) % 60

    if hours > 0 {
      return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
    } else {
      return String(format: "%02d:%02d", minutes, seconds)
    }
  }
}

// Navigation state manager
class NavigationState: ObservableObject {
    @Published var columnVisibility: NavigationSplitViewVisibility = .all
}

// Dock menu delegate to handle the dock menu
class DockMenuDelegate: NSObject, NSMenuDelegate {
  func createDockMenu() -> NSMenu {
    let menu = NSMenu()
    
    // Open Scribe
    let openItem = NSMenuItem(title: "Open Scribe", action: #selector(openApp), keyEquivalent: "")
    openItem.target = self
    menu.addItem(openItem)
    
    // New meeting
    let newMeetingItem = NSMenuItem(title: "New Meeting", action: #selector(newMeeting), keyEquivalent: "")
    newMeetingItem.target = self
    menu.addItem(newMeetingItem)
    
    // View All Notes
    let viewNotesItem = NSMenuItem(title: "View All Notes", action: #selector(viewAllNotes), keyEquivalent: "")
    viewNotesItem.target = self
    menu.addItem(viewNotesItem)
    
    menu.addItem(NSMenuItem.separator())
    
    // Version info
    let versionItem = NSMenuItem(title: "Scribe v5.273.0", action: nil, keyEquivalent: "")
    versionItem.isEnabled = false
    menu.addItem(versionItem)
    
    let updatedItem = NSMenuItem(title: "Latest version (just checked!)", action: nil, keyEquivalent: "")
    updatedItem.isEnabled = false
    menu.addItem(updatedItem)
    
    // Check for updates
    let checkUpdatesItem = NSMenuItem(title: "Check for updates", action: #selector(checkForUpdates), keyEquivalent: "")
    checkUpdatesItem.target = self
    menu.addItem(checkUpdatesItem)
    
    menu.addItem(NSMenuItem.separator())
    
    // Quit
    let quitItem = NSMenuItem(title: "Quit Scribe Completely", action: #selector(quitApp), keyEquivalent: "")
    quitItem.target = self
    menu.addItem(quitItem)
    
    return menu
  }
  
  @objc func openApp() {
    NSApp.activate(ignoringOtherApps: true)
  }
  
  @objc func newMeeting() {
    NSApp.activate(ignoringOtherApps: true)
    // Post notification to trigger new meeting
    NotificationCenter.default.post(name: Notification.Name("StartNewRecording"), object: nil)
  }
  
  @objc func viewAllNotes() {
    NSApp.activate(ignoringOtherApps: true)
    // This would navigate to the meetings list view (which is usually the default view)
  }
  
  @objc func checkForUpdates() {
    NSApp.activate(ignoringOtherApps: true)
    // Would trigger the update check mechanism
  }
  
  @objc func quitApp() {
    NSApp.terminate(nil)
  }
}

// Create a reusable RecordButton component
struct RecordButton: View {
  @ObservedObject var recordingManager: RecordingManager
  @ObservedObject private var licenseManager = LicenseManager.shared
  @ObservedObject private var trialManager = TrialManager.shared
  @Binding var showRecordingError: Bool
  @State private var showPaywall = false

  var body: some View {
    HStack(spacing: 16) {
      if recordingManager.isRecording {
        Text(recordingManager.recordingDuration.formattedDuration)
          .monospacedDigit()
          .foregroundColor(.secondary)
      }

      Button(action: {
        Task {
          // Check if license is valid or trial is active
          if licenseManager.licenseState == .registered || trialManager.trialActive {
            if recordingManager.isRecording {
              await recordingManager.stopRecording()
            } else {
              do {
                try await recordingManager.startRecording()
              } catch {
                showRecordingError = true
              }
            }
          } else {
            // Show paywall if license expired or not registered
            showPaywall = true
          }
        }
      }) {
        Image(systemName: recordingManager.isRecording ? "stop.circle.fill" : "record.circle")
          .symbolRenderingMode(.hierarchical)
          .foregroundColor(recordingManager.isRecording ? .red : nil)
          .font(.system(size: 18))
      }
      .help(recordingManager.isRecording ? "Stop Recording" : "Start Recording")
      .keyboardShortcut("r", modifiers: [.command])
      .sheet(isPresented: $showPaywall) {
        PaywallView(isModal: true)
      }
    }
  }
}

// Sidebar with native macOS styling
struct Sidebar: View {
  @ObservedObject var recordingManager: RecordingManager
  @Binding var showRecordingError: Bool
  @StateObject private var meetingsViewModel = MeetingsViewModel()

  enum NavigationItem: String {
    case meetings = "Meetings"
    case templates = "Templates"
    case settings = "Settings"

    var icon: String {
      switch self {
      case .meetings: return "waveform"
      case .templates: return "doc.text"
      case .settings: return "gear"
      }
    }
  }

  @State private var selection: NavigationItem? = .meetings

  var body: some View {
    List(selection: $selection) {
      Section {
        ForEach([NavigationItem.meetings, .templates], id: \.self) { item in
          NavigationLink(value: item) {
            Label(item.rawValue, systemImage: item.icon)
          }
        }
      }

      Section {
        NavigationLink(value: NavigationItem.settings) {
          Label(NavigationItem.settings.rawValue, systemImage: NavigationItem.settings.icon)
        }
      }
    }
    .background(AppColors.systemGray6)
    .navigationSplitViewColumnWidth(min: 200, ideal: 220)
    .navigationDestination(for: NavigationItem.self) { item in
      Group {
        switch item {
        case .meetings:
          MeetingsView(
            recordingManager: recordingManager,
            showRecordingError: $showRecordingError,
            viewModel: meetingsViewModel
          )
        case .templates:
          TemplatesView(recordingManager: recordingManager, showRecordingError: $showRecordingError)
        case .settings:
          SettingsViewWrapper(
            recordingManager: recordingManager, showRecordingError: $showRecordingError)
        }
      }
      .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
  }
}

// Meetings view with proper implementation
struct MeetingsView: View {
  @ObservedObject var recordingManager: RecordingManager
  @Binding var showRecordingError: Bool
  @ObservedObject var viewModel: MeetingsViewModel
  @State private var navigateToRecording = false
  @State private var showToast = false
  @State private var toastMessage = ""

  // SimpleToast configuration
  private let toastOptions = SimpleToastOptions(
    alignment: .bottom,
    hideAfter: 3,
    animation: .default,
    modifierType: .slide
  )

  var body: some View {
    NavigationStack {
      MeetingsListView(
        viewModel: viewModel,
        recordingManager: recordingManager,
        showRecordingError: $showRecordingError,
        navigateToRecording: $navigateToRecording
      )
      // Replace ignoresSafeArea with proper safe area handling
      .safeAreaInset(edge: .top) {
        Color.clear.frame(height: 0)
      }
      .navigationDestination(isPresented: $navigateToRecording) {
        RecordingView()
      }
    }
    // Remove negative padding
    .onAppear {
      // Setup notification observer for navigation from menu
      NotificationCenter.default.addObserver(
        forName: Notification.Name("StartNewRecording"),
        object: nil,
        queue: .main
      ) { _ in
        self.navigateToRecording = true
      }

      // Setup notification observer for missing API key
      NotificationCenter.default.addObserver(
        forName: Notification.Name("MissingAPIKey"),
        object: nil,
        queue: .main
      ) { _ in
        self.toastMessage = "OpenAI API key is required to start a new meeting"
        self.showToast = true
      }

      // Meeting data changes are now handled reactively through the NotesStore

      // Setup notification observer for license required
      NotificationCenter.default.addObserver(
        forName: Notification.Name("LicenseRequired"),
        object: nil,
        queue: .main
      ) { _ in
        self.toastMessage = "Valid license required to start a new meeting"
        self.showToast = true

        // Show paywall
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
          NotificationCenter.default.post(
            name: Notification.Name("ShowPaywall"),
            object: nil
          )
        }
      }

      // Setup notification observers for start/stop recording commands
      NotificationCenter.default.addObserver(
        forName: Notification.Name("StartRecordingCommand"),
        object: nil,
        queue: .main
      ) { _ in
        self.navigateToRecording = true
      }

      NotificationCenter.default.addObserver(
        forName: Notification.Name("StopRecordingCommand"),
        object: nil,
        queue: .main
      ) { _ in
        // Handle stop recording if needed
      }
    }
    .onDisappear {
      // Clean up notification observers
      NotificationCenter.default.removeObserver(
        self,
        name: Notification.Name("StartNewRecording"),
        object: nil
      )

      NotificationCenter.default.removeObserver(
        self,
        name: Notification.Name("MissingAPIKey"),
        object: nil
      )

      NotificationCenter.default.removeObserver(
        self,
        name: Notification.Name("StartRecordingCommand"),
        object: nil
      )

      NotificationCenter.default.removeObserver(
        self,
        name: Notification.Name("StopRecordingCommand"),
        object: nil
      )

      NotificationCenter.default.removeObserver(
        self,
        name: Notification.Name("LicenseRequired"),
        object: nil
      )

      // MeetingDataChanged observer was removed in favor of reactive pattern
    }
    .simpleToast(isPresented: $showToast, options: toastOptions) {
      // Customize SimpleToast appearance
      HStack {
        Image(systemName: "exclamationmark.circle.fill")
          .foregroundColor(.white)
        Text(toastMessage)
          .foregroundColor(.white)
          .font(AppFont.body())
      }
      .padding(.horizontal, 16)
      .padding(.vertical, 12)
      .background(AppColors.error.opacity(0.9))
      .cornerRadius(8)
      .padding(.bottom, 16)  // Add additional padding at the bottom
      .padding(.horizontal, 8)  // Add some horizontal padding as well
    }
  }
}

// Placeholder views with proper macOS styling
struct TemplatesView: View {
  @ObservedObject var recordingManager: RecordingManager
  @Binding var showRecordingError: Bool

  var body: some View {
    VStack(spacing: 16) {
      Text("Templates")
        .font(.system(size: 28, weight: .semibold))

      // Add your templates content here
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .padding()
  }
}

// Update SettingsView wrapper to use NavigationStack like MeetingsView
struct SettingsViewWrapper: View {
  @ObservedObject var recordingManager: RecordingManager
  @Binding var showRecordingError: Bool

  var body: some View {
    NavigationStack {
      SettingsView(
        recordingManager: recordingManager
      )
      // Replace ignoresSafeArea with proper safe area handling
      .safeAreaInset(edge: .top) {
        Color.clear.frame(height: 0)
      }
    }
  }
}

// Paywall view
struct PaywallView: View {
  var isModal: Bool = false
  @Environment(\.presentationMode) var presentationMode
  @State private var showLicenseView = false
  @ObservedObject private var licenseManager = LicenseManager.shared

  var body: some View {
    ZStack {
      Color.black.opacity(0.8)
        .edgesIgnoringSafeArea(.all)

      VStack(spacing: 30) {
        Image(systemName: "timer")
          .resizable()
          .aspectRatio(contentMode: .fit)
          .frame(width: 80, height: 80)
          .foregroundColor(.white)

        Text("Your Trial Has Ended")
          .font(AppFont.heading1())
          .foregroundColor(.white)

        Text("Upgrade to continue using SessionScribe")
          .font(AppFont.body())
          .foregroundColor(.white.opacity(0.8))
          .multilineTextAlignment(.center)

        CardView {
          VStack(spacing: 20) {
            HStack {
              VStack(alignment: .leading) {
                Text("Full License")
                  .font(AppFont.bodyBold())

                Text("One-time purchase, lifetime access")
                  .font(AppFont.caption())
                  .foregroundColor(AppColors.textSecondary)
              }

              Spacer()

              Text("$29.99")
                .font(AppFont.heading2())
            }

            Divider()

            VStack(alignment: .leading, spacing: 10) {
              Text("Includes:")
                .font(AppFont.bodyBold())

              Text(
                "• Unlimited meetings\n• AI-powered transcription and notes\n• All future updates\n• System audio and microphone capture\n• Local storage with optional iCloud sync"
              )
              .font(AppFont.body())
              .foregroundColor(AppColors.textSecondary)
            }

            VStack(spacing: 10) {
              Button("Purchase License") {
                // Open Gumroad purchase link
                if let url = URL(string: "https://gum.co/sessionscribe") {
                  NSWorkspace.shared.open(url)
                }
              }
              .buttonStyle(AppButton(isFullWidth: true))

              Text("or")
                .font(.caption)
                .foregroundColor(.gray)

              Button("Enter License Key") {
                showLicenseView = true
              }
              .buttonStyle(AppButton(type: .outline, isFullWidth: true))
            }
            .padding(.top)
          }
          .padding()
        }
        .frame(width: 400)

        HStack {
          Button("I Have a License") {
            showLicenseView = true
          }
          .buttonStyle(AppButton(type: .outline))

          if isModal {
            Button("Close") {
              presentationMode.wrappedValue.dismiss()
            }
            .buttonStyle(AppButton(type: .outline))
          }
        }
        .padding(.top)
      }
      .padding(40)
      .sheet(isPresented: $showLicenseView) {
        LicenseView()
          .frame(width: 500, height: 400)
          .onDisappear {
            // If license was registered successfully, dismiss the paywall
            if licenseManager.licenseState == .registered {
              if isModal {
                presentationMode.wrappedValue.dismiss()
              }
            }
          }
      }
    }
  }
}

================
File: SettingsView.swift
================
import SwiftUI

struct SettingsView: View {
  @ObservedObject var recordingManager: RecordingManager
  @Environment(\.presentationMode) var presentationMode

  @AppStorage("onboardingComplete") private var onboardingComplete: Bool = true
  @AppStorage("chunkDurationSeconds") private var chunkDurationSeconds: Int = 10
  @State private var showResetConfirmation = false
  @State private var showLicenseView = false

  @StateObject private var themeManager = ThemeManager.shared
  @StateObject private var openAIManager = OpenAIManager.shared

  @State private var isTestingAPIKey = false
  @State private var apiKeyValid = false
  @State private var showAPIKeyValidation = false
  @State private var showAPIKeyTestResult = false
  @State private var apiKeyTestMessage = ""
  
  @State private var isTestingModelCompat = false
  @State private var modelCompatValid = false
  @State private var showModelTestResult = false
  @State private var modelTestMessage = ""

  @Namespace private var animation

  var body: some View {
    ScrollView {
      VStack(alignment: .leading, spacing: 20) {
        // Header with close button
        HStack {
            Text("Settings")
                .font(AppFont.heading1())
                .foregroundColor(AppColors.primaryAccent)
            
            Spacer()
            
            Button(action: {
                presentationMode.wrappedValue.dismiss()
            }) {
                Image(systemName: "xmark.circle.fill")
                    .font(.system(size: 24))
                    .foregroundColor(AppColors.textSecondary)
            }
            .buttonStyle(PlainButtonStyle())
        }
        .padding(.bottom, 8)

        // Interface Settings
        interfaceSettingsSection
          
        // Recording Settings
        recordingSettingsSection
        
        // Transcription Settings
        transcriptionSettingsSection

        // OpenAI API Settings
        openAISettingsSection

        // App Management
        appManagementSection
      }
      .padding(.horizontal, 24)
      .padding(.top, 16)
      .padding(.bottom, 24)
      .frame(maxWidth: 800, alignment: .leading)
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(AppColors.primaryBackground)
    .alert("Reset Application", isPresented: $showResetConfirmation) {
      Button("Cancel", role: .cancel) {}
      Button("Reset", role: .destructive) {
        resetApp()
      }
    } message: {
      Text(
        "This will reset the application and show the onboarding flow again. Your meetings and settings will be preserved. Are you sure you want to continue?"
      )
    }
    .sheet(isPresented: $showLicenseView) {
      LicenseView()
        .frame(width: 500, height: 400)
    }
  }
    
  // MARK: - Interface Settings Section
  private var interfaceSettingsSection: some View {
    VStack(alignment: .leading, spacing: 12) {
      SectionHeader(title: "Interface", icon: "paintbrush.fill")

      SettingsCard {
        VStack(alignment: .leading, spacing: 16) {
          Text("Theme")
            .font(AppFont.bodyBold())
            .foregroundColor(AppColors.textPrimary)

          ThemeSelector(selectedTheme: $themeManager.currentTheme, namespace: animation)

          Text("Changes will apply immediately")
            .font(AppFont.caption())
            .foregroundColor(AppColors.textSecondary.opacity(0.8))
        }
      }
    }
  }
  
  // MARK: - Recording Settings Section
  private var recordingSettingsSection: some View {
    VStack(alignment: .leading, spacing: 12) {
      SectionHeader(title: "Recording", icon: "waveform")
      
      SettingsCard {
        VStack(alignment: .leading, spacing: 16) {
          Text("Audio Chunk Duration")
            .font(AppFont.bodyBold())
            .foregroundColor(AppColors.textPrimary)
          
          Text("Set how frequently audio is processed. Shorter chunks provide faster feedback but may cost more.")
            .font(AppFont.caption())
            .foregroundColor(AppColors.textSecondary.opacity(0.8))
          
          ChunkDurationSelector(selectedDuration: $chunkDurationSeconds)
          
          HStack(spacing: 8) {
            Image(systemName: "dollarsign.circle.fill")
              .font(.system(size: 12))
              .foregroundColor(AppColors.textSecondary.opacity(0.7))
            
            Text("OpenAI charges $0.007 per minute of audio with no partial minute billing. Shorter chunks may increase total cost.")
              .font(AppFont.caption())
              .foregroundColor(AppColors.textSecondary.opacity(0.7))
          }
          .padding(.top, 4)
        }
      }
    }
  }
  
  // MARK: - Chunk Duration Selector Component
  private struct ChunkDurationSelector: View {
    @Binding var selectedDuration: Int
    
    // Available durations in seconds
    private let durations = [10, 20, 30, 60]
    
    // Called when view appears to ensure default value is valid
    private func ensureValidDuration() {
      if !durations.contains(selectedDuration) {
        selectedDuration = 10 // Force to default if invalid
      }
    }
    
    var body: some View {
      VStack(spacing: 8) {
        HStack {
          ForEach(0..<durations.count, id: \.self) { index in
            Text(formatDuration(durations[index]))
              .font(AppFont.bodyBold())
              .foregroundColor(durations[index] == selectedDuration ? AppColors.primaryAccent : AppColors.textPrimary)
              .frame(maxWidth: .infinity)
          }
        }
        .padding(.horizontal, 8)
        
        ImprovedSlider(
          selectedIndex: Binding(
            get: {
              durations.firstIndex(of: selectedDuration) ?? 0
            },
            set: { newIndex in
              if newIndex >= 0 && newIndex < durations.count {
                selectedDuration = durations[newIndex]
              }
            }
          ),
          totalOptions: durations.count
        )
      }
      .onAppear {
        ensureValidDuration()
      }
    }
    
    private func formatDuration(_ seconds: Int) -> String {
      return seconds < 60 ? "\(seconds)s" : "1m"
    }
  }
  
  // MARK: - Improved Discrete Slider Component
  private struct ImprovedSlider: View {
    @Binding var selectedIndex: Int
    let totalOptions: Int
    
    // Calculate spacing between tick positions
    private func tickSpacing(_ width: CGFloat) -> CGFloat {
      return width / CGFloat(totalOptions - 1)
    }
    
    // Calculate position for thumb
    private func thumbPosition(_ width: CGFloat) -> CGFloat {
      let spacing = tickSpacing(width)
      return spacing * CGFloat(selectedIndex)
    }
    
    var body: some View {
      GeometryReader { geometry in
        let availableWidth = geometry.size.width - 20 // Account for thumb width
        
        ZStack(alignment: .leading) {
          // Track
          Rectangle()
            .fill(AppColors.secondaryBackground.opacity(0.5))
            .frame(height: 6)
            .cornerRadius(3)
          
          // Fill
          Rectangle()
            .fill(
              LinearGradient(
                colors: [AppColors.primaryAccent, AppColors.primaryAccent.opacity(0.8)],
                startPoint: .leading,
                endPoint: .trailing
              )
            )
            .frame(width: thumbPosition(availableWidth) + 10, height: 6)
            .cornerRadius(3)
            
          // Tick marks
          HStack(spacing: 0) {
            ForEach(0..<totalOptions, id: \.self) { index in
              // Tick mark
              Rectangle()
                .fill(index <= selectedIndex ? AppColors.primaryAccent : AppColors.textSecondary.opacity(0.3))
                .frame(width: 2, height: 12)
                .cornerRadius(1)
                
              // Add spacing except after the last tick
              if index < totalOptions - 1 {
                Spacer()
              }
            }
          }
          .padding(.horizontal, 10)
          
          // Thumb
          Circle()
            .fill(AppColors.primaryAccent)
            .frame(width: 20, height: 20)
            .shadow(color: AppColors.primaryAccent.opacity(0.3), radius: 3, x: 0, y: 2)
            .position(x: thumbPosition(availableWidth) + 10, y: 10)
            .gesture(
              DragGesture()
                .onChanged { gesture in
                  updateSelectedIndex(width: availableWidth, dragX: gesture.location.x)
                }
            )
        }
        .frame(height: 20)
        .padding(.horizontal, 10)
      }
      .frame(height: 30)
    }
    
    private func updateSelectedIndex(width: CGFloat, dragX: CGFloat) {
      let spacing = tickSpacing(width)
      let rawIndex = round(max(0, min(width, dragX)) / spacing)
      let newIndex = Int(max(0, min(Double(totalOptions - 1), rawIndex)))
      
      withAnimation(.interactiveSpring()) {
        selectedIndex = newIndex
      }
    }
  }

  // MARK: - Theme Selector Component
  private struct ThemeSelector: View {
    @Binding var selectedTheme: AppTheme
    var namespace: Namespace.ID

    var body: some View {
      HStack(spacing: 12) {
        ForEach(AppTheme.allCases) { theme in
          ThemeOption(
            theme: theme,
            isSelected: selectedTheme == theme,
            namespace: namespace,
            action: {
              withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
                selectedTheme = theme
              }
            }
          )
        }
      }
      .padding(4)
      .background(
        AppColors.secondaryBackground.opacity(0.5)
      )
      .cornerRadius(18)
    }
  }

  private struct ThemeOption: View {
    let theme: AppTheme
    let isSelected: Bool
    let namespace: Namespace.ID
    let action: () -> Void

    var body: some View {
      Button(action: action) {
        HStack(spacing: 8) {
          Image(systemName: themeIcon)
            .font(.system(size: 16, weight: .medium))
            .foregroundColor(isSelected ? .white : themeIconColor)

          Text(theme.rawValue)
            .font(AppFont.captionBold())
            .foregroundColor(isSelected ? .white : AppColors.textPrimary)
        }
        .padding(.horizontal, 16)
        .padding(.vertical, 10)
        .frame(maxWidth: .infinity)
        .background(
          ZStack {
            if isSelected {
              RoundedRectangle(cornerRadius: 14)
                .fill(
                  LinearGradient(
                    colors: gradientColors,
                    startPoint: .topLeading,
                    endPoint: .bottomTrailing
                  )
                )
                .matchedGeometryEffect(id: "selectedTheme", in: namespace)
                .shadow(color: gradientColors[0].opacity(0.3), radius: 4, x: 0, y: 2)
            }
          }
        )
        .contentShape(Rectangle())
        .animation(.spring(response: 0.3, dampingFraction: 0.7), value: isSelected)
      }
      .buttonStyle(PlainButtonStyle())
    }

    private var themeIcon: String {
      switch theme {
      case .light: return "sun.max.fill"
      case .dark: return "moon.stars.fill"
      case .system: return "gearshape.fill"
      }
    }

    private var themeIconColor: Color {
      switch theme {
      case .light: return Color.orange
      case .dark: return Color.blue
      case .system: return Color.purple
      }
    }

    private var gradientColors: [Color] {
      switch theme {
      case .light: return [Color.orange, Color.orange.opacity(0.8)]
      case .dark: return [Color.blue, Color.blue.opacity(0.8)]
      case .system: return [Color.purple, Color.purple.opacity(0.8)]
      }
    }
  }

  // MARK: - Transcription Settings Section
  private var transcriptionSettingsSection: some View {
    VStack(alignment: .leading, spacing: 12) {
      SectionHeader(title: "Transcription", icon: "waveform.and.mic")
      
      // Transcription Method Card
      SettingsCard {
        VStack(alignment: .leading, spacing: 14) {
          Text("Transcription Engine")
            .font(AppFont.bodyBold())
            .foregroundColor(AppColors.textPrimary)
          
          Text("Choose which transcription engine to use")
            .font(AppFont.caption())
            .foregroundColor(AppColors.textSecondary.opacity(0.8))
          
          TranscriptionMethodSelector(recordingManager: recordingManager)
          
          // Show WhisperKit model selection if WhisperKit is selected
          if recordingManager.transcriptionMethod == .whisperKit {
            Divider()
              .padding(.vertical, 10)
            
            Text("WhisperKit Model")
              .font(AppFont.bodyBold())
              .foregroundColor(AppColors.textPrimary)
              .padding(.top, 6)
            
            Text("Select which model size to use for local transcription")
              .font(AppFont.caption())
              .foregroundColor(AppColors.textSecondary.opacity(0.8))
            
            WhisperKitModelSelector(recordingManager: recordingManager)
            
            // Use a state variable to force refresh of model status
            let modelStatus = recordingManager.getTranscriptionModelStatus()
            
            // Model status indicator
            HStack(spacing: 8) {
              if modelStatus.isLoading {
                ProgressView()
                  .scaleEffect(0.8)
                  .frame(width: 16, height: 16)
                Text("Loading model...")
                  .font(AppFont.caption())
                  .foregroundColor(AppColors.textSecondary)
              } else if modelStatus.isLoaded {
                Image(systemName: "checkmark.circle.fill")
                  .foregroundColor(AppColors.success)
                  .imageScale(.small)
                Text("Model ready")
                  .font(AppFont.caption())
                  .foregroundColor(AppColors.success)
              } else {
                Image(systemName: "exclamationmark.circle.fill")
                  .foregroundColor(AppColors.warning)
                  .imageScale(.small)
                Text("Model not loaded")
                  .font(AppFont.caption())
                  .foregroundColor(AppColors.warning)
              }
            }
            .padding(.top, 6)
            .onAppear {
              // Force refresh when view appears
              DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                recordingManager.refreshModelStatus()
              }
            }
            .onChange(of: recordingManager.transcriptionMethod) { _ in
              // Also refresh when transcription method changes
              DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                recordingManager.refreshModelStatus()
              }
            }
            
            // Add download button if not loaded and not loading
            if !modelStatus.isLoaded && !modelStatus.isLoading {
              Button("Download Model") {
                Task {
                  await recordingManager.downloadWhisperKitModel()
                }
              }
              .buttonStyle(ModernButton(type: .primary))
              .padding(.top, 8)
            }
          }
        }
      }
    }
  }

  // MARK: - API Provider Settings Section
  private var openAISettingsSection: some View {
    VStack(alignment: .leading, spacing: 12) {
      SectionHeader(title: "API Provider Settings", icon: "key.fill")
      
      // Provider Selection Card
      SettingsCard {
        VStack(alignment: .leading, spacing: 14) {
          Text("Provider Selection")
            .font(AppFont.bodyBold())
            .foregroundColor(AppColors.textPrimary)
            
          Text("Choose which API provider to use for note generation (transcription always uses Whisper)")
            .font(AppFont.caption())
            .foregroundColor(AppColors.textSecondary.opacity(0.8))
            
          ProviderSelectionView(
            selectedProvider: $openAIManager.selectedProvider
          )
        }
      }

      // API Key Card
      SettingsCard {
        VStack(alignment: .leading, spacing: 18) {
          if openAIManager.selectedProvider == .openAI {
            AppTextField(
              title: "OpenAI API Key",
              placeholder: "Enter your OpenAI API key",
              text: $openAIManager.apiKey,
              isSecure: true,
              showValidation: showAPIKeyValidation,
              validationMessage: "Invalid API key format",
              isValid: apiKeyValid,
              icon: "key"
            )
          } else {
            AppTextField(
              title: "OpenRouter API Key",
              placeholder: "Enter your OpenRouter API key",
              text: $openAIManager.openRouterApiKey,
              isSecure: true,
              showValidation: showAPIKeyValidation,
              validationMessage: "Invalid API key format",
              isValid: apiKeyValid,
              icon: "key"
            )
          }

          HStack(spacing: 12) {
            Button("Test API Key") {
              testAPIKey()
            }
            .buttonStyle(
              ModernButton(
                type: .primary,
                isLoading: isTestingAPIKey
              )
            )
            .disabled((openAIManager.selectedProvider == .openAI ? openAIManager.apiKey.isEmpty : openAIManager.openRouterApiKey.isEmpty) || isTestingAPIKey)

            Button("Clear") {
              openAIManager.clearAPIKey()
              showAPIKeyValidation = false
              showAPIKeyTestResult = false
            }
            .buttonStyle(
              ModernButton(
                type: .secondary
              )
            )
            .disabled((openAIManager.selectedProvider == .openAI ? openAIManager.apiKey.isEmpty : openAIManager.openRouterApiKey.isEmpty) || isTestingAPIKey)

            Spacer()
          }

          if showAPIKeyTestResult {
            HStack(spacing: 8) {
              Image(systemName: apiKeyValid ? "checkmark.circle.fill" : "xmark.circle.fill")
                .foregroundColor(apiKeyValid ? AppColors.success : AppColors.error)
                .imageScale(.medium)

              Text(apiKeyTestMessage)
                .font(AppFont.caption())
                .foregroundColor(apiKeyValid ? AppColors.success : AppColors.error)
            }
            .padding(.vertical, 6)
            .padding(.horizontal, 12)
            .background(
              (apiKeyValid ? AppColors.success : AppColors.error).opacity(0.1)
            )
            .cornerRadius(8)
          }

          HStack(spacing: 8) {
            Image(systemName: "lock.shield.fill")
              .font(.system(size: 12))
              .foregroundColor(AppColors.textSecondary.opacity(0.7))

            Text("Your API key is stored securely in the macOS Keychain")
              .font(AppFont.caption())
              .foregroundColor(AppColors.textSecondary.opacity(0.7))
          }
          .padding(.top, 4)
        }
      }

      // Model Selection Card
      SettingsCard {
        VStack(alignment: .leading, spacing: 14) {
          Text("Model Selection")
            .font(AppFont.bodyBold())
            .foregroundColor(AppColors.textPrimary)

          if openAIManager.selectedProvider == .openAI {
            Text("Choose which model to use for note generation")
              .font(AppFont.caption())
              .foregroundColor(AppColors.textSecondary.opacity(0.8))

            CompactModelSelectionView(
              selectedModel: $openAIManager.selectedModel
            )
          } else {
            Text("Enter the model name to use from OpenRouter")
              .font(AppFont.caption())
              .foregroundColor(AppColors.textSecondary.opacity(0.8))
              
            AppTextField(
              title: "Model Name",
              placeholder: "Enter OpenRouter model name (e.g., google/gemini-2.0-flash-lite-001)",
              text: $openAIManager.openRouterModelName,
              isSecure: false,
              icon: "rectangle.and.text.magnifyingglass"
            )
            
            
            HStack(spacing: 12) {
              Button("Test Model") {
                testModelCompatibility()
              }
              .buttonStyle(
                ModernButton(
                  type: .primary,
                  isLoading: isTestingModelCompat
                )
              )
              .disabled(openAIManager.openRouterModelName.isEmpty || openAIManager.openRouterApiKey.isEmpty || isTestingModelCompat)
            }
            .padding(.top, 6)
            
            if showModelTestResult {
              HStack(spacing: 8) {
                Image(systemName: modelCompatValid ? "checkmark.circle.fill" : "xmark.circle.fill")
                  .foregroundColor(modelCompatValid ? AppColors.success : AppColors.error)
                  .imageScale(.medium)

                Text(modelTestMessage)
                  .font(AppFont.caption())
                  .foregroundColor(modelCompatValid ? AppColors.success : AppColors.error)
              }
              .padding(.vertical, 6)
              .padding(.horizontal, 12)
              .background(
                (modelCompatValid ? AppColors.success : AppColors.error).opacity(0.1)
              )
              .cornerRadius(8)
            }
            
            HStack(spacing: 8) {
              Image(systemName: "info.circle.fill")
                .font(.system(size: 12))
                .foregroundColor(AppColors.textSecondary.opacity(0.7))
              
              Text("Find models at openrouter.ai/models")
                .font(AppFont.caption())
                .foregroundColor(AppColors.textSecondary.opacity(0.7))
            }
            .padding(.top, 4)
          }
        }
      }
    }
  }

  // MARK: - App Management Section
  private var appManagementSection: some View {
    VStack(alignment: .leading, spacing: 12) {
      SectionHeader(title: "Application", icon: "gearshape.2.fill")

      // License Management
      SettingsCard {
        VStack(alignment: .leading, spacing: 14) {
          Text("License Management")
            .font(AppFont.bodyBold())
            .foregroundColor(AppColors.textPrimary)

          Text("Manage your license or purchase a license to unlock all features")
            .font(AppFont.caption())
            .foregroundColor(AppColors.textSecondary.opacity(0.8))

          Button(action: {
            showLicenseView = true
          }) {
            HStack {
              Text("Manage License")
              Spacer()
              Image(systemName: "chevron.right")
                .font(.system(size: 12, weight: .bold))
            }
            .frame(maxWidth: .infinity)
          }
          .buttonStyle(ModernButton(type: .primary))
          .padding(.top, 6)
        }
      }

      // Reset Application
      SettingsCard {
        VStack(alignment: .leading, spacing: 14) {
          Text("Reset Application")
            .font(AppFont.bodyBold())
            .foregroundColor(AppColors.textPrimary)

          Text(
            "If you want to see the onboarding flow again or reset the application to its initial state, you can do so here."
          )
          .font(AppFont.caption())
          .foregroundColor(AppColors.textSecondary.opacity(0.8))

          Button("Reset and Show Onboarding") {
            showResetConfirmation = true
          }
          .buttonStyle(ModernButton(type: .destructive))
          .padding(.top, 6)
          .frame(maxWidth: .infinity)  // Make the button stretch to fill the available width
        }
      }
    }
  }

  // Reset the app to show onboarding again
  private func resetApp() {
    // Set onboardingComplete to false to show the onboarding flow again
    onboardingComplete = false

    // You could add additional reset logic here if needed
    // For example, clearing certain user defaults or resetting specific settings

    // Note: We're not clearing meetings or API keys to preserve user data
  }

  // MARK: - Provider Selection View
  private struct ProviderSelectionView: View {
    @Binding var selectedProvider: APIProvider
    
    var body: some View {
      HStack(spacing: 12) {
        ForEach(APIProvider.allCases) { provider in
          ProviderSelectionButton(
            provider: provider,
            isSelected: selectedProvider == provider,
            action: {
              withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
                selectedProvider = provider
              }
            }
          )
        }
      }
      .padding(4)
      .background(
        AppColors.secondaryBackground.opacity(0.5)
      )
      .cornerRadius(18)
    }
  }
  
  private struct ProviderSelectionButton: View {
    let provider: APIProvider
    let isSelected: Bool
    let action: () -> Void
    
    var body: some View {
      Button(action: action) {
        HStack(spacing: 8) {
          Image(systemName: providerIcon)
            .font(.system(size: 16, weight: .medium))
            .foregroundColor(isSelected ? .white : providerIconColor)
          
          Text(provider.displayName)
            .font(AppFont.captionBold())
            .foregroundColor(isSelected ? .white : AppColors.textPrimary)
        }
        .padding(.horizontal, 16)
        .padding(.vertical, 10)
        .frame(maxWidth: .infinity)
        .background(
          ZStack {
            if isSelected {
              RoundedRectangle(cornerRadius: 14)
                .fill(
                  LinearGradient(
                    colors: gradientColors,
                    startPoint: .topLeading,
                    endPoint: .bottomTrailing
                  )
                )
                .shadow(color: gradientColors[0].opacity(0.3), radius: 4, x: 0, y: 2)
            }
          }
        )
        .contentShape(Rectangle())
        .animation(.spring(response: 0.3, dampingFraction: 0.7), value: isSelected)
      }
      .buttonStyle(PlainButtonStyle())
    }
    
    private var providerIcon: String {
      switch provider {
      case .openAI: return "sparkles"
      case .openRouter: return "network"
      }
    }
    
    private var providerIconColor: Color {
      switch provider {
      case .openAI: return Color.green
      case .openRouter: return Color.blue
      }
    }
    
    private var gradientColors: [Color] {
      switch provider {
      case .openAI: return [Color.green, Color.green.opacity(0.8)]
      case .openRouter: return [Color.blue, Color.blue.opacity(0.8)]
      }
    }
  }

  // MARK: - Compact Model Selection View
  private struct CompactModelSelectionView: View {
    @Binding var selectedModel: OpenAIModel

    var body: some View {
      VStack(spacing: 0) {
        ForEach(OpenAIModel.allCases) { model in
          CompactModelRow(
            model: model,
            isSelected: selectedModel == model,
            action: {
              withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
                selectedModel = model
              }
            }
          )

          if model != OpenAIModel.allCases.last {
            Divider()
              .padding(.leading, 40)
          }
        }
      }
      .background(
        RoundedRectangle(cornerRadius: 12)
          .fill(AppColors.secondaryBackground.opacity(0.5))
      )
      .clipShape(RoundedRectangle(cornerRadius: 12))
      .overlay(
        RoundedRectangle(cornerRadius: 12)
          .stroke(AppColors.textSecondary.opacity(0.1), lineWidth: 1)
      )
    }
  }

  // MARK: - Compact Model Row Component
  private struct CompactModelRow: View {
    let model: OpenAIModel
    let isSelected: Bool
    let action: () -> Void

    var body: some View {
      Button(action: action) {
        HStack(spacing: 12) {
          ZStack {
            Circle()
              .fill(isSelected ? AppColors.primaryAccent : Color.clear)
              .frame(width: 20, height: 20)

            if isSelected {
              Image(systemName: "checkmark")
                .font(.system(size: 10, weight: .bold))
                .foregroundColor(.white)
            } else {
              Circle()
                .strokeBorder(AppColors.textSecondary.opacity(0.3), lineWidth: 1.5)
                .frame(width: 20, height: 20)
            }
          }
          .frame(width: 20, height: 20)

          VStack(alignment: .leading, spacing: 2) {
            Text(model.displayName)
              .font(AppFont.bodyBold())
              .foregroundColor(AppColors.textPrimary)

            if let description = modelDescription(for: model) {
              Text(description)
                .font(AppFont.caption())
                .foregroundColor(AppColors.textSecondary.opacity(0.8))
            }
          }

          Spacer()

          ModelBadge(model: model)
        }
        .padding(.vertical, 12)
        .padding(.horizontal, 14)
        .background(
          RoundedRectangle(cornerRadius: 8)
            .fill(isSelected ? AppColors.primaryAccent.opacity(0.05) : Color.clear)
        )
        .contentShape(Rectangle())
      }
      .buttonStyle(PlainButtonStyle())
    }

    private func modelDescription(for model: OpenAIModel) -> String? {
      switch model {
      case .gpt4o:
        return "Versatile flagship model with 128K context - ~18¢ per meeting hour"
      case .gpt4oMini:
        return "Fast, affordable model for focused tasks - ~0.2¢ per meeting hour"
      case .o3Mini:
        return "High intelligence reasoning model - ~5.2¢ per meeting hour"
      case .o1:
        return "Advanced reasoning model with 200K context - ~71¢ per meeting hour"
      case .o1Mini:
        return "Affordable reasoning model with 128K context - ~5.2¢ per meeting hour"
      }
    }
  }

  // MARK: - Model Badge Component
  private struct ModelBadge: View {
    let model: OpenAIModel

    var body: some View {
      Text(modelType)
        .font(.system(size: 10, weight: .bold))
        .padding(.horizontal, 8)
        .padding(.vertical, 4)
        .background(
          Capsule()
            .fill(badgeColor.opacity(0.15))
        )
        .foregroundColor(badgeColor)
    }

    private var modelType: String {
      switch model {
      case .gpt4o, .gpt4oMini: return "GPT"
      case .o3Mini: return "GPT"
      case .o1Mini, .o1: return "GPT"
      }
    }

    private var badgeColor: Color {
      switch model {
      case .gpt4o, .gpt4oMini: return Color.green
      case .o3Mini: return Color.blue
      case .o1Mini, .o1: return Color.blue.opacity(0.8)
      }
    }
  }
  // MARK: - Section Header
  struct SectionHeader: View {
    let title: String
    let icon: String

    var body: some View {
      HStack(spacing: 10) {
        ZStack {
          Circle()
            .fill(AppColors.primaryAccent.opacity(0.1))
            .frame(width: 36, height: 36)

          Image(systemName: icon)
            .font(.system(size: 16, weight: .semibold))
            .foregroundColor(AppColors.primaryAccent)
        }

        Text(title)
          .font(AppFont.heading2())
          .foregroundColor(AppColors.textPrimary)
      }
      .padding(.top, 8)
      .padding(.bottom, 6)
    }
  }

  struct SettingsCard<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
      self.content = content()
    }

    var body: some View {
      content
        .padding(20)
        .background(
          RoundedRectangle(cornerRadius: 16)
            .fill(AppColors.secondaryBackground)
        )
        .overlay(
          RoundedRectangle(cornerRadius: 16)
            .stroke(AppColors.textSecondary.opacity(0.05), lineWidth: 1)
        )
        .shadow(color: Color.black.opacity(0.05), radius: 10, x: 0, y: 4)
    }
  }

  // MARK: - Modern Button Style
  struct ModernButton: ButtonStyle {
    enum ButtonType {
      case primary, secondary, destructive

      var foregroundColor: Color {
        switch self {
        case .primary: return .white
        case .secondary: return AppColors.textPrimary
        case .destructive: return .white
        }
      }

      var backgroundColor: Color {
        switch self {
        case .primary: return AppColors.primaryAccent
        case .secondary: return Color.clear
        case .destructive: return Color.red
        }
      }

      var borderColor: Color {
        switch self {
        case .primary: return AppColors.primaryAccent
        case .secondary: return AppColors.textSecondary.opacity(0.3)
        case .destructive: return Color.red
        }
      }
    }

    let type: ButtonType
    var isLoading: Bool = false

    func makeBody(configuration: Configuration) -> some View {
      HStack {
        if isLoading {
          ProgressView()
            .scaleEffect(0.8)
            .padding(.trailing, 5)
        }

        configuration.label
      }
      .padding(.horizontal, 16)
      .padding(.vertical, 8)
      .background(
        Capsule()
          .fill(type.backgroundColor)
      )
      .overlay(
        Capsule()
          .stroke(type.borderColor, lineWidth: type == .secondary ? 1 : 0)
      )
      .foregroundColor(type.foregroundColor)
      .font(AppFont.bodyBold())
      .opacity(configuration.isPressed ? 0.8 : 1.0)
      .scaleEffect(configuration.isPressed ? 0.98 : 1.0)
      .animation(.easeOut(duration: 0.2), value: configuration.isPressed)
    }
  }

  // MARK: - API Key Testing
  private func testAPIKey() {
    let currentKey: String
    let provider = openAIManager.selectedProvider
    
    switch provider {
    case .openAI:
      guard !openAIManager.apiKey.isEmpty else { return }
      currentKey = openAIManager.apiKey
    case .openRouter:
      guard !openAIManager.openRouterApiKey.isEmpty else { return }
      currentKey = openAIManager.openRouterApiKey
    }

    isTestingAPIKey = true
    showAPIKeyValidation = true
    showAPIKeyTestResult = false

    // Format validation based on provider
    let keyTrimmed = currentKey.trimmingCharacters(in: .whitespacesAndNewlines)
    let isValidFormat = keyTrimmed.count >= provider.apiKeyMinLength && keyTrimmed.hasPrefix(provider.apiKeyPrefix)

    if !isValidFormat {
      isTestingAPIKey = false
      apiKeyValid = false
      showAPIKeyTestResult = true
      apiKeyTestMessage = "Invalid API key format"
      return
    }

    // Perform a basic test with a real API call
    let testEndpoint: String
    
    switch provider {
    case .openAI:
      testEndpoint = "\(provider.baseURL)/models"
    case .openRouter:
      testEndpoint = "\(provider.baseURL)/models"
    }
    
    guard let url = URL(string: testEndpoint) else {
      isTestingAPIKey = false
      apiKeyValid = false
      showAPIKeyTestResult = true
      apiKeyTestMessage = "Failed to create API request"
      return
    }
    
    var request = URLRequest(url: url)
    request.httpMethod = "GET"
    
    // Set appropriate headers based on provider
    switch provider {
    case .openAI:
      request.addValue("Bearer \(keyTrimmed)", forHTTPHeaderField: "Authorization")
    case .openRouter:
      request.addValue("Bearer \(keyTrimmed)", forHTTPHeaderField: "Authorization")
      request.addValue(openAIManager.openRouterSiteUrl, forHTTPHeaderField: "HTTP-Referer")
      request.addValue(openAIManager.openRouterSiteName, forHTTPHeaderField: "X-Title")
    }
    
    let task = URLSession.shared.dataTask(with: request) { _, response, error in
      DispatchQueue.main.async {
        if let error = error {
          self.apiKeyValid = false
          self.showAPIKeyTestResult = true
          self.apiKeyTestMessage = "Error: \(error.localizedDescription)"
        } else if let httpResponse = response as? HTTPURLResponse {
          if (200...299).contains(httpResponse.statusCode) {
            self.apiKeyValid = true
            self.showAPIKeyTestResult = true
            self.apiKeyTestMessage = "API key is valid"
          } else {
            self.apiKeyValid = false
            self.showAPIKeyTestResult = true
            self.apiKeyTestMessage = "Invalid API key (HTTP \(httpResponse.statusCode))"
          }
        } else {
          self.apiKeyValid = false
          self.showAPIKeyTestResult = true
          self.apiKeyTestMessage = "Unknown error occurred"
        }
        
        self.isTestingAPIKey = false
      }
    }
    
    task.resume()
  }
  
  // MARK: - Model Compatibility Testing
  private func testModelCompatibility() {
    guard openAIManager.selectedProvider == .openRouter,
          !openAIManager.openRouterApiKey.isEmpty,
          !openAIManager.openRouterModelName.isEmpty else { return }
    
    isTestingModelCompat = true
    showModelTestResult = false
    
    // Create a very simple completion request to test the model
    let modelName = openAIManager.openRouterModelName.trimmingCharacters(in: .whitespacesAndNewlines)
    let apiKey = openAIManager.openRouterApiKey.trimmingCharacters(in: .whitespacesAndNewlines)
    
    let testEndpoint = "\(APIProvider.openRouter.baseURL)/chat/completions"
    
    guard let url = URL(string: testEndpoint) else {
      isTestingModelCompat = false
      modelCompatValid = false
      showModelTestResult = true
      modelTestMessage = "Failed to create API request"
      return
    }
    
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
    request.addValue(openAIManager.openRouterSiteUrl, forHTTPHeaderField: "HTTP-Referer")
    request.addValue(openAIManager.openRouterSiteName, forHTTPHeaderField: "X-Title")
    
    // Simple test message - keep it minimal to reduce token usage
    let requestBody: [String: Any] = [
      "model": modelName,
      "messages": [
        ["role": "system", "content": "You are a helpful assistant."],
        ["role": "user", "content": "Say hello."]
      ],
      "max_tokens": 5
    ]
    
    guard let jsonData = try? JSONSerialization.data(withJSONObject: requestBody) else {
      isTestingModelCompat = false
      modelCompatValid = false
      showModelTestResult = true
      modelTestMessage = "Failed to create request data"
      return
    }
    
    request.httpBody = jsonData
    
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
      DispatchQueue.main.async {
        if let error = error {
          self.modelCompatValid = false
          self.showModelTestResult = true
          self.modelTestMessage = "Error: \(error.localizedDescription)"
        } else if let httpResponse = response as? HTTPURLResponse {
          if (200...299).contains(httpResponse.statusCode) {
            self.modelCompatValid = true
            self.showModelTestResult = true
            self.modelTestMessage = "Model validated successfully"
          } else {
            self.modelCompatValid = false
            self.showModelTestResult = true
            
            if let data = data, let errorResponse = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
               let error = errorResponse["error"] as? [String: Any],
               let message = error["message"] as? String {
              self.modelTestMessage = "Error: \(message)"
            } else {
              self.modelTestMessage = "Invalid model (HTTP \(httpResponse.statusCode))"
            }
          }
        } else {
          self.modelCompatValid = false
          self.showModelTestResult = true
          self.modelTestMessage = "Unknown error occurred"
        }
        
        self.isTestingModelCompat = false
      }
    }
    
    task.resume()
  }

  // MARK: - Transcription Method Selector
  private struct TranscriptionMethodSelector: View {
    @ObservedObject var recordingManager: RecordingManager
    
    var body: some View {
      VStack(spacing: 8) {
        ForEach(TranscriptionMethod.allCases) { method in
          TranscriptionMethodRow(
            method: method,
            isSelected: recordingManager.transcriptionMethod == method,
            action: {
              withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
                recordingManager.setTranscriptionMethod(method)
              }
            }
          )
        }
      }
      .background(
        RoundedRectangle(cornerRadius: 12)
          .fill(AppColors.secondaryBackground.opacity(0.5))
      )
      .clipShape(RoundedRectangle(cornerRadius: 12))
      .overlay(
        RoundedRectangle(cornerRadius: 12)
          .stroke(AppColors.textSecondary.opacity(0.1), lineWidth: 1)
      )
    }
  }

  // MARK: - Transcription Method Row
  private struct TranscriptionMethodRow: View {
    let method: TranscriptionMethod
    let isSelected: Bool
    let action: () -> Void
    
    var body: some View {
      Button(action: action) {
        HStack(spacing: 12) {
          ZStack {
            Circle()
              .fill(isSelected ? method.iconColor : Color.clear)
              .frame(width: 30, height: 30)
            
            Image(systemName: method.icon)
              .font(.system(size: 14, weight: .semibold))
              .foregroundColor(isSelected ? .white : method.iconColor)
          }
          
          VStack(alignment: .leading, spacing: 2) {
            Text(method.rawValue)
              .font(AppFont.bodyBold())
              .foregroundColor(AppColors.textPrimary)
            
            Text(method.description)
              .font(AppFont.caption())
              .foregroundColor(AppColors.textSecondary.opacity(0.8))
              .lineLimit(2)
          }
          
          Spacer()
          
          if isSelected {
            Image(systemName: "checkmark")
              .font(.system(size: 14, weight: .bold))
              .foregroundColor(method.iconColor)
          }
        }
        .padding(.vertical, 12)
        .padding(.horizontal, 14)
        .background(
          RoundedRectangle(cornerRadius: 8)
            .fill(isSelected ? method.iconColor.opacity(0.1) : Color.clear)
        )
        .contentShape(Rectangle())
      }
      .buttonStyle(PlainButtonStyle())
    }
  }

  // MARK: - WhisperKit Model Selector
  private struct WhisperKitModelSelector: View {
    @ObservedObject var recordingManager: RecordingManager
    
    private let modelSizes = [
      ("tiny", "Tiny (32MB): Fastest, least accurate"),
      ("base", "Base (74MB): Fast, decent accuracy"),
      ("small", "Small (244MB): Good balance"),
    ]
    
    var body: some View {
      VStack(spacing: 8) {
        ForEach(modelSizes, id: \.0) { modelSize, description in
          ModelSizeRow(
            modelSize: modelSize,
            description: description,
            isSelected: recordingManager.whisperKitModelSize == modelSize,
            action: {
              withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
                recordingManager.whisperKitModelSize = modelSize
                // Refresh model status when model size changes
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                  recordingManager.refreshModelStatus()
                }
              }
            }
          )
          
          if modelSize != modelSizes.last?.0 {
            Divider()
              .padding(.leading, 36)
          }
        }
      }
      .background(
        RoundedRectangle(cornerRadius: 12)
          .fill(AppColors.secondaryBackground.opacity(0.5))
      )
      .clipShape(RoundedRectangle(cornerRadius: 12))
      .overlay(
        RoundedRectangle(cornerRadius: 12)
          .stroke(AppColors.textSecondary.opacity(0.1), lineWidth: 1)
      )
    }
  }

  // MARK: - Model Size Row
  private struct ModelSizeRow: View {
    let modelSize: String
    let description: String
    let isSelected: Bool
    let action: () -> Void
    
    var body: some View {
      Button(action: action) {
        HStack(spacing: 12) {
          ZStack {
            Circle()
              .fill(isSelected ? AppColors.primaryAccent : Color.clear)
              .frame(width: 20, height: 20)
            if isSelected {
              Image(systemName: "checkmark")
                .font(.system(size: 10, weight: .bold))
                .foregroundColor(.white)
            } else {
              Circle()
                .strokeBorder(AppColors.textSecondary.opacity(0.3), lineWidth: 1.5)
                .frame(width: 20, height: 20)
            }
          }
          .frame(width: 20, height: 20)
          
          VStack(alignment: .leading, spacing: 2) {
            Text(description.components(separatedBy: ":").first ?? modelSize)
              .font(AppFont.bodyBold())
              .foregroundColor(AppColors.textPrimary)
            
            if let descriptionPart = description.components(separatedBy: ":").last {
              Text(descriptionPart.trimmingCharacters(in: .whitespaces))
                .font(AppFont.caption())
                .foregroundColor(AppColors.textSecondary.opacity(0.8))
            }
          }
          
          Spacer()
        }
        .padding(.vertical, 10)
        .padding(.horizontal, 14)
        .background(
          RoundedRectangle(cornerRadius: 8)
            .fill(isSelected ? AppColors.primaryAccent.opacity(0.05) : Color.clear)
        )
        .contentShape(Rectangle())
      }
      .buttonStyle(PlainButtonStyle())
    }
  }
}

================
File: ThemeManager.swift
================
import SwiftUI

enum AppTheme: String, CaseIterable, Identifiable {
  case light = "Light"
  case dark = "Dark"
  case system = "System"

  var id: String { self.rawValue }

  var colorScheme: ColorScheme? {
    switch self {
    case .light: return .light
    case .dark: return .dark
    case .system: return nil
    }
  }
}

class ThemeManager: ObservableObject {
  static let shared = ThemeManager()

  private let themeKey = "appTheme"

  @Published var currentTheme: AppTheme = .system {
    didSet {
      setTheme(currentTheme)
      // Force UI update when theme changes
      objectWillChange.send()
    }
  }

  init() {
    if let storedThemeString = UserDefaults.standard.string(forKey: themeKey),
      let storedTheme = AppTheme(rawValue: storedThemeString)
    {
      self.currentTheme = storedTheme
    }
  }

  func setTheme(_ theme: AppTheme) {
    UserDefaults.standard.set(theme.rawValue, forKey: themeKey)

    #if os(macOS)
      // For macOS, we can set the appearance directly
      if let appearance = theme.appearance {
        NSApp.appearance = appearance
      } else {
        // For system theme, explicitly set to nil to use system setting
        NSApp.appearance = nil

        // Force update the UI when switching to system theme
        DispatchQueue.main.async {
          self.objectWillChange.send()
        }
      }
    #endif
  }

  #if os(macOS)
    var appearance: NSAppearance? {
      return currentTheme.appearance
    }
  #endif
}

#if os(macOS)
  extension AppTheme {
    var appearance: NSAppearance? {
      switch self {
      case .light:
        return NSAppearance(named: .aqua)
      case .dark:
        return NSAppearance(named: .darkAqua)
      case .system:
        return nil
      }
    }
  }
#endif

================
File: TranscriptionService.swift
================
import Foundation
import AVFoundation
import Combine
import WhisperKit
import SwiftUI

// Define a struct to represent a transcription result
struct TranscriptionResult {
    let text: String
    let timestamp: Date
    let isPartial: Bool
    let chunkIndex: Int
}

// Define callback types for completed transcriptions and errors
typealias TranscriptionCompletedCallback = (TranscriptionResult) -> Void
typealias TranscriptionErrorCallback = (Error) -> Void

// Enum for selecting transcription method
enum TranscriptionMethod: String, CaseIterable, Identifiable {
    case openAI = "OpenAI API"
    case whisperKit = "WhisperKit (Local)"
    
    var id: String { rawValue }
    
    var description: String {
        switch self {
        case .openAI:
            return "Cloud-based transcription using OpenAI's Whisper API. Requires internet and API key."
        case .whisperKit:
            return "Offline transcription using WhisperKit, processed locally on your device."
        }
    }
    
    var icon: String {
        switch self {
        case .openAI: return "cloud"
        case .whisperKit: return "desktopcomputer"
        }
    }
    
    var iconColor: Color {
        switch self {
        case .openAI: return .blue
        case .whisperKit: return .green
        }
    }
}

enum TranscriptionError: Error, LocalizedError {
    case apiKeyMissing
    case audioProcessingFailed
    case networkError(Error)
    case apiError(String)
    case modelNotLoaded
    case unknownError
    
    var errorDescription: String? {
        switch self {
        case .apiKeyMissing:
            return "OpenAI API key is missing"
        case .audioProcessingFailed:
            return "Failed to process audio file"
        case .networkError(let error):
            return "Network error: \(error.localizedDescription)"
        case .apiError(let message):
            return "API error: \(message)"
        case .modelNotLoaded:
            return "WhisperKit model is not loaded"
        case .unknownError:
            return "An unknown error occurred"
        }
    }
}

class CombinedTranscriptionService {
    // MARK: - Properties
    private var meetingTranscripts: [URL: [TranscriptionResult]] = [:]
    private var transcriptionCompletedCallback: TranscriptionCompletedCallback?
    private var transcriptionErrorCallback: TranscriptionErrorCallback?
    private var currentChunkIndex = 0
    private let transcriptionQueue = DispatchQueue(label: "com.sessionscribe.transcriptionQueue")
    
    // WhisperKit properties
    private var whisperPipeline: WhisperKit?
    private var modelLoaded = false
    private var isLoadingModel = false
    private var whisperKitModelSize = "small" // Default model size
    private var modelFolder: String? = nil
    
    // Transcription method
    private var transcriptionMethod: TranscriptionMethod
    
    // MARK: - Initialization
    init(
        transcriptionCompletedCallback: TranscriptionCompletedCallback? = nil,
        transcriptionErrorCallback: TranscriptionErrorCallback? = nil,
        transcriptionMethod: TranscriptionMethod = .openAI,
        whisperKitModelSize: String = "small",
        modelFolder: String? = nil
    ) {
        self.transcriptionCompletedCallback = transcriptionCompletedCallback
        self.transcriptionErrorCallback = transcriptionErrorCallback
        self.transcriptionMethod = transcriptionMethod
        self.whisperKitModelSize = whisperKitModelSize
        self.modelFolder = modelFolder
        
        print("CombinedTranscriptionService: Initialized with method: \(transcriptionMethod), callback: \(transcriptionCompletedCallback != nil ? "provided" : "nil")")
        
        // If using WhisperKit, start loading the model
        if transcriptionMethod == .whisperKit {
            loadWhisperKitModel()
        }
    }
    
    // MARK: - WhisperKit Setup
    
    private func loadWhisperKitModel() {
        guard !modelLoaded && !isLoadingModel else {
            print("CombinedTranscriptionService: WhisperKit model already loaded or is loading")
            return
        }
        
        isLoadingModel = true
        print("CombinedTranscriptionService: Starting to load WhisperKit model: \(whisperKitModelSize)")
        
        // Create a new WhisperKit instance
        Task {
            do {
                // Create config with model name and folder if provided
                let config = WhisperKitConfig(
                    model: whisperKitModelSize,
                    modelFolder: modelFolder,
                    verbose: true,
                    download: true  // Allow automatic download if model isn't found
                )
                print("CombinedTranscriptionService: Using model: \(whisperKitModelSize), folder: \(modelFolder ?? "default")")
                
                whisperPipeline = try await WhisperKit(config)
                
                // Models are loaded automatically in the initializer
                modelLoaded = true
                isLoadingModel = false
                print("CombinedTranscriptionService: WhisperKit model '\(whisperKitModelSize)' loaded successfully")
            } catch {
                isLoadingModel = false
                print("CombinedTranscriptionService: Failed to load WhisperKit model: \(error.localizedDescription)")
                handleError(.apiError("Failed to load model: \(error.localizedDescription)"))
            }
        }
    }
    
    // MARK: - Public Methods
    
    func setTranscriptionMethod(_ method: TranscriptionMethod) {
        transcriptionMethod = method
        print("CombinedTranscriptionService: Switching to transcription method: \(method)")
        
        // If switching to WhisperKit, check if we need to load the model
        if method == .whisperKit && !modelLoaded && !isLoadingModel {
            loadWhisperKitModel()
        }
    }
    
    func getCurrentTranscriptionMethod() -> TranscriptionMethod {
        return transcriptionMethod
    }
    
    func setWhisperKitModelSize(_ modelSize: String) {
        if whisperKitModelSize != modelSize {
            whisperKitModelSize = modelSize
            // Reset model state
            modelLoaded = false
            isLoadingModel = false
            whisperPipeline = nil
            
            // Reload model if using WhisperKit
            if transcriptionMethod == .whisperKit {
                loadWhisperKitModel()
            }
        }
    }
    
    func processAudioChunk(_ chunk: AudioChunk, chunkIndex: Int) {
        print("📥 CombinedTranscriptionService: Processing audio chunk \(chunkIndex) (isPartial: \(chunk.isPartial))")
        
        // Log the duration of the chunk for debugging
        let duration = chunk.endTime.timeIntervalSince(chunk.startTime)
        print("⏱️ CombinedTranscriptionService: Chunk \(chunkIndex) duration: \(duration) seconds")
        
        // Directly use the non-optional system audio URL
        let systemAudioURL = chunk.systemAudioURL
        let folderURL = systemAudioURL.deletingLastPathComponent()
        print("📂 CombinedTranscriptionService: Meeting folder: \(folderURL.lastPathComponent)")
        
        // Check which transcription method to use
        switch transcriptionMethod {
        case .openAI:
            // Check if we have a valid API key for OpenAI
            if OpenAIManager.hasValidAPIKey() {
                print("✅ CombinedTranscriptionService: Valid OpenAI API key found")
            } else {
                print("❌ CombinedTranscriptionService: No valid OpenAI API key")
                handleError(.apiKeyMissing)
                return
            }
            
        case .whisperKit:
            // Check if WhisperKit model is loaded or loading
            if !modelLoaded {
                if !isLoadingModel {
                    loadWhisperKitModel()
                }
                
                print("⏳ CombinedTranscriptionService: WhisperKit model not yet loaded, queuing transcription")
                // Wait a moment and check again
                transcriptionQueue.asyncAfter(deadline: .now() + 2.0) { [weak self] in
                    if self?.modelLoaded == true {
                        self?.processAudioChunk(chunk, chunkIndex: chunkIndex)
                    } else {
                        print("⚠️ CombinedTranscriptionService: WhisperKit model still loading, will retry in 2 seconds")
                        self?.transcriptionQueue.asyncAfter(deadline: .now() + 2.0) { [weak self] in
                            if self?.modelLoaded == true {
                                self?.processAudioChunk(chunk, chunkIndex: chunkIndex)
                            } else {
                                print("❌ CombinedTranscriptionService: WhisperKit model failed to load within timeout")
                                self?.handleError(.modelNotLoaded)
                            }
                        }
                    }
                }
                return
            }
        }
        
        // Process the audio and send to appropriate transcription method
        print("🔄 CombinedTranscriptionService: Queueing transcription task for chunk \(chunkIndex)")
        transcriptionQueue.async { [weak self] in
            self?.transcribeAudioChunk(chunk, chunkIndex: chunkIndex, folderURL: folderURL)
        }
    }
    
    func getAllTranscriptions(for meetingURL: URL) -> [TranscriptionResult] {
        return meetingTranscripts[meetingURL] ?? []
    }
    
    func clearTranscriptions(for meetingURL: URL) {
        meetingTranscripts[meetingURL] = []
    }
    
    func clearAllTranscriptions() {
        meetingTranscripts.removeAll()
    }
    
    // MARK: - Private Methods
    
    private func transcribeAudioChunk(_ chunk: AudioChunk, chunkIndex: Int, folderURL: URL) {
        print("🎬 CombinedTranscriptionService: Starting transcription for chunk \(chunkIndex)")
        print("📂 CombinedTranscriptionService: Meeting folder: \(folderURL.lastPathComponent)")
        print("⏱️ CombinedTranscriptionService: Chunk duration: \(chunk.endTime.timeIntervalSince(chunk.startTime)) seconds")
        
        // Log available audio sources (using non-optional properties)
        print("🔊 CombinedTranscriptionService: System audio available: \(chunk.systemAudioURL.lastPathComponent)")
        print("🎤 CombinedTranscriptionService: Mic audio available: \(chunk.micAudioURL.lastPathComponent)")
        
        // Combine the system and mic audio if needed
        let combinedAudioURL: URL?
        if let audioURL = combineAudioFiles(chunk: chunk) {
            combinedAudioURL = audioURL
            print("✅ CombinedTranscriptionService: Audio file prepared: \(audioURL.lastPathComponent)")
        } else {
            print("❌ CombinedTranscriptionService: Failed to prepare audio file")
            handleError(.audioProcessingFailed)
            return
        }
        
        guard let combinedAudioURL = combinedAudioURL else {
            print("❌ CombinedTranscriptionService: Combined audio URL is nil")
            handleError(.audioProcessingFailed)
            return
        }
        
        // Based on the selected transcription method, use OpenAI API or WhisperKit
        switch transcriptionMethod {
        case .openAI:
            transcribeWithWhisperAPI(audioURL: combinedAudioURL) { [weak self] result in
                self?.handleTranscriptionResult(result, chunk: chunk, chunkIndex: chunkIndex, folderURL: folderURL, combinedAudioURL: combinedAudioURL)
            }
            
        case .whisperKit:
            transcribeWithWhisperKit(audioURL: combinedAudioURL) { [weak self] result in
                self?.handleTranscriptionResult(result, chunk: chunk, chunkIndex: chunkIndex, folderURL: folderURL, combinedAudioURL: combinedAudioURL)
            }
        }
    }
    
    private func handleTranscriptionResult(_ result: Result<String, TranscriptionError>, chunk: AudioChunk, chunkIndex: Int, folderURL: URL, combinedAudioURL: URL) {
        switch result {
        case .success(let transcribedText):
            // Create a transcription result
            let transcriptionResult = TranscriptionResult(
                text: transcribedText,
                timestamp: chunk.startTime,
                isPartial: chunk.isPartial,
                chunkIndex: chunkIndex
            )
            
            // Store the result for this meeting
            self.transcriptionQueue.sync {
                var meetingResults = self.meetingTranscripts[folderURL] ?? []
                meetingResults.append(transcriptionResult)
                // Sort by timestamp to ensure chronological order
                meetingResults.sort { $0.timestamp < $1.timestamp }
                self.meetingTranscripts[folderURL] = meetingResults
            }
            
            // Notify callback on main thread
            DispatchQueue.main.async {
                print("CombinedTranscriptionService: Completed transcription for chunk \(chunkIndex): \"\(transcribedText.prefix(30))...\"")
                if let callback = self.transcriptionCompletedCallback {
                    callback(transcriptionResult)
                    print("CombinedTranscriptionService: Called completion callback for chunk \(chunkIndex)")
                } else {
                    print("CombinedTranscriptionService: No callback registered for completed transcription")
                }
            }
            
        case .failure(let error):
            self.handleError(error)
        }
        
        // Clean up the temporary combined audio file
        try? FileManager.default.removeItem(at: combinedAudioURL)
    }
    
    private func combineAudioFiles(chunk: AudioChunk) -> URL? {
        let systemAudioURL = chunk.systemAudioURL
        let micAudioURL = chunk.micAudioURL
        let folderURL = systemAudioURL.deletingLastPathComponent()
        
        // Debug: Log file sizes before combining using FileManager attributes.
        do {
            let systemAttributes = try FileManager.default.attributesOfItem(atPath: systemAudioURL.path)
            let micAttributes = try FileManager.default.attributesOfItem(atPath: micAudioURL.path)
            if let systemSize = systemAttributes[.size] as? UInt64, let micSize = micAttributes[.size] as? UInt64 {
                print("DEBUG: Before combining, file sizes - System audio: \(systemSize) bytes, Mic audio: \(micSize) bytes")
            }
        } catch {
            print("DEBUG: Error retrieving file sizes: \(error)")
        }
        
        // Optionally add a brief delay to allow file buffers to flush.
        Thread.sleep(forTimeInterval: 0.5)
        
        // Continue with opening the files.
        var systemAudioFile: AVAudioFile?
        var micAudioFile: AVAudioFile?
        
        do {
            systemAudioFile = try AVAudioFile(forReading: systemAudioURL)
            micAudioFile = try AVAudioFile(forReading: micAudioURL)
            
            guard let systemAudioFile = systemAudioFile, let micAudioFile = micAudioFile else {
                print("CombinedTranscriptionService: Failed to initialize audio files")
                return nil
            }
            
            let systemLength = systemAudioFile.length
            let micLength = micAudioFile.length
            
            print("CombinedTranscriptionService: System audio length: \(systemLength) frames")
            print("CombinedTranscriptionService: Mic audio length: \(micLength) frames")
            
            if systemLength == 0 && micLength == 0 {
                print("CombinedTranscriptionService: Both system and mic audio are empty")
                return nil
            } else if systemLength == 0 {
                print("CombinedTranscriptionService: System audio is empty, using mic audio only")
                return micAudioURL
            } else if micLength == 0 {
                print("CombinedTranscriptionService: Mic audio is empty, using system audio only")
                return systemAudioURL
            }
            
            // Both have data, so combine them.
            let combinedFileName = "combined_audio_\(UUID().uuidString).wav"
            let combinedURL = folderURL.appendingPathComponent(combinedFileName)
            let format = systemAudioFile.processingFormat
            let settings = systemAudioFile.fileFormat.settings
            
            // Read system audio buffer.
            guard let systemBuffer = AVAudioPCMBuffer(
                pcmFormat: format,
                frameCapacity: AVAudioFrameCount(systemLength)
            ) else {
                throw NSError(domain: "CombinedTranscriptionService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to create system buffer"])
            }
            try systemAudioFile.read(into: systemBuffer)
            
            // Read mic audio buffer.
            guard let micBuffer = AVAudioPCMBuffer(
                pcmFormat: format,
                frameCapacity: AVAudioFrameCount(micLength)
            ) else {
                throw NSError(domain: "CombinedTranscriptionService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to create mic buffer"])
            }
            try micAudioFile.read(into: micBuffer)
            
            // Create mixed buffer.
            let maxFrames = max(systemBuffer.frameLength, micBuffer.frameLength)
            guard let mixedBuffer = AVAudioPCMBuffer(
                pcmFormat: format,
                frameCapacity: maxFrames
            ) else {
                throw NSError(domain: "CombinedTranscriptionService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to create mixed buffer"])
            }
            mixedBuffer.frameLength = maxFrames
            
            // Mix the two audio buffers.
            let systemData = systemBuffer.floatChannelData?[0]
            let micData = micBuffer.floatChannelData?[0]
            let mixedData = mixedBuffer.floatChannelData?[0]
            
            for frame in 0..<Int(maxFrames) {
                let systemSample = frame < systemBuffer.frameLength ? systemData?[frame] ?? 0 : 0
                let micSample = frame < micBuffer.frameLength ? micData?[frame] ?? 0 : 0
                mixedData?[frame] = (systemSample + micSample) * 0.5
            }
            
            // Write the combined buffer to file.
            let combinedFile = try AVAudioFile(forWriting: combinedURL, settings: settings)
            try combinedFile.write(from: mixedBuffer)
            
            print("CombinedTranscriptionService: Successfully combined audio files")
            return combinedURL
        } catch {
            print("CombinedTranscriptionService: Error combining audio files: \(error.localizedDescription)")
            if let systemAudioFile = systemAudioFile, systemAudioFile.length > 0 {
                print("CombinedTranscriptionService: Falling back to system audio only")
                return systemAudioURL
            } else if let micAudioFile = micAudioFile, micAudioFile.length > 0 {
                print("CombinedTranscriptionService: Falling back to mic audio only")
                return micAudioURL
            } else {
                print("CombinedTranscriptionService: No valid audio to fall back to")
                return nil
            }
        }
    }
    
    // MARK: - OpenAI Whisper API
    
    private func transcribeWithWhisperAPI(audioURL: URL, completion: @escaping (Result<String, TranscriptionError>) -> Void) {
        print("🎙️ CombinedTranscriptionService: Starting audio transcription with OpenAI API for file: \(audioURL.lastPathComponent)")
        
        // Always use OpenAI for transcription, regardless of selected provider
        let apiKeyAccount = APIProvider.openAI.keychainKey
        guard let apiKey = KeychainManager.getKey(for: apiKeyAccount), !apiKey.isEmpty else {
            print("❌ CombinedTranscriptionService: OpenAI API key missing or empty (required for transcription)")
            completion(.failure(.apiKeyMissing))
            return
        }
        
        let modelName = "whisper-1"  // Always use Whisper
        let endpoint = "\(APIProvider.openAI.baseURL)/audio/transcriptions"
        
        print("✅ CombinedTranscriptionService: OpenAI API key found (starts with: \(apiKey.prefix(4))...)")
        print("🔄 CombinedTranscriptionService: Using model: \(modelName)")
        
        guard let url = URL(string: endpoint) else {
            print("❌ CombinedTranscriptionService: Failed to create API URL")
            completion(.failure(.unknownError))
            return
        }
        
        print("🔄 CombinedTranscriptionService: Preparing request to \(url.absoluteString)")
        
        // Create a multipart form request
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        
        let boundary = UUID().uuidString
        request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        
        var data = Data()
        
        // Set the model field to whisper-1
        data.append("--\(boundary)\r\n".data(using: .utf8)!)
        data.append("Content-Disposition: form-data; name=\"model\"\r\n\r\n".data(using: .utf8)!)
        data.append("\(modelName)\r\n".data(using: .utf8)!)
        
        // Add the audio file
        do {
            let audioData = try Data(contentsOf: audioURL)
            print("✅ CombinedTranscriptionService: Successfully loaded audio data: \(ByteCountFormatter.string(fromByteCount: Int64(audioData.count), countStyle: .file))")
            
            data.append("--\(boundary)\r\n".data(using: .utf8)!)
            data.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(audioURL.lastPathComponent)\"\r\n".data(using: .utf8)!)
            data.append("Content-Type: audio/mpeg\r\n\r\n".data(using: .utf8)!)
            data.append(audioData)
            data.append("\r\n".data(using: .utf8)!)
        } catch {
            print("❌ CombinedTranscriptionService: Failed to load audio data: \(error.localizedDescription)")
            completion(.failure(.audioProcessingFailed))
            return
        }
        
        data.append("--\(boundary)--\r\n".data(using: .utf8)!)
        request.httpBody = data
        
        print("🚀 CombinedTranscriptionService: Sending request to API (\(ByteCountFormatter.string(fromByteCount: Int64(data.count), countStyle: .file)))")
        
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                print("❌ CombinedTranscriptionService: Network error: \(error.localizedDescription)")
                completion(.failure(.networkError(error)))
                return
            }
            
            guard let httpResponse = response as? HTTPURLResponse else {
                print("❌ CombinedTranscriptionService: Invalid HTTP response")
                completion(.failure(.unknownError))
                return
            }
            
            print("🔄 CombinedTranscriptionService: Received HTTP \(httpResponse.statusCode) response")
            
            guard (200...299).contains(httpResponse.statusCode) else {
                let errorMessage = data.flatMap { String(data: $0, encoding: .utf8) } ?? "Unknown error"
                print("❌ CombinedTranscriptionService: API error (HTTP \(httpResponse.statusCode)): \(errorMessage)")
                completion(.failure(.apiError("HTTP \(httpResponse.statusCode): \(errorMessage)")))
                return
            }
            
            guard let data = data else {
                print("❌ CombinedTranscriptionService: No data received in response")
                completion(.failure(.unknownError))
                return
            }
            
            print("✅ CombinedTranscriptionService: Received \(ByteCountFormatter.string(fromByteCount: Int64(data.count), countStyle: .file)) of response data")
            
            do {
                let decoder = JSONDecoder()
                let whisperResponse = try decoder.decode(OpenAIWhisperResponse.self, from: data)
                print("✅ CombinedTranscriptionService: Successfully parsed response")
                
                // Clean up the transcript text
                let cleanedText = self.cleanTranscriptText(whisperResponse.text)
                
                print("📝 CombinedTranscriptionService: Transcription result: \"\(cleanedText.prefix(100))...\"")
                completion(.success(cleanedText))
            } catch {
                print("❌ CombinedTranscriptionService: Failed to parse response: \(error.localizedDescription)")
                if let responseString = String(data: data, encoding: .utf8) {
                    print("🔍 CombinedTranscriptionService: Raw response: \(responseString)")
                }
                completion(.failure(.apiError("Failed to parse response: \(error.localizedDescription)")))
            }
        }
        
        task.resume()
        print("🔄 CombinedTranscriptionService: Request sent, waiting for response...")
    }
    
    /// Cleans up transcript text by removing XML tags and other unwanted elements
    private func cleanTranscriptText(_ text: String) -> String {
        print("🧹 CombinedTranscriptionService: Cleaning transcript text: \"\(text.prefix(50))...\"")
        
        // Create a mutable copy of the text
        var cleanedText = text
        
        // First, check if the text contains any of the special tags
        let containsTags = text.contains("<|startoftranscript|>") || 
                          text.contains("<|en|>") || 
                          text.contains("<|transcribe|>") ||
                          text.contains("<|endoftext|>") ||
                          text.contains("<|")
        
        if !containsTags {
            print("✅ CombinedTranscriptionService: No tags found, text is already clean")
            return text
        }
        
        print("🔍 CombinedTranscriptionService: Found tags in transcript, cleaning...")
        
        // Extract the actual content between the tags
        // This pattern looks for the actual content after the initial tags
        if let startTagsRegex = try? NSRegularExpression(
            pattern: "<\\|startoftranscript\\|><\\|en\\|><\\|transcribe\\|>(?:<\\|\\d+\\.\\d+\\|>)?\\s*(.*?)(?:<\\|\\d+\\.\\d+\\|>)?(?:<\\|endoftext\\|>)?$",
            options: [.dotMatchesLineSeparators]
        ) {
            let nsString = cleanedText as NSString
            let matches = startTagsRegex.matches(
                in: cleanedText,
                options: [],
                range: NSRange(location: 0, length: nsString.length)
            )
            
            if let match = matches.first, match.numberOfRanges > 1 {
                let contentRange = match.range(at: 1)
                if contentRange.location != NSNotFound {
                    // Extract just the content
                    cleanedText = nsString.substring(with: contentRange)
                    print("✅ CombinedTranscriptionService: Extracted content between tags")
                }
            }
        }
        
        // Simple direct string replacements for any remaining tags
        let tagsToRemove = [
            "<|startoftranscript|>",
            "<|en|>",
            "<|transcribe|>",
            "<|endoftext|>"
        ]
        
        // Remove any remaining tags with direct string replacements
        for tag in tagsToRemove {
            cleanedText = cleanedText.replacingOccurrences(of: tag, with: "")
        }
        
        // Process timestamps with a more reliable approach
        // This regex pattern matches timestamp tags like <|12.34|>
        let timestampPattern = "<\\|(\\d+\\.\\d+)\\|>"
        
        if let regex = try? NSRegularExpression(pattern: timestampPattern, options: []) {
            let nsString = cleanedText as NSString
            let matches = regex.matches(in: cleanedText, options: [], range: NSRange(location: 0, length: nsString.length))
            
            // Process matches from end to beginning to avoid index shifting
            for match in matches.reversed() {
                if match.numberOfRanges > 1 {
                    let timestampRange = match.range(at: 1)
                    let timestamp = nsString.substring(with: timestampRange)
                    
                    if let seconds = Double(timestamp) {
                        let formattedTime = formatTimestamp(seconds)
                        cleanedText = (cleanedText as NSString).replacingCharacters(
                            in: match.range, 
                            with: "[\(formattedTime)]"
                        )
                    } else {
                        // If we can't parse the timestamp, just remove the tag
                        cleanedText = (cleanedText as NSString).replacingCharacters(
                            in: match.range, 
                            with: ""
                        )
                    }
                }
            }
        }
        
        // Remove any "Inaudible" markers and replace with a standard notation
        cleanedText = cleanedText.replacingOccurrences(of: "\\[ Inaudible \\]", with: "[inaudible]", options: .regularExpression)
        
        // Remove duplicate sentences that might appear in overlapping chunks
        cleanedText = removeDuplicateSentences(cleanedText)
        
        // Trim whitespace and normalize spacing
        cleanedText = cleanedText.trimmingCharacters(in: .whitespacesAndNewlines)
        cleanedText = cleanedText.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
        
        print("✅ CombinedTranscriptionService: Cleaned transcript: \"\(cleanedText.prefix(50))...\"")
        return cleanedText
    }
    
    /// Removes duplicate sentences that might appear in overlapping chunks
    private func removeDuplicateSentences(_ text: String) -> String {
        // Split text into sentences (roughly)
        let sentenceDelimiters = CharacterSet(charactersIn: ".!?")
        let sentences = text.components(separatedBy: sentenceDelimiters)
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }
        
        // Remove duplicates while preserving order
        var uniqueSentences: [String] = []
        var seenSentences: Set<String> = []
        
        for sentence in sentences {
            let normalized = sentence.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
            if !seenSentences.contains(normalized) && normalized.count > 3 {
                uniqueSentences.append(sentence)
                seenSentences.insert(normalized)
            }
        }
        
        // Rejoin with proper punctuation
        return uniqueSentences.joined(separator: ". ") + "."
    }

    /// Formats a timestamp in seconds to a readable format (MM:SS)
    private func formatTimestamp(_ seconds: Double) -> String {
        let minutes = Int(seconds) / 60
        let remainingSeconds = Int(seconds) % 60
        return String(format: "%02d:%02d", minutes, remainingSeconds)
    }
    
    // MARK: - WhisperKit Local
    
    private func transcribeWithWhisperKit(audioURL: URL, completion: @escaping (Result<String, TranscriptionError>) -> Void) {
        print("🎙️ CombinedTranscriptionService: Starting audio transcription with WhisperKit for file: \(audioURL.lastPathComponent)")
        
        guard let whisperPipeline = whisperPipeline, modelLoaded else {
            print("❌ CombinedTranscriptionService: WhisperKit model not loaded")
            completion(.failure(.modelNotLoaded))
            return
        }
        
        Task {
            do {
                // Use the audio path method which handles audio loading internally
                print("🔄 CombinedTranscriptionService: Starting transcription with WhisperKit")
                
                // Transcribe using WhisperKit - it returns an array of TranscriptionResult
                let results = try await whisperPipeline.transcribe(audioPath: audioURL.path)
                
                // Combine all segment texts into a single transcription
                let combinedText = results.map { result in
                    // We need to extract text from the TranscriptionResult
                    // Assuming TranscriptionResult has a text property or segments with text
                    return result.segments.map { $0.text }.joined(separator: " ")
                }.joined(separator: " ")
                
                // Clean up the transcript text - same as we do for OpenAI
                let cleanedText = self.cleanTranscriptText(combinedText)
                
                print("✅ CombinedTranscriptionService: WhisperKit transcription completed successfully")
                print("📝 CombinedTranscriptionService: Transcription result: \"\(cleanedText.prefix(100))...\"")
                
                DispatchQueue.main.async {
                    completion(.success(cleanedText))
                }
            } catch {
                print("❌ CombinedTranscriptionService: WhisperKit transcription failed: \(error.localizedDescription)")
                DispatchQueue.main.async {
                    completion(.failure(.apiError(error.localizedDescription)))
                }
            }
        }
    }
    
    private func handleError(_ error: TranscriptionError) {
        print("❌ CombinedTranscriptionService: Error - \(error)")
        
        // Provide more detailed error information
        switch error {
        case .apiKeyMissing:
            print("🔑 CombinedTranscriptionService: OpenAI API key is missing or invalid.")
        case .audioProcessingFailed:
            print("🔊 CombinedTranscriptionService: Failed to process audio file.")
        case .networkError(let underlyingError):
            print("🌐 CombinedTranscriptionService: Network error: \(underlyingError.localizedDescription)")
        case .apiError(let message):
            print("🔄 CombinedTranscriptionService: API error: \(message)")
        case .modelNotLoaded:
            print("⚠️ CombinedTranscriptionService: WhisperKit model not loaded")
        case .unknownError:
            print("❓ CombinedTranscriptionService: Unknown error occurred")
        }
        
        DispatchQueue.main.async { [weak self] in
            self?.transcriptionErrorCallback?(error)
        }
    }
    
    // MARK: - Helper Methods
    
    /// Checks if the WhisperKit model with the specified size is loaded
    /// - Parameter modelSize: The size of the model to check
    /// - Returns: True if the model is loaded, false otherwise
    func isWhisperKitModelLoaded(modelSize: String) -> Bool {
        // Check if the model size matches the current model and if it's loaded
        return self.whisperKitModelSize == modelSize && modelLoaded && !isLoadingModel
    }
    
    func getFullTranscriptText(for meetingURL: URL) -> String {
        let results = meetingTranscripts[meetingURL] ?? []
        // Sort by chunk index to ensure proper order
        let sortedResults = results.sorted { $0.chunkIndex < $1.chunkIndex }
        // Combine all text
        return sortedResults.map { $0.text }.joined(separator: " ")
    }
}

// Helper struct for parsing the OpenAI Whisper API response
struct OpenAIWhisperResponse: Decodable {
    let text: String
}

================
File: TrialManager.swift
================
import CryptoKit
import Foundation
import Security

class TrialManager: ObservableObject {
  static let shared = TrialManager()

  private let trialDuration: TimeInterval = 7 * 24 * 60 * 60  // 7 days in seconds
  private let trialStartDateKey = "trialStartDate"
  private let trialCheckKey = "trialIntegrityCheck"
  private let deviceIdentifierKey = "deviceIdentifier"
  private let trialActivatedKey = "trialActivated"
  private let hasValidPurchaseKey = "hasValidPurchaseKey"
  private let licenseKeyKey = "licenseKey"
  private let trialEmailKey = "trialEmail"

  @Published var trialActive = false
  @Published var trialExpired = false
  @Published var daysRemaining: Int = 0
  @Published var hasValidPurchase = false

  @Published var licenseKey: String = ""
  @Published var trialEmail: String = ""
  @Published var isValidatingLicense: Bool = false
  @Published var licenseErrorMessage: String = ""
  @Published var licenseValidated: Bool = false
  
  // Computed property for days used in trial
  var daysUsed: Int {
    guard let startDate = trialStartDate else { return 0 }
    let now = Date()
    let elapsed = now.timeIntervalSince(startDate)
    // Full days
    let used = Int(elapsed / 86400.0)
    return max(0, used)
  }

  // Computed property for trial progress (0.0 to 1.0)
  var trialProgress: Double {
    guard let startDate = trialStartDate else { return 0 }
    let now = Date()
    let elapsed = now.timeIntervalSince(startDate)
    let fraction = elapsed / trialDuration
    // Clamp between 0 and 1
    return max(0, min(1, fraction))
  }

  private var trialStartDate: Date? {
    get {
      return UserDefaults.standard.object(forKey: trialStartDateKey) as? Date
    }
    set {
      UserDefaults.standard.set(newValue, forKey: trialStartDateKey)

      if let date = newValue {
        // Also store in keychain as a backup verification method
        storeTrialDateInKeychain(date)

        // Create and store a hash for integrity verification
        createAndStoreIntegrityCheck(for: date)
      }
    }
  }

  private var deviceIdentifier: String {
    if let storedIdentifier = UserDefaults.standard.string(forKey: deviceIdentifierKey) {
      return storedIdentifier
    } else {
      let newIdentifier = generateDeviceIdentifier()
      UserDefaults.standard.set(newIdentifier, forKey: deviceIdentifierKey)
      return newIdentifier
    }
  }

  init() {
    // Check if user has already purchased
    self.hasValidPurchase = UserDefaults.standard.bool(forKey: hasValidPurchaseKey)

    // Load license key if exists
    self.licenseKey = UserDefaults.standard.string(forKey: licenseKeyKey) ?? ""

    // Load trial email if exists
    self.trialEmail = UserDefaults.standard.string(forKey: trialEmailKey) ?? ""

    // If license key exists, validate it
    if !self.licenseKey.isEmpty {
      self.licenseValidated = true
    }

    // Reload trial status
    checkTrialStatus()
  }

  func activateTrial(withEmail email: String) -> Bool {
    guard trialStartDate == nil else { return false }

    // Validate email format
    guard isValidEmail(email) else { return false }

    // Store the email
    self.trialEmail = email
    UserDefaults.standard.set(email, forKey: trialEmailKey)

    let now = Date()
    trialStartDate = now
    UserDefaults.standard.set(true, forKey: trialActivatedKey)

    checkTrialStatus()
    return true
  }

  func validateLicenseKey(completion: @escaping (Bool) -> Void) {
    guard !licenseKey.isEmpty else {
      self.licenseErrorMessage = "License key cannot be empty"
      self.licenseValidated = false
      completion(false)
      return
    }

    self.isValidatingLicense = true

    // Simple validation for demo purposes
    // In a real app, you would make an API call to validate the license
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
      // Check if license key looks like a valid format (just for demo)
      if self.licenseKey.count >= 16 && self.licenseKey.contains("-") {
        self.licenseValidated = true
        self.isValidatingLicense = false
        self.licenseErrorMessage = ""

        // Save the license key
        UserDefaults.standard.set(self.licenseKey, forKey: self.licenseKeyKey)

        // Mark as purchased
        self.recordPurchase()

        completion(true)
      } else {
        self.licenseValidated = false
        self.isValidatingLicense = false
        self.licenseErrorMessage = "Invalid license key format. Please enter a valid license key."
        completion(false)
      }
    }
  }

  // Helper to validate email format
  private func isValidEmail(_ email: String) -> Bool {
    let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
    let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx)
    return emailPred.evaluate(with: email)
  }

  func checkTrialStatus() {
    // If user has purchased, no need to check trial status
    if hasValidPurchase {
      trialActive = false
      trialExpired = false
      daysRemaining = 0
      return
    }

    // Verify trial integrity
    guard verifyTrialIntegrity() else {
      // Trial data has been tampered with, mark as expired
      trialExpired = true
      trialActive = false
      daysRemaining = 0
      return
    }

    if let startDate = trialStartDate {
      let now = Date()
      let timeElapsed = now.timeIntervalSince(startDate)

      if timeElapsed < trialDuration {
        trialActive = true
        trialExpired = false
        daysRemaining = Int(ceil((trialDuration - timeElapsed) / (24 * 60 * 60)))
      } else {
        trialActive = false
        trialExpired = true
        daysRemaining = 0
      }
    } else {
      // Trial not started yet
      trialActive = false
      trialExpired = false
      daysRemaining = 7  // Changed from 14 to 7 days
    }
  }

  func recordPurchase() {
    UserDefaults.standard.set(true, forKey: hasValidPurchaseKey)
    self.hasValidPurchase = true
    checkTrialStatus()
  }

  // MARK: - Private Methods

  private func generateDeviceIdentifier() -> String {
    let hostName = ProcessInfo.processInfo.hostName
    let systemVersion = ProcessInfo.processInfo.operatingSystemVersionString
    let combinedString = "\(hostName)_\(systemVersion)_SessionScribe"

    if let data = combinedString.data(using: .utf8) {
      let hash = SHA256.hash(data: data)
      return hash.compactMap { String(format: "%02x", $0) }.joined()
    }

    return UUID().uuidString
  }

  private func storeTrialDateInKeychain(_ date: Date) {
    let dateString = "\(date.timeIntervalSince1970)"
    guard let data = dateString.data(using: .utf8) else { return }

    let query: [String: Any] = [
      kSecClass as String: kSecClassGenericPassword,
      kSecAttrService as String: "SessionScribe",
      kSecAttrAccount as String: "TrialStartDate",
      kSecValueData as String: data,
      kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked,
    ]

    // Delete existing item first
    SecItemDelete(query as CFDictionary)

    // Add new item
    _ = SecItemAdd(query as CFDictionary, nil)
  }

  private func getTrialDateFromKeychain() -> Date? {
    let query: [String: Any] = [
      kSecClass as String: kSecClassGenericPassword,
      kSecAttrService as String: "SessionScribe",
      kSecAttrAccount as String: "TrialStartDate",
      kSecReturnData as String: true,
      kSecMatchLimit as String: kSecMatchLimitOne,
    ]

    var dataTypeRef: AnyObject?
    let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)

    if status == errSecSuccess, let data = dataTypeRef as? Data,
      let dateString = String(data: data, encoding: .utf8),
      let timeInterval = TimeInterval(dateString)
    {
      return Date(timeIntervalSince1970: timeInterval)
    }

    return nil
  }

  private func createAndStoreIntegrityCheck(for date: Date) {
    let dateString = "\(date.timeIntervalSince1970)"
    let combinedString = "\(dateString)_\(deviceIdentifier)_SessionScribe"

    guard let data = combinedString.data(using: .utf8) else { return }
    let hash = SHA256.hash(data: data)
    let hashString = hash.compactMap { String(format: "%02x", $0) }.joined()

    UserDefaults.standard.set(hashString, forKey: trialCheckKey)
  }

  private func verifyTrialIntegrity() -> Bool {
    // If trial hasn't started yet, integrity is valid
    guard let startDate = trialStartDate else { return true }

    // Compare UserDefaults date with Keychain date
    if let keychainDate = getTrialDateFromKeychain() {
      // Allow for slight time differences (up to 5 minutes)
      let tolerance: TimeInterval = 300
      if abs(startDate.timeIntervalSince(keychainDate)) > tolerance {
        return false  // Dates significantly differ, possible tampering
      }
    }

    // Verify the integrity check hash
    guard let storedHash = UserDefaults.standard.string(forKey: trialCheckKey) else {
      return false  // No hash found
    }

    let dateString = "\(startDate.timeIntervalSince1970)"
    let combinedString = "\(dateString)_\(deviceIdentifier)_SessionScribe"

    guard let data = combinedString.data(using: .utf8) else { return false }
    let hash = SHA256.hash(data: data)
    let hashString = hash.compactMap { String(format: "%02x", $0) }.joined()

    return hashString == storedHash
  }
}



================================================================
End of Codebase
================================================================
