AIHistoryView.swift 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import SwiftUI
  2. struct AIHistoryView: View {
  3. @Bindable var viewModel: AIHistoryViewModel
  4. var onOpen: (AIHistoryEntry) -> Void
  5. private enum Layout {
  6. static let horizontalPadding: CGFloat = 24
  7. }
  8. var body: some View {
  9. VStack(spacing: 0) {
  10. header
  11. filterBar
  12. if viewModel.isEmpty {
  13. emptyState
  14. } else if !viewModel.hasFilteredEntries {
  15. filteredEmptyState
  16. } else {
  17. historyList
  18. }
  19. }
  20. .background(AppTheme.background)
  21. .alert("Delete this item?", isPresented: deleteBinding) {
  22. Button("Cancel", role: .cancel) {
  23. viewModel.cancelDelete()
  24. }
  25. Button("Delete", role: .destructive) {
  26. viewModel.confirmDelete()
  27. }
  28. } message: {
  29. if let entry = viewModel.entryPendingDeletion {
  30. Text("Remove \"\(entry.displayTitle)\" from history?")
  31. }
  32. }
  33. .alert("Delete all history?", isPresented: $viewModel.showDeleteAllConfirmation) {
  34. Button("Cancel", role: .cancel) {
  35. viewModel.cancelDeleteAll()
  36. }
  37. Button("Delete All", role: .destructive) {
  38. viewModel.confirmDeleteAll()
  39. }
  40. } message: {
  41. Text("This permanently removes all saved posts, titles, and comments.")
  42. }
  43. }
  44. private var deleteBinding: Binding<Bool> {
  45. Binding(
  46. get: { viewModel.entryPendingDeletion != nil },
  47. set: { isPresented in
  48. if !isPresented {
  49. viewModel.cancelDelete()
  50. }
  51. }
  52. )
  53. }
  54. private var header: some View {
  55. VStack(spacing: 0) {
  56. HStack(spacing: 14) {
  57. ModernSidebarIconView(kind: .history, size: 40)
  58. VStack(alignment: .leading, spacing: 2) {
  59. Text("History")
  60. .font(.system(size: 18, weight: .semibold))
  61. .foregroundStyle(AppTheme.textPrimary)
  62. Text("Saved AI responses from your tools")
  63. .font(.system(size: 11))
  64. .foregroundStyle(AppTheme.textSecondary)
  65. }
  66. Spacer()
  67. if !viewModel.isEmpty {
  68. Button {
  69. viewModel.requestDeleteAll()
  70. } label: {
  71. Label("Delete All", systemImage: "trash")
  72. .font(.system(size: 11, weight: .medium))
  73. }
  74. .buttonStyle(AppDestructiveButtonStyle())
  75. }
  76. }
  77. .padding(.horizontal, Layout.horizontalPadding)
  78. .padding(.vertical, 16)
  79. FullWidthDivider()
  80. }
  81. }
  82. private var filterBar: some View {
  83. ScrollView(.horizontal, showsIndicators: false) {
  84. HStack(spacing: 6) {
  85. ForEach(AIHistoryFilter.allCases) { filter in
  86. filterChip(filter)
  87. }
  88. }
  89. .padding(.horizontal, Layout.horizontalPadding)
  90. .padding(.vertical, 12)
  91. }
  92. .zIndex(1)
  93. }
  94. private func filterChip(_ filter: AIHistoryFilter) -> some View {
  95. let isSelected = viewModel.selectedFilter == filter
  96. return Button {
  97. withAnimation(.easeInOut(duration: 0.15)) {
  98. viewModel.selectedFilter = filter
  99. }
  100. } label: {
  101. Text(filter.title)
  102. .font(.system(size: 11, weight: isSelected ? .bold : .medium))
  103. .foregroundStyle(isSelected ? Color(hex: 0xD97706) : AppTheme.textSecondary)
  104. .padding(.horizontal, 12)
  105. .padding(.vertical, 7)
  106. .background(
  107. RoundedRectangle(cornerRadius: 8, style: .continuous)
  108. .fill(isSelected ? Color(hex: 0xD97706).opacity(0.12) : AppTheme.cardBackground)
  109. )
  110. .overlay(
  111. RoundedRectangle(cornerRadius: 8, style: .continuous)
  112. .stroke(
  113. isSelected ? Color(hex: 0xD97706).opacity(0.5) : AppTheme.cardBorder,
  114. lineWidth: isSelected ? 1.5 : 1
  115. )
  116. )
  117. }
  118. .buttonStyle(.plain)
  119. .hoverCursor()
  120. }
  121. private var historyList: some View {
  122. ScrollView {
  123. LazyVStack(spacing: 8) {
  124. ForEach(viewModel.filteredEntries) { entry in
  125. AIHistoryRowView(
  126. entry: entry,
  127. onOpen: { onOpen(entry) },
  128. onDelete: { viewModel.requestDelete(entry) }
  129. )
  130. }
  131. }
  132. .padding(.horizontal, Layout.horizontalPadding)
  133. .padding(.top, 12)
  134. .padding(.bottom, 24)
  135. }
  136. }
  137. private var emptyState: some View {
  138. VStack(spacing: 12) {
  139. Image(systemName: "clock.arrow.circlepath")
  140. .font(.system(size: 32))
  141. .foregroundStyle(Color(hex: 0xD97706).opacity(0.7))
  142. Text("No history yet")
  143. .font(.system(size: 15, weight: .semibold))
  144. .foregroundStyle(AppTheme.textPrimary)
  145. Text("Generated posts, optimized titles, and comments\nwill appear here automatically.")
  146. .font(.system(size: 12))
  147. .foregroundStyle(AppTheme.textSecondary)
  148. .multilineTextAlignment(.center)
  149. }
  150. .frame(maxWidth: .infinity, maxHeight: .infinity)
  151. }
  152. private var filteredEmptyState: some View {
  153. VStack(spacing: 12) {
  154. Image(systemName: "line.3.horizontal.decrease.circle")
  155. .font(.system(size: 28))
  156. .foregroundStyle(AppTheme.textTertiary)
  157. Text("No \(viewModel.selectedFilter.title.lowercased()) in history")
  158. .font(.system(size: 14, weight: .semibold))
  159. .foregroundStyle(AppTheme.textPrimary)
  160. Text("Try another filter or generate content with the AI tools.")
  161. .font(.system(size: 12))
  162. .foregroundStyle(AppTheme.textSecondary)
  163. }
  164. .frame(maxWidth: .infinity, maxHeight: .infinity)
  165. }
  166. }
  167. private struct AIHistoryRowView: View {
  168. let entry: AIHistoryEntry
  169. var onOpen: () -> Void
  170. var onDelete: () -> Void
  171. var body: some View {
  172. HStack(alignment: .top, spacing: 12) {
  173. ModernSidebarIconView(kind: entry.toolKind.iconKind, size: 36)
  174. VStack(alignment: .leading, spacing: 4) {
  175. HStack(spacing: 6) {
  176. Text(entry.toolKind.title)
  177. .font(.system(size: 9, weight: .semibold))
  178. .foregroundStyle(AppTheme.textTertiary)
  179. .textCase(.uppercase)
  180. .tracking(0.3)
  181. Text("·")
  182. .foregroundStyle(AppTheme.textTertiary)
  183. Text(entry.createdAt, style: .relative)
  184. .font(.system(size: 9))
  185. .foregroundStyle(AppTheme.textTertiary)
  186. }
  187. Text(entry.displayTitle)
  188. .font(.system(size: 13, weight: .semibold))
  189. .foregroundStyle(AppTheme.textPrimary)
  190. .lineLimit(2)
  191. .multilineTextAlignment(.leading)
  192. Text(entry.displaySubtitle)
  193. .font(.system(size: 11))
  194. .foregroundStyle(AppTheme.textSecondary)
  195. }
  196. Spacer(minLength: 8)
  197. HStack(spacing: 6) {
  198. Button(action: onOpen) {
  199. Label("Open", systemImage: "arrow.up.right.square")
  200. .font(.system(size: 10, weight: .medium))
  201. .labelStyle(.titleAndIcon)
  202. }
  203. .buttonStyle(AppSecondaryButtonStyle())
  204. Button(action: onDelete) {
  205. Image(systemName: "trash")
  206. .font(.system(size: 10, weight: .medium))
  207. }
  208. .buttonStyle(AppDestructiveButtonStyle())
  209. }
  210. }
  211. .padding(14)
  212. .background(
  213. RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
  214. .fill(AppTheme.cardBackground)
  215. .overlay(
  216. RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
  217. .stroke(AppTheme.cardBorder, lineWidth: 1)
  218. )
  219. )
  220. .contentShape(RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall, style: .continuous))
  221. .hoverCard(cornerRadius: AppTheme.cornerRadiusSmall)
  222. .hoverCursor()
  223. .onTapGesture(perform: onOpen)
  224. }
  225. }
  226. #Preview {
  227. AIHistoryView(viewModel: AIHistoryViewModel()) { _ in }
  228. .frame(width: 880, height: 720)
  229. }