'use server'; import { Mistral } from '@mistralai/mistralai'; import { BottleMetadataSchema, AnalysisResponse, BottleMetadata } from '@/types/whisky'; import { createClient } from '@/lib/supabase/server'; import { createHash } from 'crypto'; import { trackApiUsage } from './track-api-usage'; import { checkCreditBalance, deductCredits } from './credit-service'; export async function analyzeBottlePixtral(base64Image: string, tags?: string[], locale: string = 'de'): Promise { if (!process.env.MISTRAL_API_KEY) { return { success: false, error: 'MISTRAL_API_KEY is not configured.' }; } let supabase; try { supabase = await createClient(); const { data: { session } } = await supabase.auth.getSession(); if (!session || !session.user) { return { success: false, error: 'Nicht autorisiert oder Session abgelaufen.' }; } const userId = session.user.id; const creditCheck = await checkCreditBalance(userId, 'gemini_ai'); if (!creditCheck.allowed) { return { success: false, error: `Nicht genügend Credits. Du benötigst ${creditCheck.cost} Credits, hast aber nur ${creditCheck.balance}.` }; } const base64Data = base64Image.split(',')[1] || base64Image; const imageHash = createHash('sha256').update(base64Data).digest('hex'); const { data: cachedResult } = await supabase .from('vision_cache') .select('result') .eq('hash', imageHash) .maybeSingle(); if (cachedResult) { return { success: true, data: cachedResult.result as any, }; } const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY }); const dataUrl = `data:image/jpeg;base64,${base64Data}`; const prompt = `Du bist ein Whisky-Experte und OCR-Spezialist. Analysiere dieses Etikett präzise. Sprache für Beschreibungen: ${locale === 'en' ? 'Englisch' : 'Deutsch'}. Verfügbare Tags zur Einordnung: ${tags ? tags.join(', ') : 'Keine Tags verfügbar'}. Antworte AUSSCHLIESSLICH mit gültigem JSON (kein Markdown, kein Text davor/danach): { "distillery": "Name der Brennerei (z.B. Lagavulin)", "name": "Exakter Name/Edition (z.B. 16 Year Old)", "vintage": "Jahrgang als Zahl oder null", "age": "Alter als Zahl oder null (z.B. 16)", "abv": "Alkoholgehalt als Zahl ohne % (z.B. 43)", "category": "Kategorie (z.B. Single Malt Scotch Whisky)", "search_string": "site:whiskybase.com [Brennerei] [Name] [Alter]" }`; const chatResponse = await client.chat.complete({ model: 'pixtral-large-latest', messages: [ { role: 'user', content: [ { type: 'text', text: prompt }, { type: 'image_url', imageUrl: dataUrl } ] } ], responseFormat: { type: 'json_object' }, temperature: 0.1 }); const rawContent = chatResponse.choices?.[0].message.content; if (!rawContent) throw new Error("Keine Antwort von Pixtral"); const jsonData = JSON.parse(rawContent as string); // Extract search_string before validation const searchString = jsonData.search_string; delete jsonData.search_string; // Ensure abv is a number if it came as a string if (typeof jsonData.abv === 'string') { jsonData.abv = parseFloat(jsonData.abv.replace('%', '').trim()); } // Ensure age/vintage are numbers if (jsonData.age) jsonData.age = parseInt(jsonData.age); if (jsonData.vintage) jsonData.vintage = parseInt(jsonData.vintage); const validatedData = BottleMetadataSchema.parse(jsonData); // Track usage await trackApiUsage({ userId: userId, apiType: 'gemini_ai', // Keep as generic 'gemini_ai' for now or update schema later endpoint: 'mistral/pixtral-large', success: true }); // Deduct credits await deductCredits(userId, 'gemini_ai', 'Pixtral AI analysis'); // Store in Cache await supabase .from('vision_cache') .insert({ hash: imageHash, result: validatedData }); return { success: true, data: validatedData, search_string: searchString }; } catch (error) { console.error('Pixtral Analysis Error:', error); // Track failed API call try { if (supabase) { const { data: { session } } = await supabase.auth.getSession(); if (session?.user) { await trackApiUsage({ userId: session.user.id, apiType: 'gemini_ai', endpoint: 'mistral/pixtral-large', success: false, errorMessage: error instanceof Error ? error.message : 'Unknown error' }); } } } catch (trackError) { console.error('Failed to track error:', trackError); } return { success: false, error: error instanceof Error ? error.message : 'Pixtral AI analysis failed.', }; } }