feat: improve AI resilience, add background enrichment loading states, and fix duplicate identifier in TagSelector

This commit is contained in:
2025-12-23 11:38:16 +01:00
parent 1d98bb9947
commit c134c78a2c
37 changed files with 1906 additions and 786 deletions

View File

@@ -36,12 +36,12 @@ export async function analyzeBottleMistral(input: any): Promise<AnalysisResponse
// 2. Auth & Credits
supabase = await createClient();
const { data: { session } } = await supabase.auth.getSession();
if (!session || !session.user) {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return { success: false, error: 'Nicht autorisiert oder Session abgelaufen.' };
}
const userId = session.user.id;
const userId = user.id;
const creditCheck = await checkCreditBalance(userId, 'gemini_ai');
if (!creditCheck.allowed) {
return {
@@ -85,98 +85,98 @@ export async function analyzeBottleMistral(input: any): Promise<AnalysisResponse
const prompt = getSystemPrompt(tags.length > 0 ? tags.join(', ') : 'Keine Tags verfügbar', locale);
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 }
]
}
],
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);
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 }
]
}
],
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') {
jsonData.abv = parseFloat(jsonData.abv.replace('%', '').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
});
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
});
return {
success: false,
isAiError: true,
error: aiError.message,
imageHash: imageHash
} as any;
}
if (Array.isArray(jsonData)) jsonData = jsonData[0];
console.log('[Mistral AI] JSON Response:', jsonData);
// 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);
const endParse = performance.now();
// Track usage
await trackApiUsage({
userId: userId,
apiType: 'gemini_ai',
endpoint: 'mistral/mistral-large',
success: true
});
// Deduct credits
await deductCredits(userId, 'gemini_ai', 'Mistral AI analysis');
// Store in Cache
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 (error) {
console.error('Mistral Analysis Error:', error);
if (supabase) {
const { data: { session } } = await supabase.auth.getSession();
if (session?.user) {
await trackApiUsage({
userId: session.user.id,
apiType: 'gemini_ai',
endpoint: 'mistral/mistral-large',
success: false,
errorMessage: error instanceof Error ? error.message : 'Unknown error'
});
}
}
console.error('Mistral Analysis Global Error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Mistral AI analysis failed.',