Explore the Views/SmartConnections/ directory to understand:

1. How the ConnectionsTabView displays progress during analysis
2. What UI components show the analysis progress
3. How the `analysisProgress` is bound/observed in the UI
4. Current visual feedback during "Analyzing content..." phase

Look for:
- Progress views/components
- Status message displays
- Any existing progress bars or indicators
- How the UI updates during analysis

Return the file paths and key UI components involved in showing analysis progress.


I'll explore the Views/SmartConnections/ directory to understand how progress is currently displayed during analysis.


Bash

find /Users/neog/Apps/iOS/ObsidianViewer-iOS -type f -name "*.swift" | grep -i "smartconnection\|connection.*view" | head -20


Now let me examine the main ConnectionsTabView and related components:


     1→import SwiftUI
     2→
     3→/// Main tab view for Smart Connections feature
     4→struct ConnectionsTabView: View {
     5→    @EnvironmentObject var connectionService: SmartConnectionService
     6→    @EnvironmentObject var obsidianService: ObsidianService
     7→
     8→    @State private var selectedSection: ConnectionSection = .suggestions
     9→
    10→    enum ConnectionSection: String, CaseIterable, Identifiable {
    11→        case suggestions = "Suggestions"
    12→        case map = "Map"
    13→        case orphans = "Orphans"
    14→        case progress = "Progress"
    15→
    16→        var id: String { rawValue }
    17→
    18→        var icon: String {
    19→            switch self {
    20→            case .suggestions: return "lightbulb.fill"
    21→            case .map: return "point.3.connected.trianglepath.dotted"
    22→            case .orphans: return "questionmark.circle.fill"
    23→            case .progress: return "chart.line.uptrend.xyaxis"
    24→            }
    25→        }
    26→    }
    27→
    28→    @State private var showLiveAnalysisSheet = false
    29→
    30→    var body: some View {
    31→        NavigationStack {
    32→            VStack(spacing: 0) {
    33→                // Section Picker
    34→                sectionPicker
    35→
    36→                // Live Analysis View (if analyzing)
    37→                if connectionService.isAnalyzing, let progress = connectionService.analysisProgress {
    38→                    liveAnalysisBanner(progress)
    39→                }
    40→
    41→                // Content
    42→                Group {
    43→                    switch selectedSection {
    44→                    case .suggestions:
    45→                        SuggestionsListView()
    46→                    case .map:
    47→                        ConnectionMapView()
    48→                    case .orphans:
    49→                        OrphanNotesView()
    50→                    case .progress:
    51→                        ProgressDashboardView()
    52→                    }
    53→                }
    54→            }
    55→            .navigationTitle("Smart Connections")
    56→            .navigationBarTitleDisplayMode(.inline)
    57→            .toolbar {
    58→                ToolbarItem(placement: .navigationBarTrailing) {
    59→                    analyzeButton
    60→                }
    61→            }
    62→            .sheet(isPresented: $showLiveAnalysisSheet) {
    63→                if let progress = connectionService.analysisProgress {
    64→                    NavigationStack {
    65→                        ScrollView {
    66→                            LiveAnalysisView(progress: progress)
    67→                                .padding()
    68→                        }
    69→                        .navigationTitle("Analysis Progress")
    70→                        .navigationBarTitleDisplayMode(.inline)
    71→                        .toolbar {
    72→                            ToolbarItem(placement: .navigationBarTrailing) {
    73→                                Button("Done") {
    74→                                    showLiveAnalysisSheet = false
    75→                                }
    76→                            }
    77→                        }
    78→                    }
    79→                    .presentationDetents([.medium, .large])
    80→                }
    81→            }
    82→        }
    83→        .onAppear {
    84→            connectionService.loadPersistedData()
    85→        }
    86→    }
    87→
    88→    // MARK: - Section Picker
    89→
    90→    private var sectionPicker: some View {
    91→        ScrollView(.horizontal, showsIndicators: false) {
    92→            HStack(spacing: 12) {
    93→                ForEach(ConnectionSection.allCases) { section in
    94→                    sectionButton(section)
    95→                }
    96→            }
    97→            .padding(.horizontal)
    98→            .padding(.vertical, 8)
    99→        }
   100→        .background(Color(.systemGray6))
   101→    }
   102→
   103→    private func sectionButton(_ section: ConnectionSection) -> some View {
   104→        Button {
   105→            let generator = UISelectionFeedbackGenerator()
   106→            generator.selectionChanged()
   107→            withAnimation(.easeInOut(duration: 0.2)) {
   108→                selectedSection = section
   109→            }
   110→        } label: {
   111→            HStack(spacing: 6) {
   112→                Image(systemName: section.icon)
   113→                    .font(.system(size: 14, weight: .medium))
   114→                Text(section.rawValue)
   115→                    .font(.subheadline)
   116→                    .fontWeight(.medium)
   117→
   118→                // Badge for suggestions count
   119→                if section == .suggestions && !connectionService.pendingSuggestions.isEmpty {
   120→                    Text("\(connectionService.pendingSuggestions.count)")
   121→                        .font(.caption2)
   122→                        .fontWeight(.bold)
   123→                        .foregroundColor(.white)
   124→                        .padding(.horizontal, 6)
   125→                        .padding(.vertical, 2)
   126→                        .background(Color.red)
   127→                        .clipShape(Capsule())
   128→                }
   129→
   130→                // Badge for orphans count
   131→                if section == .orphans {
   132→                    let orphanCount = connectionService.getOrphanNotes().count
   133→                    if orphanCount > 0 {
   134→                        Text("\(orphanCount)")
   135→                            .font(.caption2)
   136→                            .fontWeight(.bold)
   137→                            .foregroundColor(.white)
   138→                            .padding(.horizontal, 6)
   139→                            .padding(.vertical, 2)
   140→                            .background(Color.orange)
   141→                            .clipShape(Capsule())
   142→                    }
   143→                }
   144→            }
   145→            .padding(.horizontal, 16)
   146→            .padding(.vertical, 10)
   147→            .background(
   148→                selectedSection == section
   149→                    ? Color.purple
   150→                    : Color(.systemBackground)
   151→            )
   152→            .foregroundColor(selectedSection == section ? .white : .primary)
   153→            .cornerRadius(20)
   154→            .shadow(color: selectedSection == section ? .purple.opacity(0.3) : .clear, radius: 4, y: 2)
   155→        }
   156→        .buttonStyle(.plain)
   157→    }
   158→
   159→    // MARK: - Live Analysis Banner
   160→
   161→    private func liveAnalysisBanner(_ progress: AnalysisProgress) -> some View {
   162→        Button {
   163→            showLiveAnalysisSheet = true
   164→        } label: {
   165→            VStack(spacing: 10) {
   166→                // Header with phase and metrics
   167→                HStack {
   168→                    // Animated phase icon
   169→                    ZStack {
   170→                        Circle()
   171→                            .fill(progress.currentPhase.color.opacity(0.2))
   172→                            .frame(width: 36, height: 36)
   173→
   174→                        Image(systemName: progress.currentPhase.icon)
   175→                            .font(.system(size: 16))
   176→                            .foregroundColor(progress.currentPhase.color)
   177→                    }
   178→
   179→                    VStack(alignment: .leading, spacing: 2) {
   180→                        Text(progress.currentPhase.displayName)
   181→                            .font(.subheadline)
   182→                            .fontWeight(.medium)
   183→                            .foregroundColor(.primary)
   184→
   185→                        if let status = progress.statusMessage {
   186→                            Text(status)
   187→                                .font(.caption2)
   188→                                .foregroundColor(.secondary)
   189→                                .lineLimit(1)
   190→                        }
   191→                    }
   192→
   193→                    Spacer()
   194→
   195→                    // Metrics
   196→                    VStack(alignment: .trailing, spacing: 2) {
   197→                        Text("\(Int(progress.percentComplete * 100))%")
   198→                            .font(.headline)
   199→                            .foregroundColor(.purple)
   200→
   201→                        if let eta = progress.etaString {
   202→                            Text("ETA: \(eta)")
   203→                                .font(.caption2)
   204→                                .foregroundColor(.secondary)
   205→                        }
   206→                    }
   207→
   208→                    Image(systemName: "chevron.right")
   209→                        .font(.caption)
   210→                        .foregroundColor(.secondary)
   211→                }
   212→
   213→                // Progress bar with gradient
   214→                GeometryReader { geo in
   215→                    ZStack(alignment: .leading) {
   216→                        RoundedRectangle(cornerRadius: 4)
   217→                            .fill(Color(.systemGray5))
   218→
   219→                        RoundedRectangle(cornerRadius: 4)
   220→                            .fill(
   221→                                LinearGradient(
   222→                                    colors: [.purple, .blue],
   223→                                    startPoint: .leading,
   224→                                    endPoint: .trailing
   225→                                )
   226→                            )
   227→                            .frame(width: geo.size.width * progress.percentComplete)
   228→                            .animation(.easeInOut(duration: 0.3), value: progress.percentComplete)
   229→                    }
   230→                }
   231→                .frame(height: 8)
   232→
   233→                // Compact timeline
   234→                CompactPhaseTimelineView(progress: progress)
   235→
   236→                // Quick stats
   237→                HStack(spacing: 16) {
   238→                    Label("\(progress.processedNotes)/\(progress.totalNotes)", systemImage: "doc.text")
   239→                        .font(.caption2)
   240→                        .foregroundColor(.secondary)
   241→
   242→                    Label("\(progress.totalConnectionsFound)", systemImage: "link")
   243→                        .font(.caption2)
   244→                        .foregroundColor(.green)
   245→
   246→                    if progress.totalBatches > 0 {
   247→                        Label("\(progress.currentBatch)/\(progress.totalBatches)", systemImage: "square.stack.3d.up")
   248→                            .font(.caption2)
   249→                            .foregroundColor(.secondary)
   250→                    }
   251→
   252→                    Spacer()
   253→
   254→                    Text(progress.elapsedTimeString)
   255→                        .font(.caption2)
   256→                        .foregroundColor(.secondary)
   257→                }
   258→            }
   259→            .padding()
   260→            .background(Color.purple.opacity(0.1))
   261→        }
   262→        .buttonStyle(.plain)
   263→    }
   264→
   265→    // MARK: - Analyze Button
   266→
   267→    private var analyzeButton: some View {
   268→        Button {
   269→            startAnalysis()
   270→        } label: {
   271→            if connectionService.isAnalyzing {
   272→                ProgressView()
   273→                    .progressViewStyle(CircularProgressViewStyle())
   274→            } else {
   275→                Image(systemName: "arrow.triangle.2.circlepath")
   276→            }
   277→        }
   278→        .disabled(connectionService.isAnalyzing || !obsidianService.isConnected)
   279→    }
   280→
   281→    private func startAnalysis() {
   282→        Task {
   283→            do {
   284→                try await connectionService.analyzeVault()
   285→            } catch {
   286→                connectionService.lastError = error.localizedDescription
   287→            }
   288→        }
   289→    }
   290→}
   291→
   292→// MARK: - Preview
   293→
   294→#Preview {
   295→    ConnectionsTabView()
   296→        .environmentObject(SmartConnectionService(
   297→            obsidianService: ObsidianService(),
   298→            lmService: LMGatewayService()
   299→        ))
   300→        .environmentObject(ObsidianService())
   301→}
   302→

<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 tab view for Smart Connections feature
struct ConnectionsTabView: View {
    @EnvironmentObject var connectionService: SmartConnectionService
    @EnvironmentObject var obsidianService: ObsidianService

    @State private var selectedSection: ConnectionSection = .suggestions

    enum ConnectionSection: String, CaseIterable, Identifiable {
        case suggestions = "Suggestions"
        case map = "Map"
        case orphans = "Orphans"
        case progress = "Progress"

        var id: String { rawValue }

        var icon: String {
            switch self {
            case .suggestions: return "lightbulb.fill"
            case .map: return "point.3.connected.trianglepath.dotted"
            case .orphans: return "questionmark.circle.fill"
            case .progress: return "chart.line.uptrend.xyaxis"
            }
        }
    }

    @State private var showLiveAnalysisSheet = false

    var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                // Section Picker
                sectionPicker

                // Live Analysis View (if analyzing)
                if connectionService.isAnalyzing, let progress = connectionService.analysisProgress {
                    liveAnalysisBanner(progress)
                }

                // Content
                Group {
                    switch selectedSection {
                    case .suggestions:
                        SuggestionsListView()
                    case .map:
                        ConnectionMapView()
                    case .orphans:
                        OrphanNotesView()
                    case .progress:
                        ProgressDashboardView()
                    }
                }
            }
            .navigationTitle("Smart Connections")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    analyzeButton
                }
            }
            .sheet(isPresented: $showLiveAnalysisSheet) {
                if let progress = connectionService.analysisProgress {
                    NavigationStack {
                        ScrollView {
                            LiveAnalysisView(progress: progress)
                                .padding()
                        }
                        .navigationTitle("Analysis Progress")
                        .navigationBarTitleDisplayMode(.inline)
                        .toolbar {
                            ToolbarItem(placement: .navigationBarTrailing) {
                                Button("Done") {
                                    showLiveAnalysisSheet = false
                                }
                            }
                        }
                    }
                    .presentationDetents([.medium, .large])
                }
            }
        }
        .onAppear {
            connectionService.loadPersistedData()
        }
    }

    // MARK: - Section Picker

    private var sectionPicker: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 12) {
                ForEach(ConnectionSection.allCases) { section in
                    sectionButton(section)
                }
            }
            .padding(.horizontal)
            .padding(.vertical, 8)
        }
        .background(Color(.systemGray6))
    }

    private func sectionButton(_ section: ConnectionSection) -> some View {
        Button {
            let generator = UISelectionFeedbackGenerator()
            generator.selectionChanged()
            withAnimation(.easeInOut(duration: 0.2)) {
                selectedSection = section
            }
        } label: {
            HStack(spacing: 6) {
                Image(systemName: section.icon)
                    .font(.system(size: 14, weight: .medium))
                Text(section.rawValue)
                    .font(.subheadline)
                    .fontWeight(.medium)

                // Badge for suggestions count
                if section == .suggestions && !connectionService.pendingSuggestions.isEmpty {
                    Text("\(connectionService.pendingSuggestions.count)")
                        .font(.caption2)
                        .fontWeight(.bold)
                        .foregroundColor(.white)
                        .padding(.horizontal, 6)
                        .padding(.vertical, 2)
                        .background(Color.red)
                        .clipShape(Capsule())
                }

                // Badge for orphans count
                if section == .orphans {
                    let orphanCount = connectionService.getOrphanNotes().count
                    if orphanCount > 0 {
                        Text("\(orphanCount)")
                            .font(.caption2)
                            .fontWeight(.bold)
                            .foregroundColor(.white)
                            .padding(.horizontal, 6)
                            .padding(.vertical, 2)
                            .background(Color.orange)
                            .clipShape(Capsule())
                    }
                }
            }
            .padding(.horizontal, 16)
            .padding(.vertical, 10)
            .background(
                selectedSection == section
                    ? Color.purple
                    : Color(.systemBackground)
            )
            .foregroundColor(selectedSection == section ? .white : .primary)
            .cornerRadius(20)
            .shadow(color: selectedSection == section ? .purple.opacity(0.3) : .clear, radius: 4, y: 2)
        }
        .buttonStyle(.plain)
    }

    // MARK: - Live Analysis Banner

    private func liveAnalysisBanner(_ progress: AnalysisProgress) -> some View {
        Button {
            showLiveAnalysisSheet = true
        } label: {
            VStack(spacing: 10) {
                // Header with phase and metrics
                HStack {
                    // Animated phase icon
                    ZStack {
                        Circle()
                            .fill(progress.currentPhase.color.opacity(0.2))
                            .frame(width: 36, height: 36)

                        Image(systemName: progress.currentPhase.icon)
                            .font(.system(size: 16))
                            .foregroundColor(progress.currentPhase.color)
                    }

                    VStack(alignment: .leading, spacing: 2) {
                        Text(progress.currentPhase.displayName)
                            .font(.subheadline)
                            .fontWeight(.medium)
                            .foregroundColor(.primary)

                        if let status = progress.statusMessage {
                            Text(status)
                                .font(.caption2)
                                .foregroundColor(.secondary)
                                .lineLimit(1)
                        }
                    }

                    Spacer()

                    // Metrics
                    VStack(alignment: .trailing, spacing: 2) {
                        Text("\(Int(progress.percentComplete * 100))%")
                            .font(.headline)
                            .foregroundColor(.purple)

                        if let eta = progress.etaString {
                            Text("ETA: \(eta)")
                                .font(.caption2)
                                .foregroundColor(.secondary)
                        }
                    }

                    Image(systemName: "chevron.right")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }

                // Progress bar with gradient
                GeometryReader { geo in
                    ZStack(alignment: .leading) {
                        RoundedRectangle(cornerRadius: 4)
                            .fill(Color(.systemGray5))

                        RoundedRectangle(cornerRadius: 4)
                            .fill(
                                LinearGradient(
                                    colors: [.purple, .blue],
                                    startPoint: .leading,
                                    endPoint: .trailing
                                )
                            )
                            .frame(width: geo.size.width * progress.percentComplete)
                            .animation(.easeInOut(duration: 0.3), value: progress.percentComplete)
                    }
                }
                .frame(height: 8)

                // Compact timeline
                CompactPhaseTimelineView(progress: progress)

                // Quick stats
                HStack(spacing: 16) {
                    Label("\(progress.processedNotes)/\(progress.totalNotes)", systemImage: "doc.text")
                        .font(.caption2)
                        .foregroundColor(.secondary)

                    Label("\(progress.totalConnectionsFound)", systemImage: "link")
                        .font(.caption2)
                        .foregroundColor(.green)

                    if progress.totalBatches > 0 {
                        Label("\(progress.currentBatch)/\(progress.totalBatches)", systemImage: "square.stack.3d.up")
                            .font(.caption2)
                            .foregroundColor(.secondary)
                    }

                    Spacer()

                    Text(progress.elapsedTimeString)
                        .font(.caption2)
                        .foregroundColor(.secondary)
                }
            }
            .padding()
            .background(Color.purple.opacity(0.1))
        }
        .buttonStyle(.plain)
    }

    // MARK: - Analyze Button

    private var analyzeButton: some View {
        Button {
            startAnalysis()
        } label: {
            if connectionService.isAnalyzing {
                ProgressView()
                    .progressViewStyle(CircularProgressViewStyle())
            } else {
                Image(systemName: "arrow.triangle.2.circlepath")
            }
        }
        .disabled(connectionService.isAnalyzing || !obsidianService.isConnected)
    }

    private func startAnalysis() {
        Task {
            do {
                try await connectionService.analyzeVault()
            } catch {
                connectionService.lastError = error.localizedDescription
            }
        }
    }
}

// MARK: - Preview

#Preview {
    ConnectionsTabView()
        .environmentObject(SmartConnectionService(
            obsidianService: ObsidianService(),
            lmService: LMGatewayService()
        ))
        .environmentObject(ObsidianService())
}


     1→import SwiftUI
     2→
     3→/// Dashboard showing feedback loop metrics and vault health
     4→struct ProgressDashboardView: View {
     5→    @EnvironmentObject var connectionService: SmartConnectionService
     6→
     7→    private var metrics: FeedbackMetrics {
     8→        connectionService.feedbackMetrics
     9→    }
    10→
    11→    private var graph: VaultGraph? {
    12→        connectionService.vaultGraph
    13→    }
    14→
    15→    var body: some View {
    16→        ScrollView {
    17→            VStack(spacing: 20) {
    18→                // Hero stats
    19→                heroStatsSection
    20→
    21→                // Model accuracy
    22→                accuracySection
    23→
    24→                // Vault health
    25→                vaultHealthSection
    26→
    27→                // Connection types breakdown
    28→                connectionTypesSection
    29→
    30→                // Weekly trends
    31→                weeklyTrendsSection
    32→
    33→                // Storage info
    34→                storageSection
    35→            }
    36→            .padding()
    37→        }
    38→        .refreshable {
    39→            let generator = UIImpactFeedbackGenerator(style: .light)
    40→            generator.impactOccurred()
    41→            connectionService.loadPersistedData()
    42→        }
    43→    }
    44→
    45→    // MARK: - Hero Stats
    46→
    47→    private var heroStatsSection: some View {
    48→        HStack(spacing: 12) {
    49→            StatCard(
    50→                title: "Accepted",
    51→                value: "\(metrics.suggestionsAccepted)",
    52→                icon: "checkmark.circle.fill",
    53→                color: .green
    54→            )
    55→
    56→            StatCard(
    57→                title: "Rejected",
    58→                value: "\(metrics.suggestionsRejected)",
    59→                icon: "xmark.circle.fill",
    60→                color: .red
    61→            )
    62→
    63→            StatCard(
    64→                title: "Written",
    65→                value: "\(metrics.connectionsAddedToVault)",
    66→                icon: "doc.badge.plus",
    67→                color: .blue
    68→            )
    69→        }
    70→    }
    71→
    72→    // MARK: - Accuracy Section
    73→
    74→    private var accuracySection: some View {
    75→        VStack(alignment: .leading, spacing: 12) {
    76→            Text("AI Model Accuracy")
    77→                .font(.headline)
    78→
    79→            HStack(spacing: 20) {
    80→                // Large accuracy ring
    81→                ZStack {
    82→                    Circle()
    83→                        .stroke(Color.green.opacity(0.2), lineWidth: 12)
    84→
    85→                    Circle()
    86→                        .trim(from: 0, to: metrics.modelAccuracy)
    87→                        .stroke(
    88→                            metrics.modelAccuracy > 0.7 ? Color.green : Color.orange,
    89→                            style: StrokeStyle(lineWidth: 12, lineCap: .round)
    90→                        )
    91→                        .rotationEffect(.degrees(-90))
    92→
    93→                    VStack(spacing: 4) {
    94→                        Text("\(Int(metrics.modelAccuracy * 100))%")
    95→                            .font(.title)
    96→                            .fontWeight(.bold)
    97→
    98→                        Text("Accuracy")
    99→                            .font(.caption)
   100→                            .foregroundColor(.secondary)
   101→                    }
   102→                }
   103→                .frame(width: 120, height: 120)
   104→
   105→                VStack(alignment: .leading, spacing: 12) {
   106→                    Text("The AI learns from your feedback")
   107→                        .font(.subheadline)
   108→                        .fontWeight(.medium)
   109→
   110→                    VStack(alignment: .leading, spacing: 6) {
   111→                        HStack(spacing: 8) {
   112→                            Image(systemName: "checkmark.circle")
   113→                                .foregroundColor(.green)
   114→                            Text("Accept: Strengthens similar suggestions")
   115→                                .font(.caption)
   116→                                .foregroundColor(.secondary)
   117→                        }
   118→
   119→                        HStack(spacing: 8) {
   120→                            Image(systemName: "xmark.circle")
   121→                                .foregroundColor(.red)
   122→                            Text("Reject: Reduces similar suggestions")
   123→                                .font(.caption)
   124→                                .foregroundColor(.secondary)
   125→                        }
   126→                    }
   127→                }
   128→
   129→                Spacer()
   130→            }
   131→            .padding()
   132→            .background(Color(.systemGray6))
   133→            .cornerRadius(16)
   134→        }
   135→    }
   136→
   137→    // MARK: - Vault Health Section
   138→
   139→    private var vaultHealthSection: some View {
   140→        VStack(alignment: .leading, spacing: 12) {
   141→            Text("Vault Health")
   142→                .font(.headline)
   143→
   144→            if let graph = graph {
   145→                LazyVGrid(columns: [
   146→                    GridItem(.flexible()),
   147→                    GridItem(.flexible())
   148→                ], spacing: 12) {
   149→                    HealthMetric(
   150→                        title: "Total Notes",
   151→                        value: "\(graph.totalNotes)",
   152→                        icon: "doc.text.fill",
   153→                        color: .blue
   154→                    )
   155→
   156→                    HealthMetric(
   157→                        title: "Connections",
   158→                        value: "\(graph.connectionCount)",
   159→                        icon: "link",
   160→                        color: .green
   161→                    )
   162→
   163→                    HealthMetric(
   164→                        title: "Orphans",
   165→                        value: "\(graph.orphanCount)",
   166→                        icon: "questionmark.circle.fill",
   167→                        color: graph.orphanCount > 10 ? .orange : .gray
   168→                    )
   169→
   170→                    HealthMetric(
   171→                        title: "Clusters",
   172→                        value: "\(graph.clusters.count)",
   173→                        icon: "circle.hexagongrid.fill",
   174→                        color: .purple
   175→                    )
   176→
   177→                    HealthMetric(
   178→                        title: "Avg Connections",
   179→                        value: String(format: "%.1f", graph.averageConnectionsPerNote),
   180→                        icon: "chart.bar.fill",
   181→                        color: .blue
   182→                    )
   183→
   184→                    HealthMetric(
   185→                        title: "Last Analysis",
   186→                        value: graph.lastFullAnalysis?.formatted(.relative(presentation: .numeric)) ?? "Never",
   187→                        icon: "clock.fill",
   188→                        color: .gray
   189→                    )
   190→                }
   191→            } else {
   192→                Text("Run an analysis to see vault health metrics")
   193→                    .font(.subheadline)
   194→                    .foregroundColor(.secondary)
   195→                    .frame(maxWidth: .infinity, alignment: .center)
   196→                    .padding()
   197→                    .background(Color(.systemGray6))
   198→                    .cornerRadius(12)
   199→            }
   200→        }
   201→    }
   202→
   203→    // MARK: - Connection Types Breakdown
   204→
   205→    private var connectionTypesSection: some View {
   206→        VStack(alignment: .leading, spacing: 12) {
   207→            Text("Connection Types")
   208→                .font(.headline)
   209→
   210→            if let graph = graph, !graph.edges.isEmpty {
   211→                VStack(spacing: 8) {
   212→                    ForEach(ConnectionType.allCases) { type in
   213→                        let count = graph.connectionsByType[type] ?? 0
   214→                        let percentage = Double(count) / Double(graph.connectionCount)
   215→
   216→                        HStack {
   217→                            Image(systemName: type.icon)
   218→                                .foregroundColor(type.color)
   219→                                .frame(width: 24)
   220→
   221→                            Text(type.displayName)
   222→                                .font(.subheadline)
   223→
   224→                            Spacer()
   225→
   226→                            Text("\(count)")
   227→                                .font(.subheadline)
   228→                                .fontWeight(.medium)
   229→                        }
   230→
   231→                        GeometryReader { geo in
   232→                            ZStack(alignment: .leading) {
   233→                                Rectangle()
   234→                                    .fill(Color(.systemGray5))
   235→
   236→                                Rectangle()
   237→                                    .fill(type.color)
   238→                                    .frame(width: geo.size.width * percentage)
   239→                            }
   240→                        }
   241→                        .frame(height: 6)
   242→                        .cornerRadius(3)
   243→                    }
   244→                }
   245→                .padding()
   246→                .background(Color(.systemGray6))
   247→                .cornerRadius(12)
   248→            } else {
   249→                Text("No connections analyzed yet")
   250→                    .font(.subheadline)
   251→                    .foregroundColor(.secondary)
   252→                    .frame(maxWidth: .infinity, alignment: .center)
   253→                    .padding()
   254→                    .background(Color(.systemGray6))
   255→                    .cornerRadius(12)
   256→            }
   257→        }
   258→    }
   259→
   260→    // MARK: - Weekly Trends
   261→
   262→    private var weeklyTrendsSection: some View {
   263→        VStack(alignment: .leading, spacing: 12) {
   264→            Text("Weekly Trends")
   265→                .font(.headline)
   266→
   267→            if metrics.weeklyStats.isEmpty {
   268→                Text("Accept or reject suggestions to see trends")
   269→                    .font(.subheadline)
   270→                    .foregroundColor(.secondary)
   271→                    .frame(maxWidth: .infinity, alignment: .center)
   272→                    .padding()
   273→                    .background(Color(.systemGray6))
   274→                    .cornerRadius(12)
   275→            } else {
   276→                HStack(alignment: .bottom, spacing: 8) {
   277→                    ForEach(metrics.weeklyStats.suffix(8)) { week in
   278→                        VStack(spacing: 4) {
   279→                            // Stacked bar
   280→                            VStack(spacing: 0) {
   281→                                Rectangle()
   282→                                    .fill(Color.red.opacity(0.7))
   283→                                    .frame(height: CGFloat(week.suggestionsRejected * 4))
   284→
   285→                                Rectangle()
   286→                                    .fill(Color.green)
   287→                                    .frame(height: CGFloat(week.suggestionsAccepted * 4))
   288→                            }
   289→                            .frame(width: 30)
   290→                            .cornerRadius(4)
   291→
   292→                            Text(week.weekStartDate, format: .dateTime.day())
   293→                                .font(.caption2)
   294→                                .foregroundColor(.secondary)
   295→                        }
   296→                    }
   297→
   298→                    Spacer()
   299→
   300→                    // Legend
   301→                    VStack(alignment: .leading, spacing: 4) {
   302→                        HStack(spacing: 4) {
   303→                            Rectangle()
   304→                                .fill(Color.green)
   305→                                .frame(width: 12, height: 12)
   306→                                .cornerRadius(2)
   307→                            Text("Accepted")
   308→                                .font(.caption2)
   309→                        }
   310→                        HStack(spacing: 4) {
   311→                            Rectangle()
   312→                                .fill(Color.red.opacity(0.7))
   313→                                .frame(width: 12, height: 12)
   314→                                .cornerRadius(2)
   315→                            Text("Rejected")
   316→                                .font(.caption2)
   317→                        }
   318→                    }
   319→                }
   320→                .padding()
   321→                .background(Color(.systemGray6))
   322→                .cornerRadius(12)
   323→            }
   324→        }
   325→    }
   326→
   327→    // MARK: - Storage Section
   328→
   329→    private var storageSection: some View {
   330→        VStack(alignment: .leading, spacing: 12) {
   331→            Text("Storage")
   332→                .font(.headline)
   333→
   334→            HStack {
   335→                Image(systemName: "internaldrive.fill")
   336→                    .foregroundColor(.gray)
   337→
   338→                Text("Smart Connections Data")
   339→                    .font(.subheadline)
   340→
   341→                Spacer()
   342→
   343→                Text(ConnectionPersistence().getStorageSizeFormatted())
   344→                    .font(.subheadline)
   345→                    .foregroundColor(.secondary)
   346→            }
   347→            .padding()
   348→            .background(Color(.systemGray6))
   349→            .cornerRadius(12)
   350→        }
   351→    }
   352→}
   353→
   354→// MARK: - Stat Card
   355→
   356→struct StatCard: View {
   357→    let title: String
   358→    let value: String
   359→    let icon: String
   360→    let color: Color
   361→
   362→    var body: some View {
   363→        VStack(spacing: 8) {
   364→            Image(systemName: icon)
   365→                .font(.title2)
   366→                .foregroundColor(color)
   367→
   368→            Text(value)
   369→                .font(.title2)
   370→                .fontWeight(.bold)
   371→
   372→            Text(title)
   373→                .font(.caption)
   374→                .foregroundColor(.secondary)
   375→        }
   376→        .frame(maxWidth: .infinity)
   377→        .padding()
   378→        .background(color.opacity(0.1))
   379→        .cornerRadius(12)
   380→    }
   381→}
   382→
   383→// MARK: - Health Metric
   384→
   385→struct HealthMetric: View {
   386→    let title: String
   387→    let value: String
   388→    let icon: String
   389→    let color: Color
   390→
   391→    var body: some View {
   392→        HStack(spacing: 12) {
   393→            Image(systemName: icon)
   394→                .font(.title3)
   395→                .foregroundColor(color)
   396→                .frame(width: 30)
   397→
   398→            VStack(alignment: .leading, spacing: 2) {
   399→                Text(value)
   400→                    .font(.headline)
   401→                Text(title)
   402→                    .font(.caption)
   403→                    .foregroundColor(.secondary)
   404→            }
   405→
   406→            Spacer()
   407→        }
   408→        .padding()
   409→        .background(Color(.systemGray6))
   410→        .cornerRadius(12)
   411→    }
   412→}
   413→
   414→// MARK: - Preview
   415→
   416→#Preview {
   417→    ProgressDashboardView()
   418→        .environmentObject(SmartConnectionService(
   419→            obsidianService: ObsidianService(),
   420→            lmService: LMGatewayService()
   421→        ))
   422→}
   423→

<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

/// Dashboard showing feedback loop metrics and vault health
struct ProgressDashboardView: View {
    @EnvironmentObject var connectionService: SmartConnectionService

    private var metrics: FeedbackMetrics {
        connectionService.feedbackMetrics
    }

    private var graph: VaultGraph? {
        connectionService.vaultGraph
    }

    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                // Hero stats
                heroStatsSection

                // Model accuracy
                accuracySection

                // Vault health
                vaultHealthSection

                // Connection types breakdown
                connectionTypesSection

                // Weekly trends
                weeklyTrendsSection

                // Storage info
                storageSection
            }
            .padding()
        }
        .refreshable {
            let generator = UIImpactFeedbackGenerator(style: .light)
            generator.impactOccurred()
            connectionService.loadPersistedData()
        }
    }

    // MARK: - Hero Stats

    private var heroStatsSection: some View {
        HStack(spacing: 12) {
            StatCard(
                title: "Accepted",
                value: "\(metrics.suggestionsAccepted)",
                icon: "checkmark.circle.fill",
                color: .green
            )

            StatCard(
                title: "Rejected",
                value: "\(metrics.suggestionsRejected)",
                icon: "xmark.circle.fill",
                color: .red
            )

            StatCard(
                title: "Written",
                value: "\(metrics.connectionsAddedToVault)",
                icon: "doc.badge.plus",
                color: .blue
            )
        }
    }

    // MARK: - Accuracy Section

    private var accuracySection: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("AI Model Accuracy")
                .font(.headline)

            HStack(spacing: 20) {
                // Large accuracy ring
                ZStack {
                    Circle()
                        .stroke(Color.green.opacity(0.2), lineWidth: 12)

                    Circle()
                        .trim(from: 0, to: metrics.modelAccuracy)
                        .stroke(
                            metrics.modelAccuracy > 0.7 ? Color.green : Color.orange,
                            style: StrokeStyle(lineWidth: 12, lineCap: .round)
                        )
                        .rotationEffect(.degrees(-90))

                    VStack(spacing: 4) {
                        Text("\(Int(metrics.modelAccuracy * 100))%")
                            .font(.title)
                            .fontWeight(.bold)

                        Text("Accuracy")
                            .font(.caption)
                            .foregroundColor(.secondary)
                    }
                }
                .frame(width: 120, height: 120)

                VStack(alignment: .leading, spacing: 12) {
                    Text("The AI learns from your feedback")
                        .font(.subheadline)
                        .fontWeight(.medium)

                    VStack(alignment: .leading, spacing: 6) {
                        HStack(spacing: 8) {
                            Image(systemName: "checkmark.circle")
                                .foregroundColor(.green)
                            Text("Accept: Strengthens similar suggestions")
                                .font(.caption)
                                .foregroundColor(.secondary)
                        }

                        HStack(spacing: 8) {
                            Image(systemName: "xmark.circle")
                                .foregroundColor(.red)
                            Text("Reject: Reduces similar suggestions")
                                .font(.caption)
                                .foregroundColor(.secondary)
                        }
                    }
                }

                Spacer()
            }
            .padding()
            .background(Color(.systemGray6))
            .cornerRadius(16)
        }
    }

    // MARK: - Vault Health Section

    private var vaultHealthSection: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Vault Health")
                .font(.headline)

            if let graph = graph {
                LazyVGrid(columns: [
                    GridItem(.flexible()),
                    GridItem(.flexible())
                ], spacing: 12) {
                    HealthMetric(
                        title: "Total Notes",
                        value: "\(graph.totalNotes)",
                        icon: "doc.text.fill",
                        color: .blue
                    )

                    HealthMetric(
                        title: "Connections",
                        value: "\(graph.connectionCount)",
                        icon: "link",
                        color: .green
                    )

                    HealthMetric(
                        title: "Orphans",
                        value: "\(graph.orphanCount)",
                        icon: "questionmark.circle.fill",
                        color: graph.orphanCount > 10 ? .orange : .gray
                    )

                    HealthMetric(
                        title: "Clusters",
                        value: "\(graph.clusters.count)",
                        icon: "circle.hexagongrid.fill",
                        color: .purple
                    )

                    HealthMetric(
                        title: "Avg Connections",
                        value: String(format: "%.1f", graph.averageConnectionsPerNote),
                        icon: "chart.bar.fill",
                        color: .blue
                    )

                    HealthMetric(
                        title: "Last Analysis",
                        value: graph.lastFullAnalysis?.formatted(.relative(presentation: .numeric)) ?? "Never",
                        icon: "clock.fill",
                        color: .gray
                    )
                }
            } else {
                Text("Run an analysis to see vault health metrics")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
                    .frame(maxWidth: .infinity, alignment: .center)
                    .padding()
                    .background(Color(.systemGray6))
                    .cornerRadius(12)
            }
        }
    }

    // MARK: - Connection Types Breakdown

    private var connectionTypesSection: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Connection Types")
                .font(.headline)

            if let graph = graph, !graph.edges.isEmpty {
                VStack(spacing: 8) {
                    ForEach(ConnectionType.allCases) { type in
                        let count = graph.connectionsByType[type] ?? 0
                        let percentage = Double(count) / Double(graph.connectionCount)

                        HStack {
                            Image(systemName: type.icon)
                                .foregroundColor(type.color)
                                .frame(width: 24)

                            Text(type.displayName)
                                .font(.subheadline)

                            Spacer()

                            Text("\(count)")
                                .font(.subheadline)
                                .fontWeight(.medium)
                        }

                        GeometryReader { geo in
                            ZStack(alignment: .leading) {
                                Rectangle()
                                    .fill(Color(.systemGray5))

                                Rectangle()
                                    .fill(type.color)
                                    .frame(width: geo.size.width * percentage)
                            }
                        }
                        .frame(height: 6)
                        .cornerRadius(3)
                    }
                }
                .padding()
                .background(Color(.systemGray6))
                .cornerRadius(12)
            } else {
                Text("No connections analyzed yet")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
                    .frame(maxWidth: .infinity, alignment: .center)
                    .padding()
                    .background(Color(.systemGray6))
                    .cornerRadius(12)
            }
        }
    }

    // MARK: - Weekly Trends

    private var weeklyTrendsSection: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Weekly Trends")
                .font(.headline)

            if metrics.weeklyStats.isEmpty {
                Text("Accept or reject suggestions to see trends")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
                    .frame(maxWidth: .infinity, alignment: .center)
                    .padding()
                    .background(Color(.systemGray6))
                    .cornerRadius(12)
            } else {
                HStack(alignment: .bottom, spacing: 8) {
                    ForEach(metrics.weeklyStats.suffix(8)) { week in
                        VStack(spacing: 4) {
                            // Stacked bar
                            VStack(spacing: 0) {
                                Rectangle()
                                    .fill(Color.red.opacity(0.7))
                                    .frame(height: CGFloat(week.suggestionsRejected * 4))

                                Rectangle()
                                    .fill(Color.green)
                                    .frame(height: CGFloat(week.suggestionsAccepted * 4))
                            }
                            .frame(width: 30)
                            .cornerRadius(4)

                            Text(week.weekStartDate, format: .dateTime.day())
                                .font(.caption2)
                                .foregroundColor(.secondary)
                        }
                    }

                    Spacer()

                    // Legend
                    VStack(alignment: .leading, spacing: 4) {
                        HStack(spacing: 4) {
                            Rectangle()
                                .fill(Color.green)
                                .frame(width: 12, height: 12)
                                .cornerRadius(2)
                            Text("Accepted")
                                .font(.caption2)
                        }
                        HStack(spacing: 4) {
                            Rectangle()
                                .fill(Color.red.opacity(0.7))
                                .frame(width: 12, height: 12)
                                .cornerRadius(2)
                            Text("Rejected")
                                .font(.caption2)
                        }
                    }
                }
                .padding()
                .background(Color(.systemGray6))
                .cornerRadius(12)
            }
        }
    }

    // MARK: - Storage Section

    private var storageSection: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Storage")
                .font(.headline)

            HStack {
                Image(systemName: "internaldrive.fill")
                    .foregroundColor(.gray)

                Text("Smart Connections Data")
                    .font(.subheadline)

                Spacer()

                Text(ConnectionPersistence().getStorageSizeFormatted())
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
            .padding()
            .background(Color(.systemGray6))
            .cornerRadius(12)
        }
    }
}

// MARK: - Stat Card

struct StatCard: View {
    let title: String
    let value: String
    let icon: String
    let color: Color

    var body: some View {
        VStack(spacing: 8) {
            Image(systemName: icon)
                .font(.title2)
                .foregroundColor(color)

            Text(value)
                .font(.title2)
                .fontWeight(.bold)

            Text(title)
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .frame(maxWidth: .infinity)
        .padding()
        .background(color.opacity(0.1))
        .cornerRadius(12)
    }
}

// MARK: - Health Metric

struct HealthMetric: View {
    let title: String
    let value: String
    let icon: String
    let color: Color

    var body: some View {
        HStack(spacing: 12) {
            Image(systemName: icon)
                .font(.title3)
                .foregroundColor(color)
                .frame(width: 30)

            VStack(alignment: .leading, spacing: 2) {
                Text(value)
                    .font(.headline)
                Text(title)
                    .font(.caption)
                    .foregroundColor(.secondary)
            }

            Spacer()
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(12)
    }
}

// MARK: - Preview

#Preview {
    ProgressDashboardView()
        .environmentObject(SmartConnectionService(
            obsidianService: ObsidianService(),
            lmService: LMGatewayService()
        ))
}


     1→import SwiftUI
     2→
     3→// MARK: - Live Analysis View
     4→
     5→/// Real-time dashboard showing analysis progress with detailed metrics
     6→struct LiveAnalysisView: View {
     7→    let progress: AnalysisProgress
     8→    var onCancel: (() -> Void)? = nil
     9→
    10→    @State private var animationPhase = false
    11→
    12→    var body: some View {
    13→        VStack(spacing: 16) {
    14→            // Header with current phase
    15→            phaseHeader
    16→
    17→            // Progress bar with percentage
    18→            progressSection
    19→
    20→            // Metrics grid
    21→            metricsGrid
    22→
    23→            // Phase timeline
    24→            PhaseTimelineView(progress: progress)
    25→
    26→            // Current batch info
    27→            if !progress.currentBatchNotes.isEmpty {
    28→                currentBatchSection
    29→            }
    30→
    31→            // Errors (if any)
    32→            if progress.hasErrors {
    33→                errorsSection
    34→            }
    35→
    36→            // Cancel button
    37→            if let cancel = onCancel, progress.isActive {
    38→                Button(action: cancel) {
    39→                    Text("Cancel Analysis")
    40→                        .font(.subheadline)
    41→                        .fontWeight(.medium)
    42→                        .foregroundColor(.red)
    43→                        .padding(.horizontal, 20)
    44→                        .padding(.vertical, 10)
    45→                        .background(Color.red.opacity(0.1))
    46→                        .cornerRadius(8)
    47→                }
    48→            }
    49→        }
    50→        .padding()
    51→        .background(Color(.systemBackground))
    52→        .cornerRadius(16)
    53→        .shadow(color: .black.opacity(0.1), radius: 8, y: 4)
    54→        .onAppear {
    55→            withAnimation(.easeInOut(duration: 1.0).repeatForever(autoreverses: true)) {
    56→                animationPhase = true
    57→            }
    58→        }
    59→    }
    60→
    61→    // MARK: - Phase Header
    62→
    63→    private var phaseHeader: some View {
    64→        HStack(spacing: 12) {
    65→            // Animated phase icon
    66→            ZStack {
    67→                Circle()
    68→                    .fill(progress.currentPhase.color.opacity(0.2))
    69→                    .frame(width: 48, height: 48)
    70→
    71→                Image(systemName: progress.currentPhase.icon)
    72→                    .font(.title2)
    73→                    .foregroundColor(progress.currentPhase.color)
    74→                    .scaleEffect(animationPhase ? 1.1 : 0.9)
    75→            }
    76→
    77→            VStack(alignment: .leading, spacing: 4) {
    78→                Text(progress.currentPhase.displayName)
    79→                    .font(.headline)
    80→                    .foregroundColor(.primary)
    81→
    82→                if let message = progress.statusMessage {
    83→                    Text(message)
    84→                        .font(.caption)
    85→                        .foregroundColor(.secondary)
    86→                        .lineLimit(1)
    87→                }
    88→            }
    89→
    90→            Spacer()
    91→
    92→            // Elapsed time
    93→            VStack(alignment: .trailing, spacing: 2) {
    94→                Text(progress.elapsedTimeString)
    95→                    .font(.title3)
    96→                    .fontWeight(.semibold)
    97→                    .foregroundColor(.purple)
    98→
    99→                Text("elapsed")
   100→                    .font(.caption2)
   101→                    .foregroundColor(.secondary)
   102→            }
   103→        }
   104→    }
   105→
   106→    // MARK: - Progress Section
   107→
   108→    private var progressSection: some View {
   109→        VStack(spacing: 8) {
   110→            // Progress bar
   111→            GeometryReader { geo in
   112→                ZStack(alignment: .leading) {
   113→                    // Background
   114→                    RoundedRectangle(cornerRadius: 6)
   115→                        .fill(Color(.systemGray5))
   116→
   117→                    // Progress fill with gradient
   118→                    RoundedRectangle(cornerRadius: 6)
   119→                        .fill(
   120→                            LinearGradient(
   121→                                colors: [.purple, .blue],
   122→                                startPoint: .leading,
   123→                                endPoint: .trailing
   124→                            )
   125→                        )
   126→                        .frame(width: geo.size.width * progress.percentComplete)
   127→                        .animation(.easeInOut(duration: 0.3), value: progress.percentComplete)
   128→                }
   129→            }
   130→            .frame(height: 12)
   131→
   132→            // Progress details
   133→            HStack {
   134→                Text("\(progress.processedNotes) / \(progress.totalNotes) notes")
   135→                    .font(.caption)
   136→                    .foregroundColor(.secondary)
   137→
   138→                Spacer()
   139→
   140→                Text("\(Int(progress.percentComplete * 100))%")
   141→                    .font(.caption)
   142→                    .fontWeight(.semibold)
   143→                    .foregroundColor(.purple)
   144→
   145→                if let eta = progress.etaString {
   146→                    Text("ETA: \(eta)")
   147→                        .font(.caption)
   148→                        .foregroundColor(.secondary)
   149→                        .padding(.leading, 8)
   150→                }
   151→            }
   152→        }
   153→    }
   154→
   155→    // MARK: - Metrics Grid
   156→
   157→    private var metricsGrid: some View {
   158→        LazyVGrid(columns: [
   159→            GridItem(.flexible()),
   160→            GridItem(.flexible()),
   161→            GridItem(.flexible()),
   162→            GridItem(.flexible())
   163→        ], spacing: 12) {
   164→            MetricBadge(
   165→                value: String(format: "%.1f", progress.notesPerSecond),
   166→                label: "notes/sec",
   167→                icon: "speedometer",
   168→                color: .blue
   169→            )
   170→
   171→            MetricBadge(
   172→                value: "\(progress.totalConnectionsFound)",
   173→                label: "connections",
   174→                icon: "link",
   175→                color: .green
   176→            )
   177→
   178→            MetricBadge(
   179→                value: "\(progress.currentBatch)/\(progress.totalBatches)",
   180→                label: "batches",
   181→                icon: "square.stack.3d.up",
   182→                color: .orange
   183→            )
   184→
   185→            MetricBadge(
   186→                value: "\(progress.errors.count)",
   187→                label: "errors",
   188→                icon: progress.errors.isEmpty ? "checkmark.circle" : "exclamationmark.triangle",
   189→                color: progress.errors.isEmpty ? .gray : .red
   190→            )
   191→        }
   192→    }
   193→
   194→    // MARK: - Current Batch Section
   195→
   196→    private var currentBatchSection: some View {
   197→        VStack(alignment: .leading, spacing: 8) {
   198→            HStack {
   199→                Image(systemName: "doc.on.doc")
   200→                    .font(.caption)
   201→                    .foregroundColor(.secondary)
   202→
   203→                Text("Processing Batch \(progress.currentBatch)")
   204→                    .font(.caption)
   205→                    .fontWeight(.medium)
   206→                    .foregroundColor(.secondary)
   207→
   208→                Spacer()
   209→
   210→                Text("\(progress.connectionsFoundThisBatch) found")
   211→                    .font(.caption)
   212→                    .foregroundColor(.green)
   213→            }
   214→
   215→            // Show first few notes being processed
   216→            ScrollView(.horizontal, showsIndicators: false) {
   217→                HStack(spacing: 6) {
   218→                    ForEach(Array(progress.currentBatchNotes.prefix(5).enumerated()), id: \.offset) { _, note in
   219→                        Text(URL(fileURLWithPath: note).deletingPathExtension().lastPathComponent)
   220→                            .font(.caption2)
   221→                            .foregroundColor(.white)
   222→                            .padding(.horizontal, 8)
   223→                            .padding(.vertical, 4)
   224→                            .background(Color.purple.opacity(0.7))
   225→                            .cornerRadius(4)
   226→                            .lineLimit(1)
   227→                    }
   228→
   229→                    if progress.currentBatchNotes.count > 5 {
   230→                        Text("+\(progress.currentBatchNotes.count - 5) more")
   231→                            .font(.caption2)
   232→                            .foregroundColor(.secondary)
   233→                    }
   234→                }
   235→            }
   236→        }
   237→        .padding()
   238→        .background(Color(.systemGray6))
   239→        .cornerRadius(8)
   240→    }
   241→
   242→    // MARK: - Errors Section
   243→
   244→    private var errorsSection: some View {
   245→        VStack(alignment: .leading, spacing: 8) {
   246→            HStack {
   247→                Image(systemName: "exclamationmark.triangle.fill")
   248→                    .foregroundColor(.orange)
   249→
   250→                Text("Errors (\(progress.errors.count))")
   251→                    .font(.subheadline)
   252→                    .fontWeight(.medium)
   253→            }
   254→
   255→            ScrollView(.horizontal, showsIndicators: false) {
   256→                HStack(spacing: 8) {
   257→                    ForEach(progress.errors.prefix(3)) { error in
   258→                        VStack(alignment: .leading, spacing: 2) {
   259→                            Text(URL(fileURLWithPath: error.notePath).lastPathComponent)
   260→                                .font(.caption)
   261→                                .fontWeight(.medium)
   262→                                .lineLimit(1)
   263→
   264→                            Text(error.error)
   265→                                .font(.caption2)
   266→                                .foregroundColor(.secondary)
   267→                                .lineLimit(2)
   268→                        }
   269→                        .padding(8)
   270→                        .background(Color.orange.opacity(0.1))
   271→                        .cornerRadius(6)
   272→                    }
   273→                }
   274→            }
   275→        }
   276→        .padding()
   277→        .background(Color(.systemGray6))
   278→        .cornerRadius(8)
   279→    }
   280→}
   281→
   282→// MARK: - Metric Badge
   283→
   284→struct MetricBadge: View {
   285→    let value: String
   286→    let label: String
   287→    let icon: String
   288→    let color: Color
   289→
   290→    var body: some View {
   291→        VStack(spacing: 4) {
   292→            Image(systemName: icon)
   293→                .font(.caption)
   294→                .foregroundColor(color)
   295→
   296→            Text(value)
   297→                .font(.subheadline)
   298→                .fontWeight(.bold)
   299→                .foregroundColor(.primary)
   300→
   301→            Text(label)
   302→                .font(.caption2)
   303→                .foregroundColor(.secondary)
   304→                .lineLimit(1)
   305→        }
   306→        .frame(maxWidth: .infinity)
   307→        .padding(.vertical, 10)
   308→        .background(color.opacity(0.1))
   309→        .cornerRadius(8)
   310→    }
   311→}
   312→
   313→// MARK: - Preview
   314→
   315→#Preview("Live Analysis") {
   316→    VStack {
   317→        LiveAnalysisView(
   318→            progress: AnalysisProgress(
   319→                totalNotes: 150,
   320→                processedNotes: 67,
   321→                currentPhase: .analyzingContent,
   322→                startedAt: Date().addingTimeInterval(-45),
   323→                currentBatch: 4,
   324→                totalBatches: 8,
   325→                phaseStartTimes: [
   326→                    AnalysisPhase.loadingFiles.rawValue: Date().addingTimeInterval(-45),
   327→                    AnalysisPhase.parsingWikiLinks.rawValue: Date().addingTimeInterval(-35),
   328→                    AnalysisPhase.analyzingContent.rawValue: Date().addingTimeInterval(-20)
   329→                ],
   330→                phaseEndTimes: [
   331→                    AnalysisPhase.loadingFiles.rawValue: Date().addingTimeInterval(-35),
   332→                    AnalysisPhase.parsingWikiLinks.rawValue: Date().addingTimeInterval(-20)
   333→                ],
   334→                completedPhases: [.loadingFiles, .parsingWikiLinks],
   335→                notesPerSecond: 2.3,
   336→                batchesPerMinute: 4.5,
   337→                currentBatchNotes: ["Notes/Projects/App Design.md", "Notes/Daily/2024-01-15.md", "Notes/Ideas/Feature X.md"],
   338→                connectionsFoundThisBatch: 12,
   339→                totalConnectionsFound: 89,
   340→                statusMessage: "Processing batch 4..."
   341→            ),
   342→            onCancel: { print("Cancelled") }
   343→        )
   344→        .padding()
   345→
   346→        Spacer()
   347→    }
   348→    .background(Color(.systemGroupedBackground))
   349→}
   350→

<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

// MARK: - Live Analysis View

/// Real-time dashboard showing analysis progress with detailed metrics
struct LiveAnalysisView: View {
    let progress: AnalysisProgress
    var onCancel: (() -> Void)? = nil

    @State private var animationPhase = false

    var body: some View {
        VStack(spacing: 16) {
            // Header with current phase
            phaseHeader

            // Progress bar with percentage
            progressSection

            // Metrics grid
            metricsGrid

            // Phase timeline
            PhaseTimelineView(progress: progress)

            // Current batch info
            if !progress.currentBatchNotes.isEmpty {
                currentBatchSection
            }

            // Errors (if any)
            if progress.hasErrors {
                errorsSection
            }

            // Cancel button
            if let cancel = onCancel, progress.isActive {
                Button(action: cancel) {
                    Text("Cancel Analysis")
                        .font(.subheadline)
                        .fontWeight(.medium)
                        .foregroundColor(.red)
                        .padding(.horizontal, 20)
                        .padding(.vertical, 10)
                        .background(Color.red.opacity(0.1))
                        .cornerRadius(8)
                }
            }
        }
        .padding()
        .background(Color(.systemBackground))
        .cornerRadius(16)
        .shadow(color: .black.opacity(0.1), radius: 8, y: 4)
        .onAppear {
            withAnimation(.easeInOut(duration: 1.0).repeatForever(autoreverses: true)) {
                animationPhase = true
            }
        }
    }

    // MARK: - Phase Header

    private var phaseHeader: some View {
        HStack(spacing: 12) {
            // Animated phase icon
            ZStack {
                Circle()
                    .fill(progress.currentPhase.color.opacity(0.2))
                    .frame(width: 48, height: 48)

                Image(systemName: progress.currentPhase.icon)
                    .font(.title2)
                    .foregroundColor(progress.currentPhase.color)
                    .scaleEffect(animationPhase ? 1.1 : 0.9)
            }

            VStack(alignment: .leading, spacing: 4) {
                Text(progress.currentPhase.displayName)
                    .font(.headline)
                    .foregroundColor(.primary)

                if let message = progress.statusMessage {
                    Text(message)
                        .font(.caption)
                        .foregroundColor(.secondary)
                        .lineLimit(1)
                }
            }

            Spacer()

            // Elapsed time
            VStack(alignment: .trailing, spacing: 2) {
                Text(progress.elapsedTimeString)
                    .font(.title3)
                    .fontWeight(.semibold)
                    .foregroundColor(.purple)

                Text("elapsed")
                    .font(.caption2)
                    .foregroundColor(.secondary)
            }
        }
    }

    // MARK: - Progress Section

    private var progressSection: some View {
        VStack(spacing: 8) {
            // Progress bar
            GeometryReader { geo in
                ZStack(alignment: .leading) {
                    // Background
                    RoundedRectangle(cornerRadius: 6)
                        .fill(Color(.systemGray5))

                    // Progress fill with gradient
                    RoundedRectangle(cornerRadius: 6)
                        .fill(
                            LinearGradient(
                                colors: [.purple, .blue],
                                startPoint: .leading,
                                endPoint: .trailing
                            )
                        )
                        .frame(width: geo.size.width * progress.percentComplete)
                        .animation(.easeInOut(duration: 0.3), value: progress.percentComplete)
                }
            }
            .frame(height: 12)

            // Progress details
            HStack {
                Text("\(progress.processedNotes) / \(progress.totalNotes) notes")
                    .font(.caption)
                    .foregroundColor(.secondary)

                Spacer()

                Text("\(Int(progress.percentComplete * 100))%")
                    .font(.caption)
                    .fontWeight(.semibold)
                    .foregroundColor(.purple)

                if let eta = progress.etaString {
                    Text("ETA: \(eta)")
                        .font(.caption)
                        .foregroundColor(.secondary)
                        .padding(.leading, 8)
                }
            }
        }
    }

    // MARK: - Metrics Grid

    private var metricsGrid: some View {
        LazyVGrid(columns: [
            GridItem(.flexible()),
            GridItem(.flexible()),
            GridItem(.flexible()),
            GridItem(.flexible())
        ], spacing: 12) {
            MetricBadge(
                value: String(format: "%.1f", progress.notesPerSecond),
                label: "notes/sec",
                icon: "speedometer",
                color: .blue
            )

            MetricBadge(
                value: "\(progress.totalConnectionsFound)",
                label: "connections",
                icon: "link",
                color: .green
            )

            MetricBadge(
                value: "\(progress.currentBatch)/\(progress.totalBatches)",
                label: "batches",
                icon: "square.stack.3d.up",
                color: .orange
            )

            MetricBadge(
                value: "\(progress.errors.count)",
                label: "errors",
                icon: progress.errors.isEmpty ? "checkmark.circle" : "exclamationmark.triangle",
                color: progress.errors.isEmpty ? .gray : .red
            )
        }
    }

    // MARK: - Current Batch Section

    private var currentBatchSection: some View {
        VStack(alignment: .leading, spacing: 8) {
            HStack {
                Image(systemName: "doc.on.doc")
                    .font(.caption)
                    .foregroundColor(.secondary)

                Text("Processing Batch \(progress.currentBatch)")
                    .font(.caption)
                    .fontWeight(.medium)
                    .foregroundColor(.secondary)

                Spacer()

                Text("\(progress.connectionsFoundThisBatch) found")
                    .font(.caption)
                    .foregroundColor(.green)
            }

            // Show first few notes being processed
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 6) {
                    ForEach(Array(progress.currentBatchNotes.prefix(5).enumerated()), id: \.offset) { _, note in
                        Text(URL(fileURLWithPath: note).deletingPathExtension().lastPathComponent)
                            .font(.caption2)
                            .foregroundColor(.white)
                            .padding(.horizontal, 8)
                            .padding(.vertical, 4)
                            .background(Color.purple.opacity(0.7))
                            .cornerRadius(4)
                            .lineLimit(1)
                    }

                    if progress.currentBatchNotes.count > 5 {
                        Text("+\(progress.currentBatchNotes.count - 5) more")
                            .font(.caption2)
                            .foregroundColor(.secondary)
                    }
                }
            }
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(8)
    }

    // MARK: - Errors Section

    private var errorsSection: some View {
        VStack(alignment: .leading, spacing: 8) {
            HStack {
                Image(systemName: "exclamationmark.triangle.fill")
                    .foregroundColor(.orange)

                Text("Errors (\(progress.errors.count))")
                    .font(.subheadline)
                    .fontWeight(.medium)
            }

            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 8) {
                    ForEach(progress.errors.prefix(3)) { error in
                        VStack(alignment: .leading, spacing: 2) {
                            Text(URL(fileURLWithPath: error.notePath).lastPathComponent)
                                .font(.caption)
                                .fontWeight(.medium)
                                .lineLimit(1)

                            Text(error.error)
                                .font(.caption2)
                                .foregroundColor(.secondary)
                                .lineLimit(2)
                        }
                        .padding(8)
                        .background(Color.orange.opacity(0.1))
                        .cornerRadius(6)
                    }
                }
            }
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(8)
    }
}

// MARK: - Metric Badge

struct MetricBadge: View {
    let value: String
    let label: String
    let icon: String
    let color: Color

    var body: some View {
        VStack(spacing: 4) {
            Image(systemName: icon)
                .font(.caption)
                .foregroundColor(color)

            Text(value)
                .font(.subheadline)
                .fontWeight(.bold)
                .foregroundColor(.primary)

            Text(label)
                .font(.caption2)
                .foregroundColor(.secondary)
                .lineLimit(1)
        }
        .frame(maxWidth: .infinity)
        .padding(.vertical, 10)
        .background(color.opacity(0.1))
        .cornerRadius(8)
    }
}

// MARK: - Preview

#Preview("Live Analysis") {
    VStack {
        LiveAnalysisView(
            progress: AnalysisProgress(
                totalNotes: 150,
                processedNotes: 67,
                currentPhase: .analyzingContent,
                startedAt: Date().addingTimeInterval(-45),
                currentBatch: 4,
                totalBatches: 8,
                phaseStartTimes: [
                    AnalysisPhase.loadingFiles.rawValue: Date().addingTimeInterval(-45),
                    AnalysisPhase.parsingWikiLinks.rawValue: Date().addingTimeInterval(-35),
                    AnalysisPhase.analyzingContent.rawValue: Date().addingTimeInterval(-20)
                ],
                phaseEndTimes: [
                    AnalysisPhase.loadingFiles.rawValue: Date().addingTimeInterval(-35),
                    AnalysisPhase.parsingWikiLinks.rawValue: Date().addingTimeInterval(-20)
                ],
                completedPhases: [.loadingFiles, .parsingWikiLinks],
                notesPerSecond: 2.3,
                batchesPerMinute: 4.5,
                currentBatchNotes: ["Notes/Projects/App Design.md", "Notes/Daily/2024-01-15.md", "Notes/Ideas/Feature X.md"],
                connectionsFoundThisBatch: 12,
                totalConnectionsFound: 89,
                statusMessage: "Processing batch 4..."
            ),
            onCancel: { print("Cancelled") }
        )
        .padding()

        Spacer()
    }
    .background(Color(.systemGroupedBackground))
}


Now let me check the other related components:


find /Users/neog/Apps/iOS/ObsidianViewer-iOS/ObsidianViewer-iOS/Views/SmartConnections -name "*.swift" | sort