This commit is contained in:
2025-12-17 23:12:53 +01:00
commit 5807d949ef
323 changed files with 34158 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
'use server';
import { geminiModel, SYSTEM_INSTRUCTION } from '@/lib/gemini';
import { BottleMetadataSchema, AnalysisResponse } from '@/types/whisky';
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { createHash } from 'crypto';
export async function analyzeBottle(base64Image: string): Promise<AnalysisResponse> {
const supabase = createServerActionClient({ cookies });
if (!process.env.GEMINI_API_KEY) {
return { success: false, error: 'GEMINI_API_KEY is not configured.' };
}
try {
// Ensure user is authenticated for tracking/billing
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
return { success: false, error: 'Nicht autorisiert oder Session abgelaufen.' };
}
// 1. Generate Hash for Caching
const base64Data = base64Image.split(',')[1] || base64Image;
const imageHash = createHash('sha256').update(base64Data).digest('hex');
console.log(`[AI Cache] Checking hash: ${imageHash}`);
// 2. Check Cache
const { data: cachedResult } = await supabase
.from('vision_cache')
.select('result')
.eq('hash', imageHash)
.maybeSingle();
if (cachedResult) {
console.log(`[AI Cache] Hit! hash: ${imageHash}`);
return {
success: true,
data: cachedResult.result as any,
};
}
console.log(`[AI Cache] Miss. Calling Gemini...`);
// 3. AI Analysis
const result = await geminiModel.generateContent([
{
inlineData: {
data: base64Data,
mimeType: 'image/jpeg',
},
},
{ text: SYSTEM_INSTRUCTION },
]);
const responseText = result.response.text();
let jsonData = JSON.parse(responseText);
if (Array.isArray(jsonData)) {
jsonData = jsonData[0];
}
if (!jsonData) {
throw new Error('Keine Daten in der KI-Antwort gefunden.');
}
const validatedData = BottleMetadataSchema.parse(jsonData);
// 4. Store in Cache
const { error: storeError } = await supabase
.from('vision_cache')
.insert({ hash: imageHash, result: validatedData });
if (storeError) {
console.warn(`[AI Cache] Storage failed: ${storeError.message}`);
} else {
console.log(`[AI Cache] Stored new result for hash: ${imageHash}`);
}
return {
success: true,
data: validatedData,
};
} catch (error) {
console.error('Gemini Analysis Error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred during analysis.',
};
}
}

View File

@@ -0,0 +1,70 @@
'use server';
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { BottleMetadata } from '@/types/whisky';
import { v4 as uuidv4 } from 'uuid';
export async function saveBottle(
metadata: BottleMetadata,
base64Image: string,
_ignoredUserId: string // Keeping for signature compatibility if needed, but using session internally
) {
const supabase = createServerActionClient({ cookies });
try {
// Verify user session and get ID from the server side (secure)
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
throw new Error('Nicht autorisiert oder Session abgelaufen.');
}
const userId = session.user.id;
// 1. Upload Image to Storage
const base64Data = base64Image.split(',')[1] || base64Image;
const buffer = Buffer.from(base64Data, 'base64');
const fileName = `${userId}/${uuidv4()}.jpg`;
const { data: uploadData, error: uploadError } = await supabase.storage
.from('bottles')
.upload(fileName, buffer, {
contentType: 'image/jpeg',
upsert: true,
});
if (uploadError) throw new Error(`Upload Error: ${uploadError.message}`);
// Get Public URL
const { data: { publicUrl } } = supabase.storage
.from('bottles')
.getPublicUrl(fileName);
// 2. Save Metadata to Database
const { data: bottleData, error: dbError } = await supabase
.from('bottles')
.insert({
user_id: userId,
name: metadata.name,
distillery: metadata.distillery,
category: metadata.category,
abv: metadata.abv,
age: metadata.age,
whiskybase_id: metadata.whiskybaseId,
image_url: publicUrl,
status: 'sealed', // Default status
})
.select()
.single();
if (dbError) throw new Error(`Database Error: ${dbError.message}`);
return { success: true, data: bottleData };
} catch (error) {
console.error('Save Bottle Error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred while saving.',
};
}
}

View File

@@ -0,0 +1,47 @@
'use server';
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { revalidatePath } from 'next/cache';
export async function saveTasting(data: {
bottle_id: string;
rating: number;
nose_notes?: string;
palate_notes?: string;
finish_notes?: string;
is_sample?: boolean;
}) {
const supabase = createServerActionClient({ cookies });
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) throw new Error('Nicht autorisiert');
const { data: tasting, error } = await supabase
.from('tastings')
.insert({
bottle_id: data.bottle_id,
user_id: session.user.id,
rating: data.rating,
nose_notes: data.nose_notes,
palate_notes: data.palate_notes,
finish_notes: data.finish_notes,
is_sample: data.is_sample || false,
})
.select()
.single();
if (error) throw error;
revalidatePath(`/bottles/${data.bottle_id}`);
return { success: true, data: tasting };
} catch (error) {
console.error('Save Tasting Error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Speichern der Tasting Note',
};
}
}

View File

@@ -0,0 +1,33 @@
'use server';
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { revalidatePath } from 'next/cache';
export async function updateBottleStatus(bottleId: string, status: 'sealed' | 'open' | 'sampled' | 'empty') {
const supabase = createServerActionClient({ cookies });
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) throw new Error('Nicht autorisiert');
const { error } = await supabase
.from('bottles')
.update({ status, updated_at: new Date().toISOString() })
.eq('id', bottleId)
.eq('user_id', session.user.id);
if (error) throw error;
revalidatePath(`/bottles/${bottleId}`);
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Update Status Error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Status',
};
}
}