import { useIdentity, useAILookup, useUpload } from '@deplixo/sdk'; function App() { const { user } = useIdentity(); const upload = useUpload(); const lookup = useAILookup({ example: { groups: [ { label: 'Main south array', rows: 4, columns: 8, count: 32, notes: 'Rectangular grid on south-facing pitch, portrait orientation.', }, ], totalCount: 32, reasoning: 'Used the calibration ruler (panel width ≈ X m, panel length ≈ Y m) to reject internal cell lines and roof seams. Identified two rectangular arrays and multiplied rows × columns.', confidence: 'high', }, }); const [imageUrl, setImageUrl] = React.useState(null); const [imageName, setImageName] = React.useState(''); const [uploading, setUploading] = React.useState(false); const [uploadError, setUploadError] = React.useState(''); const [widthMetres, setWidthMetres] = React.useState(''); const [widthPanels, setWidthPanels] = React.useState(''); const [lengthMetres, setLengthMetres] = React.useState(''); const [lengthPanels, setLengthPanels] = React.useState(''); const [proofs, setProofs] = React.useState(null); // [A, B, C, D, E, F] const [analysing, setAnalysing] = React.useState(false); const [error, setError] = React.useState(''); const wM = parseFloat(widthMetres); const wN = parseFloat(widthPanels); const lM = parseFloat(lengthMetres); const lN = parseFloat(lengthPanels); const widthValid = !isNaN(wM) && !isNaN(wN) && wM > 0 && wN > 0; const lengthValid = !isNaN(lM) && !isNaN(lN) && lM > 0 && lN > 0; const panelWidth = widthValid ? wM / wN : null; const panelLength = lengthValid ? lM / lN : null; const canAnalyse = imageUrl && (widthValid || lengthValid) && !analysing; // Fresh upload flow: open the app's upload tool, save the returned URL // before any analysis can start. const handleUploadClick = async () => { setUploadError(''); setError(''); setProofs(null); setUploading(true); try { const res = await upload({ accept: 'image/*' }); if (!res) { // User cancelled — leave state unchanged, no error. return; } const url = res?.url || res?.href || (typeof res === 'string' ? res : null); const name = res?.name || res?.filename || ''; if (!url) { throw new Error('Upload did not return a URL'); } // Save the photo before analysis is allowed. setImageUrl(url); setImageName(name); } catch (err) { setImageUrl(null); setImageName(''); setUploadError( "We couldn't save your photo. Please try uploading it again. (" + (err?.message || String(err)) + ')' ); } finally { setUploading(false); } }; const buildPrompt = (proofLabel, strategyLine) => { const lines = []; lines.push(`You are counting solar panels in a bird’s-eye photograph of a building roof. This is ${proofLabel} — one of six MANDATORY INDEPENDENT PROOFS. Do NOT rely on other proofs; reach your own conclusion.`); lines.push(''); lines.push('CALIBRATION (authoritative — do not override):'); if (widthValid) { lines.push( `- Panel-WIDTH direction: ${wM} m spans exactly ${wN} panel(s) → one panel is ${panelWidth.toFixed(3)} m wide.` ); } if (lengthValid) { lines.push( `- Panel-LENGTH direction: ${lM} m spans exactly ${lN} panel(s) → one panel is ${panelLength.toFixed(3)} m long.` ); } if (!widthValid || !lengthValid) { lines.push('- Only one calibration direction was supplied. Use only what is given. Do NOT assume a standard panel size.'); } lines.push(''); lines.push(`STRATEGY FOR ${proofLabel}: ${strategyLine}`); lines.push(''); lines.push('RULES:'); lines.push('1. Treat the calculated dimension(s) above as the ONLY permitted scale ruler.'); lines.push('2. Use the ruler to reject internal cell lines, centre lines, shadows, roof seams, rails, mounting gaps, glare, dirt lines, compression artefacts, roof-sheet lines, and objects beside/under panels.'); lines.push('3. Do not assume a visible line is a panel boundary just because it resembles one — verify against the ruler.'); lines.push('4. Identify each distinct rectangular array/group and count rows × columns.'); lines.push('5. Sum all groups for the total.'); lines.push(''); lines.push('Return: an array of groups (each with a label, rows, columns, count, and notes), the totalCount, a short reasoning string that references the ruler dimensions AND the strategy above, and confidence (low/medium/high).'); return lines.join('\n'); }; const PROOFS = [ { label: 'COUNT A', strategy: 'Work TOP-DOWN group by group. For each rectangular array, count the number of full rows first, then columns, then multiply. Do not use any other method.', }, { label: 'COUNT B', strategy: 'Work LEFT-TO-RIGHT column by column. For each rectangular array, count the number of full columns first, then rows, then multiply. Do not use any other method.', }, { label: 'COUNT C', strategy: 'Work BY AREA. For each rectangular array, estimate its total footprint using the ruler, divide by one panel’s area (width × length, or the single available dimension applied consistently), then cross-check rows × columns. Report the cross-checked figure.', }, { label: 'AUDIT D — full row-sum', strategy: 'Perform a FULL ROW-SUM AUDIT. For every group, enumerate each individual row across the entire array (row 1, row 2, …), record how many panels sit in that specific row, and sum those per-row tallies to obtain the group total. This tests whether every row is fully populated and catches missing or partial rows the row×column shortcut can miss. Report the summed figure as the group count.', }, { label: 'AUDIT E — perpendicular column-sum', strategy: 'Perform a PERPENDICULAR COLUMN-SUM AUDIT, orthogonal to Audit D. For every group, enumerate each individual column (column 1, column 2, …), record how many panels sit in that specific column, and sum those per-column tallies to obtain the group total. If the column-sum disagrees with the row-sum for the same group, flag it in notes. Report the column-summed figure as the group count.', }, { label: 'AUDIT F — calibrated geometric reconstruction', strategy: 'Perform a CALIBRATED GEOMETRIC RECONSTRUCTION AUDIT. Using the calibration ruler, measure each array’s bounding box in metres directly from the image (long side and short side). Divide the long side by the appropriate panel dimension and the short side by the other panel dimension to derive rows and columns purely from geometry, independent of visually tracing panel edges. Multiply to get the group count and note the reconstructed bounding box in the group notes (e.g. "≈ 12.4 m × 6.6 m → 6 × 4").', }, ]; const analyse = async () => { if (!canAnalyse) return; setAnalysing(true); setError(''); setProofs(null); try { const runs = await Promise.all( PROOFS.map((p) => lookup.run([ { type: 'text', text: buildPrompt(p.label, p.strategy) }, { type: 'image', image: imageUrl }, ]) ) ); setProofs(runs); } catch (err) { setError('Analysis failed: ' + (err.message || String(err))); } finally { setAnalysing(false); } }; const reset = () => { setImageUrl(null); setImageName(''); setProofs(null); setError(''); setUploadError(''); }; const totals = proofs ? proofs.map((r) => r?.totalCount) : []; // Majority across all six proofs const tally = {}; totals.forEach((t) => { if (typeof t === 'number') tally[t] = (tally[t] || 0) + 1; }); let majorityTotal = null; let majorityCount = 0; Object.entries(tally).forEach(([k, v]) => { if (v > majorityCount) { majorityCount = v; majorityTotal = Number(k); } }); const allAgree = proofs && majorityCount === proofs.length; const strongAgree = proofs && !allAgree && majorityCount >= Math.ceil(proofs.length / 2) + 1; const someAgree = proofs && !allAgree && !strongAgree && majorityCount >= 2; const agreedTotal = (allAgree || strongAgree || someAgree) ? majorityTotal : null; const proofSlug = ['a', 'b', 'c', 'd', 'e', 'f']; return (

Solar Panel Counter

Upload a bird’s-eye view of a roof, supply a known-scale measurement, and get a calibrated count with six independent proofs.

1 · Calibration

Supply at least one direction. The number of panels you state is authoritative — no standard panel size is ever assumed.

Panel-width direction

{widthValid && (
One panel width {wM} ÷ {wN} = {panelWidth.toFixed(3)} m
)}

Panel-length direction

{lengthValid && (
One panel length {lM} ÷ {lN} = {panelLength.toFixed(3)} m
)}
{!widthValid && !lengthValid && (

Enter at least one full pair to build the panel ruler.

)}

2 · Roof photo

{imageUrl && ( )} {imageName && !uploading && ( {imageName} )}
{uploadError && (

{uploadError}

)} {imageUrl && (

✓ Photo saved and ready to analyse.

Saved roof photo
)}

3 · Count (six independent proofs)

The counter runs COUNT A (rows-first), COUNT B{' '} (columns-first), COUNT C (area cross-check),{' '} AUDIT D (full row-sum), AUDIT E (perpendicular column-sum), and AUDIT F (calibrated geometric reconstruction) independently. Their totals must agree — or you know the count needs a human check.

{!imageUrl && Upload a photo first.} {imageUrl && !widthValid && !lengthValid && ( Add at least one calibration pair. )} {error && analysing === false && imageUrl && (

{error}

)}
{proofs && (

Verdict

{allAgree ? 'All six proofs agree' : strongAgree ? `${majorityCount} of ${proofs.length} proofs agree` : someAgree ? `${majorityCount} of ${proofs.length} proofs agree (weak majority)` : 'Proofs disagree'} {agreedTotal != null ? agreedTotal : '—'}
A={totals[0] ?? '?'} · B={totals[1] ?? '?'} · C={totals[2] ?? '?'} · D= {totals[3] ?? '?'} · E={totals[4] ?? '?'} · F={totals[5] ?? '?'}
{!allAgree && (

{strongAgree || someAgree ? 'Treat the majority figure as provisional and inspect the outlier proofs below.' : 'No majority. A human must reconcile the six proofs below.'}

)}
)} {proofs && proofs.map((result, idx) => { const p = PROOFS[idx]; const testid = `proof-${proofSlug[idx]}-card`; return (

{p.label}

Strategy: {p.strategy}

{p.label} total {result?.totalCount ?? '—'}
{result?.confidence && ( {result.confidence} confidence )}

Panel ruler used

Arithmetic

{(result?.groups || []).map((g, i) => ( ))}
Group Rows Columns Panels
{g.label || `Group ${i + 1}`} {g.rows ?? '—'} × {g.columns ?? '—'} = {g.count}
Sum {(result?.groups || []).reduce((s, g) => s + (g.count || 0), 0)}
{(result?.groups || []).some((g) => g.notes) && ( <>

Notes per group

)} {result?.reasoning && ( <>

Reasoning

{result.reasoning}

)}
); })}
); } ReactDOM.createRoot(document.getElementById('root')).render();