diff --git a/README.md b/README.md index a4905fc..31a1efa 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This repository is the public implementation for the 2026 OpenAI Build Week hack ## Product contract -- One selected performer and model carry each turn; the live adapter never silently delegates to another model. +- One selected performer carries the journey; the audience may choose a different model between turns, and the live adapter never silently delegates to another model. - Research activity and sources are observable; hidden chain-of-thought is not exposed. - Every ready turn ends with exactly two distinct next questions. - The journey advances only after an explicit audience action. @@ -28,6 +28,35 @@ This repository is the public implementation for the 2026 OpenAI Build Week hack - Dispatch-owned Sign in with ChatGPT seam for durable identity - GitHub Actions for lint, build, and rendered-output tests +## Architecture + +The checked-in implementation runs as one public ChatGPT Site. The browser talks only to Sites routes; those routes own identity, authorization, foreground research orchestration, validation, and persistence. Live provider work goes directly to the OpenAI Responses API, while Sites-managed D1 is the canonical product database. There is no queue, scheduler, background continuation, provider fan-out, R2 bucket, Supabase project, or separately operated backend in the current deployment. + +![WonderDrive system landscape](design/wonderdrive-architecture-01-system-landscape.png) + +The editable source is [the system-landscape Excalidraw board](design/wonderdrive-architecture-01-system-landscape.excalidraw). The complete implementation views are documented in [the architecture decisions](docs/architecture.md#architecture-views). + +
+One foreground research turn + +![WonderDrive foreground research sequence](design/wonderdrive-architecture-02-research-turn.png) + +
+ +
+Inside the WonderDrive application boundary + +![WonderDrive internal components](design/wonderdrive-architecture-03-inside-wonderdrive.png) + +
+ +
+Build and deployment topology + +![WonderDrive deployment topology](design/wonderdrive-architecture-04-deployment-topology.png) + +
+ ## Local development Requirements: Node.js `22.13.0` or newer. @@ -74,12 +103,13 @@ tests/ Rendered production and fixture checks - [Phase 2 live research contract](docs/phase-2.md) - [V3 implementation contract](docs/v3-implementation.md) - [Final architecture decisions](docs/architecture.md) +- [Current architecture views](docs/architecture.md#architecture-views) - [Current code index](docs/code-index.md) - [Final product and engineering blueprint](docs/WonderDrive_Final_Product_and_Engineering_Blueprint_v3_Research_First.docx) ## Status and scope -Every journey uses a user-selected, compatible OpenAI model through one foreground Responses request with built-in text and optional image search. Presets cap tool calls, output tokens, reasoning effort, and wall time; guest and signed-in identities also have rolling live-run and estimated-spend limits. Consulted URLs, cited relations, sourced image results, provider request ID, complete usage, price snapshot, prompt/performer/model versions, and research handoff are saved with the committed turn. +Every turn starts one foreground Responses stream with the audience-selected, compatible OpenAI model, built-in text search, and optional image results. A bounded citation-repair or evidence-recovery call may follow when the initial draft does not satisfy the source contract. The model can be changed before any later turn without rewriting earlier turn metadata. Presets cap tool calls, output tokens, reasoning effort, and wall time; guest and signed-in identities also have rolling live-run and estimated-spend limits. Consulted URLs, cited relations, sourced image results, provider request IDs, complete usage, price snapshots, prompt/performer/model versions, research handoffs, and all provider-call analytics are persisted in D1. Automatic journeys, scheduled/background continuation, Trigger.dev, provider fan-out, and live parallel comparison are outside the hackathon scope. diff --git a/app/api/diagnostics/route.ts b/app/api/diagnostics/route.ts new file mode 100644 index 0000000..5a6120c --- /dev/null +++ b/app/api/diagnostics/route.ts @@ -0,0 +1,8 @@ +import { query } from "../../../lib/api"; +import { getDiagnostics } from "../../../lib/diagnostics"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + return query((viewer) => getDiagnostics(viewer)); +} diff --git a/app/api/journeys/[journeyId]/advance/route.ts b/app/api/journeys/[journeyId]/advance/route.ts index 1195113..9edfecc 100644 --- a/app/api/journeys/[journeyId]/advance/route.ts +++ b/app/api/journeys/[journeyId]/advance/route.ts @@ -14,9 +14,11 @@ export async function POST(request: Request, context: Context) { const body = await readJson(request); return advanceJourney(viewer, journeyId, body, async ({ journey, turn }) => runLiveRedraw({ + identityId: viewer.identityId, + journeyId, turn, performerId: journey.performerId, - modelId: journey.modelId, + modelId: body.modelId ?? journey.modelId, rejectedQuestions: [ ...await listRejectedQuestions(viewer, journeyId), ...turn.options.map((option) => option.question), diff --git a/app/api/research/route.ts b/app/api/research/route.ts index 4f53847..5080ea7 100644 --- a/app/api/research/route.ts +++ b/app/api/research/route.ts @@ -73,10 +73,13 @@ export async function POST(request: Request) { if (!closed) { send({ type: "error", - error: publicError( - error, - "WonderDrive could not complete live research. No partial journey was saved.", - ), + error: { + ...publicError( + error, + "WonderDrive could not complete live research. No partial journey was saved.", + ), + diagnosticId: preparation.prepared.requestId, + }, }); } } finally { diff --git a/app/client-api.ts b/app/client-api.ts index 342662e..8404bec 100644 --- a/app/client-api.ts +++ b/app/client-api.ts @@ -19,8 +19,15 @@ export type LiveResearchState = { status: "running" | "complete" | "error"; result: JourneyDetail | null; error: string | null; + diagnosticId: string | null; }; +export function starterRecommendationsUrl(performerId: PerformerId, forceRefresh = false) { + const query = new URLSearchParams({ performer: performerId }); + if (forceRefresh) query.set("refresh", "1"); + return `/api/starters?${query.toString()}`; +} + export async function api(url: string, init?: RequestInit): Promise> { const response = await fetch(url, { ...init, @@ -67,7 +74,12 @@ export async function streamLiveResearch( if (!data) continue; const event = JSON.parse(data) as LiveResearchStreamEvent; if (event.type === "started") { - setState((current) => current && { ...current, question: event.question, message: event.message }); + setState((current) => current && { + ...current, + question: event.question, + message: event.message, + diagnosticId: event.requestId, + }); } else if (event.type === "activity") { setState((current) => current && { ...current, @@ -76,6 +88,10 @@ export async function streamLiveResearch( : [...current.events, event.event], }); } else if (event.type === "error") { + setState((current) => current && { + ...current, + diagnosticId: event.error.diagnosticId ?? current.diagnosticId, + }); throw new Error(event.error.message); } else if (event.type === "complete") { complete = event; diff --git a/app/globals.css b/app/globals.css index dd15e49..bf36d47 100644 --- a/app/globals.css +++ b/app/globals.css @@ -58,6 +58,11 @@ a { color: inherit; text-decoration: none; } .active-journey-shell { min-height: calc(100vh - 115px); } .journey-view-switcher { align-items: center; background: color-mix(in srgb, var(--paper-light) 82%, transparent); border-bottom: 1px solid var(--line); display: flex; justify-content: space-between; min-height: 42px; padding: 5px clamp(24px, 4.8vw, 72px); } .journey-view-switcher > span { color: var(--muted); font-size: .58rem; max-width: 45vw; overflow: hidden; text-overflow: ellipsis; text-transform: uppercase; white-space: nowrap; } +.journey-model-switcher { align-items: center; display: flex; gap: 8px; margin-left: auto; margin-right: 14px; } +.journey-model-switcher > span { color: var(--muted); font-size: .5rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; } +.journey-model-switcher select { background: var(--paper-light); border: 1px solid var(--ink); color: var(--ink); font-size: .58rem; font-weight: 700; max-width: 190px; min-height: 28px; padding: 4px 25px 4px 8px; } +.journey-model-switcher select:focus-visible { outline: 3px solid var(--acid); outline-offset: 2px; } +.journey-model-switcher select:disabled { cursor: wait; opacity: .55; } .journey-view-switcher > div { border: 1px solid var(--ink); display: flex; } .journey-view-switcher button { background: transparent; border: 0; font-size: .58rem; font-weight: 700; min-width: 94px; padding: 6px 12px; text-transform: uppercase; } .journey-view-switcher button + button { border-left: 1px solid var(--ink); } @@ -66,7 +71,7 @@ a { color: inherit; text-decoration: none; } .identity-control > span:nth-child(2) { display: flex; flex-direction: column; font-size: .7rem; line-height: 1.2; max-width: 150px; } .identity-control strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .identity-control small { color: var(--muted); font-size: .62rem; margin-top: 3px; } -.identity-control a { border-bottom: 1px solid currentColor; font-size: .67rem; font-weight: 600; margin-left: 7px; } +.identity-action { border-bottom: 1px solid currentColor; font-size: .67rem; font-weight: 600; margin-left: 7px; white-space: nowrap; } .identity-dot { background: var(--muted); border: 3px solid var(--paper); border-radius: 50%; box-shadow: 0 0 0 1px var(--ink); height: 10px; width: 10px; } .identity-dot.guest { background: var(--acid); } .identity-dot.chatgpt { background: var(--sky); } @@ -810,6 +815,32 @@ legend > span { align-items: center; border: 1px solid var(--ink); border-radius .settings-form label { border-bottom: 1px solid var(--line); padding-bottom: 16px; } .settings-form small { color: var(--muted); font-size: .62rem; } .settings-form .launch-button { grid-column: 1 / -1; } +.diagnostics-console { background: var(--ink); border: 1px solid var(--ink); color: var(--paper-light); margin-top: 28px; padding: clamp(24px, 4vw, 44px); } +.diagnostics-console > header { align-items: flex-end; display: flex; gap: 24px; justify-content: space-between; } +.diagnostics-console h2 { font-family: var(--font-display); font-size: clamp(1.8rem, 4vw, 3.4rem); font-weight: 400; letter-spacing: -.035em; margin: 5px 0 0; } +.diagnostics-console > header button { background: var(--acid); border: 1px solid var(--paper-light); color: var(--ink); font-size: .62rem; font-weight: 700; padding: 10px 13px; text-transform: uppercase; } +.diagnostics-summary { display: grid; grid-template-columns: repeat(4, 1fr); margin-top: 30px; } +.diagnostics-summary div { border: 1px solid rgba(255,255,255,.25); display: flex; flex-direction: column; min-height: 90px; padding: 15px; } +.diagnostics-summary strong { color: var(--acid); font-family: var(--font-display); font-size: 2rem; font-weight: 400; } +.diagnostics-summary span { color: rgba(255,255,255,.62); font-size: .55rem; text-transform: uppercase; } +.diagnostics-alert { background: var(--coral); border: 1px solid var(--paper-light); color: var(--ink); display: flex; flex-direction: column; gap: 3px; margin-top: 18px; padding: 14px 16px; } +.diagnostics-alert strong { font-size: .65rem; text-transform: uppercase; } +.diagnostics-alert span { font-size: .7rem; } +.incident-list { margin-top: 18px; } +.incident-row { border-top: 1px solid rgba(255,255,255,.3); } +.incident-row:last-child { border-bottom: 1px solid rgba(255,255,255,.3); } +.incident-row summary { align-items: center; cursor: pointer; display: grid; gap: 12px; grid-template-columns: 110px 1fr 150px 190px; padding: 14px 4px; } +.incident-row summary code { color: var(--acid); font-size: .68rem; } +.incident-row summary strong { font-size: .65rem; } +.incident-row summary span, .incident-row summary time { color: rgba(255,255,255,.62); font-size: .6rem; } +.incident-row dl { display: grid; gap: 0 18px; grid-template-columns: repeat(2, 1fr); margin: 0; padding: 4px 0 14px; } +.incident-row dl div { border-top: 1px solid rgba(255,255,255,.14); padding: 8px 4px; } +.incident-row dt { color: rgba(255,255,255,.5); font-size: .5rem; text-transform: uppercase; } +.incident-row dd { font-size: .65rem; margin: 3px 0 0; overflow-wrap: anywhere; } +.incident-row > p { background: rgba(255,255,255,.07); font-size: .68rem; margin: 0 0 15px; padding: 12px; } +.diagnostics-empty { border: 1px dashed rgba(255,255,255,.3); color: rgba(255,255,255,.7); margin: 24px 0 0; padding: 18px; } +.diagnostics-privacy { color: rgba(255,255,255,.48); font-size: .55rem; margin: 14px 0 0; } +.buffering-error code { color: var(--muted); display: block; font-size: .64rem; margin-top: 8px; } @keyframes pulse { 0%, 100% { opacity: .45; transform: scale(.85); } 50% { opacity: 1; transform: scale(1.15); } } @keyframes spin { to { transform: rotate(360deg); } } @@ -833,12 +864,12 @@ legend > span { align-items: center; border: 1px solid var(--ink); border-radius @media (max-width: 820px) { .app-header { min-height: 70px; padding-inline: 16px; } - .wordmark small, .identity-control small, .identity-control a { display: none; } + .wordmark small, .identity-control small { display: none; } .identity-control > span:nth-child(2) { display: flex; max-width: 92px; } .app-nav { display: grid; grid-template-columns: repeat(4, 1fr); margin-inline: -16px; overflow: visible; width: calc(100% + 32px); } .app-nav button { min-height: 43px; min-width: 0; padding-inline: 8px; } .app-nav button:nth-child(4n) { border-inline-end: 1px solid var(--line); } - .journey-view-switcher { padding-inline: 16px; } + .journey-view-switcher { gap: 8px; padding-inline: 16px; } .phase-ribbon { justify-content: flex-start; overflow: hidden; padding-inline: 15px; white-space: nowrap; } .phase-ribbon span { margin-left: 0; } .phase-ribbon::after { display: none; } @@ -892,6 +923,9 @@ legend > span { align-items: center; border: 1px solid var(--ink); border-radius .library-grid, .compare-picker { grid-template-columns: 1fr; } .library-filters, .settings-form { grid-template-columns: 1fr; } .settings-form .launch-button { grid-column: 1; } + .diagnostics-summary { grid-template-columns: repeat(2, 1fr); } + .incident-row summary { align-items: start; grid-template-columns: 1fr 1fr; } + .incident-row dl { grid-template-columns: 1fr; } .choice-settings { grid-template-columns: 1fr; } .metadata-grid { grid-template-columns: 1fr; } .library-card, .library-card:nth-child(2n), .library-card:nth-child(3n) { border-inline-end: 1px solid var(--line-strong); } @@ -913,6 +947,7 @@ legend > span { align-items: center; border: 1px solid var(--ink); border-radius .contract-strip span { border-bottom: 1px solid var(--line); border-right: 0; min-height: 42px; } .console-heading { align-items: flex-start; flex-direction: column; justify-content: center; gap: 4px; } .performance-stage { padding-inline: 16px; } + .diagnostics-console > header { align-items: flex-start; flex-direction: column; } .performance-header h1 { font-size: 3rem; } .buffering-header { gap: 9px; grid-template-columns: minmax(0, 1fr) 124px; min-height: 86px; padding-block: 9px; } .buffering-header h1 { font-size: 1.22rem; } @@ -926,6 +961,9 @@ legend > span { align-items: center; border: 1px solid var(--ink); border-radius .article-journey-header .stage-metrics span { min-width: 48px; padding: 5px 6px; } .article-journey-header .stage-metrics strong { font-size: 1.05rem; } .journey-view-switcher > span { display: none; } + .journey-view-switcher { display: grid; gap: 6px; grid-template-columns: 1fr; padding-block: 6px; } + .journey-model-switcher { margin: 0; width: 100%; } + .journey-model-switcher select { flex: 1; max-width: none; } .journey-view-switcher > div { display: grid; grid-template-columns: 1fr 1fr; width: 100%; } .journey-view-switcher button { min-width: 0; } .contained-answer-card { padding: 17px 16px 14px 21px; } @@ -990,6 +1028,11 @@ legend > span { align-items: center; border: 1px solid var(--ink); border-radius .upgrade-banner { align-items: flex-start; flex-direction: column; gap: 7px; } } +@media (max-width: 520px) { + .identity-control > span:nth-child(2) { display: none; } + .identity-action { margin-left: 2px; } +} + @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition-duration: .01ms !important; } .starter-marquee-window { overflow-x: auto; } diff --git a/app/wonderdrive-experience.tsx b/app/wonderdrive-experience.tsx index d143e5b..9c49984 100644 --- a/app/wonderdrive-experience.tsx +++ b/app/wonderdrive-experience.tsx @@ -22,6 +22,7 @@ import type { AnswerDensity, BootstrapCatalog, CompareResult, + DiagnosticsReport, ImagePreference, JourneyDetail, JourneySnapshot, @@ -39,6 +40,7 @@ import { api, type LiveResearchState, messageFrom, + starterRecommendationsUrl, streamLiveResearch, } from "./client-api"; @@ -82,6 +84,7 @@ export function WonderDriveExperience() { const [liveResearch, setLiveResearch] = useState(null); const [catalog, setCatalog] = useState(BOOTSTRAP_CATALOG); const [preferences, setPreferences] = useState(DEFAULT_PREFERENCES); + const [nextModelId, setNextModelId] = useState(null); const [personalizedStarters, setPersonalizedStarters] = useState( BOOTSTRAP_CATALOG.discoveryStarters, ); @@ -97,7 +100,7 @@ export function WonderDriveExperience() { setJourneys(session.data.journeys); setCatalog(bootstrap.data.catalog); setPreferences(bootstrap.data.preferences); - void api("/api/starters?performer=sage&refresh=1") + void api(starterRecommendationsUrl("sage")) .then((payload) => setPersonalizedStarters(payload.data.starters)) .catch(() => setPersonalizedStarters(bootstrap.data.catalog.discoveryStarters)); } catch (cause) { @@ -133,6 +136,7 @@ export function WonderDriveExperience() { ) => { setViewer(nextViewer); setActiveJourney(detail); + setNextModelId(detail.modelId); setActiveTurnId(turnId); setView(view); if (syncLibrary) setJourneys((current) => upsertSummary(current, detail)); @@ -169,6 +173,7 @@ export function WonderDriveExperience() { status: "running", result: null, error: null, + diagnosticId: null, }); const complete = await streamLiveResearch( { kind: "create", ...config, idempotencyKey: crypto.randomUUID() }, @@ -192,6 +197,7 @@ export function WonderDriveExperience() { input: { turnId: string; optionId?: string; adventure?: number; reason?: string }, ) { if (!activeJourney) return; + const modelId = nextModelId ?? activeJourney.modelId; await runMutation(action, async () => { if (action !== "reject") { const fromTurn = activeJourney.turns.find((turn) => turn.id === input.turnId); @@ -209,6 +215,7 @@ export function WonderDriveExperience() { status: "running", result: null, error: null, + diagnosticId: null, }); const complete = await streamLiveResearch( { @@ -216,6 +223,7 @@ export function WonderDriveExperience() { journeyId: activeJourney.id, fromTurnId: input.turnId, action, + modelId, optionId: input.optionId, expectedVersion: activeJourney.version, idempotencyKey: crypto.randomUUID(), @@ -237,6 +245,7 @@ export function WonderDriveExperience() { body: JSON.stringify({ fromTurnId: input.turnId, action, + modelId, optionId: input.optionId, adventure: input.adventure, reason: input.reason, @@ -347,9 +356,11 @@ export function WonderDriveExperience() { ) : ( {viewer?.displayName ?? "Opening library…"}{viewer ? `${journeys.length}/${viewer.journeyLimit} saved` : "durable session"} )} - {viewer?.mode === "guest" && ( - Sign in - )} + {viewer?.mode === "guest" ? ( + Sign in + ) : viewer?.mode === "chatgpt" ? ( + Sign out + ) : null} @@ -457,6 +468,19 @@ export function WonderDriveExperience() {