Show all analysis phases in token chart legend and preview

This commit is contained in:
Nico committed 2026-10-21 21:39:26 +02:00
1 parent 58fc00601e
commit cbb69a2115
6 files changed
+70 -7

No files matched your search

@@ -0,0 +1,19 @@
# Kreisdiagramm über alle Phasen – 2026-10-21
Ausgangspunkt `58fc006`. Die tatsächliche Phasenzuordnung unterstützte bereits den
gesamten Ablauf, aber die erste Browservorschau enthielt ausschließlich synthetische
Untersuchungs-/Korrekturaufrufe. Die Vorschau wurde deshalb um Fallaufbau, Planung,
Hypothesen, Diagnose und Codingauftrag ergänzt. Die Legende zeigt diese Phasen nun
auch dann an, wenn noch kein Verbrauch gemessen wurde; Nullwerte erhalten keinen
erfundenen Flächenanteil.
Der neue Regressionstest prüft die Zuordnung aller sechs LLM-Phasenkategorien sowie
die zusätzliche, disjunkte Korrekturkategorie. Ein gemeinsamer Planungsaufruf wird
weiterhin nicht doppelt gezählt. Die Browserprüfung verlangt jede Phasenkategorie
in der Legende und prüft weiterhin Projektfilter, Mobilansicht und Abmeldung.
Die neue Vorschau enthält 35.300 synthetische Tokens und 22,1 % Korrekturanteil. Ein
abgeschlossener Testfall steuert auch Diagnose und Codingauftrag bei; der noch
laufende Testfall enthält keine erfundenen künftigen Abschlussaufrufe. Dies sind
ausschließlich Testdaten, keine tatsächlichen Providerkosten. Der vorherige
Prüfbericht bleibt als eigener Snapshot erhalten.
+3
View File
@@ -38,6 +38,9 @@ Telemetrie-Heartbeat wird nicht als aktiver Worker dargestellt.
- Kreisdiagramm: bekannte Eingabe- und Ausgabetokens je Phase; zusätzliche
Korrektur-/Reparatur-/Wiederholungsaufrufe haben ein eigenes orangefarbenes Segment.
Die Legende zeigt Fallaufbau, Fallplanung, Hypothesen, Untersuchung, Diagnose und
Codingauftrag auch bei null gemessenen Tokens. Nullwerte erzeugen keinen
künstlichen Tortenanteil. Gemeinsame Planungsaufrufe bleiben einmal gezählt.
- Balkendiagramm: Eingabe und Ausgabe einschließlich Denken je Kategorie.
- Der erste reguläre Aufruf bleibt bei seiner Phase, auch wenn sein Ergebnis später
zurückgewiesen wurde. Seine Korrektur wird ausschließlich dem Korrektursegment
+2 -1
View File
@@ -57,7 +57,8 @@ function renderPie(buckets) {
const legend=$('legend'); legend.replaceChildren(); let angle=-Math.PI/2;
if(!total) {ctx.beginPath();ctx.arc(180,180,140,0,Math.PI*2);ctx.fillStyle='#edf0f7';ctx.fill();}
buckets.forEach((b,i)=>{
const count=b.input_tokens+b.output_tokens; if(!count&&!b.calls)return;
const count=b.input_tokens+b.output_tokens;
if(b.id==='unknown'&&!count&&!b.calls)return;
const color=colors[i%colors.length]; if(total&&count) {const end=angle+count/total*Math.PI*2;
ctx.beginPath();ctx.moveTo(180,180);ctx.arc(180,180,140,angle,end);ctx.closePath();ctx.fillStyle=color;ctx.fill();angle=end;}
const item=document.createElement('li'), swatch=text('span','','swatch');swatch.style.background=color;
+6 -2
View File
@@ -30,15 +30,19 @@ try {
await send('Page.navigate',{url:`http://127.0.0.1:${port}/`});
await until(()=>evaluate("document.readyState === 'complete' && typeof render === 'function'"));
await evaluate("document.getElementById('token').value='ui_fixture_"+'x'.repeat(40)+"';document.getElementById('login-form').dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));");
await until(()=>evaluate("document.getElementById('token-count').textContent==='37.800'"));
await until(()=>evaluate("document.getElementById('token-count').textContent==='35.300'"));
const state=await evaluate("({workers:document.querySelectorAll('.worker').length,cases:document.querySelectorAll('#case-rows tr').length,active:document.getElementById('active-count').textContent,correction:document.getElementById('correction-count').textContent,overflow:document.documentElement.scrollWidth>innerWidth,stored:localStorage.length,progress:document.querySelector('.worker [role=progressbar]').getAttribute('aria-valuenow')})");
if(state.workers!==1||state.cases!==1||state.active!=='1'||state.stored!==0||state.overflow||state.progress!=='57')throw new Error(JSON.stringify(state));
const categories=await evaluate("Array.from(document.querySelectorAll('#legend li')).map(e=>e.textContent)");
for(const name of ['Fallaufbau','Fallplanung','Hypothesen','Untersuchung','Diagnose','Codingauftrag','Korrekturen']) {
if(!categories.some(c=>c.includes(name)))throw new Error(`Missing phase in pie legend: ${name}`);
}
await writeFile(path.join(output,'desktop.png'),Buffer.from((await send('Page.captureScreenshot',{format:'png'})).data,'base64'));
await send('Emulation.setDeviceMetricsOverride',{width:390,height:1200,deviceScaleFactor:1,mobile:true});await pause(200);
if(await evaluate('document.documentElement.scrollWidth>innerWidth'))throw new Error('Mobile horizontal overflow');
await writeFile(path.join(output,'mobile.png'),Buffer.from((await send('Page.captureScreenshot',{format:'png'})).data,'base64'));
await evaluate("document.getElementById('project-filter').value='viano-core';document.getElementById('project-filter').dispatchEvent(new Event('change')); ");
if(await evaluate("document.getElementById('token-count').textContent")!=='37.800')throw new Error('Project aggregation differs');
if(await evaluate("document.getElementById('token-count').textContent")!=='35.300')throw new Error('Project aggregation differs');
await evaluate("document.getElementById('kind').value='case_return';document.getElementById('kind').dispatchEvent(new Event('change'));");
if(!await evaluate("document.getElementById('new-files').hidden&&!document.getElementById('return-files').hidden"))throw new Error('Return form not selected');
await evaluate("document.getElementById('logout').click()");
+25 -1
View File
@@ -9,7 +9,7 @@ from starlette.testclient import TestClient
from viano_bug_analysis.api import APIConfig, Client, Project
from viano_bug_analysis.api.app import create_app
from viano_bug_analysis.api.dashboard import aggregate, case_view
from viano_bug_analysis.api.dashboard import aggregate, case_view, phase
from viano_bug_analysis.llm.accounting import classification
from viano_bug_analysis.lifecycle.storage import atomic_json
from test_case_builder import case_setup
@@ -75,6 +75,30 @@ def test_unknown_usage_is_not_reported_as_complete():
assert next(b for b in result['buckets'] if b['id']=='unknown')['output_tokens']==5
def test_pie_accounts_for_whole_pipeline_and_separates_corrections():
stages = [
('BUILDING_CASE', 'entity_resolution_plan_v4', 'building'),
('INTERPRETING_CASE', 'case_plan_v2', 'planning'),
('GENERATING_HYPOTHESES', 'hypotheses_v4', 'hypotheses'),
('INVESTIGATING_HYPOTHESES', 'investigation_lean_v21', 'investigation'),
('BUILDING_DIAGNOSIS', 'diagnosis_v8', 'diagnosis'),
('BUILDING_DIAGNOSIS', 'change_request_v9', 'coding'),
]
calls = []
for state, prompt, expected in stages:
assert phase({'phase': state}, prompt) == expected
calls.append({'bucket': expected, 'category':'regular', 'phase':expected,
'day':'2026-10-21', 'input_tokens':10, 'output_tokens':2,
'reasoning_tokens':1, 'cached_input_tokens':0})
calls.append(calls[3] | {'bucket':'correction','category':'correction'})
buckets = {b['id']:b for b in aggregate([{'calls':calls}])['buckets']}
assert all(buckets[expected]['calls']==1 for _, _, expected in stages)
assert buckets['correction']['calls']==1
assert sum(b['input_tokens']+b['output_tokens'] for b in buckets.values())==84
empty = {b['id']:b for b in aggregate([])['buckets']}
assert all(expected in empty for _, _, expected in stages)
@pytest.fixture(autouse=True)
def project_manifest(manifest):
manifest['project']={'id':'p','type':'wow','profile':'vcore'}
+15 -3
View File
@@ -37,14 +37,26 @@ def seed(root, case_id, title, completed=False):
ref=pointer+'.json';store.advance(*identity,state,step_id=state,
artifacts={pointer:ref},additions={ref:json_bytes({'analysis_revision':1})})
store.advance(*identity,'INVESTIGATING_HYPOTHESES',step_id='investigate')
for i,(category,inp,out) in enumerate([('regular',12000,3000),('correction',3000,900)]):
measured = [
('BUILDING_CASE','entity_resolution_plan_v4','regular',2500,500),
('INTERPRETING_CASE','case_interpretation_v1','regular',2000,500),
('GENERATING_HYPOTHESES','hypotheses_v4','regular',1500,500),
('INVESTIGATING_HYPOTHESES','investigation_lean_v21','regular',4000,1000),
]
if completed:
measured += [
('BUILDING_DIAGNOSIS','diagnosis_v8','regular',1500,500),
('BUILDING_DIAGNOSIS','change_request_v9','regular',500,0),
]
measured.append(('INVESTIGATING_HYPOTHESES','investigation_format_repair_v1','correction',3000,900))
for i,(phase,prompt,category,inp,out) in enumerate(measured):
ref=f'call{i}.json';request=f'request{i}.json'
call={'rounds':1,'phase':'INVESTIGATING_HYPOTHESES','request_ref':request,'outcome':{
call={'rounds':1,'phase':phase,'request_ref':request,'outcome':{
'usage_classification':category,'metadata':{'input_tokens':inp,'output_tokens':out,
'reasoning_tokens':out//2,'cached_input_tokens':1000}}}
store.advance(*identity,'INVESTIGATING_HYPOTHESES',step_id=f'call{i}',
details={'operation':'llm-round','result_ref':ref},
additions={ref:json_bytes(call),request:json_bytes({'prompt_version':'investigation_lean_v21'})})
additions={ref:json_bytes(call),request:json_bytes({'prompt_version':prompt})})
if completed:
store.advance(*identity,'BUILDING_DIAGNOSIS',step_id='diagnosis')
store.advance(*identity,'OUTPUT_READY',step_id='done')