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
This commit is contained in:
2025-12-19 14:06:13 +01:00
parent 60ca3a6190
commit f52cfb80fc
9 changed files with 61 additions and 22 deletions

View File

@@ -16,6 +16,8 @@
"@supabase/supabase-js": "^2.39.0", "@supabase/supabase-js": "^2.39.0",
"@tanstack/react-query": "^5.0.0", "@tanstack/react-query": "^5.0.0",
"canvas-confetti": "^1.9.2", "canvas-confetti": "^1.9.2",
"dexie": "^4.2.1",
"dexie-react-hooks": "^4.2.0",
"heic2any": "^0.0.4", "heic2any": "^0.0.4",
"lucide-react": "^0.300.0", "lucide-react": "^0.300.0",
"next": "14.2.23", "next": "14.2.23",

40
pnpm-lock.yaml generated
View File

@@ -23,6 +23,12 @@ importers:
canvas-confetti: canvas-confetti:
specifier: ^1.9.2 specifier: ^1.9.2
version: 1.9.4 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: heic2any:
specifier: ^0.0.4 specifier: ^0.0.4
version: 0.0.4 version: 0.0.4
@@ -1444,6 +1450,16 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} 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: didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@@ -4192,6 +4208,14 @@ snapshots:
detect-libc@2.1.2: {} 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: {} didyoumean@1.2.2: {}
dir-glob@3.0.1: dir-glob@3.0.1:
@@ -4372,8 +4396,8 @@ snapshots:
'@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 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)
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)
eslint-plugin-jsx-a11y: 6.10.2(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: 7.37.5(eslint@8.57.1)
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
@@ -4392,7 +4416,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.3 debug: 4.4.3
@@ -4403,22 +4427,22 @@ snapshots:
tinyglobby: 0.2.15 tinyglobby: 0.2.15
unrs-resolver: 1.11.1 unrs-resolver: 1.11.1
optionalDependencies: 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: transitivePeerDependencies:
- supports-color - 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: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 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: transitivePeerDependencies:
- supports-color - 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: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.9 array-includes: 3.1.9
@@ -4429,7 +4453,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 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 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3

View File

@@ -26,7 +26,7 @@ interface CameraCaptureProps {
} }
export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onSaveComplete }: CameraCaptureProps) { export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onSaveComplete }: CameraCaptureProps) {
const { t } = useI18n(); const { t, locale } = useI18n();
const supabase = createClientComponentClient(); const supabase = createClientComponentClient();
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@@ -128,13 +128,14 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS
await db.pending_scans.add({ await db.pending_scans.add({
imageBase64: compressedBase64, imageBase64: compressedBase64,
timestamp: Date.now(), timestamp: Date.now(),
provider: aiProvider provider: aiProvider,
locale: locale
}); });
setIsQueued(true); setIsQueued(true);
return; return;
} }
const response = await magicScan(compressedBase64, aiProvider); const response = await magicScan(compressedBase64, aiProvider, locale);
if (response.success && response.data) { if (response.success && response.data) {
setAnalysisResult(response.data); setAnalysisResult(response.data);

View File

@@ -37,7 +37,7 @@ export default function UploadQueue() {
const itemId = `scan-${item.id}`; const itemId = `scan-${item.id}`;
setCurrentProgress({ id: itemId, status: 'Analysiere Scan...' }); setCurrentProgress({ id: itemId, status: 'Analysiere Scan...' });
try { try {
const analysis = await analyzeBottle(item.imageBase64); const analysis = await analyzeBottle(item.imageBase64, undefined, item.locale);
if (analysis.success && analysis.data) { if (analysis.success && analysis.data) {
setCurrentProgress({ id: itemId, status: 'Speichere Flasche...' }); setCurrentProgress({ id: itemId, status: 'Speichere Flasche...' });
const save = await saveBottle(analysis.data, item.imageBase64, user.id); const save = await saveBottle(analysis.data, item.imageBase64, user.id);
@@ -62,6 +62,7 @@ export default function UploadQueue() {
try { try {
const result = await saveTasting({ const result = await saveTasting({
...item.data, ...item.data,
bottle_id: item.bottle_id,
tasted_at: item.tasted_at tasted_at: item.tasted_at
}); });
if (result.success) { if (result.success) {

View File

@@ -5,6 +5,7 @@ export interface PendingScan {
imageBase64: string; imageBase64: string;
timestamp: number; timestamp: number;
provider?: 'gemini' | 'nebius'; provider?: 'gemini' | 'nebius';
locale?: string;
} }
export interface PendingTasting { export interface PendingTasting {
@@ -45,8 +46,8 @@ export class WhiskyDexie extends Dexie {
constructor() { constructor() {
super('WhiskyVault'); super('WhiskyVault');
this.version(1).stores({ this.version(2).stores({
pending_scans: '++id, timestamp', pending_scans: '++id, timestamp, locale',
pending_tastings: '++id, bottle_id, tasted_at', pending_tastings: '++id, bottle_id, tasted_at',
cache_tags: 'id, category, name', cache_tags: 'id, category, name',
cache_buddies: 'id, name' cache_buddies: 'id, name'

View File

@@ -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. If a value is not visible, use null.
Infer the 'Category' (e.g., Islay Single Malt) based on the Distillery if possible. 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. 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) PART 2: SENSORY ANALYSIS (AUTO-FILL)
Based on the identified bottle, select the most appropriate flavor tags. Based on the identified bottle, select the most appropriate flavor tags.

View File

@@ -9,7 +9,7 @@ import { createHash } from 'crypto';
import { trackApiUsage } from './track-api-usage'; import { trackApiUsage } from './track-api-usage';
import { checkCreditBalance, deductCredits } from './credit-service'; import { checkCreditBalance, deductCredits } from './credit-service';
export async function analyzeBottleNebius(base64Image: string, tags?: string[]): Promise<AnalysisResponse & { search_string?: string }> { export async function analyzeBottleNebius(base64Image: string, tags?: string[], locale: string = 'de'): Promise<AnalysisResponse & { search_string?: string }> {
const supabase = createServerActionClient({ cookies }); const supabase = createServerActionClient({ cookies });
if (!process.env.NEBIUS_API_KEY) { 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({ const response = await aiClient.chat.completions.create({
model: "Qwen/Qwen2.5-VL-72B-Instruct", model: "Qwen/Qwen2.5-VL-72B-Instruct",

View File

@@ -8,7 +8,7 @@ import { createHash } from 'crypto';
import { trackApiUsage } from './track-api-usage'; import { trackApiUsage } from './track-api-usage';
import { checkCreditBalance, deductCredits } from './credit-service'; import { checkCreditBalance, deductCredits } from './credit-service';
export async function analyzeBottle(base64Image: string, tags?: string[]): Promise<AnalysisResponse> { export async function analyzeBottle(base64Image: string, tags?: string[], locale: string = 'de'): Promise<AnalysisResponse> {
const supabase = createServerActionClient({ cookies }); const supabase = createServerActionClient({ cookies });
if (!process.env.GEMINI_API_KEY) { 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([ const result = await geminiModel.generateContent([
{ {

View File

@@ -8,7 +8,7 @@ import { supabase } from '@/lib/supabase';
import { supabaseAdmin } from '@/lib/supabase-admin'; import { supabaseAdmin } from '@/lib/supabase-admin';
import { AnalysisResponse, BottleMetadata } from '@/types/whisky'; import { AnalysisResponse, BottleMetadata } from '@/types/whisky';
export async function magicScan(base64Image: string, provider: 'gemini' | 'nebius' = 'gemini'): Promise<AnalysisResponse & { wb_id?: string }> { export async function magicScan(base64Image: string, provider: 'gemini' | 'nebius' = 'gemini', locale: string = 'de'): Promise<AnalysisResponse & { wb_id?: string }> {
try { try {
// 0. Fetch available tags for constrained generation // 0. Fetch available tags for constrained generation
const systemTags = await getAllSystemTags(); const systemTags = await getAllSystemTags();
@@ -17,9 +17,9 @@ export async function magicScan(base64Image: string, provider: 'gemini' | 'nebiu
// 1. AI Analysis // 1. AI Analysis
let aiResponse: any; let aiResponse: any;
if (provider === 'nebius') { if (provider === 'nebius') {
aiResponse = await analyzeBottleNebius(base64Image, tagNames); aiResponse = await analyzeBottleNebius(base64Image, tagNames, locale);
} else { } else {
aiResponse = await analyzeBottle(base64Image, tagNames); aiResponse = await analyzeBottle(base64Image, tagNames, locale);
} }
if (!aiResponse.success || !aiResponse.data) { if (!aiResponse.success || !aiResponse.data) {
@@ -38,7 +38,10 @@ export async function magicScan(base64Image: string, provider: 'gemini' | 'nebiu
const { data: cacheHit } = await supabase const { data: cacheHit } = await supabase
.from('global_products') .from('global_products')
.select('wb_id') .select('wb_id')
.textSearch('search_vector', `'${searchString}'`, { config: 'simple' }) .textSearch('search_vector', searchString, {
config: 'simple',
type: 'websearch'
})
.limit(1) .limit(1)
.maybeSingle(); .maybeSingle();