Explore the WhatsAppAnalyzer macOS app UI/UX structure. I need to understand:

1. All SwiftUI view files and their structure
2. Current color schemes and styling
3. Navigation patterns
4. Component hierarchy
5. Any reusable components or styling patterns

Focus on finding all View files in /Users/neog/Apps/macOS/WhatsAppAnalyzer-macOS/Sources/Views/ and the main ContentView.

Be thorough - I need to understand the complete UI architecture to plan improvements.


I'll conduct a thorough exploration of the WhatsAppAnalyzer macOS app's UI/UX structure. Let me start by examining the project layout and then diving into the Views directory.


Bash

ls -la "/Users/neog/Apps/macOS/WhatsAppAnalyzer-macOS/" | head -20

List project root directory structure


total 8
drwxr-xr-x@  5 neog  staff   160 Dec 22 21:53 .
drwxr-xr-x@ 15 neog  staff   480 Dec 22 18:26 ..
drwx------@  9 neog  staff   288 Dec 22 21:46 Sources
drwxr-xr-x@  6 neog  staff   192 Dec 22 21:53 WhatsAppAnalyzer.xcodeproj
-rw-------@  1 neog  staff  1807 Dec 22 21:47 project.yml


Perfect! Now let me examine the Views directory structure and read all view files:


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

List all Swift files in Views directory


List complete Sources directory structure


total 40
-rw-------@ 1 neog  staff  3270 Dec 22 18:39 ContentView.swift
-rw-r--r--@ 1 neog  staff  1354 Dec 22 21:46 Info.plist
drwx------@ 4 neog  staff   128 Dec 22 21:52 Models
drwx------@ 8 neog  staff   256 Dec 22 21:54 Services
drwx------@ 6 neog  staff   192 Dec 22 21:45 Views
-rw-------@ 1 neog  staff   324 Dec 22 18:27 WhatsAppAnalyzer.entitlements
-rw-------@ 1 neog  staff  4817 Dec 22 18:40 WhatsAppAnalyzerApp.swift

/Users/neog/Apps/macOS/WhatsAppAnalyzer-macOS/Sources/Models:
total 24
-rw-------@ 1 neog  staff  1559 Dec 22 21:52 ChatSession.swift
-rw-------@ 1 neog  staff  5558 Dec 22 21:41 DeepAnalysis.swift

/Users/neog/Apps/macOS/WhatsAppAnalyzer-macOS/Sources/Services:
total 152
-rw-------@ 1 neog  staff  12478 Dec 22 19:13 AIService.swift
-rw-------@ 1 neog  staff   5878 Dec 22 21:42 AudioTranscriptionService.swift
-rw-------@ 1 neog  staff  22800 Dec 22 21:54 DeepAnalysisService.swift
-rw-------@ 1 neog  staff   7239 Dec 22 18:36 LMStudioService.swift
-rw-------@ 1 neog  staff  11395 Dec 22 20:29 PDFExportService.swift
-rw-------@ 1 neog  staff   6305 Dec 22 21:52 WhatsAppDatabase.swift

/Users/neog/Apps/macOS/WhatsAppAnalyzer-macOS/Sources/Views:
total 112
-rw-------@ 1 neog  staff  18226 Dec 22 18:50 AIAnalysisView.swift
-rw-------@ 1 neog  staff   4263 Dec 22 18:30 ChatDetailView.swift
-rw-------@ 1 neog  staff   3785 Dec 22 18:30 ChatListView.swift
-rw-------@ 1 neog  staff  21086 Dec 22 21:45 DeepAnalysisView.swift


Now let me read all the key view files and the main app structure:


     1→import SwiftUI
     2→
     3→struct ContentView: View {
     4→    @StateObject private var database = WhatsAppDatabase.shared
     5→    @StateObject private var aiService = AIService.shared
     6→    @State private var selectedChat: ChatSession?
     7→    @State private var showAnalysis = false
     8→
     9→    var body: some View {
    10→        NavigationSplitView {
    11→            // Sidebar - Chat list
    12→            ChatListView(database: database, selectedChat: $selectedChat)
    13→                .frame(minWidth: 280)
    14→                .toolbar {
    15→                    ToolbarItem(placement: .automatic) {
    16→                        Button(action: { database.loadChatSessions() }) {
    17→                            Image(systemName: "arrow.clockwise")
    18→                        }
    19→                        .help("Atualizar lista de chats")
    20→                    }
    21→                }
    22→        } detail: {
    23→            // Detail view
    24→            if let chat = selectedChat {
    25→                HSplitView {
    26→                    // Messages
    27→                    ChatDetailView(chat: chat, database: database)
    28→                        .frame(minWidth: 400)
    29→
    30→                    // AI Analysis panel
    31→                    if showAnalysis {
    32→                        AIAnalysisView(chat: chat, database: database)
    33→                            .frame(minWidth: 380, maxWidth: 500)
    34→                    }
    35→                }
    36→                .toolbar {
    37→                    ToolbarItemGroup(placement: .automatic) {
    38→                        // AI Status indicator
    39→                        HStack(spacing: 4) {
    40→                            Circle()
    41→                                .fill(aiService.isConnected ? Color.green : Color.red)
    42→                                .frame(width: 8, height: 8)
    43→                            Text(aiService.isConnected ? aiService.currentProvider : "IA Offline")
    44→                                .font(.caption)
    45→                        }
    46→
    47→                        Divider()
    48→
    49→                        // Toggle analysis panel
    50→                        Button(action: { withAnimation { showAnalysis.toggle() } }) {
    51→                            Label(
    52→                                showAnalysis ? "Ocultar Análise" : "Mostrar Análise",
    53→                                systemImage: showAnalysis ? "sidebar.right" : "sparkles"
    54→                            )
    55→                        }
    56→                        .help("Painel de Análise IA")
    57→                    }
    58→                }
    59→            } else {
    60→                // Empty state
    61→                VStack(spacing: 16) {
    62→                    Image(systemName: "message.circle")
    63→                        .font(.system(size: 64))
    64→                        .foregroundColor(.secondary)
    65→                    Text("Selecione um chat")
    66→                        .font(.title2)
    67→                        .foregroundColor(.secondary)
    68→                    Text("Escolha uma conversa na lista para visualizar e analisar")
    69→                        .font(.caption)
    70→                        .foregroundColor(.secondary)
    71→                }
    72→            }
    73→        }
    74→        .navigationTitle("WhatsApp Analyzer")
    75→        .onAppear {
    76→            // Auto-show analysis panel if AI is connected
    77→            if aiService.isConnected {
    78→                showAnalysis = true
    79→            }
    80→        }
    81→    }
    82→}
    83→
    84→#Preview {
    85→    ContentView()
    86→}
    87→

<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 ContentView: View {
    @StateObject private var database = WhatsAppDatabase.shared
    @StateObject private var aiService = AIService.shared
    @State private var selectedChat: ChatSession?
    @State private var showAnalysis = false

    var body: some View {
        NavigationSplitView {
            // Sidebar - Chat list
            ChatListView(database: database, selectedChat: $selectedChat)
                .frame(minWidth: 280)
                .toolbar {
                    ToolbarItem(placement: .automatic) {
                        Button(action: { database.loadChatSessions() }) {
                            Image(systemName: "arrow.clockwise")
                        }
                        .help("Atualizar lista de chats")
                    }
                }
        } detail: {
            // Detail view
            if let chat = selectedChat {
                HSplitView {
                    // Messages
                    ChatDetailView(chat: chat, database: database)
                        .frame(minWidth: 400)

                    // AI Analysis panel
                    if showAnalysis {
                        AIAnalysisView(chat: chat, database: database)
                            .frame(minWidth: 380, maxWidth: 500)
                    }
                }
                .toolbar {
                    ToolbarItemGroup(placement: .automatic) {
                        // AI Status indicator
                        HStack(spacing: 4) {
                            Circle()
                                .fill(aiService.isConnected ? Color.green : Color.red)
                                .frame(width: 8, height: 8)
                            Text(aiService.isConnected ? aiService.currentProvider : "IA Offline")
                                .font(.caption)
                        }

                        Divider()

                        // Toggle analysis panel
                        Button(action: { withAnimation { showAnalysis.toggle() } }) {
                            Label(
                                showAnalysis ? "Ocultar Análise" : "Mostrar Análise",
                                systemImage: showAnalysis ? "sidebar.right" : "sparkles"
                            )
                        }
                        .help("Painel de Análise IA")
                    }
                }
            } else {
                // Empty state
                VStack(spacing: 16) {
                    Image(systemName: "message.circle")
                        .font(.system(size: 64))
                        .foregroundColor(.secondary)
                    Text("Selecione um chat")
                        .font(.title2)
                        .foregroundColor(.secondary)
                    Text("Escolha uma conversa na lista para visualizar e analisar")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
            }
        }
        .navigationTitle("WhatsApp Analyzer")
        .onAppear {
            // Auto-show analysis panel if AI is connected
            if aiService.isConnected {
                showAnalysis = true
            }
        }
    }
}

#Preview {
    ContentView()
}


     1→import SwiftUI
     2→
     3→@main
     4→struct WhatsAppAnalyzerApp: App {
     5→    var body: some Scene {
     6→        WindowGroup {
     7→            ContentView()
     8→        }
     9→        .windowStyle(.automatic)
    10→        .defaultSize(width: 1200, height: 700)
    11→        .commands {
    12→            CommandGroup(replacing: .help) {
    13→                Button("Sobre WhatsApp Analyzer") {
    14→                    NSApp.orderFrontStandardAboutPanel(options: [
    15→                        .applicationName: "WhatsApp Analyzer",
    16→                        .applicationVersion: "1.0.0",
    17→                        .credits: NSAttributedString(string: "Analise seus chats do WhatsApp com IA\n\nClaude + LM Gateway\n\n© 2025 NEOG")
    18→                    ])
    19→                }
    20→            }
    21→
    22→            CommandGroup(after: .sidebar) {
    23→                Button("Atualizar Chats") {
    24→                    WhatsAppDatabase.shared.loadChatSessions()
    25→                }
    26→                .keyboardShortcut("r", modifiers: .command)
    27→
    28→                Button("Reconectar IA") {
    29→                    Task { await AIService.shared.checkConnections() }
    30→                }
    31→                .keyboardShortcut("l", modifiers: [.command, .shift])
    32→            }
    33→        }
    34→
    35→        Settings {
    36→            SettingsView()
    37→        }
    38→    }
    39→}
    40→
    41→struct SettingsView: View {
    42→    @ObservedObject private var aiService = AIService.shared
    43→
    44→    var body: some View {
    45→        Form {
    46→            Section("Provedores de IA") {
    47→                // Claude
    48→                GroupBox {
    49→                    VStack(alignment: .leading, spacing: 8) {
    50→                        HStack {
    51→                            Image(systemName: "brain.head.profile")
    52→                            Text("Claude (Anthropic)")
    53→                                .fontWeight(.medium)
    54→                            Spacer()
    55→                            Circle()
    56→                                .fill(aiService.claudeAvailable ? Color.green : Color.red)
    57→                                .frame(width: 10, height: 10)
    58→                            Text(aiService.claudeAvailable ? "Online" : "Offline")
    59→                                .font(.caption)
    60→                        }
    61→                        Text("Modelo: claude-sonnet-4-20250514")
    62→                            .font(.caption)
    63→                            .foregroundColor(.secondary)
    64→                    }
    65→                }
    66→
    67→                // LM Gateway
    68→                GroupBox {
    69→                    VStack(alignment: .leading, spacing: 8) {
    70→                        HStack {
    71→                            Image(systemName: "server.rack")
    72→                            Text("LM Gateway (M3 Ultra)")
    73→                                .fontWeight(.medium)
    74→                            Spacer()
    75→                            Circle()
    76→                                .fill(aiService.lmGatewayAvailable ? Color.green : Color.red)
    77→                                .frame(width: 10, height: 10)
    78→                            Text(aiService.lmGatewayAvailable ? "Online" : "Offline")
    79→                                .font(.caption)
    80→                        }
    81→                        Text("Endpoint: mac-studio-de-leandro.tail0360aa.ts.net")
    82→                            .font(.caption)
    83→                            .foregroundColor(.secondary)
    84→
    85→                        if !aiService.lmGatewayModels.isEmpty {
    86→                            DisclosureGroup("Modelos (\(aiService.lmGatewayModels.count))") {
    87→                                ForEach(aiService.lmGatewayModels, id: \.self) { model in
    88→                                    Text(model)
    89→                                        .font(.caption2)
    90→                                }
    91→                            }
    92→                            .font(.caption)
    93→                        }
    94→                    }
    95→                }
    96→
    97→                // Primary provider toggle
    98→                Toggle("Usar Claude como primário", isOn: Binding(
    99→                    get: { aiService.useClaudeAsPrimary },
   100→                    set: { newValue in
   101→                        aiService.useClaudeAsPrimary = newValue
   102→                        Task { await aiService.checkConnections() }
   103→                    }
   104→                ))
   105→
   106→                HStack {
   107→                    Text("Provedor atual:")
   108→                        .foregroundColor(.secondary)
   109→                    Text(aiService.currentProvider)
   110→                        .fontWeight(.medium)
   111→                    Spacer()
   112→                    Button("Reconectar") {
   113→                        Task { await aiService.checkConnections() }
   114→                    }
   115→                }
   116→            }
   117→
   118→            Section("Sobre") {
   119→                LabeledContent("Versão", value: "1.0.0")
   120→                LabeledContent("WhatsApp DB", value: "~/Library/Group Containers/...")
   121→            }
   122→        }
   123→        .padding()
   124→        .frame(width: 500, height: 400)
   125→    }
   126→}
   127→

<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

@main
struct WhatsAppAnalyzerApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .windowStyle(.automatic)
        .defaultSize(width: 1200, height: 700)
        .commands {
            CommandGroup(replacing: .help) {
                Button("Sobre WhatsApp Analyzer") {
                    NSApp.orderFrontStandardAboutPanel(options: [
                        .applicationName: "WhatsApp Analyzer",
                        .applicationVersion: "1.0.0",
                        .credits: NSAttributedString(string: "Analise seus chats do WhatsApp com IA\n\nClaude + LM Gateway\n\n© 2025 NEOG")
                    ])
                }
            }

            CommandGroup(after: .sidebar) {
                Button("Atualizar Chats") {
                    WhatsAppDatabase.shared.loadChatSessions()
                }
                .keyboardShortcut("r", modifiers: .command)

                Button("Reconectar IA") {
                    Task { await AIService.shared.checkConnections() }
                }
                .keyboardShortcut("l", modifiers: [.command, .shift])
            }
        }

        Settings {
            SettingsView()
        }
    }
}

struct SettingsView: View {
    @ObservedObject private var aiService = AIService.shared

    var body: some View {
        Form {
            Section("Provedores de IA") {
                // Claude
                GroupBox {
                    VStack(alignment: .leading, spacing: 8) {
                        HStack {
                            Image(systemName: "brain.head.profile")
                            Text("Claude (Anthropic)")
                                .fontWeight(.medium)
                            Spacer()
                            Circle()
                                .fill(aiService.claudeAvailable ? Color.green : Color.red)
                                .frame(width: 10, height: 10)
                            Text(aiService.claudeAvailable ? "Online" : "Offline")
                                .font(.caption)
                        }
                        Text("Modelo: claude-sonnet-4-20250514")
                            .font(.caption)
                            .foregroundColor(.secondary)
                    }
                }

                // LM Gateway
                GroupBox {
                    VStack(alignment: .leading, spacing: 8) {
                        HStack {
                            Image(systemName: "server.rack")
                            Text("LM Gateway (M3 Ultra)")
                                .fontWeight(.medium)
                            Spacer()
                            Circle()
                                .fill(aiService.lmGatewayAvailable ? Color.green : Color.red)
                                .frame(width: 10, height: 10)
                            Text(aiService.lmGatewayAvailable ? "Online" : "Offline")
                                .font(.caption)
                        }
                        Text("Endpoint: mac-studio-de-leandro.tail0360aa.ts.net")
                            .font(.caption)
                            .foregroundColor(.secondary)

                        if !aiService.lmGatewayModels.isEmpty {
                            DisclosureGroup("Modelos (\(aiService.lmGatewayModels.count))") {
                                ForEach(aiService.lmGatewayModels, id: \.self) { model in
                                    Text(model)
                                        .font(.caption2)
                                }
                            }
                            .font(.caption)
                        }
                    }
                }

                // Primary provider toggle
                Toggle("Usar Claude como primário", isOn: Binding(
                    get: { aiService.useClaudeAsPrimary },
                    set: { newValue in
                        aiService.useClaudeAsPrimary = newValue
                        Task { await aiService.checkConnections() }
                    }
                ))

                HStack {
                    Text("Provedor atual:")
                        .foregroundColor(.secondary)
                    Text(aiService.currentProvider)
                        .fontWeight(.medium)
                    Spacer()
                    Button("Reconectar") {
                        Task { await aiService.checkConnections() }
                    }
                }
            }

            Section("Sobre") {
                LabeledContent("Versão", value: "1.0.0")
                LabeledContent("WhatsApp DB", value: "~/Library/Group Containers/...")
            }
        }
        .padding()
        .frame(width: 500, height: 400)
    }
}


     1→import SwiftUI
     2→
     3→struct ChatListView: View {
     4→    @ObservedObject var database: WhatsAppDatabase
     5→    @Binding var selectedChat: ChatSession?
     6→    @State private var searchText = ""
     7→
     8→    var filteredChats: [ChatSession] {
     9→        database.searchChats(query: searchText)
    10→    }
    11→
    12→    var body: some View {
    13→        VStack(spacing: 0) {
    14→            // Search bar
    15→            HStack {
    16→                Image(systemName: "magnifyingglass")
    17→                    .foregroundColor(.secondary)
    18→                TextField("Buscar chat...", text: $searchText)
    19→                    .textFieldStyle(.plain)
    20→                if !searchText.isEmpty {
    21→                    Button(action: { searchText = "" }) {
    22→                        Image(systemName: "xmark.circle.fill")
    23→                            .foregroundColor(.secondary)
    24→                    }
    25→                    .buttonStyle(.plain)
    26→                }
    27→            }
    28→            .padding(10)
    29→            .background(Color(nsColor: .controlBackgroundColor))
    30→
    31→            Divider()
    32→
    33→            // Chat list
    34→            if database.isConnected {
    35→                List(filteredChats, selection: $selectedChat) { chat in
    36→                    ChatRowView(chat: chat, isSelected: selectedChat?.id == chat.id)
    37→                        .tag(chat)
    38→                }
    39→                .listStyle(.sidebar)
    40→            } else {
    41→                VStack(spacing: 16) {
    42→                    Image(systemName: "exclamationmark.triangle")
    43→                        .font(.system(size: 48))
    44→                        .foregroundColor(.orange)
    45→                    Text("WhatsApp não encontrado")
    46→                        .font(.headline)
    47→                    Text(database.error ?? "Verifique se o WhatsApp está instalado")
    48→                        .font(.caption)
    49→                        .foregroundColor(.secondary)
    50→                        .multilineTextAlignment(.center)
    51→                }
    52→                .frame(maxWidth: .infinity, maxHeight: .infinity)
    53→                .padding()
    54→            }
    55→        }
    56→    }
    57→}
    58→
    59→struct ChatRowView: View {
    60→    let chat: ChatSession
    61→    let isSelected: Bool
    62→
    63→    var body: some View {
    64→        HStack(spacing: 12) {
    65→            // Avatar
    66→            ZStack {
    67→                Circle()
    68→                    .fill(avatarColor)
    69→                    .frame(width: 40, height: 40)
    70→                Text(initials)
    71→                    .font(.system(size: 14, weight: .semibold))
    72→                    .foregroundColor(.white)
    73→            }
    74→
    75→            // Chat info
    76→            VStack(alignment: .leading, spacing: 2) {
    77→                HStack {
    78→                    Text(chat.displayName)
    79→                        .font(.system(size: 13, weight: .medium))
    80→                        .lineLimit(1)
    81→                    Spacer()
    82→                    Text(chat.formattedLastMessage)
    83→                        .font(.system(size: 11))
    84→                        .foregroundColor(.secondary)
    85→                }
    86→
    87→                HStack {
    88→                    Text("\(chat.messageCount) mensagens")
    89→                        .font(.system(size: 11))
    90→                        .foregroundColor(.secondary)
    91→                    Spacer()
    92→                }
    93→            }
    94→        }
    95→        .padding(.vertical, 4)
    96→        .contentShape(Rectangle())
    97→    }
    98→
    99→    private var initials: String {
   100→        let name = chat.displayName
   101→        let components = name.split(separator: " ")
   102→        if components.count >= 2 {
   103→            return String(components[0].prefix(1) + components[1].prefix(1)).uppercased()
   104→        }
   105→        return String(name.prefix(2)).uppercased()
   106→    }
   107→
   108→    private var avatarColor: Color {
   109→        let colors: [Color] = [.blue, .green, .orange, .purple, .pink, .teal]
   110→        let index = abs(chat.displayName.hashValue) % colors.count
   111→        return colors[index]
   112→    }
   113→}
   114→

<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 ChatListView: View {
    @ObservedObject var database: WhatsAppDatabase
    @Binding var selectedChat: ChatSession?
    @State private var searchText = ""

    var filteredChats: [ChatSession] {
        database.searchChats(query: searchText)
    }

    var body: some View {
        VStack(spacing: 0) {
            // Search bar
            HStack {
                Image(systemName: "magnifyingglass")
                    .foregroundColor(.secondary)
                TextField("Buscar chat...", text: $searchText)
                    .textFieldStyle(.plain)
                if !searchText.isEmpty {
                    Button(action: { searchText = "" }) {
                        Image(systemName: "xmark.circle.fill")
                            .foregroundColor(.secondary)
                    }
                    .buttonStyle(.plain)
                }
            }
            .padding(10)
            .background(Color(nsColor: .controlBackgroundColor))

            Divider()

            // Chat list
            if database.isConnected {
                List(filteredChats, selection: $selectedChat) { chat in
                    ChatRowView(chat: chat, isSelected: selectedChat?.id == chat.id)
                        .tag(chat)
                }
                .listStyle(.sidebar)
            } else {
                VStack(spacing: 16) {
                    Image(systemName: "exclamationmark.triangle")
                        .font(.system(size: 48))
                        .foregroundColor(.orange)
                    Text("WhatsApp não encontrado")
                        .font(.headline)
                    Text(database.error ?? "Verifique se o WhatsApp está instalado")
                        .font(.caption)
                        .foregroundColor(.secondary)
                        .multilineTextAlignment(.center)
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .padding()
            }
        }
    }
}

struct ChatRowView: View {
    let chat: ChatSession
    let isSelected: Bool

    var body: some View {
        HStack(spacing: 12) {
            // Avatar
            ZStack {
                Circle()
                    .fill(avatarColor)
                    .frame(width: 40, height: 40)
                Text(initials)
                    .font(.system(size: 14, weight: .semibold))
                    .foregroundColor(.white)
            }

            // Chat info
            VStack(alignment: .leading, spacing: 2) {
                HStack {
                    Text(chat.displayName)
                        .font(.system(size: 13, weight: .medium))
                        .lineLimit(1)
                    Spacer()
                    Text(chat.formattedLastMessage)
                        .font(.system(size: 11))
                        .foregroundColor(.secondary)
                }

                HStack {
                    Text("\(chat.messageCount) mensagens")
                        .font(.system(size: 11))
                        .foregroundColor(.secondary)
                    Spacer()
                }
            }
        }
        .padding(.vertical, 4)
        .contentShape(Rectangle())
    }

    private var initials: String {
        let name = chat.displayName
        let components = name.split(separator: " ")
        if components.count >= 2 {
            return String(components[0].prefix(1) + components[1].prefix(1)).uppercased()
        }
        return String(name.prefix(2)).uppercased()
    }

    private var avatarColor: Color {
        let colors: [Color] = [.blue, .green, .orange, .purple, .pink, .teal]
        let index = abs(chat.displayName.hashValue) % colors.count
        return colors[index]
    }
}


Now let me read the remaining view files:


     1→import SwiftUI
     2→
     3→struct ChatDetailView: View {
     4→    let chat: ChatSession
     5→    @ObservedObject var database: WhatsAppDatabase
     6→    @State private var messages: [Message] = []
     7→    @State private var messageLimit = 100
     8→
     9→    var body: some View {
    10→        VStack(spacing: 0) {
    11→            // Header
    12→            ChatHeaderView(chat: chat, messageCount: messages.count)
    13→
    14→            Divider()
    15→
    16→            // Messages
    17→            ScrollViewReader { proxy in
    18→                ScrollView {
    19→                    LazyVStack(spacing: 8) {
    20→                        ForEach(messages) { message in
    21→                            MessageBubbleView(message: message, partnerName: chat.displayName)
    22→                                .id(message.id)
    23→                        }
    24→                    }
    25→                    .padding()
    26→                }
    27→                .onChange(of: messages) { _, newMessages in
    28→                    if let lastMessage = newMessages.last {
    29→                        withAnimation {
    30→                            proxy.scrollTo(lastMessage.id, anchor: .bottom)
    31→                        }
    32→                    }
    33→                }
    34→            }
    35→
    36→            // Load more button
    37→            if messages.count >= messageLimit {
    38→                Button(action: loadMore) {
    39→                    Label("Carregar mais mensagens", systemImage: "arrow.down.circle")
    40→                        .font(.caption)
    41→                }
    42→                .buttonStyle(.plain)
    43→                .foregroundColor(.accentColor)
    44→                .padding(8)
    45→            }
    46→        }
    47→        .onAppear {
    48→            loadMessages()
    49→        }
    50→        .onChange(of: chat.id) { _, _ in
    51→            messageLimit = 100
    52→            loadMessages()
    53→        }
    54→    }
    55→
    56→    private func loadMessages() {
    57→        messages = database.getMessages(for: chat.id, limit: messageLimit)
    58→    }
    59→
    60→    private func loadMore() {
    61→        messageLimit += 100
    62→        loadMessages()
    63→    }
    64→}
    65→
    66→struct ChatHeaderView: View {
    67→    let chat: ChatSession
    68→    let messageCount: Int
    69→
    70→    var body: some View {
    71→        HStack {
    72→            VStack(alignment: .leading, spacing: 2) {
    73→                Text(chat.displayName)
    74→                    .font(.headline)
    75→                Text("\(messageCount) mensagens carregadas")
    76→                    .font(.caption)
    77→                    .foregroundColor(.secondary)
    78→            }
    79→            Spacer()
    80→            if let lastDate = chat.lastMessageDate {
    81→                VStack(alignment: .trailing, spacing: 2) {
    82→                    Text("Última mensagem")
    83→                        .font(.caption2)
    84→                        .foregroundColor(.secondary)
    85→                    Text(lastDate, style: .relative)
    86→                        .font(.caption)
    87→                        .foregroundColor(.secondary)
    88→                }
    89→            }
    90→        }
    91→        .padding()
    92→        .background(Color(nsColor: .controlBackgroundColor))
    93→    }
    94→}
    95→
    96→struct MessageBubbleView: View {
    97→    let message: Message
    98→    let partnerName: String
    99→
   100→    var body: some View {
   101→        HStack {
   102→            if message.isFromMe { Spacer(minLength: 60) }
   103→
   104→            VStack(alignment: message.isFromMe ? .trailing : .leading, spacing: 4) {
   105→                // Sender label (for received messages)
   106→                if !message.isFromMe {
   107→                    Text(message.senderName ?? partnerName)
   108→                        .font(.caption2)
   109→                        .foregroundColor(.secondary)
   110→                }
   111→
   112→                // Message bubble
   113→                HStack(alignment: .bottom, spacing: 6) {
   114→                    Text(message.text)
   115→                        .font(.system(size: 13))
   116→                        .textSelection(.enabled)
   117→
   118→                    Text(message.formattedTime)
   119→                        .font(.system(size: 9))
   120→                        .foregroundColor(message.isFromMe ? .white.opacity(0.7) : .secondary)
   121→                }
   122→                .padding(.horizontal, 12)
   123→                .padding(.vertical, 8)
   124→                .background(bubbleColor)
   125→                .clipShape(RoundedRectangle(cornerRadius: 16))
   126→            }
   127→
   128→            if !message.isFromMe { Spacer(minLength: 60) }
   129→        }
   130→    }
   131→
   132→    private var bubbleColor: Color {
   133→        message.isFromMe ? Color.green : Color(nsColor: .controlBackgroundColor)
   134→    }
   135→}
   136→

<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 ChatDetailView: View {
    let chat: ChatSession
    @ObservedObject var database: WhatsAppDatabase
    @State private var messages: [Message] = []
    @State private var messageLimit = 100

    var body: some View {
        VStack(spacing: 0) {
            // Header
            ChatHeaderView(chat: chat, messageCount: messages.count)

            Divider()

            // Messages
            ScrollViewReader { proxy in
                ScrollView {
                    LazyVStack(spacing: 8) {
                        ForEach(messages) { message in
                            MessageBubbleView(message: message, partnerName: chat.displayName)
                                .id(message.id)
                        }
                    }
                    .padding()
                }
                .onChange(of: messages) { _, newMessages in
                    if let lastMessage = newMessages.last {
                        withAnimation {
                            proxy.scrollTo(lastMessage.id, anchor: .bottom)
                        }
                    }
                }
            }

            // Load more button
            if messages.count >= messageLimit {
                Button(action: loadMore) {
                    Label("Carregar mais mensagens", systemImage: "arrow.down.circle")
                        .font(.caption)
                }
                .buttonStyle(.plain)
                .foregroundColor(.accentColor)
                .padding(8)
            }
        }
        .onAppear {
            loadMessages()
        }
        .onChange(of: chat.id) { _, _ in
            messageLimit = 100
            loadMessages()
        }
    }

    private func loadMessages() {
        messages = database.getMessages(for: chat.id, limit: messageLimit)
    }

    private func loadMore() {
        messageLimit += 100
        loadMessages()
    }
}

struct ChatHeaderView: View {
    let chat: ChatSession
    let messageCount: Int

    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 2) {
                Text(chat.displayName)
                    .font(.headline)
                Text("\(messageCount) mensagens carregadas")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
            Spacer()
            if let lastDate = chat.lastMessageDate {
                VStack(alignment: .trailing, spacing: 2) {
                    Text("Última mensagem")
                        .font(.caption2)
                        .foregroundColor(.secondary)
                    Text(lastDate, style: .relative)
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
            }
        }
        .padding()
        .background(Color(nsColor: .controlBackgroundColor))
    }
}

struct MessageBubbleView: View {
    let message: Message
    let partnerName: String

    var body: some View {
        HStack {
            if message.isFromMe { Spacer(minLength: 60) }

            VStack(alignment: message.isFromMe ? .trailing : .leading, spacing: 4) {
                // Sender label (for received messages)
                if !message.isFromMe {
                    Text(message.senderName ?? partnerName)
                        .font(.caption2)
                        .foregroundColor(.secondary)
                }

                // Message bubble
                HStack(alignment: .bottom, spacing: 6) {
                    Text(message.text)
                        .font(.system(size: 13))
                        .textSelection(.enabled)

                    Text(message.formattedTime)
                        .font(.system(size: 9))
                        .foregroundColor(message.isFromMe ? .white.opacity(0.7) : .secondary)
                }
                .padding(.horizontal, 12)
                .padding(.vertical, 8)
                .background(bubbleColor)
                .clipShape(RoundedRectangle(cornerRadius: 16))
            }

            if !message.isFromMe { Spacer(minLength: 60) }
        }
    }

    private var bubbleColor: Color {
        message.isFromMe ? Color.green : Color(nsColor: .controlBackgroundColor)
    }
}


     1→import SwiftUI
     2→
     3→struct AIAnalysisView: View {
     4→    let chat: ChatSession
     5→    @ObservedObject var database: WhatsAppDatabase
     6→    @StateObject private var aiService = AIService.shared
     7→
     8→    @State private var selectedTab = 0
     9→    @State private var analysis: ChatAnalysis?
    10→    @State private var isAnalyzing = false
    11→    @State private var error: String?
    12→    @State private var customPrompt = ""
    13→    @State private var customResponse = ""
    14→    @State private var showingReplySheet = false
    15→    @State private var replyText = ""
    16→
    17→    var body: some View {
    18→        VStack(spacing: 0) {
    19→            // Tab selector
    20→            Picker("Modo de Análise", selection: $selectedTab) {
    21→                Text("Rápida").tag(0)
    22→                Text("Profunda").tag(1)
    23→            }
    24→            .pickerStyle(.segmented)
    25→            .padding()
    26→
    27→            Divider()
    28→
    29→            // Content based on tab
    30→            if selectedTab == 0 {
    31→                QuickAnalysisContent(
    32→                    chat: chat,
    33→                    database: database,
    34→                    aiService: aiService,
    35→                    analysis: $analysis,
    36→                    isAnalyzing: $isAnalyzing,
    37→                    error: $error,
    38→                    customPrompt: $customPrompt,
    39→                    customResponse: $customResponse,
    40→                    showingReplySheet: $showingReplySheet,
    41→                    replyText: $replyText
    42→                )
    43→            } else {
    44→                DeepAnalysisView(chat: chat, database: database)
    45→            }
    46→        }
    47→        .sheet(isPresented: $showingReplySheet) {
    48→            QuickReplySheet(
    49→                chat: chat,
    50→                replyText: $replyText,
    51→                onSend: sendReply
    52→            )
    53→        }
    54→    }
    55→
    56→    private func sendReply(_ text: String) {
    57→        let phone = chat.contactJID.replacingOccurrences(of: "@lid", with: "")
    58→        if let encoded = text.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
    59→           let url = URL(string: "whatsapp://send?phone=\(phone)&text=\(encoded)") {
    60→            NSWorkspace.shared.open(url)
    61→        }
    62→        showingReplySheet = false
    63→    }
    64→}
    65→
    66→// MARK: - Quick Analysis Content
    67→
    68→struct QuickAnalysisContent: View {
    69→    let chat: ChatSession
    70→    @ObservedObject var database: WhatsAppDatabase
    71→    @ObservedObject var aiService: AIService
    72→
    73→    @Binding var analysis: ChatAnalysis?
    74→    @Binding var isAnalyzing: Bool
    75→    @Binding var error: String?
    76→    @Binding var customPrompt: String
    77→    @Binding var customResponse: String
    78→    @Binding var showingReplySheet: Bool
    79→    @Binding var replyText: String
    80→
    81→    var body: some View {
    82→        ScrollView {
    83→            VStack(alignment: .leading, spacing: 20) {
    84→                // Connection status
    85→                AIConnectionStatusView(aiService: aiService)
    86→
    87→                Divider()
    88→
    89→                // Quick analysis section
    90→                QuickAnalysisSectionView(
    91→                    chat: chat,
    92→                    analysis: analysis,
    93→                    isAnalyzing: isAnalyzing,
    94→                    error: error,
    95→                    onAnalyze: analyzeChat
    96→                )
    97→
    98→                if let analysis = analysis {
    99→                    Divider()
   100→
   101→                    // Results with reply buttons
   102→                    QuickAnalysisResultsView(
   103→                        analysis: analysis,
   104→                        onReply: { text in
   105→                            replyText = text
   106→                            showingReplySheet = true
   107→                        }
   108→                    )
   109→                }
   110→
   111→                Divider()
   112→
   113→                // Custom prompt
   114→                CustomPromptWithReplyView(
   115→                    prompt: $customPrompt,
   116→                    response: $customResponse,
   117→                    isProcessing: aiService.isProcessing,
   118→                    onSend: sendCustomPrompt,
   119→                    onReply: { text in
   120→                        replyText = text
   121→                        showingReplySheet = true
   122→                    }
   123→                )
   124→            }
   125→            .padding()
   126→        }
   127→    }
   128→
   129→    private func analyzeChat() {
   130→        guard aiService.isConnected else {
   131→            error = "Nenhum provedor de IA disponível"
   132→            return
   133→        }
   134→
   135→        isAnalyzing = true
   136→        error = nil
   137→
   138→        let messages = database.getMessagesForAnalysis(for: chat.id, limit: 50)
   139→
   140→        Task {
   141→            do {
   142→                let result = try await aiService.analyzeChat(messages: messages, partnerName: chat.displayName)
   143→                await MainActor.run {
   144→                    analysis = result
   145→                    isAnalyzing = false
   146→                }
   147→            } catch {
   148→                await MainActor.run {
   149→                    self.error = error.localizedDescription
   150→                    isAnalyzing = false
   151→                }
   152→            }
   153→        }
   154→    }
   155→
   156→    private func sendCustomPrompt() {
   157→        guard !customPrompt.isEmpty, aiService.isConnected else { return }
   158→
   159→        let context = database.getMessagesForAnalysis(for: chat.id, limit: 30)
   160→
   161→        Task {
   162→            do {
   163→                let response = try await aiService.generateResponse(
   164→                    context: context,
   165→                    instruction: customPrompt
   166→                )
   167→                await MainActor.run {
   168→                    customResponse = response
   169→                }
   170→            } catch {
   171→                await MainActor.run {
   172→                    customResponse = "Erro: \(error.localizedDescription)"
   173→                }
   174→            }
   175→        }
   176→    }
   177→}
   178→
   179→// MARK: - Quick Reply Sheet
   180→
   181→struct QuickReplySheet: View {
   182→    let chat: ChatSession
   183→    @Binding var replyText: String
   184→    let onSend: (String) -> Void
   185→
   186→    @Environment(\.dismiss) private var dismiss
   187→
   188→    var body: some View {
   189→        VStack(spacing: 20) {
   190→            HStack {
   191→                Image(systemName: "paperplane.circle.fill")
   192→                    .font(.title)
   193→                    .foregroundColor(.green)
   194→                Text("Responder para \(chat.displayName)")
   195→                    .font(.headline)
   196→            }
   197→
   198→            TextEditor(text: $replyText)
   199→                .frame(height: 120)
   200→                .padding(8)
   201→                .background(Color(nsColor: .controlBackgroundColor))
   202→                .cornerRadius(8)
   203→                .overlay(
   204→                    RoundedRectangle(cornerRadius: 8)
   205→                        .stroke(Color.secondary.opacity(0.3), lineWidth: 1)
   206→                )
   207→
   208→            Text("Isso abrirá o WhatsApp com a mensagem preenchida")
   209→                .font(.caption)
   210→                .foregroundColor(.secondary)
   211→
   212→            HStack {
   213→                Button("Cancelar") {
   214→                    dismiss()
   215→                }
   216→                .keyboardShortcut(.escape)
   217→
   218→                Spacer()
   219→
   220→                Button(action: { onSend(replyText) }) {
   221→                    HStack {
   222→                        Image(systemName: "paperplane.fill")
   223→                        Text("Abrir no WhatsApp")
   224→                    }
   225→                }
   226→                .buttonStyle(.borderedProminent)
   227→                .tint(.green)
   228→                .keyboardShortcut(.return)
   229→            }
   230→        }
   231→        .padding()
   232→        .frame(width: 450)
   233→    }
   234→}
   235→
   236→// MARK: - AI Connection Status
   237→
   238→struct AIConnectionStatusView: View {
   239→    @ObservedObject var aiService: AIService
   240→
   241→    var body: some View {
   242→        VStack(alignment: .leading, spacing: 8) {
   243→            HStack {
   244→                Circle()
   245→                    .fill(aiService.isConnected ? Color.green : Color.red)
   246→                    .frame(width: 8, height: 8)
   247→
   248→                Text(aiService.isConnected ? "Conectado" : "Offline")
   249→                    .font(.caption)
   250→                    .foregroundColor(.secondary)
   251→
   252→                if aiService.isConnected {
   253→                    Text("via \(aiService.currentProvider)")
   254→                        .font(.caption)
   255→                        .foregroundColor(.accentColor)
   256→                        .fontWeight(.medium)
   257→                }
   258→
   259→                Spacer()
   260→
   261→                Button(action: {
   262→                    Task { await aiService.checkConnections() }
   263→                }) {
   264→                    Image(systemName: "arrow.clockwise")
   265→                        .font(.caption)
   266→                }
   267→                .buttonStyle(.plain)
   268→            }
   269→
   270→            HStack(spacing: 16) {
   271→                HStack(spacing: 4) {
   272→                    Image(systemName: "brain.head.profile")
   273→                        .font(.caption2)
   274→                    Text("Claude")
   275→                        .font(.caption2)
   276→                    Circle()
   277→                        .fill(aiService.claudeAvailable ? Color.green : Color.gray)
   278→                        .frame(width: 6, height: 6)
   279→                }
   280→                .foregroundColor(aiService.currentProvider == "Claude" ? .accentColor : .secondary)
   281→
   282→                HStack(spacing: 4) {
   283→                    Image(systemName: "server.rack")
   284→                        .font(.caption2)
   285→                    Text("LM Gateway")
   286→                        .font(.caption2)
   287→                    Circle()
   288→                        .fill(aiService.lmGatewayAvailable ? Color.green : Color.gray)
   289→                        .frame(width: 6, height: 6)
   290→                }
   291→                .foregroundColor(aiService.currentProvider == "LM Gateway" ? .accentColor : .secondary)
   292→
   293→                Spacer()
   294→
   295→                Toggle("Claude primário", isOn: Binding(
   296→                    get: { aiService.useClaudeAsPrimary },
   297→                    set: { newValue in
   298→                        aiService.useClaudeAsPrimary = newValue
   299→                        Task { await aiService.checkConnections() }
   300→                    }
   301→                ))
   302→                .toggleStyle(.switch)
   303→                .scaleEffect(0.7)
   304→                .font(.caption2)
   305→            }
   306→        }
   307→        .padding(10)
   308→        .background(Color(nsColor: .controlBackgroundColor))
   309→        .cornerRadius(8)
   310→    }
   311→}
   312→
   313→// MARK: - Quick Analysis Section
   314→
   315→struct QuickAnalysisSectionView: View {
   316→    let chat: ChatSession
   317→    let analysis: ChatAnalysis?
   318→    let isAnalyzing: Bool
   319→    let error: String?
   320→    let onAnalyze: () -> Void
   321→
   322→    var body: some View {
   323→        VStack(alignment: .leading, spacing: 12) {
   324→            HStack {
   325→                Text("Análise Rápida")
   326→                    .font(.headline)
   327→                Spacer()
   328→                Text("últimas 50 msgs")
   329→                    .font(.caption2)
   330→                    .foregroundColor(.secondary)
   331→            }
   332→
   333→            Button(action: onAnalyze) {
   334→                HStack {
   335→                    if isAnalyzing {
   336→                        ProgressView()
   337→                            .scaleEffect(0.7)
   338→                    } else {
   339→                        Image(systemName: "sparkles")
   340→                    }
   341→                    Text(isAnalyzing ? "Analisando..." : "Analisar")
   342→                }
   343→                .frame(maxWidth: .infinity)
   344→                .padding(.vertical, 8)
   345→            }
   346→            .buttonStyle(.borderedProminent)
   347→            .disabled(isAnalyzing)
   348→
   349→            if let error = error {
   350→                Text(error)
   351→                    .font(.caption)
   352→                    .foregroundColor(.red)
   353→            }
   354→        }
   355→    }
   356→}
   357→
   358→// MARK: - Quick Analysis Results with Reply
   359→
   360→struct QuickAnalysisResultsView: View {
   361→    let analysis: ChatAnalysis
   362→    let onReply: (String) -> Void
   363→
   364→    var body: some View {
   365→        VStack(alignment: .leading, spacing: 16) {
   366→            // Summary
   367→            VStack(alignment: .leading, spacing: 8) {
   368→                Label("Resumo", systemImage: "doc.text")
   369→                    .font(.subheadline.bold())
   370→                Text(analysis.summary)
   371→                    .font(.body)
   372→                    .textSelection(.enabled)
   373→            }
   374→            .padding()
   375→            .frame(maxWidth: .infinity, alignment: .leading)
   376→            .background(Color.blue.opacity(0.1))
   377→            .cornerRadius(8)
   378→
   379→            // Topics
   380→            if !analysis.topics.isEmpty {
   381→                VStack(alignment: .leading, spacing: 8) {
   382→                    Label("Tópicos", systemImage: "tag")
   383→                        .font(.subheadline.bold())
   384→                    FlowLayout(spacing: 8) {
   385→                        ForEach(analysis.topics, id: \.self) { topic in
   386→                            Text(topic)
   387→                                .font(.caption)
   388→                                .padding(.horizontal, 10)
   389→                                .padding(.vertical, 4)
   390→                                .background(Color.purple.opacity(0.2))
   391→                                .cornerRadius(12)
   392→                        }
   393→                    }
   394→                }
   395→            }
   396→
   397→            // Sentiment
   398→            HStack {
   399→                Label("Sentimento", systemImage: "face.smiling")
   400→                    .font(.subheadline.bold())
   401→                Text(analysis.sentiment.capitalized)
   402→                    .font(.body)
   403→                    .padding(.horizontal, 12)
   404→                    .padding(.vertical, 6)
   405→                    .background(sentimentColor.opacity(0.2))
   406→                    .cornerRadius(8)
   407→            }
   408→
   409→            // Suggestions with Reply buttons
   410→            if !analysis.suggestedResponses.isEmpty {
   411→                VStack(alignment: .leading, spacing: 8) {
   412→                    Label("Sugestões de Resposta", systemImage: "text.bubble")
   413→                        .font(.subheadline.bold())
   414→
   415→                    ForEach(analysis.suggestedResponses.indices, id: \.self) { index in
   416→                        Button(action: { onReply(analysis.suggestedResponses[index]) }) {
   417→                            HStack {
   418→                                Text("\(index + 1).")
   419→                                    .foregroundColor(.secondary)
   420→                                Text(analysis.suggestedResponses[index])
   421→                                    .multilineTextAlignment(.leading)
   422→                                Spacer()
   423→                                Image(systemName: "paperplane.fill")
   424→                                    .foregroundColor(.green)
   425→                            }
   426→                            .padding(10)
   427→                            .background(Color.green.opacity(0.1))
   428→                            .cornerRadius(8)
   429→                        }
   430→                        .buttonStyle(.plain)
   431→                    }
   432→                }
   433→            }
   434→        }
   435→    }
   436→
   437→    private var sentimentColor: Color {
   438→        switch analysis.sentiment.lowercased() {
   439→        case "positivo": return .green
   440→        case "negativo": return .red
   441→        case "neutro": return .gray
   442→        default: return .orange
   443→        }
   444→    }
   445→}
   446→
   447→// MARK: - Custom Prompt with Reply
   448→
   449→struct CustomPromptWithReplyView: View {
   450→    @Binding var prompt: String
   451→    @Binding var response: String
   452→    let isProcessing: Bool
   453→    let onSend: () -> Void
   454→    let onReply: (String) -> Void
   455→
   456→    var body: some View {
   457→        VStack(alignment: .leading, spacing: 12) {
   458→            Text("Pergunte à IA")
   459→                .font(.headline)
   460→
   461→            HStack {
   462→                TextField("Ex: Sugira uma resposta para continuar a conversa", text: $prompt)
   463→                    .textFieldStyle(.roundedBorder)
   464→
   465→                Button(action: onSend) {
   466→                    if isProcessing {
   467→                        ProgressView()
   468→                            .scaleEffect(0.7)
   469→                    } else {
   470→                        Image(systemName: "paperplane.fill")
   471→                    }
   472→                }
   473→                .buttonStyle(.borderedProminent)
   474→                .disabled(prompt.isEmpty || isProcessing)
   475→            }
   476→
   477→            if !response.isEmpty {
   478→                VStack(alignment: .leading, spacing: 8) {
   479→                    HStack {
   480→                        Text("Resposta:")
   481→                            .font(.caption.bold())
   482→                            .foregroundColor(.secondary)
   483→                        Spacer()
   484→                        Button(action: { onReply(response) }) {
   485→                            Label("Usar como resposta", systemImage: "paperplane")
   486→                                .font(.caption)
   487→                        }
   488→                        .buttonStyle(.bordered)
   489→                        .tint(.green)
   490→                    }
   491→
   492→                    Text(response)
   493→                        .font(.body)
   494→                        .textSelection(.enabled)
   495→                        .padding()
   496→                        .frame(maxWidth: .infinity, alignment: .leading)
   497→                        .background(Color(nsColor: .controlBackgroundColor))
   498→                        .cornerRadius(8)
   499→                }
   500→            }
   501→        }
   502→    }
   503→}
   504→
   505→// MARK: - Flow Layout
   506→
   507→struct FlowLayout: Layout {
   508→    var spacing: CGFloat = 8
   509→
   510→    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
   511→        let result = FlowResult(in: proposal.width ?? 0, subviews: subviews, spacing: spacing)
   512→        return result.size
   513→    }
   514→
   515→    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
   516→        let result = FlowResult(in: bounds.width, subviews: subviews, spacing: spacing)
   517→        for (index, subview) in subviews.enumerated() {
   518→            subview.place(at: CGPoint(x: bounds.minX + result.positions[index].x,
   519→                                       y: bounds.minY + result.positions[index].y),
   520→                          proposal: .unspecified)
   521→        }
   522→    }
   523→
   524→    struct FlowResult {
   525→        var size: CGSize = .zero
   526→        var positions: [CGPoint] = []
   527→
   528→        init(in maxWidth: CGFloat, subviews: Subviews, spacing: CGFloat) {
   529→            var x: CGFloat = 0
   530→            var y: CGFloat = 0
   531→            var rowHeight: CGFloat = 0
   532→
   533→            for subview in subviews {
   534→                let size = subview.sizeThatFits(.unspecified)
   535→                if x + size.width > maxWidth && x > 0 {
   536→                    x = 0
   537→                    y += rowHeight + spacing
   538→                    rowHeight = 0
   539→                }
   540→                positions.append(CGPoint(x: x, y: y))
   541→                rowHeight = max(rowHeight, size.height)
   542→                x += size.width + spacing
   543→            }
   544→
   545→            self.size = CGSize(width: maxWidth, height: y + rowHeight)
   546→        }
   547→    }
   548→}
   549→

<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 AIAnalysisView: View {
    let chat: ChatSession
    @ObservedObject var database: WhatsAppDatabase
    @StateObject private var aiService = AIService.shared

    @State private var selectedTab = 0
    @State private var analysis: ChatAnalysis?
    @State private var isAnalyzing = false
    @State private var error: String?
    @State private var customPrompt = ""
    @State private var customResponse = ""
    @State private var showingReplySheet = false
    @State private var replyText = ""

    var body: some View {
        VStack(spacing: 0) {
            // Tab selector
            Picker("Modo de Análise", selection: $selectedTab) {
                Text("Rápida").tag(0)
                Text("Profunda").tag(1)
            }
            .pickerStyle(.segmented)
            .padding()

            Divider()

            // Content based on tab
            if selectedTab == 0 {
                QuickAnalysisContent(
                    chat: chat,
                    database: database,
                    aiService: aiService,
                    analysis: $analysis,
                    isAnalyzing: $isAnalyzing,
                    error: $error,
                    customPrompt: $customPrompt,
                    customResponse: $customResponse,
                    showingReplySheet: $showingReplySheet,
                    replyText: $replyText
                )
            } else {
                DeepAnalysisView(chat: chat, database: database)
            }
        }
        .sheet(isPresented: $showingReplySheet) {
            QuickReplySheet(
                chat: chat,
                replyText: $replyText,
                onSend: sendReply
            )
        }
    }

    private func sendReply(_ text: String) {
        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
    }
}

// MARK: - Quick Analysis Content

struct QuickAnalysisContent: View {
    let chat: ChatSession
    @ObservedObject var database: WhatsAppDatabase
    @ObservedObject var aiService: AIService

    @Binding var analysis: ChatAnalysis?
    @Binding var isAnalyzing: Bool
    @Binding var error: String?
    @Binding var customPrompt: String
    @Binding var customResponse: String
    @Binding var showingReplySheet: Bool
    @Binding var replyText: String

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 20) {
                // Connection status
                AIConnectionStatusView(aiService: aiService)

                Divider()

                // Quick analysis section
                QuickAnalysisSectionView(
                    chat: chat,
                    analysis: analysis,
                    isAnalyzing: isAnalyzing,
                    error: error,
                    onAnalyze: analyzeChat
                )

                if let analysis = analysis {
                    Divider()

                    // Results with reply buttons
                    QuickAnalysisResultsView(
                        analysis: analysis,
                        onReply: { text in
                            replyText = text
                            showingReplySheet = true
                        }
                    )
                }

                Divider()

                // Custom prompt
                CustomPromptWithReplyView(
                    prompt: $customPrompt,
                    response: $customResponse,
                    isProcessing: aiService.isProcessing,
                    onSend: sendCustomPrompt,
                    onReply: { text in
                        replyText = text
                        showingReplySheet = true
                    }
                )
            }
            .padding()
        }
    }

    private func analyzeChat() {
        guard aiService.isConnected else {
            error = "Nenhum provedor de IA disponível"
            return
        }

        isAnalyzing = true
        error = nil

        let messages = database.getMessagesForAnalysis(for: chat.id, limit: 50)

        Task {
            do {
                let result = try await aiService.analyzeChat(messages: messages, partnerName: chat.displayName)
                await MainActor.run {
                    analysis = result
                    isAnalyzing = false
                }
            } catch {
                await MainActor.run {
                    self.error = error.localizedDescription
                    isAnalyzing = false
                }
            }
        }
    }

    private func sendCustomPrompt() {
        guard !customPrompt.isEmpty, aiService.isConnected else { return }

        let context = database.getMessagesForAnalysis(for: chat.id, limit: 30)

        Task {
            do {
                let response = try await aiService.generateResponse(
                    context: context,
                    instruction: customPrompt
                )
                await MainActor.run {
                    customResponse = response
                }
            } catch {
                await MainActor.run {
                    customResponse = "Erro: \(error.localizedDescription)"
                }
            }
        }
    }
}

// MARK: - Quick Reply Sheet

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

    @Environment(\.dismiss) private var dismiss

    var body: some View {
        VStack(spacing: 20) {
            HStack {
                Image(systemName: "paperplane.circle.fill")
                    .font(.title)
                    .foregroundColor(.green)
                Text("Responder para \(chat.displayName)")
                    .font(.headline)
            }

            TextEditor(text: $replyText)
                .frame(height: 120)
                .padding(8)
                .background(Color(nsColor: .controlBackgroundColor))
                .cornerRadius(8)
                .overlay(
                    RoundedRectangle(cornerRadius: 8)
                        .stroke(Color.secondary.opacity(0.3), lineWidth: 1)
                )

            Text("Isso abrirá o WhatsApp com a mensagem preenchida")
                .font(.caption)
                .foregroundColor(.secondary)

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

                Spacer()

                Button(action: { onSend(replyText) }) {
                    HStack {
                        Image(systemName: "paperplane.fill")
                        Text("Abrir no WhatsApp")
                    }
                }
                .buttonStyle(.borderedProminent)
                .tint(.green)
                .keyboardShortcut(.return)
            }
        }
        .padding()
        .frame(width: 450)
    }
}

// MARK: - AI Connection Status

struct AIConnectionStatusView: View {
    @ObservedObject var aiService: AIService

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            HStack {
                Circle()
                    .fill(aiService.isConnected ? Color.green : Color.red)
                    .frame(width: 8, height: 8)

                Text(aiService.isConnected ? "Conectado" : "Offline")
                    .font(.caption)
                    .foregroundColor(.secondary)

                if aiService.isConnected {
                    Text("via \(aiService.currentProvider)")
                        .font(.caption)
                        .foregroundColor(.accentColor)
                        .fontWeight(.medium)
                }

                Spacer()

                Button(action: {
                    Task { await aiService.checkConnections() }
                }) {
                    Image(systemName: "arrow.clockwise")
                        .font(.caption)
                }
                .buttonStyle(.plain)
            }

            HStack(spacing: 16) {
                HStack(spacing: 4) {
                    Image(systemName: "brain.head.profile")
                        .font(.caption2)
                    Text("Claude")
                        .font(.caption2)
                    Circle()
                        .fill(aiService.claudeAvailable ? Color.green : Color.gray)
                        .frame(width: 6, height: 6)
                }
                .foregroundColor(aiService.currentProvider == "Claude" ? .accentColor : .secondary)

                HStack(spacing: 4) {
                    Image(systemName: "server.rack")
                        .font(.caption2)
                    Text("LM Gateway")
                        .font(.caption2)
                    Circle()
                        .fill(aiService.lmGatewayAvailable ? Color.green : Color.gray)
                        .frame(width: 6, height: 6)
                }
                .foregroundColor(aiService.currentProvider == "LM Gateway" ? .accentColor : .secondary)

                Spacer()

                Toggle("Claude primário", isOn: Binding(
                    get: { aiService.useClaudeAsPrimary },
                    set: { newValue in
                        aiService.useClaudeAsPrimary = newValue
                        Task { await aiService.checkConnections() }
                    }
                ))
                .toggleStyle(.switch)
                .scaleEffect(0.7)
                .font(.caption2)
            }
        }
        .padding(10)
        .background(Color(nsColor: .controlBackgroundColor))
        .cornerRadius(8)
    }
}

// MARK: - Quick Analysis Section

struct QuickAnalysisSectionView: View {
    let chat: ChatSession
    let analysis: ChatAnalysis?
    let isAnalyzing: Bool
    let error: String?
    let onAnalyze: () -> Void

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Text("Análise Rápida")
                    .font(.headline)
                Spacer()
                Text("últimas 50 msgs")
                    .font(.caption2)
                    .foregroundColor(.secondary)
            }

            Button(action: onAnalyze) {
                HStack {
                    if isAnalyzing {
                        ProgressView()
                            .scaleEffect(0.7)
                    } else {
                        Image(systemName: "sparkles")
                    }
                    Text(isAnalyzing ? "Analisando..." : "Analisar")
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 8)
            }
            .buttonStyle(.borderedProminent)
            .disabled(isAnalyzing)

            if let error = error {
                Text(error)
                    .font(.caption)
                    .foregroundColor(.red)
            }
        }
    }
}

// MARK: - Quick Analysis Results with Reply

struct QuickAnalysisResultsView: View {
    let analysis: ChatAnalysis
    let onReply: (String) -> Void

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            // Summary
            VStack(alignment: .leading, spacing: 8) {
                Label("Resumo", systemImage: "doc.text")
                    .font(.subheadline.bold())
                Text(analysis.summary)
                    .font(.body)
                    .textSelection(.enabled)
            }
            .padding()
            .frame(maxWidth: .infinity, alignment: .leading)
            .background(Color.blue.opacity(0.1))
            .cornerRadius(8)

            // Topics
            if !analysis.topics.isEmpty {
                VStack(alignment: .leading, spacing: 8) {
                    Label("Tópicos", systemImage: "tag")
                        .font(.subheadline.bold())
                    FlowLayout(spacing: 8) {
                        ForEach(analysis.topics, id: \.self) { topic in
                            Text(topic)
                                .font(.caption)
                                .padding(.horizontal, 10)
                                .padding(.vertical, 4)
                                .background(Color.purple.opacity(0.2))
                                .cornerRadius(12)
                        }
                    }
                }
            }

            // Sentiment
            HStack {
                Label("Sentimento", systemImage: "face.smiling")
                    .font(.subheadline.bold())
                Text(analysis.sentiment.capitalized)
                    .font(.body)
                    .padding(.horizontal, 12)
                    .padding(.vertical, 6)
                    .background(sentimentColor.opacity(0.2))
                    .cornerRadius(8)
            }

            // Suggestions with Reply buttons
            if !analysis.suggestedResponses.isEmpty {
                VStack(alignment: .leading, spacing: 8) {
                    Label("Sugestões de Resposta", systemImage: "text.bubble")
                        .font(.subheadline.bold())

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

    private var sentimentColor: Color {
        switch analysis.sentiment.lowercased() {
        case "positivo": return .green
        case "negativo": return .red
        case "neutro": return .gray
        default: return .orange
        }
    }
}

// MARK: - Custom Prompt with Reply

struct CustomPromptWithReplyView: View {
    @Binding var prompt: String
    @Binding var response: String
    let isProcessing: Bool
    let onSend: () -> Void
    let onReply: (String) -> Void

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Pergunte à IA")
                .font(.headline)

            HStack {
                TextField("Ex: Sugira uma resposta para continuar a conversa", text: $prompt)
                    .textFieldStyle(.roundedBorder)

                Button(action: onSend) {
                    if isProcessing {
                        ProgressView()
                            .scaleEffect(0.7)
                    } else {
                        Image(systemName: "paperplane.fill")
                    }
                }
                .buttonStyle(.borderedProminent)
                .disabled(prompt.isEmpty || isProcessing)
            }

            if !response.isEmpty {
                VStack(alignment: .leading, spacing: 8) {
                    HStack {
                        Text("Resposta:")
                            .font(.caption.bold())
                            .foregroundColor(.secondary)
                        Spacer()
                        Button(action: { onReply(response) }) {
                            Label("Usar como resposta", systemImage: "paperplane")
                                .font(.caption)
                        }
                        .buttonStyle(.bordered)
                        .tint(.green)
                    }

                    Text(response)
                        .font(.body)
                        .textSelection(.enabled)
                        .padding()
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .background(Color(nsColor: .controlBackgroundColor))
                        .cornerRadius(8)
                }
            }
        }
    }
}

// MARK: - Flow Layout

struct FlowLayout: Layout {
    var spacing: CGFloat = 8

    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let result = FlowResult(in: proposal.width ?? 0, subviews: subviews, spacing: spacing)
        return result.size
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        let result = FlowResult(in: bounds.width, subviews: subviews, spacing: spacing)
        for (index, subview) in subviews.enumerated() {
            subview.place(at: CGPoint(x: bounds.minX + result.positions[index].x,
                                       y: bounds.minY + result.positions[index].y),
                          proposal: .unspecified)
        }
    }

    struct FlowResult {
        var size: CGSize = .zero
        var positions: [CGPoint] = []

        init(in maxWidth: CGFloat, subviews: Subviews, spacing: CGFloat) {
            var x: CGFloat = 0
            var y: CGFloat = 0
            var rowHeight: CGFloat = 0

            for subview in subviews {
                let size = subview.sizeThatFits(.unspecified)
                if x + size.width > maxWidth && x > 0 {
                    x = 0
                    y += rowHeight + spacing
                    rowHeight = 0
                }
                positions.append(CGPoint(x: x, y: y))
                rowHeight = max(rowHeight, size.height)
                x += size.width + spacing
            }

            self.size = CGSize(width: maxWidth, height: y + rowHeight)
        }
    }
}