prompt modification

This commit is contained in:
Maciek "mab122" Bator 2026-01-26 14:14:17 +01:00
parent efc950f7bc
commit afc09182a7
6 changed files with 185 additions and 26 deletions

View File

@ -33,6 +33,13 @@ class AnalyzeResponse(BaseModel):
existing_labels: list[str] # Labels that exist in Homebox existing_labels: list[str] # Labels that exist in Homebox
new_labels: list[str] # AI-suggested labels that don't exist yet new_labels: list[str] # AI-suggested labels that don't exist yet
suggested_location: str # Location name from available locations 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 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"] location_names = [loc.name for loc in locations] if locations else ["Storage"]
label_names = [lbl.name for lbl in labels] if labels else [] 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 "name": "Descriptive item name (include brand if visible)",
- labels: Array of 1-4 relevant category labels for this item "description": "Brief description - color, size, condition, features",
- location: Best storage location from the AVAILABLE LOCATIONS below (use exact name) "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) prompt += "\n".join(f"- {name}" for name in location_names)
if label_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".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: 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 return prompt
def parse_response(response_text: str, locations: list[LocationOption], labels: list[LabelOption]) -> dict: 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 # Extract JSON from response
data = {} data = {}
try: try:
@ -77,15 +92,38 @@ def parse_response(response_text: str, locations: list[LocationOption], labels:
except json.JSONDecodeError: except json.JSONDecodeError:
pass pass
# Get name and description # Get basic fields
name = str(data.get("name", "Unknown Item")) name = str(data.get("name", "Unknown Item"))
description = str(data.get("description", "")) 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 # 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", "")) suggested_loc = str(data.get("location", ""))
if suggested_loc.lower() in location_names: if suggested_loc.lower() in location_names_map:
location = location_names[suggested_loc.lower()] location = location_names_map[suggested_loc.lower()]
elif locations: elif locations:
location = locations[0].name location = locations[0].name
else: else:
@ -117,6 +155,12 @@ def parse_response(response_text: str, locations: list[LocationOption], labels:
"existing_labels": existing_labels, "existing_labels": existing_labels,
"new_labels": new_labels, "new_labels": new_labels,
"location": location, "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"], existing_labels=parsed["existing_labels"],
new_labels=parsed["new_labels"], new_labels=parsed["new_labels"],
suggested_location=parsed["location"], 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, raw_response=raw_response,
) )

View File

@ -38,6 +38,7 @@ class ItemCreate(BaseModel):
class ItemResponse(BaseModel): class ItemResponse(BaseModel):
id: str id: str
name: str name: str
url: str # Direct link to item in Homebox
class AttachmentUpload(BaseModel): class AttachmentUpload(BaseModel):
@ -190,7 +191,9 @@ async def create_item(
response = await client.post("/api/v1/items", json=payload, headers=auth) response = await client.post("/api/v1/items", json=payload, headers=auth)
response.raise_for_status() response.raise_for_status()
data = response.json() 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: except httpx.HTTPStatusError as e:
raise HTTPException(status_code=e.response.status_code, detail=f"Failed to create item: {e.response.text}") raise HTTPException(status_code=e.response.status_code, detail=f"Failed to create item: {e.response.text}")
except httpx.RequestError as e: except httpx.RequestError as e:

View File

@ -6,6 +6,7 @@ const state = {
selectedLabels: [], // [{id, name, isNew}] selectedLabels: [], // [{id, name, isNew}]
capturedImage: null, capturedImage: null,
stream: null, stream: null,
lastItemUrl: null,
}; };
// DOM Elements // DOM Elements
@ -52,6 +53,9 @@ const elements = {
// Success/Error // Success/Error
successSection: document.getElementById('success-section'), 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'), newItemBtn: document.getElementById('new-item-btn'),
errorSection: document.getElementById('error-section'), errorSection: document.getElementById('error-section'),
errorText: document.getElementById('error-text'), errorText: document.getElementById('error-text'),
@ -184,9 +188,9 @@ async function analyzeImage() {
}), }),
}); });
// Populate form // Populate main form fields
elements.itemName.value = result.name; elements.itemName.value = result.name || '';
elements.itemDescription.value = result.description; elements.itemDescription.value = result.description || '';
// Set location (AI returns exact name from our list) // Set location (AI returns exact name from our list)
const matchedLocation = state.locations.find( const matchedLocation = state.locations.find(
@ -198,6 +202,9 @@ async function analyzeImage() {
elements.itemLocation.value = state.locations[0].id; elements.itemLocation.value = state.locations[0].id;
} }
// Set quantity
elements.itemQuantity.value = result.quantity || 1;
// Add labels - existing ones from Homebox // Add labels - existing ones from Homebox
state.selectedLabels = []; state.selectedLabels = [];
(result.existing_labels || []).forEach(labelName => { (result.existing_labels || []).forEach(labelName => {
@ -211,6 +218,18 @@ async function analyzeImage() {
addLabel({ id: null, name: labelName, isNew: true }); 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(); hideStatus();
elements.formSection.classList.remove('hidden'); elements.formSection.classList.remove('hidden');
} catch (err) { } catch (err) {
@ -340,7 +359,7 @@ async function saveItem() {
} }
hideStatus(); hideStatus();
showSuccess(); showSuccess(item.url);
} catch (err) { } catch (err) {
hideStatus(); hideStatus();
showError(`Failed to save: ${err.message}`); showError(`Failed to save: ${err.message}`);
@ -367,9 +386,41 @@ function hideError() {
elements.errorSection.classList.add('hidden'); elements.errorSection.classList.add('hidden');
} }
function showSuccess() { function showSuccess(itemUrl) {
elements.formSection.classList.add('hidden'); 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'); 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() { function resetToCapture() {
@ -441,6 +492,7 @@ async function init() {
elements.retakeBtn.addEventListener('click', retakePhoto); elements.retakeBtn.addEventListener('click', retakePhoto);
elements.saveBtn.addEventListener('click', saveItem); elements.saveBtn.addEventListener('click', saveItem);
elements.newItemBtn.addEventListener('click', resetToCapture); elements.newItemBtn.addEventListener('click', resetToCapture);
elements.copyLinkBtn.addEventListener('click', copyItemLink);
elements.retryBtn.addEventListener('click', () => { elements.retryBtn.addEventListener('click', () => {
hideError(); hideError();
if (state.capturedImage) { if (state.capturedImage) {

View File

@ -127,6 +127,13 @@
<div id="success-section" class="hidden"> <div id="success-section" class="hidden">
<div class="success-icon">&#10003;</div> <div class="success-icon">&#10003;</div>
<p>Item saved successfully!</p> <p>Item saved successfully!</p>
<div class="item-link-container">
<a id="item-link" href="#" target="_blank" rel="noopener">Open in Homebox</a>
<button id="copy-link-btn" class="icon-btn" title="Copy link">
<span class="copy-icon">&#128203;</span>
</button>
</div>
<p id="copy-feedback" class="copy-feedback hidden">Link copied!</p>
<button id="new-item-btn" class="primary-btn">Add Another Item</button> <button id="new-item-btn" class="primary-btn">Add Another Item</button>
</div> </div>

View File

@ -366,6 +366,53 @@ button:disabled {
font-size: 32px; 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 */
#error-section { #error-section {
background: #ff5252; background: #ff5252;

View File

@ -18,8 +18,8 @@ http {
client_max_body_size 50M; client_max_body_size 50M;
server { server {
listen 443 ssl; listen 0.0.0.0:443 ssl;
server_name localhost; server_name _;
ssl_certificate /etc/nginx/certs/cert.pem; ssl_certificate /etc/nginx/certs/cert.pem;
ssl_certificate_key /etc/nginx/certs/key.pem; ssl_certificate_key /etc/nginx/certs/key.pem;
@ -51,8 +51,8 @@ http {
# Redirect HTTP to HTTPS # Redirect HTTP to HTTPS
server { server {
listen 80; listen 0.0.0.0:80;
server_name localhost; server_name _;
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }
} }