// // HistoryView.swift // oAI // // Command history viewer with search // import os import SwiftUI struct HistoryView: View { @Environment(\.dismiss) var dismiss @State private var searchText = "" @State private var historyEntries: [HistoryEntry] = [] @State private var selectedIndex: Int = 0 @FocusState private var isListFocused: Bool var onSelect: ((String) -> Void)? private var filteredHistory: [HistoryEntry] { if searchText.isEmpty { return historyEntries } return historyEntries.filter { $0.input.lowercased().contains(searchText.lowercased()) || $0.formattedDate.contains(searchText) } } var body: some View { VStack(spacing: 0) { // Header HStack { Text("Command History") .font(.system(size: 18, weight: .bold)) Spacer() Button { dismiss() } label: { Image(systemName: "xmark.circle.fill") .font(.title2) .foregroundStyle(.secondary) } .buttonStyle(.plain) .keyboardShortcut(.escape, modifiers: []) } .padding(.horizontal, 24) .padding(.top, 20) .padding(.bottom, 12) // Search bar HStack(spacing: 8) { Image(systemName: "magnifyingglass") .foregroundStyle(.secondary) TextField("Search by text or date (dd.mm.yyyy)...", text: $searchText) .textFieldStyle(.plain) if !searchText.isEmpty { Button { searchText = "" } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(.tertiary) } .buttonStyle(.plain) } } .padding(10) .background(.quaternary.opacity(0.5), in: RoundedRectangle(cornerRadius: 8)) .padding(.horizontal, 24) .padding(.bottom, 12) Divider() // Content if filteredHistory.isEmpty { Spacer() VStack(spacing: 8) { Image(systemName: searchText.isEmpty ? "list.bullet" : "magnifyingglass") .font(.largeTitle) .foregroundStyle(.tertiary) Text(searchText.isEmpty ? "No Command History" : "No Matches") .font(.headline) .foregroundStyle(.secondary) Text(searchText.isEmpty ? "Your command history will appear here" : "Try a different search term or date") .font(.caption) .foregroundStyle(.tertiary) } Spacer() } else { ScrollViewReader { proxy in List { ForEach(Array(filteredHistory.enumerated()), id: \.element.id) { index, entry in HistoryRow( entry: entry, isSelected: index == selectedIndex ) .contentShape(Rectangle()) .onTapGesture { selectedIndex = index onSelect?(entry.input) dismiss() } .id(entry.id) } } .listStyle(.plain) .focused($isListFocused) .onChange(of: selectedIndex) { withAnimation { proxy.scrollTo(filteredHistory[selectedIndex].id, anchor: .center) } } .onChange(of: searchText) { selectedIndex = 0 } #if os(macOS) .onKeyPress(.upArrow) { if selectedIndex > 0 { selectedIndex -= 1 } return .handled } .onKeyPress(.downArrow) { if selectedIndex < filteredHistory.count - 1 { selectedIndex += 1 } return .handled } .onKeyPress(.return) { if !filteredHistory.isEmpty && selectedIndex < filteredHistory.count { onSelect?(filteredHistory[selectedIndex].input) dismiss() } return .handled } #endif } } } .frame(minWidth: 600, minHeight: 400) .background(Color.oaiBackground) .task { loadHistory() isListFocused = true } } private func loadHistory() { do { let records = try DatabaseService.shared.loadCommandHistory() historyEntries = records.map { HistoryEntry(input: $0.input, timestamp: $0.timestamp) } } catch { Log.db.error("Failed to load command history: \(error.localizedDescription)") } } } struct HistoryRow: View { let entry: HistoryEntry let isSelected: Bool private let settings = SettingsService.shared var body: some View { VStack(alignment: .leading, spacing: 6) { // Input text Text(entry.input) .font(.system(size: settings.dialogTextSize)) .foregroundStyle(.primary) .lineLimit(3) // Timestamp HStack(spacing: 4) { Image(systemName: "clock") .font(.caption2) .foregroundStyle(.secondary) Text(entry.formattedDate) .font(.system(size: 11)) .foregroundStyle(.secondary) } } .padding(.vertical, 8) .padding(.horizontal, 4) .listRowBackground(isSelected ? Color.oaiAccent.opacity(0.2) : Color.clear) } } #Preview { HistoryView { input in print("Selected: \(input)") } } #Preview("History Row") { HistoryRow( entry: HistoryEntry( input: "Tell me about Swift concurrency", timestamp: Date() ), isSelected: true ) }