- cực ít moving parts
- dễ demo local
- không phụ thuộc backend riêng
- giữ được Gemini API key
- đủ sạch để pitch như một hệ thống có thể mở rộng
[Next.js Frontend]
├─ POS Order Builder
├─ Tray Capture / Upload UI
├─ Verification Result UI
├─ Mock menu + mock order state
└─ IndexedDB (lưu session demo, lịch sử verify)
|
| POST /api/verify-tray
v
[Next.js Route Handler]
├─ nhận order + image
├─ build prompt
├─ gọi Gemini API bằng fetch/curl-style request
└─ trả structured JSON về frontend
|
v
[Google Gemini API]
└─ phân tích ảnh + đối chiếu expected order
- Frontend vẫn là trung tâm
- không cần dựng ASP.NET hay DB thật
- route handler chỉ là lớp proxy siêu mỏng
- API key không lộ ra browser
- đủ “có kiến trúc” để giám khảo thấy bạn không code bừa
Các module:
menu catalogorder buildercamera/upload tray imageverification dashboardhistory panel
Bạn có thể giữ đơn giản:
-
React state / Zustand đều được
-
IndexedDB chỉ dùng cho:
- lưu order gần nhất
- lưu ảnh demo metadata
- lưu lịch sử kết quả verify
Ví dụ:
app/api/verify-tray/route.ts
Nó làm 4 việc:
- nhận
expectedOrder - nhận
imageBase64 - build prompt + schema instruction
- call Gemini và parse JSON trả về
Tôi khuyên chỉ làm 3 màn hình chính hoặc 1 trang chia 3 panel.
Mục tiêu: cashier nhập source of truth.
-
danh sách sản phẩm cố định
-
mỗi item có:
- tên
- thumbnail/icon
- nút
- / +
-
panel bên phải hiện:
- selected items
- tổng số lượng item
- nút
Create Order
{
"orderId": "ORD-001",
"items": [
{ "productId": "snack_red", "name": "Snack Red", "quantity": 2 },
{ "productId": "cola_can", "name": "Cola Can", "quantity": 1 }
]
}Mục tiêu: lấy ảnh khay để verify.
- tab
Upload Image - tab
Use Webcam - preview ảnh đã chọn
- nút
Verify Tray
-
hiển thị order hiện tại phía trên
-
nếu chưa có order thì disable verify
-
có note nhỏ:
- đặt khay trong vùng rõ
- chụp từ trên xuống hoặc hơi chéo
- tránh rung mờ
Mục tiêu: giám khảo nhìn 3 giây là hiểu.
- Snack Red ×2
- Cola Can ×1
- Snack Red ×1
- Cola Can ×1
- Snack Blue ×1
-
Mismatch Detected -
Missing:
- Snack Red ×1
-
Extra:
- Snack Blue ×1
MatchedMismatchMissing ItemsExtra ItemsQuantity Error
Nên làm schema thật rõ và không quá thông minh. Đừng bắt model trả mấy field quá thơ ca.
{
"success": true,
"verificationStatus": "matched",
"summary": "Tray matches the expected order.",
"detectedItems": [
{
"productId": "snack_red",
"productName": "Snack Red",
"quantity": 2
},
{
"productId": "cola_can",
"productName": "Cola Can",
"quantity": 1
}
],
"missingItems": [],
"extraItems": [],
"quantityMismatches": [],
"notes": [
"Image is clear and all items are visible."
]
}{
"success": true,
"verificationStatus": "mismatch",
"summary": "Tray does not match the expected order.",
"detectedItems": [
{
"productId": "snack_red",
"productName": "Snack Red",
"quantity": 1
},
{
"productId": "cola_can",
"productName": "Cola Can",
"quantity": 1
}
],
"missingItems": [
{
"productId": "snack_red",
"productName": "Snack Red",
"expectedQuantity": 2,
"detectedQuantity": 1,
"missingQuantity": 1
}
],
"extraItems": [],
"quantityMismatches": [
{
"productId": "snack_red",
"productName": "Snack Red",
"expectedQuantity": 2,
"detectedQuantity": 1
}
],
"notes": [
"One Snack Red appears to be missing."
]
}{
"success": true,
"verificationStatus": "mismatch",
"summary": "Tray contains extra items not present in the order.",
"detectedItems": [
{
"productId": "snack_red",
"productName": "Snack Red",
"quantity": 2
},
{
"productId": "snack_blue",
"productName": "Snack Blue",
"quantity": 1
}
],
"missingItems": [],
"extraItems": [
{
"productId": "snack_blue",
"productName": "Snack Blue",
"detectedQuantity": 1
}
],
"quantityMismatches": [],
"notes": [
"Detected an extra item that is not part of the expected order."
]
}Chỉ cần 2 giá trị:
matchedmismatch
Đừng tách quá nhiều enum phụ, frontend render sẽ mệt.
Đây là phần rất quan trọng. Bạn hỏi nên nhét tên, id, tag mô tả thế nào — câu trả lời là: nên có cả 3.
[
{
"productId": "snack_red",
"displayName": "Snack Red",
"shortLabel": "Red Snack Pack",
"category": "snack_pack",
"visualTags": [
"red package",
"small snack bag",
"bright red wrapper"
]
},
{
"productId": "snack_blue",
"displayName": "Snack Blue",
"shortLabel": "Blue Snack Pack",
"category": "snack_pack",
"visualTags": [
"blue package",
"small snack bag",
"bright blue wrapper"
]
},
{
"productId": "cola_can",
"displayName": "Cola Can",
"shortLabel": "Cola Can",
"category": "drink_can",
"visualTags": [
"soft drink can",
"aluminum can",
"red soda can"
]
},
{
"productId": "water_bottle",
"displayName": "Water Bottle",
"shortLabel": "Water Bottle",
"category": "drink_bottle",
"visualTags": [
"clear plastic bottle",
"water bottle",
"transparent bottle"
]
}
]productId: để frontend compare ổn địnhdisplayName: để hiển thị UIcategory: giúp model hiểu nhóm vật thểvisualTags: đây là “gợi ý thị giác” rất hữu ích khi packaging khác màu
Tag mô tả phải:
- ngắn
- cụ thể
- có tính thị giác
- không viết marketing
Sai:
- “delicious snack”
- “premium refreshing cola”
Đúng:
- “red package”
- “plastic bottle”
- “silver can top”
- “blue snack bag”
Bạn đang làm verification, nên prompt phải ép model theo đúng vai trò đó.
You are an AI vision verification assistant for a fast-food POS system.
Your task is to analyze a tray image and compare the visible items against the expected POS order.
Important rules:
1. Only identify items from the provided allowed menu list.
2. Do not invent new products outside the allowed menu.
3. Count visible items conservatively and carefully.
4. If an item is uncertain, prefer the closest allowed product based on visual tags, but do not hallucinate.
5. Your job is not only to detect items, but also to compare them with the expected order.
6. Return valid JSON only.
7. Do not include markdown fences.
8. Do not include explanations outside the JSON.
Allowed menu list:
{{menuJson}}
Expected POS order:
{{orderJson}}
Task:
Analyze the tray image and detect the visible items using only the allowed menu list.
Then compare the detected items with the expected POS order.
Return JSON with this exact structure:
{
"success": true,
"verificationStatus": "matched" | "mismatch",
"summary": "string",
"detectedItems": [
{
"productId": "string",
"productName": "string",
"quantity": number
}
],
"missingItems": [
{
"productId": "string",
"productName": "string",
"expectedQuantity": number,
"detectedQuantity": number,
"missingQuantity": number
}
],
"extraItems": [
{
"productId": "string",
"productName": "string",
"detectedQuantity": number
}
],
"quantityMismatches": [
{
"productId": "string",
"productName": "string",
"expectedQuantity": number,
"detectedQuantity": number
}
],
"notes": ["string"]
}
Decision rules:
- If all products and quantities match the expected order exactly, set verificationStatus to "matched".
- Otherwise set verificationStatus to "mismatch".
- An item in detectedItems must belong to the allowed menu list.
- missingItems should contain items where detected quantity is lower than expected.
- extraItems should contain detected items not expected in the order.
- quantityMismatches should contain items whose detected quantity differs from expected quantity.
- Keep notes short and practical.
Bạn sẽ gửi:
- prompt text
- image input
Có thể thêm:
The tray contains only a small number of items. Focus on precise counting.
Câu này hợp scope của bạn, giúp model bớt over-detect.
Pseudo-flow:
Frontend gửi:
- expectedOrder
- menuCatalog
- imageBase64
Route handler:
- validate payload
- build prompt
- call Gemini
- parse JSON
- nếu parse fail thì fallback response
- trả JSON cho frontend
Nếu model trả JSON lỗi:
{
"success": false,
"verificationStatus": "mismatch",
"summary": "Unable to verify tray reliably.",
"detectedItems": [],
"missingItems": [],
"extraItems": [],
"quantityMismatches": [],
"notes": ["Model response could not be parsed safely."]
}Hackathon demo mà không có fallback là tự gài mìn.
Đây là phần quan trọng nhất. Không phải model mạnh là thắng. Demo ít vỡ mới thắng.
Dàn dựng để AI có cơ hội đúng.
Nghe hơi sân khấu, nhưng hackathon là nơi “controlled reality”.
- nền bàn cố định
- ánh sáng ổn định
- khoảng cách camera gần cố định
- vật thể không chồng che nhau nhiều
- mỗi item nhìn rõ phần nhận diện chính
Nên ưu tiên:
- snack đỏ
- snack xanh
- lon nước ngọt
- chai nước
- gói snack vàng nếu có
- lon khác màu nếu đủ khác biệt
Tránh:
- 3 món nhìn na ná cùng shape cùng tone
- packaging quá giống nhau
Bạn có 2 mode demo:
- safe mode: upload ảnh đã chụp đẹp sẵn
- live mode: webcam nếu tình hình ổn
Khi trình diễn chính thức, tôi khuyên:
- demo 1 case webcam
- còn lại dùng ảnh pre-shot chuẩn Đừng all-in vào live camera như phim hành động.
Order:
- Snack Red ×2
- Cola Can ×1
Tray:
- đúng y chang
Kết quả:
Matched
Order:
- Snack Red ×2
- Cola Can ×1
Tray:
- Snack Red ×1
- Cola Can ×1
Kết quả:
- thiếu 1 Snack Red
Order:
- Snack Red ×2
- Cola Can ×1
Tray:
- Snack Red ×2
- Water Bottle ×1
Kết quả:
- extra Water Bottle
- thiếu Cola Can
Ba case này đủ để kể business story.
Tối đa:
- 1 intro case
- 2 mismatch case
Nhiều hơn là giám khảo bắt đầu mệt và demo có thêm cơ hội phản chủ.
Rất nên có:
Load Demo ALoad Demo BLoad Demo C
Khi lên demo, bạn không phải cộng tay từng món. Tiết kiệm thời gian và giảm thao tác lỗi.
Ví dụ:
- Demo A → ảnh đúng
- Demo B → ảnh thiếu món
- Demo C → ảnh sai món
Nút:
Use Sample Image
Cực kỳ thực dụng. Cực kỳ hackathon.
Bạn nên nói:
- cashier nhập order thủ công dễ sai
- giờ cao điểm dễ nghẽn hàng
- AI không thay cashier
- AI là lớp kiểm tra cuối trước khi giao khay
- giảm sai đơn và giảm khiếu nại
Câu này nghe doanh nghiệp hơn rất nhiều.
POS Menu
- add/remove item
- current order
Tray Image
- upload
- webcam capture
- preview
AI Verification
- status badge
- expected vs detected
- missing / extra / mismatch
Create OrderCapture / UploadVerify TrayLoad Demo Case
Đừng phụ thuộc 100% vào AI để kết luận matched/mismatch. Tốt hơn là:
- AI trả
detectedItems - frontend/backend nhẹ tự compare lại với expected order
Tức là Gemini làm tốt nhất phần:
- nhận diện item
- đếm số lượng
Còn phần:
- matched hay mismatch
- thiếu gì
- dư gì
thì app của bạn tự tính lại từ expectedOrder và detectedItems.
- deterministic hơn
- dễ debug hơn
- nếu AI note hơi ngáo, logic vẫn đúng
- hệ thống trông “engineering” hơn, ít phụ thuộc model
Gemini vẫn có thể trả full schema, nhưng app nên:
- parse
detectedItems - chạy local compare function
- dùng compare result local làm source of truth cuối cùng
Cái này là nước đi khôn.
- Next.js App Router
- Route Handler cho
/api/verify-tray - IndexedDB cho demo history
- menu mock JSON local
- sample images local/public
- Gemini vision call
- local compare engine
- AI chỉ làm phần khó nhất: nhìn ảnh
- app tự làm phần logic: so sánh order
- demo được thiết kế có kiểm soát
- UI phải cực rõ expected vs detected vs result
- Next.js frontend
- Next.js route handler proxy Gemini
- IndexedDB lưu lịch sử demo
- menu mock local
- AI vision để detect item
- local logic để compare order
- chọn món + số lượng
- upload/chụp ảnh khay
- verify
- hiện expected / detected / mismatch result
detectedItemsmissingItemsextraItemsquantityMismatchesverificationStatussummarynotes
- chỉ detect từ fixed menu
- dùng
productId + displayName + visualTags - trả JSON only
- tập trung đếm số lượng chính xác
- ảnh đẹp, nền cố định, item ít
- 4–6 sản phẩm khác nhau rõ rệt
- 3 demo case kinh điển
- có sample order + sample image
- webcam chỉ dùng như bonus, không all-in
