Three coordinated changes after reviewing the wave 2 subagent reports:
1. Restore Presentations as the 9th Studio workshop.
Phase 10's subagent dropped Presentations from the workshop grid
because the spec's eight-workshop list didn't include it. But
.planning/PROJECT.md explicitly lists "Presentations & video
generation via Remotion" as an Active v1.7 requirement and the
existing PresentationPanel.tsx is already a real working Remotion
generator. Dropping it was silent feature regression.
- workshops.ts: add "presentations" slug + Presentation Lucide
icon, placed between social and convert in canonical order
- classifyIntent.ts: add pitch-deck / slide-deck / demo-video /
keynote intent routing (before social so "pitch deck" wins)
- StudioWorkshopDetail.tsx: import PresentationPanel and add a
"presentation" case in WorkshopBody
- workshops.test.ts: expected canonical order updated to 9 slugs
- classifyIntent.test.ts: 4 new parameterized rows for presentations
- WorkshopCard.test.tsx: index 7 is now PRESENTATIONS, 8 is CONVERT
- WorkshopGrid.test.tsx: expected card count 9, canonical order
2. ProjectCard hero-stat derivatives instead of em-dash city.
The shared Project record has none of the fields the spec §7.1 card
depends on: milestoneProgress, nextGate, costBurned, per-project
agent count, phase/milestone array. Wave 2 shipped every card with
"—%" and blank hero numbers — visually underwhelming for a layout
whose whole point is the 72px volt performance stat.
Compute best-effort proxies on the client from data that exists:
- progress: closed_issues / total_issues × 100, from a single
issuesApi.list(companyId) query grouped by projectId
- nextGateName: first pending approval whose payload.projectId
matches, from a single approvalsApi.list(companyId, "pending")
query
- lastActivity: max(project.updatedAt, newest issue.updatedAt in
the project), rendered as "8m ago"-style diff
Each proxy is annotated with // TODO(phase-11.5) for replacement
when real backend aggregates land. phase, costBurnedCents, and
per-project agent count remain hard gaps — rendered as null which
the card surfaces as em-dashes. These three are explicitly queued
for Phase 11.5.
No backend changes; everything derives from existing endpoints.
Two new useQuery calls in Projects.tsx (issues + pending approvals)
both fire per-company, not per-project, so they stay cheap for the
~dozens-of-projects scale the list targets.
3. Spec updated 8 → 9 workshops everywhere it referred to the count.
docs/specs/2026-04-11-nexus-layout-overhaul.md:
- §2 IA table: 8 → 9 workshops
- §6 ASCII header: Eight → Nine
- §6.3 section title: Eight workshops → Nine workshops
- §11 decisions log #16: amendment note explaining the 8→9 bump
- §13 phase 10 description: 8-card → 9-card, with Presentations
explicitly called out
- "Folds into Studio as the 8th workshop" → "Folds into Studio
as a workshop (the legacy /convert route is preserved for
backwards compat)"
Verification: 75/75 studio tests passing; 52/52 projects tests
passing; tsc clean on studio/ + projects/ + Projects.tsx +
ProjectDetail.tsx + StudioWorkshopDetail.tsx.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
84 lines
3 KiB
TypeScript
84 lines
3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { classifyIntent } from "./classifyIntent";
|
|
import type { WorkshopSlug } from "./workshops";
|
|
|
|
describe("classifyIntent", () => {
|
|
it("returns null for empty or whitespace-only input", () => {
|
|
expect(classifyIntent("")).toBeNull();
|
|
expect(classifyIntent(" ")).toBeNull();
|
|
expect(classifyIntent("\n\t ")).toBeNull();
|
|
});
|
|
|
|
it.each<[string, WorkshopSlug]>([
|
|
// Convert — must be checked first
|
|
["convert this pdf to markdown", "convert"],
|
|
["can you convert it", "convert"],
|
|
["from docx to pdf please", "convert"],
|
|
["pdf to docx", "convert"],
|
|
["transform html to md", "convert"],
|
|
|
|
// Wallpapers
|
|
["I need a 1920x1080 wallpaper of a forest", "wallpapers"],
|
|
["make a desktop bg with neon lights", "wallpapers"],
|
|
["generate wallpapers for my lock screen", "wallpapers"],
|
|
|
|
// Diagrams
|
|
["diagram of the auth flow", "diagrams"],
|
|
["flowchart for the checkout process", "diagrams"],
|
|
["mermaid sequence diagram", "diagrams"],
|
|
["architecture diagram of the platform", "diagrams"],
|
|
|
|
// Icons
|
|
["I want an icon set for my app", "icons"],
|
|
["svg icon of a rocket", "icons"],
|
|
["icon pack please", "icons"],
|
|
|
|
// Themes
|
|
["build me a theme from #166534", "themes"],
|
|
["color palette based on volt", "themes"],
|
|
["generate a palette", "themes"],
|
|
|
|
// Brand kits (checked before documents — logo beats document)
|
|
["create a brand identity for an AI startup", "brand-kits"],
|
|
["I need a logo", "brand-kits"],
|
|
["style guide please", "brand-kits"],
|
|
|
|
// Presentations (checked before social — "pitch deck" is presentation, not social)
|
|
["build me a pitch deck about our platform", "presentations"],
|
|
["slide deck for the board", "presentations"],
|
|
["demo video of the onboarding flow", "presentations"],
|
|
["keynote-style presentation", "presentations"],
|
|
|
|
// Social
|
|
["twitter post about our launch", "social"],
|
|
["linkedin carousel on hiring", "social"],
|
|
["instagram post for the team", "social"],
|
|
|
|
// Documents (must come after convert/brand)
|
|
["generate a quarterly report", "documents"],
|
|
["one-pager about the product", "documents"],
|
|
["an invoice for client acme", "documents"],
|
|
])("routes %j → %s", (prompt, expectedSlug) => {
|
|
const result = classifyIntent(prompt);
|
|
expect(result).not.toBeNull();
|
|
expect(result?.slug).toBe(expectedSlug);
|
|
expect(result?.prefilledPrompt).toBe(prompt);
|
|
});
|
|
|
|
it.each([
|
|
"random musing about life",
|
|
"what's the weather today",
|
|
"tell me a joke",
|
|
"remind me to buy milk",
|
|
"hello world",
|
|
])("returns null for unclassified prompt %j", (prompt) => {
|
|
expect(classifyIntent(prompt)).toBeNull();
|
|
});
|
|
|
|
it("preserves the original prompt verbatim in prefilledPrompt (no lowercasing)", () => {
|
|
const prompt = "Diagram Of The Auth Flow, MERMAID style";
|
|
const result = classifyIntent(prompt);
|
|
expect(result?.prefilledPrompt).toBe(prompt);
|
|
expect(result?.slug).toBe("diagrams");
|
|
});
|
|
});
|