feat: implement offline queue, background sync and AI robustness

This commit is contained in:
2025-12-17 23:25:12 +01:00
parent fe82d52a85
commit 6f08bb3c4c
70 changed files with 3132 additions and 55 deletions

View File

@@ -12,9 +12,10 @@ export const geminiModel = genAI.getGenerativeModel({
});
export const SYSTEM_INSTRUCTION = `
You are a sommelier and database clerk. Analyze the whisky bottle image. Extract precise metadata.
If a value is not visible, use null.
Infer the 'Category' (e.g., Islay Single Malt) based on the Distillery if possible.
You are a sommelier and database clerk. Analyze the whisky bottle image. Extract precise metadata.
If the image is NOT a whisky bottle or if you are very unsure, set "is_whisky" to false and provide a low "confidence" score.
If a value is not visible, use null.
Infer the 'Category' (e.g., Islay Single Malt) based on the Distillery if possible.
Search specifically for a "Whiskybase ID" or "WB ID" on the label.
Output raw JSON matching the following schema:
{
@@ -25,6 +26,8 @@ Output raw JSON matching the following schema:
"age": number | null,
"vintage": string | null,
"bottleCode": string | null,
"whiskybaseId": string | null
"whiskybaseId": string | null,
"is_whisky": boolean,
"confidence": number (0-100)
}
`;

66
src/lib/offline-db.ts Normal file
View File

@@ -0,0 +1,66 @@
export interface PendingBottle {
id: string;
imageBase64: string;
timestamp: number;
}
const DB_NAME = 'WhiskyVaultOffline';
const STORE_NAME = 'pendingCaptures';
const DB_VERSION = 1;
export const openDB = (): Promise<IDBDatabase> => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
}
};
request.onsuccess = (event) => {
resolve((event.target as IDBOpenDBRequest).result);
};
request.onerror = (event) => {
reject((event.target as IDBOpenDBRequest).error);
};
});
};
export const savePendingBottle = async (bottle: PendingBottle): Promise<void> => {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.put(bottle);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
};
export const getAllPendingBottles = async (): Promise<PendingBottle[]> => {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
};
export const deletePendingBottle = async (id: string): Promise<void> => {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
};