#28 — Return key now inserts new line in AI chat: TextField replaced with TextField(axis: .vertical) + lineLimit(1...5). Return = new line, send button submits. Removed .onSubmit. #27 — Delete foods in food library: Long-press context menu with "Delete" option on each food item. Calls DELETE /api/fitness/foods/{id} and reloads. #30 — Camera + photo library options: Replaced PhotosPicker with a Menu offering "Take Photo" (camera) and "Photo Library" (picker). CameraView wraps UIImagePickerController. handleCameraImage() in ViewModel resizes and sends same as photo picker. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@
|
|||||||
A10067 /* TripImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10067 /* TripImageView.swift */; };
|
A10067 /* TripImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10067 /* TripImageView.swift */; };
|
||||||
A10068 /* TripPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10068 /* TripPlaceholderView.swift */; };
|
A10068 /* TripPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10068 /* TripPlaceholderView.swift */; };
|
||||||
A10069 /* TripDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10069 /* TripDetailView.swift */; };
|
A10069 /* TripDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10069 /* TripDetailView.swift */; };
|
||||||
|
A10070 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10070 /* CameraView.swift */; };
|
||||||
A10006 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10006 /* LoginView.swift */; };
|
A10006 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10006 /* LoginView.swift */; };
|
||||||
A10007 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10007 /* HomeView.swift */; };
|
A10007 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10007 /* HomeView.swift */; };
|
||||||
A10008 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10008 /* HomeViewModel.swift */; };
|
A10008 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10008 /* HomeViewModel.swift */; };
|
||||||
@@ -110,6 +111,7 @@
|
|||||||
B10067 /* TripImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripImageView.swift; sourceTree = "<group>"; };
|
B10067 /* TripImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripImageView.swift; sourceTree = "<group>"; };
|
||||||
B10068 /* TripPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripPlaceholderView.swift; sourceTree = "<group>"; };
|
B10068 /* TripPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripPlaceholderView.swift; sourceTree = "<group>"; };
|
||||||
B10069 /* TripDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripDetailView.swift; sourceTree = "<group>"; };
|
B10069 /* TripDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripDetailView.swift; sourceTree = "<group>"; };
|
||||||
|
B10070 /* CameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
|
||||||
B10006 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
|
B10006 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
|
||||||
B10007 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
B10007 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||||
B10008 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
|
B10008 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
|
||||||
@@ -331,6 +333,7 @@
|
|||||||
B10027 /* MacroRing.swift */,
|
B10027 /* MacroRing.swift */,
|
||||||
B10028 /* MacroBar.swift */,
|
B10028 /* MacroBar.swift */,
|
||||||
B10029 /* LoadingView.swift */,
|
B10029 /* LoadingView.swift */,
|
||||||
|
B10070 /* CameraView.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -598,6 +601,7 @@
|
|||||||
A10067 /* TripImageView.swift in Sources */,
|
A10067 /* TripImageView.swift in Sources */,
|
||||||
A10068 /* TripPlaceholderView.swift in Sources */,
|
A10068 /* TripPlaceholderView.swift in Sources */,
|
||||||
A10069 /* TripDetailView.swift in Sources */,
|
A10069 /* TripDetailView.swift in Sources */,
|
||||||
|
A10070 /* CameraView.swift in Sources */,
|
||||||
A10006 /* LoginView.swift in Sources */,
|
A10006 /* LoginView.swift in Sources */,
|
||||||
A10007 /* HomeView.swift in Sources */,
|
A10007 /* HomeView.swift in Sources */,
|
||||||
A10008 /* HomeViewModel.swift in Sources */,
|
A10008 /* HomeViewModel.swift in Sources */,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import PhotosUI
|
|||||||
|
|
||||||
struct AssistantChatView: View {
|
struct AssistantChatView: View {
|
||||||
@State private var vm = AssistantViewModel()
|
@State private var vm = AssistantViewModel()
|
||||||
|
@State private var showCamera = false
|
||||||
|
@State private var showPhotoPicker = false
|
||||||
var onFoodAdded: (() -> Void)?
|
var onFoodAdded: (() -> Void)?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -83,19 +85,30 @@ struct AssistantChatView: View {
|
|||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
// Input bar
|
// Input bar
|
||||||
HStack(spacing: 10) {
|
HStack(alignment: .bottom, spacing: 10) {
|
||||||
PhotosPicker(selection: $vm.selectedPhoto, matching: .images) {
|
Menu {
|
||||||
|
Button {
|
||||||
|
showCamera = true
|
||||||
|
} label: {
|
||||||
|
Label("Take Photo", systemImage: "camera")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
showPhotoPicker = true
|
||||||
|
} label: {
|
||||||
|
Label("Photo Library", systemImage: "photo.on.rectangle")
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
Image(systemName: "camera.fill")
|
Image(systemName: "camera.fill")
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
.foregroundStyle(Color.accentWarm)
|
.foregroundStyle(Color.accentWarm)
|
||||||
}
|
}
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
|
||||||
TextField("Describe your food...", text: $vm.inputText)
|
// Multiline text input — Return = new line, send button submits
|
||||||
|
TextField("Describe your food...", text: $vm.inputText, axis: .vertical)
|
||||||
.textFieldStyle(.plain)
|
.textFieldStyle(.plain)
|
||||||
.onSubmit {
|
.lineLimit(1...5)
|
||||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
|
||||||
Task { await vm.send() }
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
@@ -110,6 +123,7 @@ struct AssistantChatView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.disabled(vm.inputText.trimmingCharacters(in: .whitespaces).isEmpty || vm.isLoading)
|
.disabled(vm.inputText.trimmingCharacters(in: .whitespaces).isEmpty || vm.isLoading)
|
||||||
|
.padding(.bottom, 4)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
@@ -126,6 +140,13 @@ struct AssistantChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.photosPicker(isPresented: $showPhotoPicker, selection: $vm.selectedPhoto, matching: .images)
|
||||||
|
.fullScreenCover(isPresented: $showCamera) {
|
||||||
|
CameraView { image in
|
||||||
|
showCamera = false
|
||||||
|
Task { await vm.handleCameraImage(image) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Chat Bubble
|
// MARK: - Chat Bubble
|
||||||
|
|||||||
@@ -173,6 +173,23 @@ final class AssistantViewModel {
|
|||||||
selectedPhoto = nil
|
selectedPhoto = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleCameraImage(_ image: UIImage) async {
|
||||||
|
let maxDim: CGFloat = 800
|
||||||
|
let scale = min(maxDim / image.size.width, maxDim / image.size.height, 1.0)
|
||||||
|
let newSize = CGSize(width: image.size.width * scale, height: image.size.height * scale)
|
||||||
|
let renderer = UIGraphicsImageRenderer(size: newSize)
|
||||||
|
let resized = renderer.image { _ in
|
||||||
|
image.draw(in: CGRect(origin: .zero, size: newSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let jpegData = resized.jpegData(compressionQuality: 0.7) {
|
||||||
|
let base64 = jpegData.base64EncodedString()
|
||||||
|
imageDataUrl = "data:image/jpeg;base64,\(base64)"
|
||||||
|
messages.append(ChatMessage(role: "user", content: "[Photo attached]"))
|
||||||
|
await doSend(action: "chat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
private func draftToDict(_ draft: FitnessDraft) -> [String: Any] {
|
private func draftToDict(_ draft: FitnessDraft) -> [String: Any] {
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ struct FitnessAPI {
|
|||||||
try await api.get("\(basePath)/foods/\(id)")
|
try await api.get("\(basePath)/foods/\(id)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deleteFood(id: String) async throws -> SuccessResponse {
|
||||||
|
try await api.delete("\(basePath)/foods/\(id)")
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Templates
|
// MARK: - Templates
|
||||||
|
|
||||||
func getTemplates() async throws -> [MealTemplate] {
|
func getTemplates() async throws -> [MealTemplate] {
|
||||||
|
|||||||
@@ -76,6 +76,16 @@ struct FoodLibraryView: View {
|
|||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
}
|
}
|
||||||
|
.contextMenu {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
Task {
|
||||||
|
_ = try? await FitnessAPI().deleteFood(id: food.id)
|
||||||
|
await vm.loadInitial()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
Divider().padding(.leading, 20)
|
Divider().padding(.leading, 20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
ios/Platform/Platform/Shared/Components/CameraView.swift
Normal file
39
ios/Platform/Platform/Shared/Components/CameraView.swift
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// UIImagePickerController wrapper for taking photos with the camera.
|
||||||
|
struct CameraView: UIViewControllerRepresentable {
|
||||||
|
let onCapture: (UIImage) -> Void
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||||
|
let picker = UIImagePickerController()
|
||||||
|
picker.sourceType = .camera
|
||||||
|
picker.delegate = context.coordinator
|
||||||
|
return picker
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(onCapture: onCapture)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||||
|
let onCapture: (UIImage) -> Void
|
||||||
|
|
||||||
|
init(onCapture: @escaping (UIImage) -> Void) {
|
||||||
|
self.onCapture = onCapture
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||||
|
if let image = info[.originalImage] as? UIImage {
|
||||||
|
onCapture(image)
|
||||||
|
}
|
||||||
|
picker.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||||
|
picker.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user