init
This commit is contained in:
91
src/services/analyze-bottle.ts
Normal file
91
src/services/analyze-bottle.ts
Normal 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.',
|
||||
};
|
||||
}
|
||||
}
|
||||
70
src/services/save-bottle.ts
Normal file
70
src/services/save-bottle.ts
Normal 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.',
|
||||
};
|
||||
}
|
||||
}
|
||||
47
src/services/save-tasting.ts
Normal file
47
src/services/save-tasting.ts
Normal 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',
|
||||
};
|
||||
}
|
||||
}
|
||||
33
src/services/update-bottle-status.ts
Normal file
33
src/services/update-bottle-status.ts
Normal 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',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user