feat: implement AI custom tag proposals
- AI now suggests dominant notes not in the system list (Part 3: Custom Suggestions) - Updated TagSelector to show 'Neu anlegen?' buttons for AI-proposed custom tags - Added suggested_custom_tags to bottles table and metadata schema - Updated TastingNoteForm to handle both system and custom AI suggestions
This commit is contained in:
@@ -9,7 +9,7 @@ import { createHash } from 'crypto';
|
||||
import { trackApiUsage } from './track-api-usage';
|
||||
import { checkCreditBalance, deductCredits } from './credit-service';
|
||||
|
||||
export async function analyzeBottleNebius(base64Image: string): Promise<AnalysisResponse & { search_string?: string }> {
|
||||
export async function analyzeBottleNebius(base64Image: string, tags?: string[]): Promise<AnalysisResponse & { search_string?: string }> {
|
||||
const supabase = createServerActionClient({ cookies });
|
||||
|
||||
if (!process.env.NEBIUS_API_KEY) {
|
||||
@@ -24,7 +24,6 @@ export async function analyzeBottleNebius(base64Image: string): Promise<Analysis
|
||||
|
||||
const userId = session.user.id;
|
||||
|
||||
// Check credit balance (using same gemini_ai type for now or create new one)
|
||||
const creditCheck = await checkCreditBalance(userId, 'gemini_ai');
|
||||
if (!creditCheck.allowed) {
|
||||
return {
|
||||
@@ -36,7 +35,6 @@ export async function analyzeBottleNebius(base64Image: string): Promise<Analysis
|
||||
const base64Data = base64Image.split(',')[1] || base64Image;
|
||||
const imageHash = createHash('sha256').update(base64Data).digest('hex');
|
||||
|
||||
// Check Cache (Optional: skip if you want fresh AI results for testing)
|
||||
const { data: cachedResult } = await supabase
|
||||
.from('vision_cache')
|
||||
.select('result')
|
||||
@@ -44,21 +42,20 @@ export async function analyzeBottleNebius(base64Image: string): Promise<Analysis
|
||||
.maybeSingle();
|
||||
|
||||
if (cachedResult) {
|
||||
console.log(`[Nebius Cache] Hit! hash: ${imageHash}`);
|
||||
return {
|
||||
success: true,
|
||||
data: cachedResult.result as any,
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`[Nebius AI] Calling Qwen2.5-VL...`);
|
||||
const instruction = GEMINI_SYSTEM_INSTRUCTION.replace('{AVAILABLE_TAGS}', tags ? tags.join(', ') : 'No tags available') + "\nAdditionally, generate a 'search_string' field for Whiskybase in this format: 'site:whiskybase.com [Distillery] [Name] [Vintage]'. Include this field in the JSON object.";
|
||||
|
||||
const response = await aiClient.chat.completions.create({
|
||||
model: "Qwen/Qwen2.5-VL-72B-Instruct",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: GEMINI_SYSTEM_INSTRUCTION + "\nAdditionally, generate a 'search_string' field for Whiskybase in this format: 'site:whiskybase.com [Distillery] [Name] [Vintage]'. Include this field in the JSON object."
|
||||
content: instruction
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createHash } from 'crypto';
|
||||
import { trackApiUsage } from './track-api-usage';
|
||||
import { checkCreditBalance, deductCredits } from './credit-service';
|
||||
|
||||
export async function analyzeBottle(base64Image: string): Promise<AnalysisResponse> {
|
||||
export async function analyzeBottle(base64Image: string, tags?: string[]): Promise<AnalysisResponse> {
|
||||
const supabase = createServerActionClient({ cookies });
|
||||
|
||||
if (!process.env.GEMINI_API_KEY) {
|
||||
@@ -16,7 +16,7 @@ export async function analyzeBottle(base64Image: string): Promise<AnalysisRespon
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure user is authenticated for tracking/billing
|
||||
// ... (auth and credit check remain same) ...
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
if (!session || !session.user) {
|
||||
return { success: false, error: 'Nicht autorisiert oder Session abgelaufen.' };
|
||||
@@ -24,7 +24,6 @@ export async function analyzeBottle(base64Image: string): Promise<AnalysisRespon
|
||||
|
||||
const userId = session.user.id;
|
||||
|
||||
// Check credit balance before making API call
|
||||
const creditCheck = await checkCreditBalance(userId, 'gemini_ai');
|
||||
if (!creditCheck.allowed) {
|
||||
return {
|
||||
@@ -33,12 +32,9 @@ export async function analyzeBottle(base64Image: string): Promise<AnalysisRespon
|
||||
};
|
||||
}
|
||||
|
||||
// 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')
|
||||
@@ -46,16 +42,14 @@ export async function analyzeBottle(base64Image: string): Promise<AnalysisRespon
|
||||
.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...`);
|
||||
const instruction = SYSTEM_INSTRUCTION.replace('{AVAILABLE_TAGS}', tags ? tags.join(', ') : 'No tags available');
|
||||
|
||||
// 3. AI Analysis
|
||||
const result = await geminiModel.generateContent([
|
||||
{
|
||||
inlineData: {
|
||||
@@ -63,7 +57,7 @@ export async function analyzeBottle(base64Image: string): Promise<AnalysisRespon
|
||||
mimeType: 'image/jpeg',
|
||||
},
|
||||
},
|
||||
{ text: SYSTEM_INSTRUCTION },
|
||||
{ text: instruction },
|
||||
]);
|
||||
|
||||
const responseText = result.response.text();
|
||||
|
||||
@@ -3,18 +3,23 @@
|
||||
import { analyzeBottle } from './analyze-bottle';
|
||||
import { analyzeBottleNebius } from './analyze-bottle-nebius';
|
||||
import { searchBraveForWhiskybase } from './brave-search';
|
||||
import { getAllSystemTags } from './tags';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import { AnalysisResponse, BottleMetadata } from '@/types/whisky';
|
||||
|
||||
export async function magicScan(base64Image: string, provider: 'gemini' | 'nebius' = 'gemini'): Promise<AnalysisResponse & { wb_id?: string }> {
|
||||
try {
|
||||
// 0. Fetch available tags for constrained generation
|
||||
const systemTags = await getAllSystemTags();
|
||||
const tagNames = systemTags.map(t => t.name);
|
||||
|
||||
// 1. AI Analysis
|
||||
let aiResponse: any;
|
||||
if (provider === 'nebius') {
|
||||
aiResponse = await analyzeBottleNebius(base64Image);
|
||||
aiResponse = await analyzeBottleNebius(base64Image, tagNames);
|
||||
} else {
|
||||
aiResponse = await analyzeBottle(base64Image);
|
||||
aiResponse = await analyzeBottle(base64Image, tagNames);
|
||||
}
|
||||
|
||||
if (!aiResponse.success || !aiResponse.data) {
|
||||
|
||||
@@ -67,6 +67,8 @@ export async function saveBottle(
|
||||
distilled_at: metadata.distilled_at,
|
||||
bottled_at: metadata.bottled_at,
|
||||
batch_info: metadata.batch_info,
|
||||
suggested_tags: metadata.suggested_tags,
|
||||
suggested_custom_tags: metadata.suggested_custom_tags,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
@@ -35,6 +35,26 @@ export async function getTagsByCategory(category: TagCategory): Promise<Tag[]> {
|
||||
return data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all system default tags
|
||||
*/
|
||||
export async function getAllSystemTags(): Promise<Tag[]> {
|
||||
const supabase = createServerActionClient({ cookies });
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('tags')
|
||||
.select('*')
|
||||
.eq('is_system_default', true)
|
||||
.order('name');
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching all system tags:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a custom user tag
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user