feat: granular performance tracking and cache hit indicators for AI scans
This commit is contained in:
@@ -35,7 +35,14 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile }: ScanAnd
|
|||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
const [perfMetrics, setPerfMetrics] = useState<{ comp: number; ai: number; prep: number } | null>(null);
|
const [perfMetrics, setPerfMetrics] = useState<{
|
||||||
|
comp: number;
|
||||||
|
aiTotal: number;
|
||||||
|
aiApi: number;
|
||||||
|
aiParse: number;
|
||||||
|
uploadSize: number;
|
||||||
|
prep: number
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
// Admin Check
|
// Admin Check
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -107,7 +114,10 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile }: ScanAnd
|
|||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
setPerfMetrics({
|
setPerfMetrics({
|
||||||
comp: endComp - startComp,
|
comp: endComp - startComp,
|
||||||
ai: endAi - startAi,
|
aiTotal: endAi - startAi,
|
||||||
|
aiApi: result.perf?.apiDuration || 0,
|
||||||
|
aiParse: result.perf?.parseDuration || 0,
|
||||||
|
uploadSize: result.perf?.uploadSize || 0,
|
||||||
prep: endPrep - startPrep
|
prep: endPrep - startPrep
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -228,18 +238,32 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile }: ScanAnd
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{isAdmin && perfMetrics && (
|
{isAdmin && perfMetrics && (
|
||||||
<div className="mt-8 p-4 bg-black/40 backdrop-blur-md rounded-2xl border border-orange-500/20 text-[10px] font-mono text-zinc-400 animate-in fade-in slide-in-from-bottom-2">
|
<div className="mt-8 p-6 bg-zinc-950/80 backdrop-blur-xl rounded-3xl border border-orange-500/20 text-[10px] font-mono text-zinc-400 animate-in fade-in slide-in-from-bottom-4 shadow-2xl">
|
||||||
<div className="grid grid-cols-3 gap-4 text-center">
|
<div className="grid grid-cols-3 gap-6 text-center">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-zinc-500 mb-1 uppercase tracking-widest text-[8px]">Comp</p>
|
<p className="text-zinc-500 mb-2 uppercase tracking-widest text-[8px]">Client</p>
|
||||||
<p className="text-orange-500 font-bold">{perfMetrics.comp.toFixed(0)}ms</p>
|
<p className="text-orange-500 font-bold">{perfMetrics.comp.toFixed(0)}ms</p>
|
||||||
|
<p className="text-[8px] opacity-40 mt-1">{(perfMetrics.uploadSize / 1024).toFixed(0)} KB</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-zinc-500 mb-1 uppercase tracking-widest text-[8px]">AI</p>
|
<p className="text-zinc-500 mb-2 uppercase tracking-widest text-[8px]">AI Engine</p>
|
||||||
<p className="text-orange-500 font-bold">{perfMetrics.ai.toFixed(0)}ms</p>
|
{perfMetrics.aiApi === 0 ? (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<p className="text-green-500 font-bold tracking-tighter">CACHE HIT</p>
|
||||||
|
<p className="text-[7px] opacity-40 mt-1">DB RESULTS</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-orange-500 font-bold">{perfMetrics.aiTotal.toFixed(0)}ms</p>
|
||||||
|
<div className="flex flex-col gap-0.5 mt-1 text-[7px] opacity-60">
|
||||||
|
<span>API: {perfMetrics.aiApi.toFixed(0)}ms</span>
|
||||||
|
<span>Parse: {perfMetrics.aiParse.toFixed(0)}ms</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-zinc-500 mb-1 uppercase tracking-widest text-[8px]">Prep</p>
|
<p className="text-zinc-500 mb-2 uppercase tracking-widest text-[8px]">App Logic</p>
|
||||||
<p className="text-orange-500 font-bold">{perfMetrics.prep.toFixed(0)}ms</p>
|
<p className="text-orange-500 font-bold">{perfMetrics.prep.toFixed(0)}ms</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,14 +313,29 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile }: ScanAnd
|
|||||||
activeSessionId={activeSession?.id}
|
activeSessionId={activeSession?.id}
|
||||||
/>
|
/>
|
||||||
{isAdmin && perfMetrics && (
|
{isAdmin && perfMetrics && (
|
||||||
<div className="absolute top-24 left-6 z-50 p-2 bg-black/60 backdrop-blur-md rounded-lg border border-orange-500/30 text-[9px] font-mono text-white/90 pointer-events-none">
|
<div className="absolute top-24 left-6 right-6 z-50 p-3 bg-zinc-950/80 backdrop-blur-md rounded-2xl border border-orange-500/20 text-[9px] font-mono text-white/90 shadow-xl overflow-x-auto">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between gap-4 whitespace-nowrap">
|
||||||
<Clock size={10} className="text-orange-500" />
|
<div className="flex items-center gap-2">
|
||||||
<span>Comp: {perfMetrics.comp.toFixed(0)}ms</span>
|
<Clock size={10} className="text-orange-500" />
|
||||||
<span className="opacity-30">|</span>
|
<span className="text-zinc-500">CLIENT:</span>
|
||||||
<span>AI: {perfMetrics.ai.toFixed(0)}ms</span>
|
<span className="text-orange-500 font-bold">{perfMetrics.comp.toFixed(0)}ms</span>
|
||||||
<span className="opacity-30">|</span>
|
<span className="text-zinc-600">({(perfMetrics.uploadSize / 1024).toFixed(0)}KB)</span>
|
||||||
<span>Prep: {perfMetrics.prep.toFixed(0)}ms</span>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-zinc-500">AI:</span>
|
||||||
|
{perfMetrics.aiApi === 0 ? (
|
||||||
|
<span className="text-green-500 font-bold tracking-tight">CACHE HIT ⚡</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="text-orange-500 font-bold">{perfMetrics.aiTotal.toFixed(0)}ms</span>
|
||||||
|
<span className="text-zinc-600 ml-1">(API: {perfMetrics.aiApi.toFixed(0)}ms / Pars: {perfMetrics.aiParse.toFixed(0)}ms)</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-zinc-500">APP:</span>
|
||||||
|
<span className="text-orange-500 font-bold">{perfMetrics.prep.toFixed(0)}ms</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ export async function analyzeBottleMistral(input: any): Promise<AnalysisResponse
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: cachedResult.result as any,
|
data: cachedResult.result as any,
|
||||||
|
perf: {
|
||||||
|
apiDuration: 0,
|
||||||
|
parseDuration: 0,
|
||||||
|
uploadSize: buffer.length
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,9 +81,11 @@ export async function analyzeBottleMistral(input: any): Promise<AnalysisResponse
|
|||||||
const base64Data = buffer.toString('base64');
|
const base64Data = buffer.toString('base64');
|
||||||
const mimeType = file.type || 'image/webp';
|
const mimeType = file.type || 'image/webp';
|
||||||
const dataUrl = `data:${mimeType};base64,${base64Data}`;
|
const dataUrl = `data:${mimeType};base64,${base64Data}`;
|
||||||
|
const uploadSize = buffer.length;
|
||||||
|
|
||||||
const prompt = getSystemPrompt(tags.length > 0 ? tags.join(', ') : 'Keine Tags verfügbar', locale);
|
const prompt = getSystemPrompt(tags.length > 0 ? tags.join(', ') : 'Keine Tags verfügbar', locale);
|
||||||
|
|
||||||
|
const startApi = performance.now();
|
||||||
const chatResponse = await client.chat.complete({
|
const chatResponse = await client.chat.complete({
|
||||||
model: 'mistral-large-latest',
|
model: 'mistral-large-latest',
|
||||||
messages: [
|
messages: [
|
||||||
@@ -93,7 +100,9 @@ export async function analyzeBottleMistral(input: any): Promise<AnalysisResponse
|
|||||||
responseFormat: { type: 'json_object' },
|
responseFormat: { type: 'json_object' },
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
});
|
});
|
||||||
|
const endApi = performance.now();
|
||||||
|
|
||||||
|
const startParse = performance.now();
|
||||||
const rawContent = chatResponse.choices?.[0].message.content;
|
const rawContent = chatResponse.choices?.[0].message.content;
|
||||||
if (!rawContent) throw new Error("Keine Antwort von Mistral");
|
if (!rawContent) throw new Error("Keine Antwort von Mistral");
|
||||||
|
|
||||||
@@ -121,6 +130,7 @@ export async function analyzeBottleMistral(input: any): Promise<AnalysisResponse
|
|||||||
if (jsonData.vintage) jsonData.vintage = parseInt(jsonData.vintage);
|
if (jsonData.vintage) jsonData.vintage = parseInt(jsonData.vintage);
|
||||||
|
|
||||||
const validatedData = BottleMetadataSchema.parse(jsonData);
|
const validatedData = BottleMetadataSchema.parse(jsonData);
|
||||||
|
const endParse = performance.now();
|
||||||
|
|
||||||
// Track usage
|
// Track usage
|
||||||
await trackApiUsage({
|
await trackApiUsage({
|
||||||
@@ -141,7 +151,12 @@ export async function analyzeBottleMistral(input: any): Promise<AnalysisResponse
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: validatedData,
|
data: validatedData,
|
||||||
search_string: searchString
|
search_string: searchString,
|
||||||
|
perf: {
|
||||||
|
apiDuration: endApi - startApi,
|
||||||
|
parseDuration: endParse - startParse,
|
||||||
|
uploadSize: uploadSize
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
|
|||||||
|
|
||||||
// 4. Hash für Cache erstellen (direkt vom Buffer -> sehr schnell)
|
// 4. Hash für Cache erstellen (direkt vom Buffer -> sehr schnell)
|
||||||
const imageHash = createHash('sha256').update(buffer).digest('hex');
|
const imageHash = createHash('sha256').update(buffer).digest('hex');
|
||||||
|
|
||||||
// Cache Check
|
// Cache Check
|
||||||
const { data: cachedResult } = await supabase
|
const { data: cachedResult } = await supabase
|
||||||
.from('vision_cache')
|
.from('vision_cache')
|
||||||
@@ -69,7 +68,15 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
|
|||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
if (cachedResult) {
|
if (cachedResult) {
|
||||||
return { success: true, data: cachedResult.result as any };
|
return {
|
||||||
|
success: true,
|
||||||
|
data: cachedResult.result as any,
|
||||||
|
perf: {
|
||||||
|
apiDuration: 0,
|
||||||
|
parseDuration: 0,
|
||||||
|
uploadSize: buffer.length
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Für Gemini vorbereiten
|
// 5. Für Gemini vorbereiten
|
||||||
@@ -77,10 +84,12 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
|
|||||||
// extrem effizient. Das Problem vorher war der JSON Parser von Next.js.
|
// extrem effizient. Das Problem vorher war der JSON Parser von Next.js.
|
||||||
const base64Data = buffer.toString('base64');
|
const base64Data = buffer.toString('base64');
|
||||||
const mimeType = file.type || 'image/webp'; // Fallback
|
const mimeType = file.type || 'image/webp'; // Fallback
|
||||||
|
const uploadSize = buffer.length;
|
||||||
|
|
||||||
const instruction = getSystemPrompt(tags.length > 0 ? tags.join(', ') : 'No tags available', locale);
|
const instruction = getSystemPrompt(tags.length > 0 ? tags.join(', ') : 'No tags available', locale);
|
||||||
|
|
||||||
// API Call
|
// API Call
|
||||||
|
const startApi = performance.now();
|
||||||
const result = await geminiModel.generateContent([
|
const result = await geminiModel.generateContent([
|
||||||
{
|
{
|
||||||
inlineData: {
|
inlineData: {
|
||||||
@@ -90,7 +99,9 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
|
|||||||
},
|
},
|
||||||
{ text: instruction },
|
{ text: instruction },
|
||||||
]);
|
]);
|
||||||
|
const endApi = performance.now();
|
||||||
|
|
||||||
|
const startParse = performance.now();
|
||||||
const responseText = result.response.text();
|
const responseText = result.response.text();
|
||||||
|
|
||||||
// JSON Parsing der ANTWORT (das ist klein, das schafft der N100 locker)
|
// JSON Parsing der ANTWORT (das ist klein, das schafft der N100 locker)
|
||||||
@@ -111,6 +122,7 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
|
|||||||
if (!jsonData) throw new Error('Keine Daten in der KI-Antwort gefunden.');
|
if (!jsonData) throw new Error('Keine Daten in der KI-Antwort gefunden.');
|
||||||
|
|
||||||
const validatedData = BottleMetadataSchema.parse(jsonData);
|
const validatedData = BottleMetadataSchema.parse(jsonData);
|
||||||
|
const endParse = performance.now();
|
||||||
|
|
||||||
// 6. Tracking & Credits (bleibt gleich)
|
// 6. Tracking & Credits (bleibt gleich)
|
||||||
await trackApiUsage({
|
await trackApiUsage({
|
||||||
@@ -132,7 +144,12 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: validatedData,
|
data: validatedData,
|
||||||
search_string: searchString
|
search_string: searchString,
|
||||||
|
perf: {
|
||||||
|
apiDuration: endApi - startApi,
|
||||||
|
parseDuration: endParse - startParse,
|
||||||
|
uploadSize: uploadSize
|
||||||
|
}
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ export async function magicScan(input: any): Promise<AnalysisResponse & { wb_id?
|
|||||||
console.log(`[Magic Scan] Cache Hit for ${searchString}: ${cacheHit.wb_id}`);
|
console.log(`[Magic Scan] Cache Hit for ${searchString}: ${cacheHit.wb_id}`);
|
||||||
return {
|
return {
|
||||||
...aiResponse,
|
...aiResponse,
|
||||||
wb_id: cacheHit.wb_id
|
wb_id: cacheHit.wb_id,
|
||||||
|
perf: aiResponse.perf
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,13 +107,15 @@ export async function magicScan(input: any): Promise<AnalysisResponse & { wb_id?
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...aiResponse,
|
...aiResponse,
|
||||||
wb_id: braveResult.id
|
wb_id: braveResult.id,
|
||||||
|
perf: aiResponse.perf
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...aiResponse,
|
...aiResponse,
|
||||||
wb_id: undefined
|
wb_id: undefined,
|
||||||
|
perf: aiResponse.perf
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -96,4 +96,9 @@ export interface AnalysisResponse {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
data?: BottleMetadata;
|
data?: BottleMetadata;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
perf?: {
|
||||||
|
apiDuration: number;
|
||||||
|
parseDuration: number;
|
||||||
|
uploadSize: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user