prompt modification
This commit is contained in:
parent
efc950f7bc
commit
afc09182a7
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,13 @@
|
||||||
<div id="success-section" class="hidden">
|
<div id="success-section" class="hidden">
|
||||||
<div class="success-icon">✓</div>
|
<div class="success-icon">✓</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">📋</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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue