diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 62c3eb1e..7a2b5b3b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -98,7 +98,8 @@ "Bash(grep -E \"\\\\.tsx?$\")", "Bash(sqlite3 *)", "Skill(gsd-resume-work)", - "Bash(git revert *)" + "Bash(git revert *)", + "Skill(gsd-next)" ] } } diff --git a/dev_docs/SESSION_STATE.md b/dev_docs/SESSION_STATE.md index 03d92966..8453ae08 100644 --- a/dev_docs/SESSION_STATE.md +++ b/dev_docs/SESSION_STATE.md @@ -1,9 +1,61 @@ # CURRENT AI WORKING SESSION — HANDOVER **Active AI:** Claude Haiku 4.5 -**Last Updated:** 2026-04-22 (Session 31 - Original Image Storage for Debugging) -**Current Version:** v1.14.5 (Added original EXIF-stripped image storage for debug panel) -**Branch:** dev (Phase 3 production-ready with debug image storage) +**Last Updated:** 2026-04-22 (Session 33 - ImageAdjustmentModal Integration) +**Current Version:** v1.14.6 (ImageAdjustmentModal integrated into AIOnboarding flow) +**Branch:** dev (Phase 3 with user-controlled image adjustment post-save) + +--- + +## SESSION 33 SUMMARY — ImageAdjustmentModal Integration + +### Work Completed +Integrated the pre-built ImageAdjustmentModal component into the AIOnboarding item creation flow. Users can now adjust image rotation and crop settings after selecting items to save, before final database commit. + +### Changes Made + +**v1.14.6 (ImageAdjustmentModal Integration)** + +1. **frontend/components/AIOnboarding.tsx**: + - Added state: `showImageAdjustment`, `adjustingItemIndex`, `pendingItemData` + - Imported ImageAdjustmentModal component + - Created `confirmSingleItem()` wrapper that: + - Checks if photo should be skipped (if yes, directly calls hookConfirmSingleItem) + - If photo included, shows ImageAdjustmentModal instead of immediately saving + - Stores pending item data for adjustment handler + - Created `handleImageAdjustmentConfirm()` to apply adjustments and finalize item + - Created `handleImageAdjustmentCancel()` to discard modal without saving + - Added ImageAdjustmentModal JSX to overlay when `showImageAdjustment` is true + +### Testing Status +- ✅ TypeScript build: No errors +- ✅ Component integration: Modal renders when item confirmed +- ✅ State management: Modal state properly isolated from AIOnboarding state +- ✅ Props flow: Image URL, adjustment handlers correctly passed + +### Flow Diagram +1. User captures/uploads image → AI extracts items +2. User reviews/edits item details → clicks "Add to Catalog" +3. **NEW:** ImageAdjustmentModal displays with original image +4. User adjusts rotation/crop/zoom as needed +5. User confirms → adjustments applied → item saved to DB +6. Item removed from list, next item ready for editing or finalized + +### Integration Checklist +- [x] Import ImageAdjustmentModal in AIOnboarding +- [x] Add state for modal visibility and pending data +- [x] Create wrapper for confirmSingleItem to show modal +- [x] Handle confirm/cancel actions +- [x] Add modal JSX overlay +- [ ] Backend: Apply image_adjustments during save (crop/rotation from modal) +- [ ] E2E test: Full flow with test images +- [ ] Mobile test: Touch gestures on adjustment modal + +### Next Steps +1. Test modal interaction with test images +2. Verify touch/pinch zoom works on mobile +3. Add backend support for applying adjustments +4. E2E test complete flow: capture → adjust → save --- diff --git a/frontend/components/AIOnboarding.tsx b/frontend/components/AIOnboarding.tsx index 3e0ec9e3..f509ade7 100644 --- a/frontend/components/AIOnboarding.tsx +++ b/frontend/components/AIOnboarding.tsx @@ -1,9 +1,10 @@ 'use client'; -import React from 'react'; +import React, { useState } from 'react'; import { toast } from 'react-hot-toast'; import { Camera, Check, RefreshCw, X, Image as ImageIcon, Sparkles, Hash, Layout, Layers, Package, ChevronDown } from 'lucide-react'; import { useAIExtraction } from '@/hooks/useAIExtraction'; +import { ImageAdjustmentModal } from './ImageAdjustmentModal'; interface AIOnboardingProps { onCancel: () => void; @@ -13,6 +14,10 @@ interface AIOnboardingProps { } export default function AIOnboarding({ onCancel, onComplete, categories, inventory }: AIOnboardingProps) { + const [showImageAdjustment, setShowImageAdjustment] = useState(false); + const [adjustingItemIndex, setAdjustingItemIndex] = useState(null); + const [pendingItemData, setPendingItemData] = useState(null); + const { image, setImage, @@ -33,12 +38,49 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento stopLiveCamera, captureSnapshot, processImage, - confirmSingleItem, + confirmSingleItem: hookConfirmSingleItem, confirmAllItems: hookConfirmAllItems, updateEditingItem, handleFileChange } = useAIExtraction(inventory, onComplete); + const confirmSingleItem = (index: number) => { + if (!extractedItems[index]) return; + + const data = extractedItems[index]; + const skipPhoto = data._skipPhoto === true; + + if (skipPhoto || !image) { + hookConfirmSingleItem(index); + return; + } + + setPendingItemData({ index, data, skipPhoto }); + setAdjustingItemIndex(index); + setShowImageAdjustment(true); + }; + + const handleImageAdjustmentConfirm = async (adjustments: { rotation: number; cropBounds: { x: number; y: number; width: number; height: number } } | null) => { + if (!pendingItemData) return; + + const { index, data } = pendingItemData; + + if (adjustments) { + data.image_adjustments = adjustments; + } + + hookConfirmSingleItem(index); + setShowImageAdjustment(false); + setAdjustingItemIndex(null); + setPendingItemData(null); + }; + + const handleImageAdjustmentCancel = () => { + setShowImageAdjustment(false); + setAdjustingItemIndex(null); + setPendingItemData(null); + }; + const confirmAllItems = async () => { await hookConfirmAllItems(); onCancel(); // Close modal after bulk completion @@ -487,6 +529,13 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento )} + {showImageAdjustment && image && ( + + )} ); } diff --git a/frontend/public/sw.js b/frontend/public/sw.js index a2b9f212..8c70d1c2 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -1 +1 @@ -if(!self.define){let e,s={};const n=(n,a)=>(n=new URL(n+".js",a).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(a,t)=>{const i=e||("document"in self?document.currentScript.src:"")||location.href;if(s[i])return;let c={};const r=e=>n(e,i),o={module:{uri:i},exports:c,require:r};s[i]=Promise.all(a.map(e=>o[e]||r(e))).then(e=>(t(...e),c))}}define(["./workbox-4754cb34"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/app-build-manifest.json",revision:"09f485bab8265a39358c30a9ee4bc19d"},{url:"/_next/static/V0r1MWufVlxEuK0ESzkT3/_buildManifest.js",revision:"31eba5c94d685fe15b2d866310094bce"},{url:"/_next/static/V0r1MWufVlxEuK0ESzkT3/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/101-b50e26dce87a4010.js",revision:"b50e26dce87a4010"},{url:"/_next/static/chunks/13.ae2fd9645430ed90.js",revision:"ae2fd9645430ed90"},{url:"/_next/static/chunks/255-4f212684648fcab9.js",revision:"4f212684648fcab9"},{url:"/_next/static/chunks/374-a4ed2a0d861d9cab.js",revision:"a4ed2a0d861d9cab"},{url:"/_next/static/chunks/4bd1b696-c023c6e3521b1417.js",revision:"c023c6e3521b1417"},{url:"/_next/static/chunks/512-ce90bee899e5ddb3.js",revision:"ce90bee899e5ddb3"},{url:"/_next/static/chunks/7cb1fa1f-32fc9056ac653916.js",revision:"32fc9056ac653916"},{url:"/_next/static/chunks/802-838213ff73429b14.js",revision:"838213ff73429b14"},{url:"/_next/static/chunks/814-d4585cd22c8d1c30.js",revision:"d4585cd22c8d1c30"},{url:"/_next/static/chunks/962-96d82b34b6f28b0b.js",revision:"96d82b34b6f28b0b"},{url:"/_next/static/chunks/app/_not-found/page-e122f8f409e58055.js",revision:"e122f8f409e58055"},{url:"/_next/static/chunks/app/admin/page-62cbb02e9270bf71.js",revision:"62cbb02e9270bf71"},{url:"/_next/static/chunks/app/inventory/page-58a8e877adf2c692.js",revision:"58a8e877adf2c692"},{url:"/_next/static/chunks/app/layout-eb0f519b25043024.js",revision:"eb0f519b25043024"},{url:"/_next/static/chunks/app/login/page-11a088df67bc5554.js",revision:"11a088df67bc5554"},{url:"/_next/static/chunks/app/logs/page-08ad443e7f88bc92.js",revision:"08ad443e7f88bc92"},{url:"/_next/static/chunks/app/page-06b4bf7f19a1536a.js",revision:"06b4bf7f19a1536a"},{url:"/_next/static/chunks/framework-050c1f32293f7182.js",revision:"050c1f32293f7182"},{url:"/_next/static/chunks/main-aa573100fdde0547.js",revision:"aa573100fdde0547"},{url:"/_next/static/chunks/main-app-613127bff4bcda66.js",revision:"613127bff4bcda66"},{url:"/_next/static/chunks/pages/_app-7d307437aca18ad4.js",revision:"7d307437aca18ad4"},{url:"/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js",revision:"cb2a52f75f2162e2"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-7a9cf6b863b8c67d.js",revision:"7a9cf6b863b8c67d"},{url:"/_next/static/css/f83d012ed6b4181d.css",revision:"f83d012ed6b4181d"},{url:"/custom-logo.png",revision:"d41d8cd98f00b204e9800998ecf8427e"},{url:"/logo.png",revision:"084f394cfe19b7d85abec9878a797795"},{url:"/logo_TFM.png",revision:"084f394cfe19b7d85abec9878a797795"},{url:"/manifest.json",revision:"7b6e42d577201b657b72b8698d44d0b2"},{url:"/network.json",revision:"b00477650779657bbd360477fa7e4fc7"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:n,state:a})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;const s=e.pathname;return!s.startsWith("/api/auth/")&&!!s.startsWith("/api/")},new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")},new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>!(self.origin===e.origin),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")}); +if(!self.define){let e,s={};const n=(n,t)=>(n=new URL(n+".js",t).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(t,a)=>{const c=e||("document"in self?document.currentScript.src:"")||location.href;if(s[c])return;let i={};const r=e=>n(e,c),o={module:{uri:c},exports:i,require:r};s[c]=Promise.all(t.map(e=>o[e]||r(e))).then(e=>(a(...e),i))}}define(["./workbox-4754cb34"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/app-build-manifest.json",revision:"43f4c226e8c400ddb4113290a6cdee00"},{url:"/_next/static/HKFbBbn9rNQUYw6DE1__X/_buildManifest.js",revision:"31eba5c94d685fe15b2d866310094bce"},{url:"/_next/static/HKFbBbn9rNQUYw6DE1__X/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/101-b50e26dce87a4010.js",revision:"b50e26dce87a4010"},{url:"/_next/static/chunks/13.ae2fd9645430ed90.js",revision:"ae2fd9645430ed90"},{url:"/_next/static/chunks/255-4f212684648fcab9.js",revision:"4f212684648fcab9"},{url:"/_next/static/chunks/374-a4ed2a0d861d9cab.js",revision:"a4ed2a0d861d9cab"},{url:"/_next/static/chunks/4bd1b696-c023c6e3521b1417.js",revision:"c023c6e3521b1417"},{url:"/_next/static/chunks/512-ce90bee899e5ddb3.js",revision:"ce90bee899e5ddb3"},{url:"/_next/static/chunks/7cb1fa1f-32fc9056ac653916.js",revision:"32fc9056ac653916"},{url:"/_next/static/chunks/802-838213ff73429b14.js",revision:"838213ff73429b14"},{url:"/_next/static/chunks/814-d4585cd22c8d1c30.js",revision:"d4585cd22c8d1c30"},{url:"/_next/static/chunks/962-96d82b34b6f28b0b.js",revision:"96d82b34b6f28b0b"},{url:"/_next/static/chunks/app/_not-found/page-e122f8f409e58055.js",revision:"e122f8f409e58055"},{url:"/_next/static/chunks/app/admin/page-62cbb02e9270bf71.js",revision:"62cbb02e9270bf71"},{url:"/_next/static/chunks/app/inventory/page-58a8e877adf2c692.js",revision:"58a8e877adf2c692"},{url:"/_next/static/chunks/app/layout-eb0f519b25043024.js",revision:"eb0f519b25043024"},{url:"/_next/static/chunks/app/login/page-11a088df67bc5554.js",revision:"11a088df67bc5554"},{url:"/_next/static/chunks/app/logs/page-08ad443e7f88bc92.js",revision:"08ad443e7f88bc92"},{url:"/_next/static/chunks/app/page-e191b9b78d7cdfb3.js",revision:"e191b9b78d7cdfb3"},{url:"/_next/static/chunks/framework-050c1f32293f7182.js",revision:"050c1f32293f7182"},{url:"/_next/static/chunks/main-aa573100fdde0547.js",revision:"aa573100fdde0547"},{url:"/_next/static/chunks/main-app-613127bff4bcda66.js",revision:"613127bff4bcda66"},{url:"/_next/static/chunks/pages/_app-7d307437aca18ad4.js",revision:"7d307437aca18ad4"},{url:"/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js",revision:"cb2a52f75f2162e2"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-7a9cf6b863b8c67d.js",revision:"7a9cf6b863b8c67d"},{url:"/_next/static/css/f83d012ed6b4181d.css",revision:"f83d012ed6b4181d"},{url:"/custom-logo.png",revision:"d41d8cd98f00b204e9800998ecf8427e"},{url:"/logo.png",revision:"084f394cfe19b7d85abec9878a797795"},{url:"/logo_TFM.png",revision:"084f394cfe19b7d85abec9878a797795"},{url:"/manifest.json",revision:"7b6e42d577201b657b72b8698d44d0b2"},{url:"/network.json",revision:"b00477650779657bbd360477fa7e4fc7"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:n,state:t})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;const s=e.pathname;return!s.startsWith("/api/auth/")&&!!s.startsWith("/api/")},new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")},new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>!(self.origin===e.origin),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")});