#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:
@@ -3,6 +3,8 @@ import PhotosUI
|
||||
|
||||
struct AssistantChatView: View {
|
||||
@State private var vm = AssistantViewModel()
|
||||
@State private var showCamera = false
|
||||
@State private var showPhotoPicker = false
|
||||
var onFoodAdded: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
@@ -83,19 +85,30 @@ struct AssistantChatView: View {
|
||||
Divider()
|
||||
|
||||
// Input bar
|
||||
HStack(spacing: 10) {
|
||||
PhotosPicker(selection: $vm.selectedPhoto, matching: .images) {
|
||||
HStack(alignment: .bottom, spacing: 10) {
|
||||
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")
|
||||
.font(.title3)
|
||||
.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)
|
||||
.onSubmit {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
Task { await vm.send() }
|
||||
}
|
||||
.lineLimit(1...5)
|
||||
|
||||
Button {
|
||||
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)
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.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
|
||||
|
||||
@@ -173,6 +173,23 @@ final class AssistantViewModel {
|
||||
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
|
||||
|
||||
private func draftToDict(_ draft: FitnessDraft) -> [String: Any] {
|
||||
|
||||
@@ -68,6 +68,10 @@ struct FitnessAPI {
|
||||
try await api.get("\(basePath)/foods/\(id)")
|
||||
}
|
||||
|
||||
func deleteFood(id: String) async throws -> SuccessResponse {
|
||||
try await api.delete("\(basePath)/foods/\(id)")
|
||||
}
|
||||
|
||||
// MARK: - Templates
|
||||
|
||||
func getTemplates() async throws -> [MealTemplate] {
|
||||
|
||||
@@ -76,6 +76,16 @@ struct FoodLibraryView: View {
|
||||
.padding(.horizontal, 20)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
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