From 11fd59e88f219d7cb275a13fdabcbcbb7365c353 Mon Sep 17 00:00:00 2001 From: Yusuf Suleman Date: Fri, 3 Apr 2026 20:53:25 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20editable=20AI=20draft=20card=20?= =?UTF-8?q?=E2=80=94=20edit=20food/macros=20before=20adding=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FitnessDraft properties changed from let to var (mutable) - New EditableDraftCard with Edit/Done toggle: - Editable food name - Editable macros (calories, protein, carbs, fat, sugar, fiber) - Meal type picker (dropdown menu) - Editable quantity - Edited values flow through draftToDict → apply endpoint - No backend changes needed — purely iOS UI - Default view is read-only (same as before), tap Edit to modify Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Platform.xcodeproj/project.pbxproj | 4 + .../Assistant/AssistantChatView.swift | 47 +--- .../Assistant/EditableDraftCard.swift | 202 ++++++++++++++++++ .../Fitness/Models/FitnessModels.swift | 26 +-- 4 files changed, 221 insertions(+), 58 deletions(-) create mode 100644 ios/Platform/Platform/Features/Assistant/EditableDraftCard.swift diff --git a/ios/Platform/Platform.xcodeproj/project.pbxproj b/ios/Platform/Platform.xcodeproj/project.pbxproj index c522120..98a81b1 100644 --- a/ios/Platform/Platform.xcodeproj/project.pbxproj +++ b/ios/Platform/Platform.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ A10004 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10004 /* APIClient.swift */; }; A10005 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10005 /* AuthManager.swift */; }; A10050 /* AppearanceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10050 /* AppearanceManager.swift */; }; + A10051 /* EditableDraftCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10051 /* EditableDraftCard.swift */; }; A10006 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10006 /* LoginView.swift */; }; A10007 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10007 /* HomeView.swift */; }; A10008 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10008 /* HomeViewModel.swift */; }; @@ -59,6 +60,7 @@ B10004 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = ""; }; B10005 /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = ""; }; B10050 /* AppearanceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceManager.swift; sourceTree = ""; }; + B10051 /* EditableDraftCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableDraftCard.swift; sourceTree = ""; }; B10006 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; B10007 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; B10008 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; @@ -242,6 +244,7 @@ children = ( B10025 /* AssistantChatView.swift */, B10026 /* AssistantViewModel.swift */, + B10051 /* EditableDraftCard.swift */, ); path = Assistant; sourceTree = ""; @@ -415,6 +418,7 @@ A10004 /* APIClient.swift in Sources */, A10005 /* AuthManager.swift in Sources */, A10050 /* AppearanceManager.swift in Sources */, + A10051 /* EditableDraftCard.swift in Sources */, A10006 /* LoginView.swift in Sources */, A10007 /* HomeView.swift in Sources */, A10008 /* HomeViewModel.swift in Sources */, diff --git a/ios/Platform/Platform/Features/Assistant/AssistantChatView.swift b/ios/Platform/Platform/Features/Assistant/AssistantChatView.swift index eb80eff..8c1b169 100644 --- a/ios/Platform/Platform/Features/Assistant/AssistantChatView.swift +++ b/ios/Platform/Platform/Features/Assistant/AssistantChatView.swift @@ -151,53 +151,10 @@ struct AssistantChatView: View { .padding(.horizontal, 12) } - // MARK: - Draft Card + // MARK: - Draft Card (Editable) private func draftCard(_ draft: FitnessDraft) -> some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "doc.text.fill") - .foregroundStyle(Color.accentWarm) - Text("Draft") - .font(.subheadline.weight(.semibold)) - .foregroundStyle(Color.textPrimary) - } - - Text(draft.foodName) - .font(.headline) - .foregroundStyle(Color.textPrimary) - - HStack(spacing: 16) { - miniStat("Cal", value: Int(draft.calories)) - miniStat("P", value: Int(draft.protein)) - miniStat("C", value: Int(draft.carbs)) - miniStat("F", value: Int(draft.fat)) - } - - HStack { - Text("\(draft.mealType.capitalized) \u{2022} \(draft.quantity, specifier: "%.1f") \(draft.unit)") - .font(.caption) - .foregroundStyle(Color.textSecondary) - Spacer() - } - - Button { - Task { await vm.applyDraft() } - } label: { - Text("Add it") - .font(.subheadline.weight(.semibold)) - .foregroundStyle(.white) - .frame(maxWidth: .infinity) - .padding(.vertical, 10) - .background(Color.emerald) - .clipShape(RoundedRectangle(cornerRadius: 10)) - } - } - .padding() - .background(Color.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: 14)) - .shadow(color: .black.opacity(0.06), radius: 6, y: 2) - .padding(.horizontal, 12) + EditableDraftCard(vm: vm) } private var multipleDraftsCard: some View { diff --git a/ios/Platform/Platform/Features/Assistant/EditableDraftCard.swift b/ios/Platform/Platform/Features/Assistant/EditableDraftCard.swift new file mode 100644 index 0000000..4525ef6 --- /dev/null +++ b/ios/Platform/Platform/Features/Assistant/EditableDraftCard.swift @@ -0,0 +1,202 @@ +import SwiftUI + +struct EditableDraftCard: View { + @Bindable var vm: AssistantViewModel + @State private var isEditing = false + + private var draft: FitnessDraft? { vm.currentDraft } + + var body: some View { + if let draft { + VStack(alignment: .leading, spacing: 10) { + // Header + HStack { + Image(systemName: "doc.text.fill") + .foregroundStyle(Color.accentWarm) + Text("Draft") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(Color.textPrimary) + Spacer() + Button { + withAnimation(.easeInOut(duration: 0.2)) { isEditing.toggle() } + } label: { + Text(isEditing ? "Done" : "Edit") + .font(.caption.weight(.semibold)) + .foregroundStyle(Color.accentWarm) + } + } + + // Food name + if isEditing { + TextField("Food name", text: binding(\.foodName)) + .font(.headline) + .foregroundStyle(Color.textPrimary) + .textFieldStyle(.plain) + } else { + Text(draft.foodName) + .font(.headline) + .foregroundStyle(Color.textPrimary) + } + + // Macros — editable or static + if isEditing { + LazyVGrid(columns: [ + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()), + ], spacing: 10) { + editableMacro("Cal", value: binding(\.calories), color: .emerald) + editableMacro("Protein", value: binding(\.protein), color: .macroProtein) + editableMacro("Carbs", value: binding(\.carbs), color: .macroCarbs) + editableMacro("Fat", value: binding(\.fat), color: .macroFat) + editableMacro("Sugar", value: binding(\.sugar), color: .orange) + editableMacro("Fiber", value: binding(\.fiber), color: .green) + } + } else { + HStack(spacing: 16) { + miniStat("Cal", value: Int(draft.calories)) + miniStat("P", value: Int(draft.protein)) + miniStat("C", value: Int(draft.carbs)) + miniStat("F", value: Int(draft.fat)) + } + } + + // Meal type + quantity + if isEditing { + HStack(spacing: 12) { + // Meal picker + Menu { + ForEach(MealType.allCases, id: \.self) { meal in + Button { + vm.currentDraft?.mealType = meal.rawValue + } label: { + Label(meal.displayName, systemImage: meal.icon) + } + } + } label: { + HStack(spacing: 4) { + Image(systemName: MealType(rawValue: draft.mealType)?.icon ?? "fork.knife") + .font(.caption) + Text(draft.mealType.capitalized) + .font(.caption.weight(.medium)) + Image(systemName: "chevron.up.chevron.down") + .font(.system(size: 8)) + } + .foregroundStyle(Color.accentWarm) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(Color.accentWarm.opacity(0.1)) + .clipShape(Capsule()) + } + + // Quantity + HStack(spacing: 6) { + Text("Qty:") + .font(.caption) + .foregroundStyle(Color.textSecondary) + TextField("1.0", text: quantityBinding) + .font(.caption.weight(.semibold).monospacedDigit()) + .foregroundStyle(Color.textPrimary) + .keyboardType(.decimalPad) + .frame(width: 40) + .multilineTextAlignment(.center) + Text(draft.unit) + .font(.caption) + .foregroundStyle(Color.textTertiary) + } + + Spacer() + } + } else { + HStack { + Text("\(draft.mealType.capitalized) \u{2022} \(draft.quantity, specifier: "%.1f") \(draft.unit)") + .font(.caption) + .foregroundStyle(Color.textSecondary) + Spacer() + } + } + + // Add button + Button { + isEditing = false + Task { await vm.applyDraft() } + } label: { + Text("Add it") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + .background(Color.emerald) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } + .padding() + .background(Color.surfaceCard) + .clipShape(RoundedRectangle(cornerRadius: 14)) + .shadow(color: .black.opacity(0.06), radius: 6, y: 2) + .padding(.horizontal, 12) + } + } + + // MARK: - Editable macro cell + + private func editableMacro(_ label: String, value: Binding, color: Color) -> some View { + VStack(spacing: 2) { + TextField("0", text: doubleBinding(value)) + .font(.caption.weight(.bold).monospacedDigit()) + .foregroundStyle(color) + .multilineTextAlignment(.center) + .keyboardType(.numberPad) + .frame(height: 24) + Text(label) + .font(.system(size: 9).weight(.medium)) + .foregroundStyle(Color.textTertiary) + } + .padding(.vertical, 4) + .background(color.opacity(0.06)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + } + + // MARK: - Static mini stat + + private func miniStat(_ label: String, value: Int) -> some View { + VStack(spacing: 2) { + Text("\(value)") + .font(.caption.weight(.bold).monospacedDigit()) + .foregroundStyle(Color.textPrimary) + Text(label) + .font(.system(size: 9).weight(.medium)) + .foregroundStyle(Color.textTertiary) + } + } + + // MARK: - Bindings into vm.currentDraft + + private func binding(_ keyPath: WritableKeyPath) -> Binding { + Binding( + get: { vm.currentDraft?[keyPath: keyPath] ?? "" }, + set: { vm.currentDraft?[keyPath: keyPath] = $0 } + ) + } + + private func binding(_ keyPath: WritableKeyPath) -> Binding { + Binding( + get: { vm.currentDraft?[keyPath: keyPath] ?? 0 }, + set: { vm.currentDraft?[keyPath: keyPath] = $0 } + ) + } + + private func doubleBinding(_ binding: Binding) -> Binding { + Binding( + get: { String(Int(binding.wrappedValue)) }, + set: { binding.wrappedValue = Double($0) ?? 0 } + ) + } + + private var quantityBinding: Binding { + Binding( + get: { String(format: "%.1f", vm.currentDraft?.quantity ?? 1) }, + set: { vm.currentDraft?.quantity = Double($0) ?? 1 } + ) + } +} diff --git a/ios/Platform/Platform/Features/Fitness/Models/FitnessModels.swift b/ios/Platform/Platform/Features/Fitness/Models/FitnessModels.swift index ca5974d..9de1f1e 100644 --- a/ios/Platform/Platform/Features/Fitness/Models/FitnessModels.swift +++ b/ios/Platform/Platform/Features/Fitness/Models/FitnessModels.swift @@ -252,19 +252,19 @@ struct TemplateLogResponse: Decodable { struct FitnessDraft: Identifiable { let id = UUID() - let foodName: String - let mealType: String - let entryDate: String - let quantity: Double - let unit: String - let calories: Double - let protein: Double - let carbs: Double - let fat: Double - let sugar: Double - let fiber: Double - let note: String - let defaultServingLabel: String + var foodName: String + var mealType: String + var entryDate: String + var quantity: Double + var unit: String + var calories: Double + var protein: Double + var carbs: Double + var fat: Double + var sugar: Double + var fiber: Double + var note: String + var defaultServingLabel: String init(from dict: [String: Any]) { foodName = dict["food_name"] as? String ?? ""