Skip to content

PR 13: Feedback UI - Provider Badge Component #14

Description

@gilmanb1

Overview

Add a ProviderBadge component to the Feedback view to show users which AI provider generated the categorization result. This provides transparency about the AI system.

Dependencies

Note: Can be developed in parallel with most other PRs.

Files to Modify

File Action Description
Sources/SortAI/App/FeedbackView.swift Modify Add ProviderBadge
Sources/SortAI/App/Components/ProviderBadge.swift Create Badge component

Implementation Details

1. ProviderBadge Component

import SwiftUI

/// A badge showing which AI provider generated a result
struct ProviderBadge: View {
    let provider: String
    var showLabel: Bool = true
    var size: BadgeSize = .medium
    
    enum BadgeSize {
        case small, medium, large
        
        var fontSize: Font {
            switch self {
            case .small: return .caption2
            case .medium: return .caption
            case .large: return .footnote
            }
        }
        
        var iconSize: CGFloat {
            switch self {
            case .small: return 10
            case .medium: return 12
            case .large: return 14
            }
        }
        
        var padding: EdgeInsets {
            switch self {
            case .small: return EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4)
            case .medium: return EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8)
            case .large: return EdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10)
            }
        }
    }
    
    var body: some View {
        HStack(spacing: 4) {
            Image(systemName: iconName)
                .font(.system(size: size.iconSize))
            
            if showLabel {
                Text(displayName)
            }
        }
        .font(size.fontSize)
        .padding(size.padding)
        .background(backgroundColor.opacity(0.15))
        .foregroundColor(foregroundColor)
        .cornerRadius(size == .small ? 4 : 8)
        .overlay(
            RoundedRectangle(cornerRadius: size == .small ? 4 : 8)
                .stroke(backgroundColor.opacity(0.3), lineWidth: 1)
        )
        .help(tooltip)
    }
    
    // MARK: - Provider Styling
    
    private var displayName: String {
        switch provider.lowercased() {
        case "apple-intelligence": return "Apple Intelligence"
        case "ollama": return "Ollama"
        case "openai": return "OpenAI"
        case "anthropic": return "Anthropic"
        case "local-ml": return "Local ML"
        default: return provider.capitalized
        }
    }
    
    private var iconName: String {
        switch provider.lowercased() {
        case "apple-intelligence": return "apple.logo"
        case "ollama": return "server.rack"
        case "openai": return "cloud.fill"
        case "anthropic": return "cloud.fill"
        case "local-ml": return "cpu"
        default: return "questionmark.circle"
        }
    }
    
    private var backgroundColor: Color {
        switch provider.lowercased() {
        case "apple-intelligence": return .blue
        case "ollama": return .green
        case "openai": return .teal
        case "anthropic": return .orange
        case "local-ml": return .purple
        default: return .gray
        }
    }
    
    private var foregroundColor: Color {
        backgroundColor
    }
    
    private var tooltip: String {
        switch provider.lowercased() {
        case "apple-intelligence":
            return "Categorized using Apple Intelligence (on-device)"
        case "ollama":
            return "Categorized using Ollama (local server)"
        case "openai":
            return "Categorized using OpenAI API (cloud)"
        case "anthropic":
            return "Categorized using Anthropic API (cloud)"
        case "local-ml":
            return "Categorized using local machine learning heuristics"
        default:
            return "Categorized using \(displayName)"
        }
    }
}

// MARK: - Preview

#Preview {
    VStack(spacing: 20) {
        HStack(spacing: 16) {
            ProviderBadge(provider: "apple-intelligence")
            ProviderBadge(provider: "ollama")
            ProviderBadge(provider: "openai")
        }
        
        HStack(spacing: 16) {
            ProviderBadge(provider: "anthropic")
            ProviderBadge(provider: "local-ml")
        }
        
        // Size variations
        HStack(spacing: 16) {
            ProviderBadge(provider: "apple-intelligence", size: .small)
            ProviderBadge(provider: "apple-intelligence", size: .medium)
            ProviderBadge(provider: "apple-intelligence", size: .large)
        }
        
        // Icon only
        HStack(spacing: 16) {
            ProviderBadge(provider: "apple-intelligence", showLabel: false, size: .small)
            ProviderBadge(provider: "ollama", showLabel: false, size: .small)
        }
    }
    .padding()
}

2. Integration in FeedbackView

// In FeedbackView.swift

struct FeedbackView: View {
    let result: BrainResult
    // ... existing properties
    
    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            // Header with provider badge
            HStack {
                Text(result.suggestedCategory)
                    .font(.headline)
                
                Spacer()
                
                // Confidence and provider
                HStack(spacing: 12) {
                    ConfidenceBadge(confidence: result.confidence)
                    
                    if let provider = result.provider {
                        ProviderBadge(provider: provider, size: .small)
                    }
                }
            }
            
            // ... rest of the view
        }
    }
}

// Alternative: Provider info row
struct CategorizationInfoRow: View {
    let result: BrainResult
    
    var body: some View {
        HStack {
            Label {
                Text("Categorized by")
            } icon: {
                Image(systemName: "sparkles")
            }
            .foregroundColor(.secondary)
            
            Spacer()
            
            if let provider = result.provider {
                ProviderBadge(provider: provider)
            } else {
                Text("Unknown")
                    .foregroundColor(.secondary)
            }
        }
        .font(.caption)
    }
}

3. Usage in ProcessingItem List

// In ContentView.swift or ProcessingItemRow.swift

struct ProcessingItemRow: View {
    let item: ProcessingItem
    
    var body: some View {
        HStack {
            // File icon and name
            FileIconView(item: item)
            
            VStack(alignment: .leading) {
                Text(item.filename)
                    .font(.body)
                
                if let category = item.suggestedCategory {
                    Text(category)
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
            }
            
            Spacer()
            
            // Status badges
            HStack(spacing: 8) {
                if let confidence = item.confidence {
                    ConfidenceBadge(confidence: confidence, compact: true)
                }
                
                if let provider = item.provider {
                    ProviderBadge(provider: provider, showLabel: false, size: .small)
                }
            }
        }
    }
}

Visual Design

┌────────────────────────────────────────────────────────────────┐
│  Processing Results                                             │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  📄 quarterly_report.pdf                                        │
│     Documents / Financial / Reports                            │
│     Confidence: 0.92 ██████████░  │ 🍎 Apple Intel │          │
│                                                                │
│  📄 meeting_notes.docx                                          │
│     Documents / Work / Meetings                                │
│     Confidence: 0.78 ████████░░░  │ 🖥 Ollama │                │
│                                                                │
│  📄 random_file.dat                                             │
│     Documents / Misc                                           │
│     Confidence: 0.45 █████░░░░░░  │ 🟣 Local ML │              │
│                                                                │
└────────────────────────────────────────────────────────────────┘

Badge Styles:
┌──────────────────┐  ┌─────────┐  ┌───────────┐  ┌──────────┐
│ 🍎 Apple Intel │  │ 🖥 Ollama│  │ ☁ OpenAI│  │ 🟣 Local │
└──────────────────┘  └─────────┘  └───────────┘  └──────────┘
     (blue)            (green)       (teal)         (purple)

Acceptance Criteria

  • Badge displays correct icon for each provider
  • Badge displays correct color for each provider
  • Tooltip explains what the provider is
  • Multiple size options work correctly
  • Icon-only mode works (showLabel: false)
  • Badge integrates well with FeedbackView
  • Badge shows in processing item rows
  • Accessibility: VoiceOver reads provider name

Testing

func testProviderBadgeDisplay() {
    let providers = ["apple-intelligence", "ollama", "openai", "anthropic", "local-ml"]
    
    for provider in providers {
        let badge = ProviderBadge(provider: provider)
        
        // Each provider should have a valid display name and icon
        XCTAssertFalse(badge.displayName.isEmpty)
        XCTAssertFalse(badge.iconName.isEmpty)
    }
}

func testProviderBadgeSizes() {
    let sizes: [ProviderBadge.BadgeSize] = [.small, .medium, .large]
    
    for size in sizes {
        let badge = ProviderBadge(provider: "apple-intelligence", size: size)
        // Badge should render without errors
        XCTAssertNotNil(badge)
    }
}

func testUnknownProvider() {
    let badge = ProviderBadge(provider: "unknown-provider")
    
    XCTAssertEqual(badge.displayName, "Unknown-Provider")
    XCTAssertEqual(badge.iconName, "questionmark.circle")
}

Estimated Size

~50 lines of code

Risk Assessment

Low - Simple SwiftUI component with no external dependencies.

Activity

  1. added
    enhancementNew feature or request
    uiUser interface changes
    phase-2Phase 2 - Parallel development
    on Jan 13, 2026
  2. gilmanb1 commented on Jan 13, 2026

    @gilmanb1
    OwnerAuthor

    Dependencies: Blocked by #2

  3. gilmanb1 commented on Jan 13, 2026

    @gilmanb1
    OwnerAuthor

    ✅ Implementation Complete

    This feature was already implemented as part of the LLM_Research.md implementation. The code exists in the codebase and all 233 tests pass.

    A minor build fix was applied in commit 4341283 to fix the Apple Intelligence availability check API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestphase-2Phase 2 - Parallel developmentuiUser interface changes

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions