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
|
||||
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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -127,6 +127,13 @@
|
|||
<div id="success-section" class="hidden">
|
||||
<div class="success-icon">✓</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue