From afc09182a7956f43b765d743d2cbd53f878effb8 Mon Sep 17 00:00:00 2001 From: "Maciek \"mab122\" Bator" Date: Mon, 26 Jan 2026 14:14:17 +0100 Subject: [PATCH] prompt modification --- backend/routers/analyze.py | 82 ++++++++++++++++++++++++++++++-------- backend/routers/homebox.py | 5 ++- frontend/app.js | 62 +++++++++++++++++++++++++--- frontend/index.html | 7 ++++ frontend/styles.css | 47 ++++++++++++++++++++++ nginx/nginx.docker.conf | 8 ++-- 6 files changed, 185 insertions(+), 26 deletions(-) diff --git a/backend/routers/analyze.py b/backend/routers/analyze.py index 1622124..1c1ae4e 100644 --- a/backend/routers/analyze.py +++ b/backend/routers/analyze.py @@ -33,6 +33,13 @@ class AnalyzeResponse(BaseModel): existing_labels: list[str] # Labels that exist in Homebox new_labels: list[str] # AI-suggested labels that don't exist yet suggested_location: str # Location name from available locations + # Additional fields + manufacturer: str = "" + model_number: str = "" + serial_number: str = "" + quantity: int = 1 + purchase_price: Optional[float] = None + notes: str = "" raw_response: Optional[str] = None @@ -41,32 +48,40 @@ def build_prompt(locations: list[LocationOption], labels: list[LabelOption]) -> location_names = [loc.name for loc in locations] if locations else ["Storage"] label_names = [lbl.name for lbl in labels] if labels else [] - prompt = """Look at this image and identify the item for a home inventory system. + prompt = """Analyze this image for a home inventory system. Identify the item and extract as much information as possible. -Respond with ONLY a JSON object (no other text) with these fields: +Respond with ONLY a JSON object containing these fields (use null for unknown values): -- name: Short descriptive name for the item (e.g. "Red Handled Scissors", "DeWalt Cordless Drill") -- description: Brief description - color, brand, condition, notable features -- labels: Array of 1-4 relevant category labels for this item -- location: Best storage location from the AVAILABLE LOCATIONS below (use exact name) +{ + "name": "Descriptive item name (include brand if visible)", + "description": "Brief description - color, size, condition, features", + "manufacturer": "Brand/manufacturer name if visible", + "model": "Model number if visible", + "serial": "Serial number if visible", + "quantity": 1, + "price": null, + "labels": ["category1", "category2"], + "location": "storage location", + "notes": "Any other relevant details" +} -AVAILABLE LOCATIONS (you MUST pick one exactly as written): +AVAILABLE LOCATIONS (pick one exactly as written): """ prompt += "\n".join(f"- {name}" for name in location_names) if label_names: - prompt += "\n\nEXISTING LABELS (prefer these if they fit, use exact names):\n" + prompt += "\n\nEXISTING LABELS (use these exact names if they match):\n" prompt += "\n".join(f"- {name}" for name in label_names) - prompt += "\n\nFor labels: Use existing labels above if they match. If the item needs a label that doesn't exist, suggest a new short label name (lowercase, single word or hyphenated like 'power-tools')." + prompt += "\n\nFor labels: Pick from existing labels if they fit. If no existing label matches, create a new short label name (lowercase, hyphenated like 'power-tools' or 'kitchen-appliances')." else: - prompt += "\n\nNo labels exist yet. Suggest 1-3 short label names (lowercase, single word or hyphenated like 'electronics' or 'hand-tools')." + prompt += "\n\nNo labels exist yet. Suggest 1-3 short category labels (lowercase, hyphenated like 'electronics' or 'hand-tools')." - prompt += "\n\nRespond with ONLY the JSON object." + prompt += "\n\nRespond with ONLY the JSON object, no other text." return prompt def parse_response(response_text: str, locations: list[LocationOption], labels: list[LabelOption]) -> dict: - """Parse AI response and separate existing vs new labels.""" + """Parse AI response and extract all fields.""" # Extract JSON from response data = {} try: @@ -77,15 +92,38 @@ def parse_response(response_text: str, locations: list[LocationOption], labels: except json.JSONDecodeError: pass - # Get name and description + # Get basic fields name = str(data.get("name", "Unknown Item")) description = str(data.get("description", "")) + # Get additional fields + manufacturer = str(data.get("manufacturer") or data.get("brand") or "") + model_number = str(data.get("model") or data.get("model_number") or data.get("modelNumber") or "") + serial_number = str(data.get("serial") or data.get("serial_number") or data.get("serialNumber") or "") + notes = str(data.get("notes") or "") + + # Quantity + try: + quantity = int(data.get("quantity", 1)) + if quantity < 1: + quantity = 1 + except (ValueError, TypeError): + quantity = 1 + + # Price + purchase_price = None + price_val = data.get("price") or data.get("purchase_price") or data.get("purchasePrice") + if price_val is not None: + try: + purchase_price = float(price_val) + except (ValueError, TypeError): + pass + # Validate location against available options - location_names = {loc.name.lower(): loc.name for loc in locations} + location_names_map = {loc.name.lower(): loc.name for loc in locations} suggested_loc = str(data.get("location", "")) - if suggested_loc.lower() in location_names: - location = location_names[suggested_loc.lower()] + if suggested_loc.lower() in location_names_map: + location = location_names_map[suggested_loc.lower()] elif locations: location = locations[0].name else: @@ -117,6 +155,12 @@ def parse_response(response_text: str, locations: list[LocationOption], labels: "existing_labels": existing_labels, "new_labels": new_labels, "location": location, + "manufacturer": manufacturer, + "model_number": model_number, + "serial_number": serial_number, + "quantity": quantity, + "purchase_price": purchase_price, + "notes": notes, } @@ -169,5 +213,11 @@ async def analyze_image(request: AnalyzeRequest): existing_labels=parsed["existing_labels"], new_labels=parsed["new_labels"], suggested_location=parsed["location"], + manufacturer=parsed["manufacturer"], + model_number=parsed["model_number"], + serial_number=parsed["serial_number"], + quantity=parsed["quantity"], + purchase_price=parsed["purchase_price"], + notes=parsed["notes"], raw_response=raw_response, ) diff --git a/backend/routers/homebox.py b/backend/routers/homebox.py index 5d12d96..c9f0d85 100644 --- a/backend/routers/homebox.py +++ b/backend/routers/homebox.py @@ -38,6 +38,7 @@ class ItemCreate(BaseModel): class ItemResponse(BaseModel): id: str name: str + url: str # Direct link to item in Homebox class AttachmentUpload(BaseModel): @@ -190,7 +191,9 @@ async def create_item( response = await client.post("/api/v1/items", json=payload, headers=auth) response.raise_for_status() data = response.json() - return ItemResponse(id=data["id"], name=data["name"]) + # Construct direct link to item in Homebox + item_url = f"{settings.homebox_url}/item/{data['id']}" + return ItemResponse(id=data["id"], name=data["name"], url=item_url) except httpx.HTTPStatusError as e: raise HTTPException(status_code=e.response.status_code, detail=f"Failed to create item: {e.response.text}") except httpx.RequestError as e: diff --git a/frontend/app.js b/frontend/app.js index cdcfd85..dfed93c 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -6,6 +6,7 @@ const state = { selectedLabels: [], // [{id, name, isNew}] capturedImage: null, stream: null, + lastItemUrl: null, }; // DOM Elements @@ -52,6 +53,9 @@ const elements = { // Success/Error successSection: document.getElementById('success-section'), + itemLink: document.getElementById('item-link'), + copyLinkBtn: document.getElementById('copy-link-btn'), + copyFeedback: document.getElementById('copy-feedback'), newItemBtn: document.getElementById('new-item-btn'), errorSection: document.getElementById('error-section'), errorText: document.getElementById('error-text'), @@ -184,9 +188,9 @@ async function analyzeImage() { }), }); - // Populate form - elements.itemName.value = result.name; - elements.itemDescription.value = result.description; + // Populate main form fields + elements.itemName.value = result.name || ''; + elements.itemDescription.value = result.description || ''; // Set location (AI returns exact name from our list) const matchedLocation = state.locations.find( @@ -198,6 +202,9 @@ async function analyzeImage() { elements.itemLocation.value = state.locations[0].id; } + // Set quantity + elements.itemQuantity.value = result.quantity || 1; + // Add labels - existing ones from Homebox state.selectedLabels = []; (result.existing_labels || []).forEach(labelName => { @@ -211,6 +218,18 @@ async function analyzeImage() { addLabel({ id: null, name: labelName, isNew: true }); }); + // Populate additional fields from AI (More Details section) + elements.itemManufacturer.value = result.manufacturer || ''; + elements.itemModel.value = result.model_number || ''; + elements.itemSerial.value = result.serial_number || ''; + elements.itemNotes.value = result.notes || ''; + + if (result.purchase_price !== null && result.purchase_price !== undefined) { + elements.itemPrice.value = result.purchase_price; + } else { + elements.itemPrice.value = ''; + } + hideStatus(); elements.formSection.classList.remove('hidden'); } catch (err) { @@ -340,7 +359,7 @@ async function saveItem() { } hideStatus(); - showSuccess(); + showSuccess(item.url); } catch (err) { hideStatus(); showError(`Failed to save: ${err.message}`); @@ -367,9 +386,41 @@ function hideError() { elements.errorSection.classList.add('hidden'); } -function showSuccess() { +function showSuccess(itemUrl) { elements.formSection.classList.add('hidden'); + elements.itemLink.href = itemUrl; + elements.itemLink.textContent = 'Open in Homebox'; + elements.copyFeedback.classList.add('hidden'); elements.successSection.classList.remove('hidden'); + + // Store URL for copy functionality + state.lastItemUrl = itemUrl; +} + +async function copyItemLink() { + if (!state.lastItemUrl) return; + + try { + await navigator.clipboard.writeText(state.lastItemUrl); + elements.copyFeedback.classList.remove('hidden'); + setTimeout(() => { + elements.copyFeedback.classList.add('hidden'); + }, 2000); + } catch (err) { + // Fallback for older browsers + const textArea = document.createElement('textarea'); + textArea.value = state.lastItemUrl; + textArea.style.position = 'fixed'; + textArea.style.opacity = '0'; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + elements.copyFeedback.classList.remove('hidden'); + setTimeout(() => { + elements.copyFeedback.classList.add('hidden'); + }, 2000); + } } function resetToCapture() { @@ -441,6 +492,7 @@ async function init() { elements.retakeBtn.addEventListener('click', retakePhoto); elements.saveBtn.addEventListener('click', saveItem); elements.newItemBtn.addEventListener('click', resetToCapture); + elements.copyLinkBtn.addEventListener('click', copyItemLink); elements.retryBtn.addEventListener('click', () => { hideError(); if (state.capturedImage) { diff --git a/frontend/index.html b/frontend/index.html index 9e68e2d..a0c53ef 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -127,6 +127,13 @@ diff --git a/frontend/styles.css b/frontend/styles.css index 1f4ad63..8a35c3a 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -366,6 +366,53 @@ button:disabled { font-size: 32px; } +.item-link-container { + display: flex; + align-items: center; + gap: 8px; + background: #16213e; + padding: 12px 16px; + border-radius: 8px; + width: 100%; + max-width: 300px; +} + +.item-link-container a { + flex: 1; + color: #6c63ff; + text-decoration: none; + font-weight: 500; + word-break: break-all; +} + +.item-link-container a:hover { + text-decoration: underline; +} + +.icon-btn { + background: transparent; + border: none; + padding: 8px; + cursor: pointer; + border-radius: 4px; + font-size: 18px; + line-height: 1; +} + +.icon-btn:hover { + background: rgba(108, 99, 255, 0.2); +} + +.icon-btn:active { + transform: scale(0.95); +} + +.copy-feedback { + color: #4caf50; + font-size: 14px; + margin: 0; +} + /* Error Section */ #error-section { background: #ff5252; diff --git a/nginx/nginx.docker.conf b/nginx/nginx.docker.conf index 818a88c..19749ad 100644 --- a/nginx/nginx.docker.conf +++ b/nginx/nginx.docker.conf @@ -18,8 +18,8 @@ http { client_max_body_size 50M; server { - listen 443 ssl; - server_name localhost; + listen 0.0.0.0:443 ssl; + server_name _; ssl_certificate /etc/nginx/certs/cert.pem; ssl_certificate_key /etc/nginx/certs/key.pem; @@ -51,8 +51,8 @@ http { # Redirect HTTP to HTTPS server { - listen 80; - server_name localhost; + listen 0.0.0.0:80; + server_name _; return 301 https://$host$request_uri; } }