From e5a6946d8bbed90a35d8e123eb31bbdb886c54ae Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 2 Apr 2026 11:25:44 +0200 Subject: [PATCH] feat(manacore/web): add model comparison tab to LLM test page Add a "Compare" tab that sequentially runs the same prompt against all available models (currently Qwen 2.5 1.5B and 0.5B), showing results side-by-side with a stats table (latency, tok/s, token counts) and streaming preview during inference. Also includes fixes from earlier: $derived.by for statusText, removed unused generateText import, added chat auto-scroll with max-height. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/routes/(app)/llm-test/+page.svelte | 406 ++++++++++++++---- 1 file changed, 329 insertions(+), 77 deletions(-) diff --git a/apps/manacore/apps/web/src/routes/(app)/llm-test/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/llm-test/+page.svelte index fd3362c54..6a9b0d70b 100644 --- a/apps/manacore/apps/web/src/routes/(app)/llm-test/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/llm-test/+page.svelte @@ -12,9 +12,11 @@ } from '@manacore/local-llm'; import { Robot, Trash, PaperPlaneRight } from '@manacore/shared-icons'; + const modelKeys = Object.keys(MODELS) as ModelKey[]; + // --- State --- let selectedModel: ModelKey = $state('qwen-2.5-1.5b'); - let activeTab: 'chat' | 'extract' | 'classify' = $state('chat'); + let activeTab: 'chat' | 'extract' | 'classify' | 'compare' = $state('chat'); const supported = isLocalLlmSupported(); const status = getLocalLlmStatus(); @@ -42,6 +44,27 @@ let classifyResult = $state(''); let classifyLoading = $state(false); + // Compare tab + interface CompareResult { + model: ModelKey; + displayName: string; + content: string; + latencyMs: number; + promptTokens: number; + completionTokens: number; + tokPerSec: number; + error?: string; + } + + let comparePrompt = $state(''); + let compareSystemPrompt = $state(''); + let compareTemperature = $state(0.7); + let compareMaxTokens = $state(1024); + let compareResults = $state([]); + let compareRunning = $state(false); + let compareCurrentModel = $state(null); + let compareStreamingContent = $state(''); + // --- Derived --- let isReady = $derived(status.current.state === 'ready'); let isLoading = $derived( @@ -108,7 +131,6 @@ if (systemPrompt.trim()) { msgs.push({ role: 'system', content: systemPrompt.trim() }); } - // Include conversation history for (const m of messages) { msgs.push({ role: m.role, content: m.content }); } @@ -174,6 +196,75 @@ } } + async function handleCompare() { + if (!comparePrompt.trim() || compareRunning) return; + compareRunning = true; + compareResults = []; + compareStreamingContent = ''; + + const msgs: { role: 'system' | 'user'; content: string }[] = []; + if (compareSystemPrompt.trim()) { + msgs.push({ role: 'system', content: compareSystemPrompt.trim() }); + } + msgs.push({ role: 'user', content: comparePrompt.trim() }); + + for (const modelKey of modelKeys) { + compareCurrentModel = MODELS[modelKey].displayName; + compareStreamingContent = ''; + + try { + await loadLocalLlm(modelKey); + + const result = await generate({ + messages: msgs, + temperature: compareTemperature, + maxTokens: compareMaxTokens, + onToken: (token) => { + compareStreamingContent += token; + }, + }); + + const tokPerSec = + result.latencyMs > 0 + ? Math.round((result.usage.completion_tokens / result.latencyMs) * 1000) + : 0; + + compareResults = [ + ...compareResults, + { + model: modelKey, + displayName: MODELS[modelKey].displayName, + content: result.content, + latencyMs: result.latencyMs, + promptTokens: result.usage.prompt_tokens, + completionTokens: result.usage.completion_tokens, + tokPerSec, + }, + ]; + } catch (err) { + compareResults = [ + ...compareResults, + { + model: modelKey, + displayName: MODELS[modelKey].displayName, + content: '', + latencyMs: 0, + promptTokens: 0, + completionTokens: 0, + tokPerSec: 0, + error: err instanceof Error ? err.message : String(err), + }, + ]; + } + + await unloadLocalLlm(); + } + + compareCurrentModel = null; + compareStreamingContent = ''; + compareRunning = false; + } + function handleClear() { messages = []; streamingContent = ''; @@ -194,7 +285,6 @@
-

Local LLM Test

@@ -202,7 +292,6 @@

- {#if !supported}

WebGPU nicht verfügbar

@@ -212,79 +301,78 @@

{:else} - -
-
- -
- - -
- - -
- Download: ~{modelInfo.downloadSizeMb} MB - RAM: ~{modelInfo.ramUsageMb} MB -
- - -
- {#if isReady} - - {:else} - - {/if} + {#each Object.entries(MODELS) as [key, model]} + + {/each} + +
+ +
+ Download: ~{modelInfo.downloadSizeMb} MB + RAM: ~{modelInfo.ramUsageMb} MB +
+ +
+ {#if isReady} + + {:else} + + {/if} +
+ +
+
+ {statusText} +
- -
-
- {statusText} -
+ {#if progress !== null} +
+
+
+ {/if}
- - - {#if progress !== null} -
-
-
- {/if} -
+ {/if}
- {#each [{ id: 'chat', label: 'Chat' }, { id: 'extract', label: 'JSON Extract' }, { id: 'classify', label: 'Classify' }] as tab} + {#each [{ id: 'chat', label: 'Chat' }, { id: 'extract', label: 'JSON Extract' }, { id: 'classify', label: 'Classify' }, { id: 'compare', label: 'Compare' }] as tab} +
+ + + + {#if compareRunning && compareCurrentModel} +
+
+
+ {compareCurrentModel} + + ({compareResults.length + 1}/{modelKeys.length}) + +
+ {#if compareStreamingContent} +
+ {compareStreamingContent}| +
+ {:else} +
{statusText}
+ {/if} +
+ {/if} + + + {#if compareResults.length > 0} + +
+ + + + + {#each compareResults as r} + + {/each} + + + + + + {#each compareResults as r} + + {/each} + + + + {#each compareResults as r} + + {/each} + + + + {#each compareResults as r} + + {/each} + + + + {#each compareResults as r} + + {/each} + + +
Metrik + {r.displayName} +
Latenz + {r.error ? 'Fehler' : `${(r.latencyMs / 1000).toFixed(1)}s`} +
Speed + {r.error ? '—' : `${r.tokPerSec} tok/s`} +
Prompt Tokens + {r.error ? '—' : r.promptTokens} +
Completion Tokens + {r.error ? '—' : r.completionTokens} +
+
+ + +
+ {#each compareResults as r} +
+
+ {r.displayName} + {(r.latencyMs / 1000).toFixed(1)}s +
+ {#if r.error} +
+ {r.error} +
+ {:else} +
+ {r.content} +
+ {/if} +
+ {/each} +
+ {/if} + + {/if} {/if}