Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/GUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ Figma `claude` session and `docker logs -f`).
| **Prerequisites** | Runs the prereq check and shows a pass/fail checklist with actionable guidance. | `scripts/check-prerequisites.sh` |
| **Setup wizard** | Scaffolds a project (slug, theme starter, Canva/Woo/multisite/port…). | `scripts/init/` `resolveDefaults()` + `apply()` (in-process) |
| **WordPress (Docker)** | Build/start/stop/restart/install, live container status, theme activation, log streaming; quick links to the site / wp-admin / phpMyAdmin, first-run ordering guidance, and actionable error hints (Docker not running, port conflicts) linking to `docs/docker-troubleshooting.md`. | `wordpress-local.sh` + `docker compose` |
| **Convert design** | Pick Figma / Canva / InDesign, provide the input (Figma URL with validation + Dev Mode/Professional-plan note; native folder picker for Canva; native `.idml`/PDF file picker for InDesign), launch the existing workflow, and stream progress. Links to each pipeline's docs. | `claude -p` / init canva path / `flavian pipeline indesign` |
| **Visual QA** | Runs visual-diff + Lighthouse and renders the artifacts (diff triptychs, scores, report). | `pnpm visual:diff` / `pnpm lighthouse:run` |
| **Convert design** | Pick Figma / Canva / InDesign, provide the input (Figma URL with validation + Dev Mode/Professional-plan note; native folder picker for Canva; native `.idml`/PDF file picker for InDesign), launch the existing workflow, and stream progress with running/success/failure states. On success, surfaces the theme location with a direct **Activate** action; on failure, links to the troubleshooting docs. Links to each pipeline's docs. | `claude -p` / init canva path / `flavian pipeline indesign` |
| **Visual QA** | Runs visual-diff + Lighthouse and renders the artifacts: regression diff triptychs, Lighthouse scores, the QA agent's report, and (where available) **design-vs-result** side-by-side from the visual-qa-agent screenshots. | `pnpm visual:diff` / `pnpm lighthouse:run` |

The **Setup wizard** mirrors `pnpm run init` — the same inputs and flags documented in
**[CLI-WIZARD.md](CLI-WIZARD.md)** — and runs the wizard's `apply()` / `resolveDefaults()`
Expand Down
32 changes: 32 additions & 0 deletions packages/gui/src/core/qa/discover.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { promises as fs } from 'node:fs';
import { join } from 'node:path';
import type {
DesignComparison,
LighthouseSummary,
QaArtifacts,
VisualReport,
Expand Down Expand Up @@ -80,6 +81,31 @@ export function mapLighthouse(raw: RawAssertion[]): LighthouseSummary {
};
}

/** Pair design screenshots (figma/) with rendered results (wordpress/chromium/) by page stem. */
export function pairDesignResults(designs: string[], results: string[]): DesignComparison[] {
return designs
.filter((d) => /\.png$/i.test(d))
.map((d) => {
const stem = d.replace(/\.png$/i, '');
const match =
results.find((r) => r.startsWith(`${stem}-`) && /desktop/i.test(r)) ??
results.find((r) => r.startsWith(`${stem}-`) || r.startsWith(`${stem}.`) || r === d);
return {
name: stem,
designRel: `.claude/visual-qa/screenshots/figma/${d}`,
resultRel: match ? `.claude/visual-qa/screenshots/wordpress/chromium/${match}` : undefined,
};
});
}

async function listPngs(dir: string): Promise<string[]> {
try {
return (await fs.readdir(dir)).filter((f) => f.toLowerCase().endsWith('.png')).sort();
} catch {
return [];
}
}

/** Discover whatever QA artifacts exist on disk for the project. */
export async function discoverQaArtifacts(repoRoot: string): Promise<QaArtifacts> {
const rawVisual = await readJson<RawVisualReport>(
Expand All @@ -93,9 +119,15 @@ export async function discoverQaArtifacts(repoRoot: string): Promise<QaArtifacts
? qaReport
: null;

const design = pairDesignResults(
await listPngs(join(repoRoot, '.claude', 'visual-qa', 'screenshots', 'figma')),
await listPngs(join(repoRoot, '.claude', 'visual-qa', 'screenshots', 'wordpress', 'chromium')),
);

return {
visual: rawVisual ? mapVisualReport(rawVisual) : null,
lighthouse: Array.isArray(rawLh) ? mapLighthouse(rawLh) : null,
design,
qaReportPath,
};
}
70 changes: 60 additions & 10 deletions packages/gui/src/renderer/components/PipelinePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from 'react';
import type { PipelineInput, PipelineKind } from '../../shared/types/pipeline';
import { bridge } from '../api/bridge';
import { usePipeline } from '../hooks/usePipeline';
import { useTaskStream } from '../hooks/useTaskStream';
import { LogStream } from './LogStream';

const KINDS: { value: PipelineKind; label: string }[] = [
Expand Down Expand Up @@ -55,6 +56,18 @@ export function PipelinePanel() {
void bridge().openPath(DOCS[kind]);
};

const [activateTaskId, setActivateTaskId] = useState<string | null>(null);
const activate = useTaskStream(activateTaskId);
const activateTheme = (themeSlug: string): void => {
void bridge()
.runDocker('activate-theme', themeSlug)
.then(({ taskId }) => setActivateTaskId(taskId))
.catch(() => {});
};
const openHelp = (relPath: string): void => {
void bridge().openPath(relPath);
};

const launch = () => {
const input: PipelineInput = { kind, slug: slug.trim(), figmaUrl, canvaExport, indesignFile };
run(input);
Expand Down Expand Up @@ -87,17 +100,54 @@ export function PipelinePanel() {
</button>
</header>
{result.ok ? (
<div className="banner banner-ok">
<strong>Conversion finished ({result.kind})</strong>
<span className="summary">
Theme: <code>themes/{result.slug}</code> — activate it from the WordPress tab.
</span>
</div>
<>
<div className="banner banner-ok">
<strong>Conversion finished ({result.kind})</strong>
<span className="summary">
Theme: <code>themes/{result.slug}</code>
</span>
</div>
<div className="form-actions">
<button
type="button"
onClick={() => activateTheme(result.slug)}
disabled={activate.state === 'running'}
>
{activate.state === 'running' ? 'Activating…' : 'Activate theme'}
</button>
{activate.state === 'succeeded' && (
<span className="activate-note"> Activated — open it from the WordPress tab.</span>
)}
{activate.state === 'failed' && (
<span className="field-error"> Activation failed — start WordPress first, then retry.</span>
)}
</div>
</>
) : (
<div className="banner banner-error">
<strong>Conversion failed</strong>
<span>{result.error}</span>
</div>
<>
<div className="banner banner-error">
<strong>Conversion failed</strong>
<span>{result.error}</span>
</div>
<p className="panel-intro">
Troubleshooting:{' '}
<button
type="button"
className="link-btn"
onClick={() => openHelp('docs/COMMON-FAILURES-FIXES.md')}
>
Common failures &amp; fixes
</button>
{' · '}
<button
type="button"
className="link-btn"
onClick={() => openHelp('docs/TROUBLESHOOTING.md')}
>
Troubleshooting guide
</button>
</p>
</>
)}
<details className="raw" open={!result.ok}>
<summary>Conversion log</summary>
Expand Down
27 changes: 27 additions & 0 deletions packages/gui/src/renderer/components/QaPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,33 @@ export function QaPanel() {
)}
</div>

{artifacts && artifacts.design.length > 0 && (
<div className="group">
<h2>Design vs result</h2>
{artifacts.design.map((d) => (
<div className="diff-row" key={d.name}>
<div className="diff-head">
<span>{d.name}</span>
</div>
<div className="diff-images">
<figure>
<figcaption>Design</figcaption>
<ArtifactImage relPath={d.designRel} alt={`design ${d.name}`} />
</figure>
<figure>
<figcaption>Result</figcaption>
{d.resultRel ? (
<ArtifactImage relPath={d.resultRel} alt={`result ${d.name}`} />
) : (
<div className="art-img art-img-state">no render yet</div>
)}
</figure>
</div>
</div>
))}
</div>
)}

<div className="group">
<h2>Lighthouse</h2>
{!lighthouse ? (
Expand Down
5 changes: 5 additions & 0 deletions packages/gui/src/renderer/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,11 @@ button:disabled {
font-size: 12px;
}

.activate-note {
color: var(--ok);
font-size: 13px;
}

.field-check {
display: flex;
align-items: center;
Expand Down
9 changes: 9 additions & 0 deletions packages/gui/src/shared/types/qa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,20 @@ export interface LighthouseSummary {
failures: LighthouseFailure[];
}

/** A design screenshot paired with its rendered result (visual-qa-agent output). */
export interface DesignComparison {
name: string;
designRel: string;
resultRel?: string;
}

export interface QaArtifacts {
/** Parsed tests/visual/report.json (pixel-diff results), or null if absent. */
visual: VisualReport | null;
/** Parsed .lighthouseci/assertion-results.json summary, or null if absent. */
lighthouse: LighthouseSummary | null;
/** Design-vs-result pairs from .claude/visual-qa/screenshots (empty if none). */
design: DesignComparison[];
/** Repo-relative path to .claude/visual-qa/report.md if present. */
qaReportPath: string | null;
}
18 changes: 17 additions & 1 deletion packages/gui/tests/core/qa/qa.test.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mapLighthouse, mapVisualReport } from '../../../src/core/qa/discover';
import { mapLighthouse, mapVisualReport, pairDesignResults } from '../../../src/core/qa/discover';
import { isQaScript, qaCommandSpec } from '../../../src/core/qa/qa-commands';
import { CommandBuilder } from '../../../src/core/shell/command-builder';
import type { ShellResolver } from '../../../src/core/shell/shell-resolver';
Expand Down Expand Up @@ -44,6 +44,22 @@ test('qaCommandSpec runs a pnpm script via `bash -c`', async () => {
assert.deepEqual(spec.args, ['-c', 'pnpm run visual:diff']);
});

test('pairDesignResults pairs design shots with desktop results by page stem', () => {
const pairs = pairDesignResults(
['home.png', 'about.png'],
['home-desktop-1440px.png', 'home-mobile-375px.png'],
);
assert.equal(pairs.length, 2);
const home = pairs.find((p) => p.name === 'home');
assert.equal(home?.designRel, '.claude/visual-qa/screenshots/figma/home.png');
assert.equal(
home?.resultRel,
'.claude/visual-qa/screenshots/wordpress/chromium/home-desktop-1440px.png',
);
const about = pairs.find((p) => p.name === 'about');
assert.equal(about?.resultRel, undefined); // no matching result render
});

test('isQaScript validates the script set', () => {
assert.ok(isQaScript('visual:diff'));
assert.ok(isQaScript('lighthouse:run'));
Expand Down
Loading