From f52cfb80fc72512899854bef0b367440cd9e7f7f Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 19 Dec 2025 14:06:13 +0100 Subject: [PATCH] fix: resolve magic scan crash and implement context-aware AI languages - Fixed SQL syntax error in magicScan caused by single quotes - Implemented dynamic locale-aware AI suggestions (Technical: EN, Custom Tags: Localized) - Updated Dexie schema to version 2 (added locale to pending_scans) - Fixed missing bottle_id in UploadQueue synchronization - Installed missing dexie dependencies via pnpm --- package.json | 2 ++ pnpm-lock.yaml | 40 +++++++++++++++++++++------ src/components/CameraCapture.tsx | 7 +++-- src/components/UploadQueue.tsx | 3 +- src/lib/db.ts | 5 ++-- src/lib/gemini.ts | 2 ++ src/services/analyze-bottle-nebius.ts | 7 +++-- src/services/analyze-bottle.ts | 6 ++-- src/services/magic-scan.ts | 11 +++++--- 9 files changed, 61 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 8249dec..9a576b2 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "@supabase/supabase-js": "^2.39.0", "@tanstack/react-query": "^5.0.0", "canvas-confetti": "^1.9.2", + "dexie": "^4.2.1", + "dexie-react-hooks": "^4.2.0", "heic2any": "^0.0.4", "lucide-react": "^0.300.0", "next": "14.2.23", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c954e07..14218b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,12 @@ importers: canvas-confetti: specifier: ^1.9.2 version: 1.9.4 + dexie: + specifier: ^4.2.1 + version: 4.2.1 + dexie-react-hooks: + specifier: ^4.2.0 + version: 4.2.0(@types/react@18.3.27)(dexie@4.2.1)(react@18.3.1) heic2any: specifier: ^0.0.4 version: 0.0.4 @@ -1444,6 +1450,16 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dexie-react-hooks@4.2.0: + resolution: {integrity: sha512-u7KqTX9JpBQK8+tEyA9X0yMGXlSCsbm5AU64N6gjvGk/IutYDpLBInMYEAEC83s3qhIvryFS+W+sqLZUBEvePQ==} + peerDependencies: + '@types/react': '>=16' + dexie: '>=4.2.0-alpha.1 <5.0.0' + react: '>=16' + + dexie@4.2.1: + resolution: {integrity: sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -4192,6 +4208,14 @@ snapshots: detect-libc@2.1.2: {} + dexie-react-hooks@4.2.0(@types/react@18.3.27)(dexie@4.2.1)(react@18.3.1): + dependencies: + '@types/react': 18.3.27 + dexie: 4.2.1 + react: 18.3.1 + + dexie@4.2.1: {} + didyoumean@1.2.2: {} dir-glob@3.0.1: @@ -4372,8 +4396,8 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) @@ -4392,7 +4416,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -4403,22 +4427,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -4429,7 +4453,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 diff --git a/src/components/CameraCapture.tsx b/src/components/CameraCapture.tsx index e6673e8..8aff6d4 100644 --- a/src/components/CameraCapture.tsx +++ b/src/components/CameraCapture.tsx @@ -26,7 +26,7 @@ interface CameraCaptureProps { } export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onSaveComplete }: CameraCaptureProps) { - const { t } = useI18n(); + const { t, locale } = useI18n(); const supabase = createClientComponentClient(); const router = useRouter(); const searchParams = useSearchParams(); @@ -128,13 +128,14 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS await db.pending_scans.add({ imageBase64: compressedBase64, timestamp: Date.now(), - provider: aiProvider + provider: aiProvider, + locale: locale }); setIsQueued(true); return; } - const response = await magicScan(compressedBase64, aiProvider); + const response = await magicScan(compressedBase64, aiProvider, locale); if (response.success && response.data) { setAnalysisResult(response.data); diff --git a/src/components/UploadQueue.tsx b/src/components/UploadQueue.tsx index fcc8a6a..bfb3f39 100644 --- a/src/components/UploadQueue.tsx +++ b/src/components/UploadQueue.tsx @@ -37,7 +37,7 @@ export default function UploadQueue() { const itemId = `scan-${item.id}`; setCurrentProgress({ id: itemId, status: 'Analysiere Scan...' }); try { - const analysis = await analyzeBottle(item.imageBase64); + const analysis = await analyzeBottle(item.imageBase64, undefined, item.locale); if (analysis.success && analysis.data) { setCurrentProgress({ id: itemId, status: 'Speichere Flasche...' }); const save = await saveBottle(analysis.data, item.imageBase64, user.id); @@ -62,6 +62,7 @@ export default function UploadQueue() { try { const result = await saveTasting({ ...item.data, + bottle_id: item.bottle_id, tasted_at: item.tasted_at }); if (result.success) { diff --git a/src/lib/db.ts b/src/lib/db.ts index 90c8de6..ee714c4 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -5,6 +5,7 @@ export interface PendingScan { imageBase64: string; timestamp: number; provider?: 'gemini' | 'nebius'; + locale?: string; } export interface PendingTasting { @@ -45,8 +46,8 @@ export class WhiskyDexie extends Dexie { constructor() { super('WhiskyVault'); - this.version(1).stores({ - pending_scans: '++id, timestamp', + this.version(2).stores({ + pending_scans: '++id, timestamp, locale', pending_tastings: '++id, bottle_id, tasted_at', cache_tags: 'id, category, name', cache_buddies: 'id, name' diff --git a/src/lib/gemini.ts b/src/lib/gemini.ts index 7e74f25..8600f0a 100644 --- a/src/lib/gemini.ts +++ b/src/lib/gemini.ts @@ -19,6 +19,8 @@ Extract precise metadata. If the image is NOT a whisky bottle or if you are very 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. +IMPORTANT: Extract technical metadata (name, distillery, category) in English. +The 'suggested_custom_tags' MUST be localized in {LANGUAGE}. PART 2: SENSORY ANALYSIS (AUTO-FILL) Based on the identified bottle, select the most appropriate flavor tags. diff --git a/src/services/analyze-bottle-nebius.ts b/src/services/analyze-bottle-nebius.ts index 0313e61..454e68e 100644 --- a/src/services/analyze-bottle-nebius.ts +++ b/src/services/analyze-bottle-nebius.ts @@ -9,7 +9,7 @@ import { createHash } from 'crypto'; import { trackApiUsage } from './track-api-usage'; import { checkCreditBalance, deductCredits } from './credit-service'; -export async function analyzeBottleNebius(base64Image: string, tags?: string[]): Promise { +export async function analyzeBottleNebius(base64Image: string, tags?: string[], locale: string = 'de'): Promise { const supabase = createServerActionClient({ cookies }); if (!process.env.NEBIUS_API_KEY) { @@ -48,7 +48,10 @@ export async function analyzeBottleNebius(base64Image: string, tags?: string[]): }; } - const instruction = GEMINI_SYSTEM_INSTRUCTION.replace('{AVAILABLE_TAGS}', tags ? tags.join(', ') : 'No tags available') + "\nAdditionally, generate a 'search_string' field for Whiskybase in this format: 'site:whiskybase.com [Distillery] [Name] [Vintage]'. Include this field in the JSON object."; + const instruction = GEMINI_SYSTEM_INSTRUCTION + .replace('{AVAILABLE_TAGS}', tags ? tags.join(', ') : 'No tags available') + .replace('{LANGUAGE}', locale === 'en' ? 'English' : 'German') + + "\nAdditionally, generate a 'search_string' field for Whiskybase in this format: 'site:whiskybase.com [Distillery] [Name] [Vintage]'. Include this field in the JSON object."; const response = await aiClient.chat.completions.create({ model: "Qwen/Qwen2.5-VL-72B-Instruct", diff --git a/src/services/analyze-bottle.ts b/src/services/analyze-bottle.ts index 9383a1c..fe298fa 100644 --- a/src/services/analyze-bottle.ts +++ b/src/services/analyze-bottle.ts @@ -8,7 +8,7 @@ import { createHash } from 'crypto'; import { trackApiUsage } from './track-api-usage'; import { checkCreditBalance, deductCredits } from './credit-service'; -export async function analyzeBottle(base64Image: string, tags?: string[]): Promise { +export async function analyzeBottle(base64Image: string, tags?: string[], locale: string = 'de'): Promise { const supabase = createServerActionClient({ cookies }); if (!process.env.GEMINI_API_KEY) { @@ -48,7 +48,9 @@ export async function analyzeBottle(base64Image: string, tags?: string[]): Promi }; } - const instruction = SYSTEM_INSTRUCTION.replace('{AVAILABLE_TAGS}', tags ? tags.join(', ') : 'No tags available'); + const instruction = SYSTEM_INSTRUCTION + .replace('{AVAILABLE_TAGS}', tags ? tags.join(', ') : 'No tags available') + .replace('{LANGUAGE}', locale === 'en' ? 'English' : 'German'); const result = await geminiModel.generateContent([ { diff --git a/src/services/magic-scan.ts b/src/services/magic-scan.ts index 392efe3..9997837 100644 --- a/src/services/magic-scan.ts +++ b/src/services/magic-scan.ts @@ -8,7 +8,7 @@ import { supabase } from '@/lib/supabase'; import { supabaseAdmin } from '@/lib/supabase-admin'; import { AnalysisResponse, BottleMetadata } from '@/types/whisky'; -export async function magicScan(base64Image: string, provider: 'gemini' | 'nebius' = 'gemini'): Promise { +export async function magicScan(base64Image: string, provider: 'gemini' | 'nebius' = 'gemini', locale: string = 'de'): Promise { try { // 0. Fetch available tags for constrained generation const systemTags = await getAllSystemTags(); @@ -17,9 +17,9 @@ export async function magicScan(base64Image: string, provider: 'gemini' | 'nebiu // 1. AI Analysis let aiResponse: any; if (provider === 'nebius') { - aiResponse = await analyzeBottleNebius(base64Image, tagNames); + aiResponse = await analyzeBottleNebius(base64Image, tagNames, locale); } else { - aiResponse = await analyzeBottle(base64Image, tagNames); + aiResponse = await analyzeBottle(base64Image, tagNames, locale); } if (!aiResponse.success || !aiResponse.data) { @@ -38,7 +38,10 @@ export async function magicScan(base64Image: string, provider: 'gemini' | 'nebiu const { data: cacheHit } = await supabase .from('global_products') .select('wb_id') - .textSearch('search_vector', `'${searchString}'`, { config: 'simple' }) + .textSearch('search_vector', searchString, { + config: 'simple', + type: 'websearch' + }) .limit(1) .maybeSingle();