Explore how the WhatsAppAnalyzer app currently handles sending messages. I need to understand:

1. The current reply mechanism in DeepAnalysisView.swift (sendReply function)
2. The ReplySheetView component
3. How WhatsApp stores messages in the database (ZWAMESSAGE table structure)
4. If there's any way to write to the WhatsApp database or send messages programmatically
5. The WhatsApp URL scheme being used (whatsapp://)

Also check if there are any other message input components in the app.

Focus on /Users/neog/Apps/macOS/WhatsAppAnalyzer-macOS/Sources/


I'll explore the WhatsAppAnalyzer app to understand how it handles sending messages and interacts with WhatsApp. Let me start by examining the project structure and then focus on the key components you mentioned.


Bash

find /Users/neog/Apps/macOS/WhatsAppAnalyzer-macOS/Sources -type f -name "*.swift" | head -20

List Swift files in Sources directory


Error: EISDIR: illegal operation on a directory, read


Now let me examine the key files you mentioned:


     1→import SwiftUI
     2→
     3→struct DeepAnalysisView: View {
     4→    let chat: ChatSession
     5→    @ObservedObject var database: WhatsAppDatabase
     6→    @StateObject private var deepService = DeepAnalysisService.shared
     7→    @StateObject private var cache = AnalysisCacheManager.shared
     8→    @StateObject private var audioService = AudioTranscriptionService.shared
     9→
    10→    @State private var deepAnalysis: DeepAnalysis?
    11→    @State private var showingReplySheet = false
    12→    @State private var selectedReply = ""
    13→    @State private var showingAudioTest = false
    14→    @State private var audioTestResult: String = ""
    15→
    16→    var cachedAnalysis: DeepAnalysis? {
    17→        cache.getCachedAnalysis(for: chat.id)
    18→    }
    19→
    20→    var body: some View {
    21→        ScrollView {
    22→            VStack(alignment: .leading, spacing: 20) {
    23→                // Header
    24→                DeepAnalysisHeader(chat: chat, cachedAnalysis: cachedAnalysis)
    25→
    26→                // Progress or Start button
    27→                if deepService.progress.isProcessing {
    28→                    ProcessingProgressView(progress: deepService.progress)
    29→                } else if let analysis = deepAnalysis ?? cachedAnalysis {
    30→                    // Results
    31→                    DeepAnalysisResultsView(
    32→                        analysis: analysis,
    33→                        onReply: { reply in
    34→                            selectedReply = reply
    35→                            showingReplySheet = true
    36→                        }
    37→                    )
    38→                } else {
    39→                    // Start analysis button
    40→                    StartAnalysisView(
    41→                        chat: chat,
    42→                        onStart: startDeepAnalysis
    43→                    )
    44→                }
    45→
    46→                // Action buttons (if has results)
    47→                if let analysis = deepAnalysis ?? cachedAnalysis, !deepService.progress.isProcessing {
    48→                    HStack(spacing: 12) {
    49→                        Button(action: startDeepAnalysis) {
    50→                            Label("Analisar Novamente", systemImage: "arrow.clockwise")
    51→                        }
    52→                        .buttonStyle(.bordered)
    53→
    54→                        Button(action: testAudioTranscription) {
    55→                            Label("Testar Áudio", systemImage: "waveform")
    56→                        }
    57→                        .buttonStyle(.bordered)
    58→                        .tint(.orange)
    59→
    60→                        Button(action: { exportToPDF(analysis) }) {
    61→                            Label("Exportar PDF", systemImage: "arrow.down.doc")
    62→                        }
    63→                        .buttonStyle(.borderedProminent)
    64→                        .tint(.purple)
    65→                    }
    66→                }
    67→            }
    68→            .padding()
    69→        }
    70→        .sheet(isPresented: $showingReplySheet) {
    71→            ReplySheetView(
    72→                chat: chat,
    73→                replyText: $selectedReply,
    74→                onSend: sendReply
    75→            )
    76→        }
    77→        .sheet(isPresented: $showingAudioTest) {
    78→            AudioTestSheet(result: audioTestResult)
    79→        }
    80→        .onAppear {
    81→            if let cached = cachedAnalysis {
    82→                deepAnalysis = cached
    83→            }
    84→        }
    85→    }
    86→
    87→    private func startDeepAnalysis() {
    88→        let allMessages = database.getMessages(for: chat.id, limit: 5000)
    89→
    90→        Task {
    91→            do {
    92→                let result = try await deepService.analyzeFullChat(
    93→                    chatId: chat.id,
    94→                    partnerName: chat.displayName,
    95→                    messages: allMessages
    96→                )
    97→                await MainActor.run {
    98→                    deepAnalysis = result
    99→                }
   100→            } catch {
   101→                await MainActor.run {
   102→                    deepService.progress.fail(with: error.localizedDescription)
   103→                }
   104→            }
   105→        }
   106→    }
   107→
   108→    private func sendReply(_ text: String) {
   109→        // Open WhatsApp with the message
   110→        // Format: whatsapp://send?phone=NUMBER&text=MESSAGE
   111→        let phone = chat.contactJID.replacingOccurrences(of: "@lid", with: "")
   112→        if let encoded = text.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
   113→           let url = URL(string: "whatsapp://send?phone=\(phone)&text=\(encoded)") {
   114→            NSWorkspace.shared.open(url)
   115→        }
   116→        showingReplySheet = false
   117→    }
   118→
   119→    private func exportToPDF(_ analysis: DeepAnalysis) {
   120→        PDFExportService.shared.exportAnalysisToPDF(analysis)
   121→    }
   122→
   123→    private func testAudioTranscription() {
   124→        Task {
   125→            await MainActor.run {
   126→                audioTestResult = "🔄 Buscando áudios..."
   127→                showingAudioTest = true
   128→            }
   129→
   130→            let audioFiles = audioService.findAudioFiles()
   131→
   132→            if audioFiles.isEmpty {
   133→                await MainActor.run {
   134→                    audioTestResult = "❌ Nenhum arquivo de áudio encontrado"
   135→                }
   136→                return
   137→            }
   138→
   139→            await MainActor.run {
   140→                audioTestResult = "🎙 Encontrados \(audioFiles.count) áudios\n\n🔄 Transcrevendo primeiro áudio..."
   141→            }
   142→
   143→            do {
   144→                let transcription = try await audioService.transcribeOpusFile(audioFiles[0])
   145→                await MainActor.run {
   146→                    audioTestResult = """
   147→                    ✅ TRANSCRIÇÃO SUCESSO!
   148→
   149→                    📁 Arquivo: \(audioFiles[0].lastPathComponent)
   150→
   151→                    📝 Texto:
   152→                    "\(transcription)"
   153→
   154→                    Total de áudios disponíveis: \(audioFiles.count)
   155→                    """
   156→                }
   157→            } catch {
   158→                await MainActor.run {
   159→                    audioTestResult = "❌ Erro: \(error.localizedDescription)"
   160→                }
   161→            }
   162→        }
   163→    }
   164→}
   165→
   166→// MARK: - Audio Test Sheet
   167→
   168→struct AudioTestSheet: View {
   169→    let result: String
   170→    @Environment(\.dismiss) private var dismiss
   171→
   172→    var body: some View {
   173→        VStack(spacing: 20) {
   174→            HStack {
   175→                Image(systemName: "waveform.circle.fill")
   176→                    .font(.title)
   177→                    .foregroundColor(.orange)
   178→                Text("Teste de Transcrição")
   179→                    .font(.headline)
   180→                Spacer()
   181→                Button(action: { dismiss() }) {
   182→                    Image(systemName: "xmark.circle.fill")
   183→                        .foregroundColor(.secondary)
   184→                }
   185→                .buttonStyle(.plain)
   186→            }
   187→
   188→            ScrollView {
   189→                Text(result)
   190→                    .font(.system(.body, design: .monospaced))
   191→                    .textSelection(.enabled)
   192→                    .frame(maxWidth: .infinity, alignment: .leading)
   193→            }
   194→            .frame(maxHeight: 300)
   195→            .padding()
   196→            .background(Color(nsColor: .controlBackgroundColor))
   197→            .cornerRadius(8)
   198→
   199→            Button("Fechar") {
   200→                dismiss()
   201→            }
   202→            .buttonStyle(.borderedProminent)
   203→        }
   204→        .padding()
   205→        .frame(width: 500, height: 400)
   206→    }
   207→}
   208→
   209→// MARK: - Subviews
   210→
   211→struct DeepAnalysisHeader: View {
   212→    let chat: ChatSession
   213→    let cachedAnalysis: DeepAnalysis?
   214→
   215→    var body: some View {
   216→        VStack(alignment: .leading, spacing: 8) {
   217→            HStack {
   218→                Image(systemName: "chart.bar.doc.horizontal")
   219→                    .font(.title2)
   220→                Text("Análise Profunda")
   221→                    .font(.title2.bold())
   222→                Spacer()
   223→
   224→                if let analysis = cachedAnalysis {
   225→                    VStack(alignment: .trailing) {
   226→                        Text("Cache: \(analysis.formattedDate)")
   227→                            .font(.caption2)
   228→                            .foregroundColor(.secondary)
   229→                        Text("\(analysis.messagesAnalyzed) msgs analisadas")
   230→                            .font(.caption2)
   231→                            .foregroundColor(.secondary)
   232→                    }
   233→                }
   234→            }
   235→
   236→            Text("Analisa todas as \(chat.messageCount) mensagens do chat com \(chat.displayName)")
   237→                .font(.caption)
   238→                .foregroundColor(.secondary)
   239→        }
   240→        .padding()
   241→        .background(Color.purple.opacity(0.1))
   242→        .cornerRadius(12)
   243→    }
   244→}
   245→
   246→struct ProcessingProgressView: View {
   247→    @ObservedObject var progress: DeepAnalysisProgress
   248→
   249→    var body: some View {
   250→        VStack(spacing: 16) {
   251→            ProgressView(value: progress.progress) {
   252→                Text(progress.currentPhase)
   253→                    .font(.caption)
   254→            }
   255→            .progressViewStyle(.linear)
   256→
   257→            HStack {
   258→                Text("Lote \(progress.completedChunks)/\(progress.totalChunks)")
   259→                    .font(.caption)
   260→                    .foregroundColor(.secondary)
   261→                Spacer()
   262→                Text("\(Int(progress.progress * 100))%")
   263→                    .font(.caption.bold())
   264→            }
   265→
   266→            if let error = progress.error {
   267→                Text(error)
   268→                    .font(.caption)
   269→                    .foregroundColor(.red)
   270→            }
   271→        }
   272→        .padding()
   273→        .background(Color(nsColor: .controlBackgroundColor))
   274→        .cornerRadius(12)
   275→    }
   276→}
   277→
   278→struct StartAnalysisView: View {
   279→    let chat: ChatSession
   280→    let onStart: () -> Void
   281→
   282→    var body: some View {
   283→        VStack(spacing: 16) {
   284→            Image(systemName: "sparkles.rectangle.stack")
   285→                .font(.system(size: 48))
   286→                .foregroundColor(.purple)
   287→
   288→            Text("Análise Completa do Chat")
   289→                .font(.headline)
   290→
   291→            Text("Processa todas as \(chat.messageCount) mensagens usando IA para gerar insights detalhados sobre sua conversa com \(chat.displayName).")
   292→                .font(.caption)
   293→                .foregroundColor(.secondary)
   294→                .multilineTextAlignment(.center)
   295→
   296→            VStack(alignment: .leading, spacing: 4) {
   297→                Label("Resumo geral do relacionamento", systemImage: "doc.text")
   298→                Label("Tópicos mais discutidos", systemImage: "tag")
   299→                Label("Padrões de comunicação", systemImage: "chart.line.uptrend.xyaxis")
   300→                Label("Momentos importantes", systemImage: "star")
   301→                Label("Sugestões de resposta", systemImage: "text.bubble")
   302→                Label("Transcrição de áudios", systemImage: "waveform")
   303→                    .foregroundColor(.orange)
   304→            }
   305→            .font(.caption)
   306→            .foregroundColor(.secondary)
   307→
   308→            Button(action: onStart) {
   309→                HStack {
   310→                    Image(systemName: "play.fill")
   311→                    Text("Iniciar Análise Profunda")
   312→                }
   313→                .frame(maxWidth: .infinity)
   314→                .padding(.vertical, 8)
   315→            }
   316→            .buttonStyle(.borderedProminent)
   317→            .tint(.purple)
   318→        }
   319→        .padding()
   320→        .background(Color(nsColor: .controlBackgroundColor))
   321→        .cornerRadius(12)
   322→    }
   323→}
   324→
   325→struct DeepAnalysisResultsView: View {
   326→    let analysis: DeepAnalysis
   327→    let onReply: (String) -> Void
   328→
   329→    var body: some View {
   330→        VStack(alignment: .leading, spacing: 20) {
   331→            // Overview
   332→            GroupBox {
   333→                VStack(alignment: .leading, spacing: 8) {
   334→                    Label("Resumo Geral", systemImage: "doc.text")
   335→                        .font(.headline)
   336→                    Text(analysis.overallSummary)
   337→                        .textSelection(.enabled)
   338→
   339→                    Divider()
   340→
   341→                    HStack {
   342→                        Label(analysis.relationshipType, systemImage: "person.2")
   343→                            .font(.caption)
   344→                            .padding(.horizontal, 8)
   345→                            .padding(.vertical, 4)
   346→                            .background(Color.blue.opacity(0.2))
   347→                            .cornerRadius(8)
   348→
   349→                        if analysis.hasAudioData {
   350→                            Label("\(analysis.audioCount ?? 0) áudios", systemImage: "waveform")
   351→                                .font(.caption)
   352→                                .padding(.horizontal, 8)
   353→                                .padding(.vertical, 4)
   354→                                .background(Color.orange.opacity(0.2))
   355→                                .cornerRadius(8)
   356→                        }
   357→
   358→                        Spacer()
   359→
   360→                        Text("via \(analysis.provider)")
   361→                            .font(.caption2)
   362→                            .foregroundColor(.secondary)
   363→                    }
   364→                }
   365→            }
   366→
   367→            // Audio Insights (if available)
   368→            if let audioInsights = analysis.audioInsights, !audioInsights.isEmpty {
   369→                GroupBox {
   370→                    VStack(alignment: .leading, spacing: 8) {
   371→                        Label("Insights dos Áudios", systemImage: "waveform.circle")
   372→                            .font(.headline)
   373→                            .foregroundColor(.orange)
   374→                        Text(audioInsights)
   375→                            .textSelection(.enabled)
   376→                    }
   377→                }
   378→            }
   379→
   380→            // Audio Transcriptions (if available)
   381→            if let transcriptions = analysis.audioTranscriptions, !transcriptions.isEmpty {
   382→                GroupBox {
   383→                    VStack(alignment: .leading, spacing: 8) {
   384→                        Label("Transcrições de Áudio (\(transcriptions.count))", systemImage: "text.bubble")
   385→                            .font(.headline)
   386→                            .foregroundColor(.orange)
   387→
   388→                        ForEach(transcriptions) { trans in
   389→                            VStack(alignment: .leading, spacing: 4) {
   390→                                HStack {
   391→                                    Image(systemName: trans.isFromMe ? "arrow.up.circle.fill" : "arrow.down.circle.fill")
   392→                                        .foregroundColor(trans.isFromMe ? .green : .blue)
   393→                                    Text(trans.isFromMe ? "Você" : "Contato")
   394→                                        .font(.caption.bold())
   395→                                    Spacer()
   396→                                    Text("\(trans.duration)s")
   397→                                        .font(.caption)
   398→                                        .foregroundColor(.secondary)
   399→                                }
   400→                                Text(trans.text)
   401→                                    .font(.caption)
   402→                                    .foregroundColor(.secondary)
   403→                                    .textSelection(.enabled)
   404→                            }
   405→                            .padding(8)
   406→                            .background(Color.orange.opacity(0.1))
   407→                            .cornerRadius(8)
   408→                        }
   409→                    }
   410→                }
   411→            }
   412→
   413→            // Topics
   414→            if !analysis.mainTopics.isEmpty {
   415→                GroupBox {
   416→                    VStack(alignment: .leading, spacing: 8) {
   417→                        Label("Tópicos Principais", systemImage: "tag")
   418→                            .font(.headline)
   419→
   420→                        ForEach(analysis.mainTopics) { topic in
   421→                            HStack {
   422→                                Text(topic.topic)
   423→                                    .fontWeight(.medium)
   424→                                Spacer()
   425→                                if topic.frequency > 0 {
   426→                                    Text("\(topic.frequency)x")
   427→                                        .font(.caption)
   428→                                        .foregroundColor(.secondary)
   429→                                }
   430→                            }
   431→                            if !topic.description.isEmpty {
   432→                                Text(topic.description)
   433→                                    .font(.caption)
   434→                                    .foregroundColor(.secondary)
   435→                            }
   436→                            Divider()
   437→                        }
   438→                    }
   439→                }
   440→            }
   441→
   442→            // Communication Patterns
   443→            GroupBox {
   444→                VStack(alignment: .leading, spacing: 8) {
   445→                    Label("Padrões de Comunicação", systemImage: "chart.line.uptrend.xyaxis")
   446→                        .font(.headline)
   447→
   448→                    LabeledContent("Estilo", value: analysis.communicationPatterns.conversationStyle)
   449→                    LabeledContent("Frequência", value: analysis.communicationPatterns.averageResponseTime)
   450→                    LabeledContent("Balanço", value: analysis.communicationPatterns.messageBalance)
   451→
   452→                    if !analysis.communicationPatterns.mostActiveHours.isEmpty {
   453→                        Text("Horários ativos: \(analysis.communicationPatterns.mostActiveHours.joined(separator: ", "))")
   454→                            .font(.caption)
   455→                            .foregroundColor(.secondary)
   456→                    }
   457→                }
   458→            }
   459→
   460→            // Key Moments
   461→            if !analysis.keyMoments.isEmpty {
   462→                GroupBox {
   463→                    VStack(alignment: .leading, spacing: 8) {
   464→                        Label("Momentos Importantes", systemImage: "star")
   465→                            .font(.headline)
   466→
   467→                        ForEach(analysis.keyMoments) { moment in
   468→                            VStack(alignment: .leading, spacing: 4) {
   469→                                HStack {
   470→                                    if !moment.date.isEmpty {
   471→                                        Text(moment.date)
   472→                                            .font(.caption)
   473→                                            .foregroundColor(.secondary)
   474→                                    }
   475→                                    Spacer()
   476→                                }
   477→                                Text(moment.description)
   478→                                    .font(.subheadline)
   479→                                if !moment.significance.isEmpty {
   480→                                    Text(moment.significance)
   481→                                        .font(.caption)
   482→                                        .foregroundColor(.secondary)
   483→                                        .italic()
   484→                                }
   485→                            }
   486→                            .padding(.vertical, 4)
   487→                            Divider()
   488→                        }
   489→                    }
   490→                }
   491→            }
   492→
   493→            // Suggested Responses
   494→            if !analysis.suggestedResponses.isEmpty {
   495→                GroupBox {
   496→                    VStack(alignment: .leading, spacing: 8) {
   497→                        Label("Sugestões de Resposta", systemImage: "text.bubble")
   498→                            .font(.headline)
   499→
   500→                        ForEach(analysis.suggestedResponses.indices, id: \.self) { index in
   501→                            Button(action: { onReply(analysis.suggestedResponses[index]) }) {
   502→                                HStack {
   503→                                    Text(analysis.suggestedResponses[index])
   504→                                        .multilineTextAlignment(.leading)
   505→                                    Spacer()
   506→                                    Image(systemName: "paperplane")
   507→                                }
   508→                                .padding(8)
   509→                                .background(Color.green.opacity(0.1))
   510→                                .cornerRadius(8)
   511→                            }
   512→                            .buttonStyle(.plain)
   513→                        }
   514→                    }
   515→                }
   516→            }
   517→
   518→            // Stats
   519→            HStack {
   520→                Spacer()
   521→                VStack(alignment: .trailing) {
   522→                    Text("\(analysis.messagesAnalyzed) de \(analysis.totalMessages) mensagens analisadas")
   523→                    Text("Processado em \(String(format: "%.1f", analysis.processingTimeSeconds))s")
   524→                }
   525→                .font(.caption2)
   526→                .foregroundColor(.secondary)
   527→            }
   528→        }
   529→    }
   530→}
   531→
   532→struct ReplySheetView: View {
   533→    let chat: ChatSession
   534→    @Binding var replyText: String
   535→    let onSend: (String) -> Void
   536→
   537→    @Environment(\.dismiss) private var dismiss
   538→
   539→    var body: some View {
   540→        VStack(spacing: 20) {
   541→            Text("Responder para \(chat.displayName)")
   542→                .font(.headline)
   543→
   544→            TextEditor(text: $replyText)
   545→                .frame(height: 100)
   546→                .padding(8)
   547→                .background(Color(nsColor: .controlBackgroundColor))
   548→                .cornerRadius(8)
   549→
   550→            HStack {
   551→                Button("Cancelar") {
   552→                    dismiss()
   553→                }
   554→                .keyboardShortcut(.escape)
   555→
   556→                Spacer()
   557→
   558→                Button("Abrir no WhatsApp") {
   559→                    onSend(replyText)
   560→                }
   561→                .buttonStyle(.borderedProminent)
   562→                .keyboardShortcut(.return)
   563→            }
   564→        }
   565→        .padding()
   566→        .frame(width: 400)
   567→    }
   568→}
   569→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import SwiftUI

struct DeepAnalysisView: View {
    let chat: ChatSession
    @ObservedObject var database: WhatsAppDatabase
    @StateObject private var deepService = DeepAnalysisService.shared
    @StateObject private var cache = AnalysisCacheManager.shared
    @StateObject private var audioService = AudioTranscriptionService.shared

    @State private var deepAnalysis: DeepAnalysis?
    @State private var showingReplySheet = false
    @State private var selectedReply = ""
    @State private var showingAudioTest = false
    @State private var audioTestResult: String = ""

    var cachedAnalysis: DeepAnalysis? {
        cache.getCachedAnalysis(for: chat.id)
    }

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 20) {
                // Header
                DeepAnalysisHeader(chat: chat, cachedAnalysis: cachedAnalysis)

                // Progress or Start button
                if deepService.progress.isProcessing {
                    ProcessingProgressView(progress: deepService.progress)
                } else if let analysis = deepAnalysis ?? cachedAnalysis {
                    // Results
                    DeepAnalysisResultsView(
                        analysis: analysis,
                        onReply: { reply in
                            selectedReply = reply
                            showingReplySheet = true
                        }
                    )
                } else {
                    // Start analysis button
                    StartAnalysisView(
                        chat: chat,
                        onStart: startDeepAnalysis
                    )
                }

                // Action buttons (if has results)
                if let analysis = deepAnalysis ?? cachedAnalysis, !deepService.progress.isProcessing {
                    HStack(spacing: 12) {
                        Button(action: startDeepAnalysis) {
                            Label("Analisar Novamente", systemImage: "arrow.clockwise")
                        }
                        .buttonStyle(.bordered)

                        Button(action: testAudioTranscription) {
                            Label("Testar Áudio", systemImage: "waveform")
                        }
                        .buttonStyle(.bordered)
                        .tint(.orange)

                        Button(action: { exportToPDF(analysis) }) {
                            Label("Exportar PDF", systemImage: "arrow.down.doc")
                        }
                        .buttonStyle(.borderedProminent)
                        .tint(.purple)
                    }
                }
            }
            .padding()
        }
        .sheet(isPresented: $showingReplySheet) {
            ReplySheetView(
                chat: chat,
                replyText: $selectedReply,
                onSend: sendReply
            )
        }
        .sheet(isPresented: $showingAudioTest) {
            AudioTestSheet(result: audioTestResult)
        }
        .onAppear {
            if let cached = cachedAnalysis {
                deepAnalysis = cached
            }
        }
    }

    private func startDeepAnalysis() {
        let allMessages = database.getMessages(for: chat.id, limit: 5000)

        Task {
            do {
                let result = try await deepService.analyzeFullChat(
                    chatId: chat.id,
                    partnerName: chat.displayName,
                    messages: allMessages
                )
                await MainActor.run {
                    deepAnalysis = result
                }
            } catch {
                await MainActor.run {
                    deepService.progress.fail(with: error.localizedDescription)
                }
            }
        }
    }

    private func sendReply(_ text: String) {
        // Open WhatsApp with the message
        // Format: whatsapp://send?phone=NUMBER&text=MESSAGE
        let phone = chat.contactJID.replacingOccurrences(of: "@lid", with: "")
        if let encoded = text.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
           let url = URL(string: "whatsapp://send?phone=\(phone)&text=\(encoded)") {
            NSWorkspace.shared.open(url)
        }
        showingReplySheet = false
    }

    private func exportToPDF(_ analysis: DeepAnalysis) {
        PDFExportService.shared.exportAnalysisToPDF(analysis)
    }

    private func testAudioTranscription() {
        Task {
            await MainActor.run {
                audioTestResult = "🔄 Buscando áudios..."
                showingAudioTest = true
            }

            let audioFiles = audioService.findAudioFiles()

            if audioFiles.isEmpty {
                await MainActor.run {
                    audioTestResult = "❌ Nenhum arquivo de áudio encontrado"
                }
                return
            }

            await MainActor.run {
                audioTestResult = "🎙 Encontrados \(audioFiles.count) áudios\n\n🔄 Transcrevendo primeiro áudio..."
            }

            do {
                let transcription = try await audioService.transcribeOpusFile(audioFiles[0])
                await MainActor.run {
                    audioTestResult = """
                    ✅ TRANSCRIÇÃO SUCESSO!

                    📁 Arquivo: \(audioFiles[0].lastPathComponent)

                    📝 Texto:
                    "\(transcription)"

                    Total de áudios disponíveis: \(audioFiles.count)
                    """
                }
            } catch {
                await MainActor.run {
                    audioTestResult = "❌ Erro: \(error.localizedDescription)"
                }
            }
        }
    }
}

// MARK: - Audio Test Sheet

struct AudioTestSheet: View {
    let result: String
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        VStack(spacing: 20) {
            HStack {
                Image(systemName: "waveform.circle.fill")
                    .font(.title)
                    .foregroundColor(.orange)
                Text("Teste de Transcrição")
                    .font(.headline)
                Spacer()
                Button(action: { dismiss() }) {
                    Image(systemName: "xmark.circle.fill")
                        .foregroundColor(.secondary)
                }
                .buttonStyle(.plain)
            }

            ScrollView {
                Text(result)
                    .font(.system(.body, design: .monospaced))
                    .textSelection(.enabled)
                    .frame(maxWidth: .infinity, alignment: .leading)
            }
            .frame(maxHeight: 300)
            .padding()
            .background(Color(nsColor: .controlBackgroundColor))
            .cornerRadius(8)

            Button("Fechar") {
                dismiss()
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
        .frame(width: 500, height: 400)
    }
}

// MARK: - Subviews

struct DeepAnalysisHeader: View {
    let chat: ChatSession
    let cachedAnalysis: DeepAnalysis?

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            HStack {
                Image(systemName: "chart.bar.doc.horizontal")
                    .font(.title2)
                Text("Análise Profunda")
                    .font(.title2.bold())
                Spacer()

                if let analysis = cachedAnalysis {
                    VStack(alignment: .trailing) {
                        Text("Cache: \(analysis.formattedDate)")
                            .font(.caption2)
                            .foregroundColor(.secondary)
                        Text("\(analysis.messagesAnalyzed) msgs analisadas")
                            .font(.caption2)
                            .foregroundColor(.secondary)
                    }
                }
            }

            Text("Analisa todas as \(chat.messageCount) mensagens do chat com \(chat.displayName)")
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .padding()
        .background(Color.purple.opacity(0.1))
        .cornerRadius(12)
    }
}

struct ProcessingProgressView: View {
    @ObservedObject var progress: DeepAnalysisProgress

    var body: some View {
        VStack(spacing: 16) {
            ProgressView(value: progress.progress) {
                Text(progress.currentPhase)
                    .font(.caption)
            }
            .progressViewStyle(.linear)

            HStack {
                Text("Lote \(progress.completedChunks)/\(progress.totalChunks)")
                    .font(.caption)
                    .foregroundColor(.secondary)
                Spacer()
                Text("\(Int(progress.progress * 100))%")
                    .font(.caption.bold())
            }

            if let error = progress.error {
                Text(error)
                    .font(.caption)
                    .foregroundColor(.red)
            }
        }
        .padding()
        .background(Color(nsColor: .controlBackgroundColor))
        .cornerRadius(12)
    }
}

struct StartAnalysisView: View {
    let chat: ChatSession
    let onStart: () -> Void

    var body: some View {
        VStack(spacing: 16) {
            Image(systemName: "sparkles.rectangle.stack")
                .font(.system(size: 48))
                .foregroundColor(.purple)

            Text("Análise Completa do Chat")
                .font(.headline)

            Text("Processa todas as \(chat.messageCount) mensagens usando IA para gerar insights detalhados sobre sua conversa com \(chat.displayName).")
                .font(.caption)
                .foregroundColor(.secondary)
                .multilineTextAlignment(.center)

            VStack(alignment: .leading, spacing: 4) {
                Label("Resumo geral do relacionamento", systemImage: "doc.text")
                Label("Tópicos mais discutidos", systemImage: "tag")
                Label("Padrões de comunicação", systemImage: "chart.line.uptrend.xyaxis")
                Label("Momentos importantes", systemImage: "star")
                Label("Sugestões de resposta", systemImage: "text.bubble")
                Label("Transcrição de áudios", systemImage: "waveform")
                    .foregroundColor(.orange)
            }
            .font(.caption)
            .foregroundColor(.secondary)

            Button(action: onStart) {
                HStack {
                    Image(systemName: "play.fill")
                    Text("Iniciar Análise Profunda")
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 8)
            }
            .buttonStyle(.borderedProminent)
            .tint(.purple)
        }
        .padding()
        .background(Color(nsColor: .controlBackgroundColor))
        .cornerRadius(12)
    }
}

struct DeepAnalysisResultsView: View {
    let analysis: DeepAnalysis
    let onReply: (String) -> Void

    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            // Overview
            GroupBox {
                VStack(alignment: .leading, spacing: 8) {
                    Label("Resumo Geral", systemImage: "doc.text")
                        .font(.headline)
                    Text(analysis.overallSummary)
                        .textSelection(.enabled)

                    Divider()

                    HStack {
                        Label(analysis.relationshipType, systemImage: "person.2")
                            .font(.caption)
                            .padding(.horizontal, 8)
                            .padding(.vertical, 4)
                            .background(Color.blue.opacity(0.2))
                            .cornerRadius(8)

                        if analysis.hasAudioData {
                            Label("\(analysis.audioCount ?? 0) áudios", systemImage: "waveform")
                                .font(.caption)
                                .padding(.horizontal, 8)
                                .padding(.vertical, 4)
                                .background(Color.orange.opacity(0.2))
                                .cornerRadius(8)
                        }

                        Spacer()

                        Text("via \(analysis.provider)")
                            .font(.caption2)
                            .foregroundColor(.secondary)
                    }
                }
            }

            // Audio Insights (if available)
            if let audioInsights = analysis.audioInsights, !audioInsights.isEmpty {
                GroupBox {
                    VStack(alignment: .leading, spacing: 8) {
                        Label("Insights dos Áudios", systemImage: "waveform.circle")
                            .font(.headline)
                            .foregroundColor(.orange)
                        Text(audioInsights)
                            .textSelection(.enabled)
                    }
                }
            }

            // Audio Transcriptions (if available)
            if let transcriptions = analysis.audioTranscriptions, !transcriptions.isEmpty {
                GroupBox {
                    VStack(alignment: .leading, spacing: 8) {
                        Label("Transcrições de Áudio (\(transcriptions.count))", systemImage: "text.bubble")
                            .font(.headline)
                            .foregroundColor(.orange)

                        ForEach(transcriptions) { trans in
                            VStack(alignment: .leading, spacing: 4) {
                                HStack {
                                    Image(systemName: trans.isFromMe ? "arrow.up.circle.fill" : "arrow.down.circle.fill")
                                        .foregroundColor(trans.isFromMe ? .green : .blue)
                                    Text(trans.isFromMe ? "Você" : "Contato")
                                        .font(.caption.bold())
                                    Spacer()
                                    Text("\(trans.duration)s")
                                        .font(.caption)
                                        .foregroundColor(.secondary)
                                }
                                Text(trans.text)
                                    .font(.caption)
                                    .foregroundColor(.secondary)
                                    .textSelection(.enabled)
                            }
                            .padding(8)
                            .background(Color.orange.opacity(0.1))
                            .cornerRadius(8)
                        }
                    }
                }
            }

            // Topics
            if !analysis.mainTopics.isEmpty {
                GroupBox {
                    VStack(alignment: .leading, spacing: 8) {
                        Label("Tópicos Principais", systemImage: "tag")
                            .font(.headline)

                        ForEach(analysis.mainTopics) { topic in
                            HStack {
                                Text(topic.topic)
                                    .fontWeight(.medium)
                                Spacer()
                                if topic.frequency > 0 {
                                    Text("\(topic.frequency)x")
                                        .font(.caption)
                                        .foregroundColor(.secondary)
                                }
                            }
                            if !topic.description.isEmpty {
                                Text(topic.description)
                                    .font(.caption)
                                    .foregroundColor(.secondary)
                            }
                            Divider()
                        }
                    }
                }
            }

            // Communication Patterns
            GroupBox {
                VStack(alignment: .leading, spacing: 8) {
                    Label("Padrões de Comunicação", systemImage: "chart.line.uptrend.xyaxis")
                        .font(.headline)

                    LabeledContent("Estilo", value: analysis.communicationPatterns.conversationStyle)
                    LabeledContent("Frequência", value: analysis.communicationPatterns.averageResponseTime)
                    LabeledContent("Balanço", value: analysis.communicationPatterns.messageBalance)

                    if !analysis.communicationPatterns.mostActiveHours.isEmpty {
                        Text("Horários ativos: \(analysis.communicationPatterns.mostActiveHours.joined(separator: ", "))")
                            .font(.caption)
                            .foregroundColor(.secondary)
                    }
                }
            }

            // Key Moments
            if !analysis.keyMoments.isEmpty {
                GroupBox {
                    VStack(alignment: .leading, spacing: 8) {
                        Label("Momentos Importantes", systemImage: "star")
                            .font(.headline)

                        ForEach(analysis.keyMoments) { moment in
                            VStack(alignment: .leading, spacing: 4) {
                                HStack {
                                    if !moment.date.isEmpty {
                                        Text(moment.date)
                                            .font(.caption)
                                            .foregroundColor(.secondary)
                                    }
                                    Spacer()
                                }
                                Text(moment.description)
                                    .font(.subheadline)
                                if !moment.significance.isEmpty {
                                    Text(moment.significance)
                                        .font(.caption)
                                        .foregroundColor(.secondary)
                                        .italic()
                                }
                            }
                            .padding(.vertical, 4)
                            Divider()
                        }
                    }
                }
            }

            // Suggested Responses
            if !analysis.suggestedResponses.isEmpty {
                GroupBox {
                    VStack(alignment: .leading, spacing: 8) {
                        Label("Sugestões de Resposta", systemImage: "text.bubble")
                            .font(.headline)

                        ForEach(analysis.suggestedResponses.indices, id: \.self) { index in
                            Button(action: { onReply(analysis.suggestedResponses[index]) }) {
                                HStack {
                                    Text(analysis.suggestedResponses[index])
                                        .multilineTextAlignment(.leading)
                                    Spacer()
                                    Image(systemName: "paperplane")
                                }
                                .padding(8)
                                .background(Color.green.opacity(0.1))
                                .cornerRadius(8)
                            }
                            .buttonStyle(.plain)
                        }
                    }
                }
            }

            // Stats
            HStack {
                Spacer()
                VStack(alignment: .trailing) {
                    Text("\(analysis.messagesAnalyzed) de \(analysis.totalMessages) mensagens analisadas")
                    Text("Processado em \(String(format: "%.1f", analysis.processingTimeSeconds))s")
                }
                .font(.caption2)
                .foregroundColor(.secondary)
            }
        }
    }
}

struct ReplySheetView: View {
    let chat: ChatSession
    @Binding var replyText: String
    let onSend: (String) -> Void

    @Environment(\.dismiss) private var dismiss

    var body: some View {
        VStack(spacing: 20) {
            Text("Responder para \(chat.displayName)")
                .font(.headline)

            TextEditor(text: $replyText)
                .frame(height: 100)
                .padding(8)
                .background(Color(nsColor: .controlBackgroundColor))
                .cornerRadius(8)

            HStack {
                Button("Cancelar") {
                    dismiss()
                }
                .keyboardShortcut(.escape)

                Spacer()

                Button("Abrir no WhatsApp") {
                    onSend(replyText)
                }
                .buttonStyle(.borderedProminent)
                .keyboardShortcut(.return)
            }
        }
        .padding()
        .frame(width: 400)
    }
}


Found 2 files
Sources/Views/DeepAnalysisView.swift
Sources/Views/AIAnalysisView.swift


Now let me check the WhatsAppDatabase to understand the table structures:


     1→import Foundation
     2→import SQLite3
     3→
     4→class WhatsAppDatabase: ObservableObject {
     5→    static let shared = WhatsAppDatabase()
     6→
     7→    private let dbPath: String
     8→    private var db: OpaquePointer?
     9→
    10→    @Published var chatSessions: [ChatSession] = []
    11→    @Published var isConnected = false
    12→    @Published var error: String?
    13→
    14→    // WhatsApp SQLite date offset (Core Data reference date: Jan 1, 2001)
    15→    private let dateOffset: TimeInterval = 978307200
    16→
    17→    init() {
    18→        let home = FileManager.default.homeDirectoryForCurrentUser.path
    19→        self.dbPath = "\(home)/Library/Group Containers/group.net.whatsapp.WhatsApp.shared/ChatStorage.sqlite"
    20→        connect()
    21→    }
    22→
    23→    private func connect() {
    24→        guard FileManager.default.fileExists(atPath: dbPath) else {
    25→            error = "WhatsApp database not found. Make sure WhatsApp is installed."
    26→            return
    27→        }
    28→
    29→        // Open in read-only mode
    30→        if sqlite3_open_v2(dbPath, &db, SQLITE_OPEN_READONLY, nil) == SQLITE_OK {
    31→            isConnected = true
    32→            loadChatSessions()
    33→        } else {
    34→            error = "Failed to open WhatsApp database"
    35→        }
    36→    }
    37→
    38→    func loadChatSessions() {
    39→        guard isConnected else { return }
    40→
    41→        let query = """
    42→            SELECT
    43→                Z_PK,
    44→                ZCONTACTJID,
    45→                ZPARTNERNAME,
    46→                ZMESSAGECOUNTER,
    47→                ZLASTMESSAGEDATE
    48→            FROM ZWACHATSESSION
    49→            WHERE ZMESSAGECOUNTER > 0
    50→            ORDER BY ZLASTMESSAGEDATE DESC
    51→        """
    52→
    53→        var statement: OpaquePointer?
    54→        var sessions: [ChatSession] = []
    55→
    56→        if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
    57→            while sqlite3_step(statement) == SQLITE_ROW {
    58→                let id = sqlite3_column_int64(statement, 0)
    59→                let jid = String(cString: sqlite3_column_text(statement, 1) ?? UnsafePointer<UInt8>(bitPattern: 0)!)
    60→                let name = sqlite3_column_text(statement, 2).map { String(cString: $0) } ?? ""
    61→                let count = Int(sqlite3_column_int(statement, 3))
    62→
    63→                var lastDate: Date? = nil
    64→                if sqlite3_column_type(statement, 4) != SQLITE_NULL {
    65→                    let timestamp = sqlite3_column_double(statement, 4)
    66→                    lastDate = Date(timeIntervalSince1970: timestamp + dateOffset)
    67→                }
    68→
    69→                let session = ChatSession(
    70→                    id: id,
    71→                    contactJID: jid,
    72→                    partnerName: name,
    73→                    messageCount: count,
    74→                    lastMessageDate: lastDate
    75→                )
    76→                sessions.append(session)
    77→            }
    78→        }
    79→        sqlite3_finalize(statement)
    80→
    81→        DispatchQueue.main.async {
    82→            self.chatSessions = sessions
    83→        }
    84→    }
    85→
    86→    func getMessages(for chatId: Int64, limit: Int = 100) -> [Message] {
    87→        guard isConnected else { return [] }
    88→
    89→        let query = """
    90→            SELECT
    91→                m.Z_PK,
    92→                m.ZTEXT,
    93→                m.ZISFROMME,
    94→                m.ZMESSAGEDATE,
    95→                m.ZPUSHNAME,
    96→                m.ZMESSAGETYPE,
    97→                m.ZMEDIALOCALPATH,
    98→                mi.ZMOVIEDURATION
    99→            FROM ZWAMESSAGE m
   100→            LEFT JOIN ZWAMEDIAITEM mi ON mi.ZMESSAGE = m.Z_PK
   101→            WHERE m.ZCHATSESSION = ? AND (m.ZTEXT IS NOT NULL OR m.ZMESSAGETYPE = 2)
   102→            ORDER BY m.ZMESSAGEDATE DESC
   103→            LIMIT ?
   104→        """
   105→
   106→        var statement: OpaquePointer?
   107→        var messages: [Message] = []
   108→
   109→        if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
   110→            sqlite3_bind_int64(statement, 1, chatId)
   111→            sqlite3_bind_int(statement, 2, Int32(limit))
   112→
   113→            while sqlite3_step(statement) == SQLITE_ROW {
   114→                let id = sqlite3_column_int64(statement, 0)
   115→                let text = sqlite3_column_text(statement, 1).map { String(cString: $0) } ?? ""
   116→                let isFromMe = sqlite3_column_int(statement, 2) == 1
   117→                let timestamp = sqlite3_column_double(statement, 3)
   118→                let senderName = sqlite3_column_text(statement, 4).map { String(cString: $0) }
   119→
   120→                // Audio-related fields
   121→                let messageType: Int? = sqlite3_column_type(statement, 5) != SQLITE_NULL
   122→                    ? Int(sqlite3_column_int(statement, 5))
   123→                    : nil
   124→                let mediaPath = sqlite3_column_text(statement, 6).map { String(cString: $0) }
   125→                let mediaDuration: Int? = sqlite3_column_type(statement, 7) != SQLITE_NULL
   126→                    ? Int(sqlite3_column_int(statement, 7))
   127→                    : nil
   128→
   129→                // Extract media file name from path
   130→                let mediaName = mediaPath.flatMap { path -> String? in
   131→                    let components = path.components(separatedBy: "/")
   132→                    return components.last
   133→                }
   134→
   135→                let date = Date(timeIntervalSince1970: timestamp + dateOffset)
   136→
   137→                let message = Message(
   138→                    id: id,
   139→                    text: text,
   140→                    isFromMe: isFromMe,
   141→                    date: date,
   142→                    senderName: senderName,
   143→                    messageType: messageType,
   144→                    mediaName: mediaName,
   145→                    mediaDuration: mediaDuration
   146→                )
   147→                messages.append(message)
   148→            }
   149→        }
   150→        sqlite3_finalize(statement)
   151→
   152→        return messages.reversed() // Chronological order
   153→    }
   154→
   155→    func getMessagesForAnalysis(for chatId: Int64, limit: Int = 50) -> String {
   156→        let messages = getMessages(for: chatId, limit: limit)
   157→
   158→        return messages.map { msg in
   159→            let sender = msg.isFromMe ? "Eu" : (msg.senderName ?? "Contato")
   160→            return "[\(msg.formattedDate)] \(sender): \(msg.text)"
   161→        }.joined(separator: "\n")
   162→    }
   163→
   164→    func searchChats(query: String) -> [ChatSession] {
   165→        if query.isEmpty {
   166→            return chatSessions
   167→        }
   168→        return chatSessions.filter {
   169→            $0.partnerName.localizedCaseInsensitiveContains(query) ||
   170→            $0.contactJID.localizedCaseInsensitiveContains(query)
   171→        }
   172→    }
   173→
   174→    deinit {
   175→        if db != nil {
   176→            sqlite3_close(db)
   177→        }
   178→    }
   179→}
   180→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import Foundation
import SQLite3

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

    private let dbPath: String
    private var db: OpaquePointer?

    @Published var chatSessions: [ChatSession] = []
    @Published var isConnected = false
    @Published var error: String?

    // WhatsApp SQLite date offset (Core Data reference date: Jan 1, 2001)
    private let dateOffset: TimeInterval = 978307200

    init() {
        let home = FileManager.default.homeDirectoryForCurrentUser.path
        self.dbPath = "\(home)/Library/Group Containers/group.net.whatsapp.WhatsApp.shared/ChatStorage.sqlite"
        connect()
    }

    private func connect() {
        guard FileManager.default.fileExists(atPath: dbPath) else {
            error = "WhatsApp database not found. Make sure WhatsApp is installed."
            return
        }

        // Open in read-only mode
        if sqlite3_open_v2(dbPath, &db, SQLITE_OPEN_READONLY, nil) == SQLITE_OK {
            isConnected = true
            loadChatSessions()
        } else {
            error = "Failed to open WhatsApp database"
        }
    }

    func loadChatSessions() {
        guard isConnected else { return }

        let query = """
            SELECT
                Z_PK,
                ZCONTACTJID,
                ZPARTNERNAME,
                ZMESSAGECOUNTER,
                ZLASTMESSAGEDATE
            FROM ZWACHATSESSION
            WHERE ZMESSAGECOUNTER > 0
            ORDER BY ZLASTMESSAGEDATE DESC
        """

        var statement: OpaquePointer?
        var sessions: [ChatSession] = []

        if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
            while sqlite3_step(statement) == SQLITE_ROW {
                let id = sqlite3_column_int64(statement, 0)
                let jid = String(cString: sqlite3_column_text(statement, 1) ?? UnsafePointer<UInt8>(bitPattern: 0)!)
                let name = sqlite3_column_text(statement, 2).map { String(cString: $0) } ?? ""
                let count = Int(sqlite3_column_int(statement, 3))

                var lastDate: Date? = nil
                if sqlite3_column_type(statement, 4) != SQLITE_NULL {
                    let timestamp = sqlite3_column_double(statement, 4)
                    lastDate = Date(timeIntervalSince1970: timestamp + dateOffset)
                }

                let session = ChatSession(
                    id: id,
                    contactJID: jid,
                    partnerName: name,
                    messageCount: count,
                    lastMessageDate: lastDate
                )
                sessions.append(session)
            }
        }
        sqlite3_finalize(statement)

        DispatchQueue.main.async {
            self.chatSessions = sessions
        }
    }

    func getMessages(for chatId: Int64, limit: Int = 100) -> [Message] {
        guard isConnected else { return [] }

        let query = """
            SELECT
                m.Z_PK,
                m.ZTEXT,
                m.ZISFROMME,
                m.ZMESSAGEDATE,
                m.ZPUSHNAME,
                m.ZMESSAGETYPE,
                m.ZMEDIALOCALPATH,
                mi.ZMOVIEDURATION
            FROM ZWAMESSAGE m
            LEFT JOIN ZWAMEDIAITEM mi ON mi.ZMESSAGE = m.Z_PK
            WHERE m.ZCHATSESSION = ? AND (m.ZTEXT IS NOT NULL OR m.ZMESSAGETYPE = 2)
            ORDER BY m.ZMESSAGEDATE DESC
            LIMIT ?
        """

        var statement: OpaquePointer?
        var messages: [Message] = []

        if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
            sqlite3_bind_int64(statement, 1, chatId)
            sqlite3_bind_int(statement, 2, Int32(limit))

            while sqlite3_step(statement) == SQLITE_ROW {
                let id = sqlite3_column_int64(statement, 0)
                let text = sqlite3_column_text(statement, 1).map { String(cString: $0) } ?? ""
                let isFromMe = sqlite3_column_int(statement, 2) == 1
                let timestamp = sqlite3_column_double(statement, 3)
                let senderName = sqlite3_column_text(statement, 4).map { String(cString: $0) }

                // Audio-related fields
                let messageType: Int? = sqlite3_column_type(statement, 5) != SQLITE_NULL
                    ? Int(sqlite3_column_int(statement, 5))
                    : nil
                let mediaPath = sqlite3_column_text(statement, 6).map { String(cString: $0) }
                let mediaDuration: Int? = sqlite3_column_type(statement, 7) != SQLITE_NULL
                    ? Int(sqlite3_column_int(statement, 7))
                    : nil

                // Extract media file name from path
                let mediaName = mediaPath.flatMap { path -> String? in
                    let components = path.components(separatedBy: "/")
                    return components.last
                }

                let date = Date(timeIntervalSince1970: timestamp + dateOffset)

                let message = Message(
                    id: id,
                    text: text,
                    isFromMe: isFromMe,
                    date: date,
                    senderName: senderName,
                    messageType: messageType,
                    mediaName: mediaName,
                    mediaDuration: mediaDuration
                )
                messages.append(message)
            }
        }
        sqlite3_finalize(statement)

        return messages.reversed() // Chronological order
    }

    func getMessagesForAnalysis(for chatId: Int64, limit: Int = 50) -> String {
        let messages = getMessages(for: chatId, limit: limit)

        return messages.map { msg in
            let sender = msg.isFromMe ? "Eu" : (msg.senderName ?? "Contato")
            return "[\(msg.formattedDate)] \(sender): \(msg.text)"
        }.joined(separator: "\n")
    }

    func searchChats(query: String) -> [ChatSession] {
        if query.isEmpty {
            return chatSessions
        }
        return chatSessions.filter {
            $0.partnerName.localizedCaseInsensitiveContains(query) ||
            $0.contactJID.localizedCaseInsensitiveContains(query)
        }
    }

    deinit {
        if db != nil {
            sqlite3_close(db)
        }
    }
}


Found 5 files
Sources/Services/WhatsAppDatabase.swift
Sources/Models/ChatSession.swift
Sources/Services/AIService.swift
Sources/Services/LMStudioService.swift
Sources/Views/ChatDetailView.swift


Now let me check the model files for ChatSession and Message structures:


find /Users/neog/Apps/macOS/WhatsAppAnalyzer-macOS/Sources/Models -type f -name "*.swift"

List model files in Models directory


     1→import Foundation
     2→
     3→struct ChatSession: Identifiable, Hashable {
     4→    let id: Int64
     5→    let contactJID: String
     6→    let partnerName: String
     7→    let messageCount: Int
     8→    let lastMessageDate: Date?
     9→
    10→    var displayName: String {
    11→        partnerName.isEmpty ? contactJID : partnerName
    12→    }
    13→
    14→    var formattedLastMessage: String {
    15→        guard let date = lastMessageDate else { return "" }
    16→        let formatter = RelativeDateTimeFormatter()
    17→        formatter.locale = Locale(identifier: "pt-BR")
    18→        return formatter.localizedString(for: date, relativeTo: Date())
    19→    }
    20→}
    21→
    22→struct Message: Identifiable, Hashable {
    23→    let id: Int64
    24→    let text: String
    25→    let isFromMe: Bool
    26→    let date: Date
    27→    let senderName: String?
    28→    let messageType: Int?       // 0 = text, 2 = audio, etc.
    29→    let mediaName: String?      // Media file name (for audio messages)
    30→    let mediaDuration: Int?     // Duration in seconds (for audio)
    31→
    32→    var formattedDate: String {
    33→        let formatter = DateFormatter()
    34→        formatter.dateFormat = "dd/MM HH:mm"
    35→        return formatter.string(from: date)
    36→    }
    37→
    38→    var formattedTime: String {
    39→        let formatter = DateFormatter()
    40→        formatter.dateFormat = "HH:mm"
    41→        return formatter.string(from: date)
    42→    }
    43→
    44→    var isAudio: Bool {
    45→        messageType == 2
    46→    }
    47→}
    48→
    49→struct ChatAnalysis: Identifiable {
    50→    let id = UUID()
    51→    let chatId: Int64
    52→    let partnerName: String
    53→    let summary: String
    54→    let topics: [String]
    55→    let sentiment: String
    56→    let suggestedResponses: [String]
    57→    let generatedAt: Date
    58→}
    59→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import Foundation

struct ChatSession: Identifiable, Hashable {
    let id: Int64
    let contactJID: String
    let partnerName: String
    let messageCount: Int
    let lastMessageDate: Date?

    var displayName: String {
        partnerName.isEmpty ? contactJID : partnerName
    }

    var formattedLastMessage: String {
        guard let date = lastMessageDate else { return "" }
        let formatter = RelativeDateTimeFormatter()
        formatter.locale = Locale(identifier: "pt-BR")
        return formatter.localizedString(for: date, relativeTo: Date())
    }
}

struct Message: Identifiable, Hashable {
    let id: Int64
    let text: String
    let isFromMe: Bool
    let date: Date
    let senderName: String?
    let messageType: Int?       // 0 = text, 2 = audio, etc.
    let mediaName: String?      // Media file name (for audio messages)
    let mediaDuration: Int?     // Duration in seconds (for audio)

    var formattedDate: String {
        let formatter = DateFormatter()
        formatter.dateFormat = "dd/MM HH:mm"
        return formatter.string(from: date)
    }

    var formattedTime: String {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm"
        return formatter.string(from: date)
    }

    var isAudio: Bool {
        messageType == 2
    }
}

struct ChatAnalysis: Identifiable {
    let id = UUID()
    let chatId: Int64
    let partnerName: String
    let summary: String
    let topics: [String]
    let sentiment: String
    let suggestedResponses: [String]
    let generatedAt: Date
}


/Users/neog/Apps/macOS/WhatsAppAnalyzer-macOS/Sources/Models/DeepAnalysis.swift
/Users/neog/Apps/macOS/WhatsAppAnalyzer-macOS/Sources/Models/ChatSession.swift


Let me check the AIAnalysisView to see if there are other message input components: