'use server'; import { Mistral } from '@mistralai/mistralai'; import { getSystemPrompt } from '@/lib/ai-prompts'; 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'; // WICHTIG: Wir akzeptieren jetzt FormData statt Strings export async function analyzeBottleMistral(input: any): Promise { if (!process.env.MISTRAL_API_KEY) { return { success: false, error: 'MISTRAL_API_KEY is not configured.' }; } let supabase; try { // Helper to get value from either FormData or POJO const getValue = (obj: any, key: string): any => { if (obj && typeof obj.get === 'function') return obj.get(key); if (obj && typeof obj[key] !== 'undefined') return obj[key]; return null; }; // 1. Daten extrahieren const file = getValue(input, 'file') as File; const tagsString = getValue(input, 'tags') as string; const locale = getValue(input, 'locale') || 'de'; if (!file) { return { success: false, error: 'Kein Bild empfangen.' }; } const tags = tagsString ? (typeof tagsString === 'string' ? JSON.parse(tagsString) : tagsString) : []; // 2. Auth & Credits supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) { return { success: false, error: 'Nicht autorisiert oder Session abgelaufen.' }; } const userId = user.id; const creditCheck = await checkCreditBalance(userId, 'gemini_ai'); if (!creditCheck.allowed) { return { success: false, error: `Nicht genügend Credits. Benötigt: ${creditCheck.cost}, Verfügbar: ${creditCheck.balance}.` }; } // 3. Datei in Buffer umwandeln const arrayBuffer = await file.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); // 4. Hash für Cache erstellen const imageHash = createHash('sha256').update(buffer).digest('hex'); // Cache Check const { data: cachedResult } = await supabase .from('vision_cache') .select('result') .eq('hash', imageHash) .maybeSingle(); if (cachedResult) { return { success: true, data: cachedResult.result as any, perf: { apiDuration: 0, parseDuration: 0, uploadSize: buffer.length } }; } // 5. Für Mistral vorbereiten const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY }); const base64Data = buffer.toString('base64'); const mimeType = file.type || 'image/webp'; const dataUrl = `data:${mimeType};base64,${base64Data}`; const uploadSize = buffer.length; const prompt = getSystemPrompt(tags.length > 0 ? tags.join(', ') : 'Keine Tags verfügbar', locale); try { const startApi = performance.now(); const chatResponse = await client.chat.complete({ model: 'mistral-large-latest', messages: [ { role: 'user', content: [ { type: 'text', text: prompt }, { type: 'image_url', imageUrl: dataUrl }H ] } ], responseFormat: { type: 'json_object' }, temperature: 0.1 }); const endApi = performance.now(); const startParse = performance.now(); const rawContent = chatResponse.choices?.[0].message.content; if (!rawContent) throw new Error("Keine Antwort von Mistral"); let jsonData; try { jsonData = JSON.parse(rawContent as string); } catch (e) { const cleanedText = (rawContent as string).replace(/```json/g, '').replace(/```/g, ''); jsonData = JSON.parse(cleanedText); } if (Array.isArray(jsonData)) jsonData = jsonData[0]; console.log('[Mistral AI] JSON Response:', jsonData); const searchString = jsonData.search_string; delete jsonData.search_string; if (typeof jsonData.abv === 'string') { // Fix: Global replace to remove all % signs jsonData.abv = parseFloat(jsonData.abv.replace(/%/g, '').trim()); } if (jsonData.age) jsonData.age = parseInt(jsonData.age); if (jsonData.vintage) jsonData.vintage = parseInt(jsonData.vintage); const validatedData = BottleMetadataSchema.parse(jsonData); const endParse = performance.now(); await trackApiUsage({ userId: userId, apiType: 'gemini_ai', endpoint: 'mistral/mistral-large', success: true, provider: 'mistral', model: 'mistral-large-latest', responseText: rawContent as string }); await deductCredits(userId, 'gemini_ai', 'Mistral AI analysis'); await supabase .from('vision_cache') .insert({ hash: imageHash, result: validatedData }); return { success: true, data: validatedData, search_string: searchString, perf: { apiDuration: endApi - startApi, parseDuration: endParse - startParse, uploadSize: uploadSize }, raw: jsonData }; } catch (aiError: any) { console.warn('[MistralAnalysis] AI Analysis failed, providing fallback path:', aiError.message); await trackApiUsage({ userId: userId, apiType: 'gemini_ai', endpoint: 'mistral/mistral-large', success: false, errorMessage: aiError.message, provider: 'mistral', model: 'mistral-large-latest' }); return { success: false, isAiError: true, error: aiError.message, imageHash: imageHash } as any; } } catch (error) { console.error('Mistral Analysis Global Error:', error); return { success: false, error: error instanceof Error ? error.message : 'Mistral AI analysis failed.', }; } }