| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 |
- import SwiftUI
- struct AIHistoryView: View {
- @Bindable var viewModel: AIHistoryViewModel
- var onOpen: (AIHistoryEntry) -> Void
- private enum Layout {
- static let horizontalPadding: CGFloat = 24
- }
- var body: some View {
- VStack(spacing: 0) {
- header
- filterBar
- if viewModel.isEmpty {
- emptyState
- } else if !viewModel.hasFilteredEntries {
- filteredEmptyState
- } else {
- historyList
- }
- }
- .background(AppTheme.background)
- .alert("Delete this item?", isPresented: deleteBinding) {
- Button("Cancel", role: .cancel) {
- viewModel.cancelDelete()
- }
- Button("Delete", role: .destructive) {
- viewModel.confirmDelete()
- }
- } message: {
- if let entry = viewModel.entryPendingDeletion {
- Text("Remove \"\(entry.displayTitle)\" from history?")
- }
- }
- .alert("Delete all history?", isPresented: $viewModel.showDeleteAllConfirmation) {
- Button("Cancel", role: .cancel) {
- viewModel.cancelDeleteAll()
- }
- Button("Delete All", role: .destructive) {
- viewModel.confirmDeleteAll()
- }
- } message: {
- Text("This permanently removes all saved posts, titles, and comments.")
- }
- }
- private var deleteBinding: Binding<Bool> {
- Binding(
- get: { viewModel.entryPendingDeletion != nil },
- set: { isPresented in
- if !isPresented {
- viewModel.cancelDelete()
- }
- }
- )
- }
- private var header: some View {
- VStack(spacing: 0) {
- HStack(spacing: 14) {
- ModernSidebarIconView(kind: .history, size: 40)
- VStack(alignment: .leading, spacing: 2) {
- Text("History")
- .font(.system(size: 18, weight: .semibold))
- .foregroundStyle(AppTheme.textPrimary)
- Text("Saved AI responses from your tools")
- .font(.system(size: 11))
- .foregroundStyle(AppTheme.textSecondary)
- }
- Spacer()
- if !viewModel.isEmpty {
- Button {
- viewModel.requestDeleteAll()
- } label: {
- Label("Delete All", systemImage: "trash")
- .font(.system(size: 11, weight: .medium))
- }
- .buttonStyle(AppDestructiveButtonStyle())
- }
- }
- .padding(.horizontal, Layout.horizontalPadding)
- .padding(.vertical, 16)
- FullWidthDivider()
- }
- }
- private var filterBar: some View {
- ScrollView(.horizontal, showsIndicators: false) {
- HStack(spacing: 6) {
- ForEach(AIHistoryFilter.allCases) { filter in
- filterChip(filter)
- }
- }
- .padding(.horizontal, Layout.horizontalPadding)
- .padding(.vertical, 12)
- }
- .zIndex(1)
- }
- private func filterChip(_ filter: AIHistoryFilter) -> some View {
- let isSelected = viewModel.selectedFilter == filter
- return Button {
- withAnimation(.easeInOut(duration: 0.15)) {
- viewModel.selectedFilter = filter
- }
- } label: {
- Text(filter.title)
- .font(.system(size: 11, weight: isSelected ? .bold : .medium))
- .foregroundStyle(isSelected ? Color(hex: 0xD97706) : AppTheme.textSecondary)
- .padding(.horizontal, 12)
- .padding(.vertical, 7)
- .background(
- RoundedRectangle(cornerRadius: 8, style: .continuous)
- .fill(isSelected ? Color(hex: 0xD97706).opacity(0.12) : AppTheme.cardBackground)
- )
- .overlay(
- RoundedRectangle(cornerRadius: 8, style: .continuous)
- .stroke(
- isSelected ? Color(hex: 0xD97706).opacity(0.5) : AppTheme.cardBorder,
- lineWidth: isSelected ? 1.5 : 1
- )
- )
- }
- .buttonStyle(.plain)
- .hoverCursor()
- }
- private var historyList: some View {
- ScrollView {
- LazyVStack(spacing: 8) {
- ForEach(viewModel.filteredEntries) { entry in
- AIHistoryRowView(
- entry: entry,
- onOpen: { onOpen(entry) },
- onDelete: { viewModel.requestDelete(entry) }
- )
- }
- }
- .padding(.horizontal, Layout.horizontalPadding)
- .padding(.top, 12)
- .padding(.bottom, 24)
- }
- }
- private var emptyState: some View {
- VStack(spacing: 12) {
- Image(systemName: "clock.arrow.circlepath")
- .font(.system(size: 32))
- .foregroundStyle(Color(hex: 0xD97706).opacity(0.7))
- Text("No history yet")
- .font(.system(size: 15, weight: .semibold))
- .foregroundStyle(AppTheme.textPrimary)
- Text("Generated posts, optimized titles, and comments\nwill appear here automatically.")
- .font(.system(size: 12))
- .foregroundStyle(AppTheme.textSecondary)
- .multilineTextAlignment(.center)
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- }
- private var filteredEmptyState: some View {
- VStack(spacing: 12) {
- Image(systemName: "line.3.horizontal.decrease.circle")
- .font(.system(size: 28))
- .foregroundStyle(AppTheme.textTertiary)
- Text("No \(viewModel.selectedFilter.title.lowercased()) in history")
- .font(.system(size: 14, weight: .semibold))
- .foregroundStyle(AppTheme.textPrimary)
- Text("Try another filter or generate content with the AI tools.")
- .font(.system(size: 12))
- .foregroundStyle(AppTheme.textSecondary)
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- }
- }
- private struct AIHistoryRowView: View {
- let entry: AIHistoryEntry
- var onOpen: () -> Void
- var onDelete: () -> Void
- var body: some View {
- HStack(alignment: .top, spacing: 12) {
- ModernSidebarIconView(kind: entry.toolKind.iconKind, size: 36)
- VStack(alignment: .leading, spacing: 4) {
- HStack(spacing: 6) {
- Text(entry.toolKind.title)
- .font(.system(size: 9, weight: .semibold))
- .foregroundStyle(AppTheme.textTertiary)
- .textCase(.uppercase)
- .tracking(0.3)
- Text("·")
- .foregroundStyle(AppTheme.textTertiary)
- Text(entry.createdAt, style: .relative)
- .font(.system(size: 9))
- .foregroundStyle(AppTheme.textTertiary)
- }
- Text(entry.displayTitle)
- .font(.system(size: 13, weight: .semibold))
- .foregroundStyle(AppTheme.textPrimary)
- .lineLimit(2)
- .multilineTextAlignment(.leading)
- Text(entry.displaySubtitle)
- .font(.system(size: 11))
- .foregroundStyle(AppTheme.textSecondary)
- }
- Spacer(minLength: 8)
- HStack(spacing: 6) {
- Button(action: onOpen) {
- Label("Open", systemImage: "arrow.up.right.square")
- .font(.system(size: 10, weight: .medium))
- .labelStyle(.titleAndIcon)
- }
- .buttonStyle(AppSecondaryButtonStyle())
- Button(action: onDelete) {
- Image(systemName: "trash")
- .font(.system(size: 10, weight: .medium))
- }
- .buttonStyle(AppDestructiveButtonStyle())
- }
- }
- .padding(14)
- .background(
- RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
- .fill(AppTheme.cardBackground)
- .overlay(
- RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
- .stroke(AppTheme.cardBorder, lineWidth: 1)
- )
- )
- .contentShape(RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall, style: .continuous))
- .hoverCard(cornerRadius: AppTheme.cornerRadiusSmall)
- .hoverCursor()
- .onTapGesture(perform: onOpen)
- }
- }
- #Preview {
- AIHistoryView(viewModel: AIHistoryViewModel()) { _ in }
- .frame(width: 880, height: 720)
- }
|