From fd168fea0e697077c6f6c7e4fb0fd7ad7a48be64 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 22 Dec 2025 10:45:05 +0100 Subject: [PATCH] feat: granular performance tracking and cache hit indicators for AI scans --- src/components/ScanAndTasteFlow.tsx | 71 ++++++++++++++++++++------ src/services/analyze-bottle-mistral.ts | 17 +++++- src/services/analyze-bottle.ts | 23 +++++++-- src/services/magic-scan.ts | 9 ++-- src/types/whisky.ts | 5 ++ 5 files changed, 102 insertions(+), 23 deletions(-) diff --git a/src/components/ScanAndTasteFlow.tsx b/src/components/ScanAndTasteFlow.tsx index 7a3a017..ba51f70 100644 --- a/src/components/ScanAndTasteFlow.tsx +++ b/src/components/ScanAndTasteFlow.tsx @@ -35,7 +35,14 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile }: ScanAnd const { locale } = useI18n(); const supabase = createClient(); 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 useEffect(() => { @@ -107,7 +114,10 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile }: ScanAnd if (isAdmin) { setPerfMetrics({ 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 }); } @@ -228,18 +238,32 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile }: ScanAnd {isAdmin && perfMetrics && ( -
-
+
+
-

Comp

+

Client

{perfMetrics.comp.toFixed(0)}ms

+

{(perfMetrics.uploadSize / 1024).toFixed(0)} KB

-

AI

-

{perfMetrics.ai.toFixed(0)}ms

+

AI Engine

+ {perfMetrics.aiApi === 0 ? ( +
+

CACHE HIT

+

DB RESULTS

+
+ ) : ( + <> +

{perfMetrics.aiTotal.toFixed(0)}ms

+
+ API: {perfMetrics.aiApi.toFixed(0)}ms + Parse: {perfMetrics.aiParse.toFixed(0)}ms +
+ + )}
-

Prep

+

App Logic

{perfMetrics.prep.toFixed(0)}ms

@@ -289,14 +313,29 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile }: ScanAnd activeSessionId={activeSession?.id} /> {isAdmin && perfMetrics && ( -
-
- - Comp: {perfMetrics.comp.toFixed(0)}ms - | - AI: {perfMetrics.ai.toFixed(0)}ms - | - Prep: {perfMetrics.prep.toFixed(0)}ms +
+
+
+ + CLIENT: + {perfMetrics.comp.toFixed(0)}ms + ({(perfMetrics.uploadSize / 1024).toFixed(0)}KB) +
+
+ AI: + {perfMetrics.aiApi === 0 ? ( + CACHE HIT ⚡ + ) : ( + <> + {perfMetrics.aiTotal.toFixed(0)}ms + (API: {perfMetrics.aiApi.toFixed(0)}ms / Pars: {perfMetrics.aiParse.toFixed(0)}ms) + + )} +
+
+ APP: + {perfMetrics.prep.toFixed(0)}ms +
)} diff --git a/src/services/analyze-bottle-mistral.ts b/src/services/analyze-bottle-mistral.ts index a4c4397..e912976 100644 --- a/src/services/analyze-bottle-mistral.ts +++ b/src/services/analyze-bottle-mistral.ts @@ -68,6 +68,11 @@ export async function analyzeBottleMistral(input: any): Promise 0 ? tags.join(', ') : 'Keine Tags verfügbar', locale); + const startApi = performance.now(); const chatResponse = await client.chat.complete({ model: 'mistral-large-latest', messages: [ @@ -93,7 +100,9 @@ export async function analyzeBottleMistral(input: any): Promise { // 4. Hash für Cache erstellen (direkt vom Buffer -> sehr schnell) const imageHash = createHash('sha256').update(buffer).digest('hex'); - // Cache Check const { data: cachedResult } = await supabase .from('vision_cache') @@ -69,7 +68,15 @@ export async function analyzeBottle(input: any): Promise { .maybeSingle(); 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 @@ -77,10 +84,12 @@ export async function analyzeBottle(input: any): Promise { // extrem effizient. Das Problem vorher war der JSON Parser von Next.js. const base64Data = buffer.toString('base64'); const mimeType = file.type || 'image/webp'; // Fallback + const uploadSize = buffer.length; const instruction = getSystemPrompt(tags.length > 0 ? tags.join(', ') : 'No tags available', locale); // API Call + const startApi = performance.now(); const result = await geminiModel.generateContent([ { inlineData: { @@ -90,7 +99,9 @@ export async function analyzeBottle(input: any): Promise { }, { text: instruction }, ]); + const endApi = performance.now(); + const startParse = performance.now(); const responseText = result.response.text(); // JSON Parsing der ANTWORT (das ist klein, das schafft der N100 locker) @@ -111,6 +122,7 @@ export async function analyzeBottle(input: any): Promise { if (!jsonData) throw new Error('Keine Daten in der KI-Antwort gefunden.'); const validatedData = BottleMetadataSchema.parse(jsonData); + const endParse = performance.now(); // 6. Tracking & Credits (bleibt gleich) await trackApiUsage({ @@ -132,7 +144,12 @@ export async function analyzeBottle(input: any): Promise { return { success: true, data: validatedData, - search_string: searchString + search_string: searchString, + perf: { + apiDuration: endApi - startApi, + parseDuration: endParse - startParse, + uploadSize: uploadSize + } } as any; } catch (error) { diff --git a/src/services/magic-scan.ts b/src/services/magic-scan.ts index 5036f05..dd07eee 100644 --- a/src/services/magic-scan.ts +++ b/src/services/magic-scan.ts @@ -79,7 +79,8 @@ export async function magicScan(input: any): Promise