From 96dc7c7923a9e0751c9d9d75ecfe7f7c2e8fc720 Mon Sep 17 00:00:00 2001 From: Callum Flack Date: Tue, 24 Mar 2026 15:49:13 +1000 Subject: [PATCH 1/4] docs(home): add source pipeline and app creation plans --- ...peline-home-and-local-app-creation-spec.md | 263 ++++++++++++++++++ ...-local-app-creation-implementation-plan.md | 252 +++++++++++++++++ 2 files changed, 515 insertions(+) create mode 100644 docs/260324-source-pipeline-home-and-local-app-creation-spec.md create mode 100644 docs/plans/260324-source-pipeline-home-and-local-app-creation-implementation-plan.md diff --git a/docs/260324-source-pipeline-home-and-local-app-creation-spec.md b/docs/260324-source-pipeline-home-and-local-app-creation-spec.md new file mode 100644 index 00000000..f19730fa --- /dev/null +++ b/docs/260324-source-pipeline-home-and-local-app-creation-spec.md @@ -0,0 +1,263 @@ +# 260324 Source Pipeline Home And Local App Creation Spec + +## Goal + +Make Home immediately answer two questions: + +1. What can I import next? +2. What can I do with data I already imported? + +The page should optimize for more imports without making imported sources feel +dead or "done". + +## Product decisions + +- Home is a `source pipeline` page, not a storage/status page. +- Keep `available sources` always visible. +- Do not use tabs for `available` vs `imported`. +- Use a two-column board: + - left: `Available / Importing` + - right: `Imported / Ready to use` +- Keep the current blocking policy: + - block only while user action is required for auth/sign-in + - once the run is backgroundable, the app remains usable +- Do not auto-build an app after import completes. +- Do immediately offer app creation when import completes. +- Route app-creation intent into the source overview page. +- Prefer a local-first handoff over cloud execution. + +## Problem statement + +The current Home page correctly separates `imported data` and `import sources`, +but the relationship between those sections is weak. A user can see both, but +the UI does not clearly express the lifecycle: + +`source available -> import running -> source imported -> use data` + +As a result: + +- `available sources` does not feel like the primary next action +- `imported data` does not clearly suggest what to do next +- finishing an import does not create momentum into app usage or app creation + +## Proposed Home IA + +### Left column: Available / Importing + +Purpose: start more imports. + +Contents: + +- all importable sources +- running imports remain in this column while active +- running cards keep lightweight progress state +- cards remain blocked only during credential-required states + +Primary CTA: + +- `Connect {Source}` + +Running state: + +- keep current backgrounding behavior +- keep spinner/status copy on the active card +- optionally add one short reassurance line only: + - `You can keep using the app` + +### Right column: Imported / Ready to use + +Purpose: turn imported data into action. + +Contents per source: + +- source name +- last updated +- `View data` +- `Create app` +- `Sync` or `Fetch latest` + +Primary CTAs: + +- `View data` +- `Create app` + +## Source overview page role + +The source overview page is the `use this data` surface. + +It should remain the destination for `View data`. + +It should also expose `Create app from this data` as a first-class action in the +sidebar or primary action area. This is the right place for the app-creation +handoff because the user can inspect the exported JSON first and keep the mental +model anchored on one source. + +## Import completion flow + +When an import finishes: + +1. the source leaves the left column +2. the source appears in the right column +3. show a global completion toast + +Toast copy: + +- title: `{Source} import complete` +- actions: + - `View data` + - `Create app` + +Rules: + +- do not interrupt the user with a modal +- `View data` routes to the source overview page +- `Create app` routes to the source overview page with app-creation intent in + the URL search params + +Suggested pattern: + +- `/sources/:platformId?intent=create-app` + +This lets the overview page open a sheet/dialog on top of the source context +without inventing a separate mental model or route family. + +## App creation proposal + +### Core decision + +App creation should be an explicit user action that generates a local handoff +for an external coding agent, not a cloud-hosted build pipeline. + +### Delivery phases + +#### V1 + +Generate an agent-agnostic handoff bundle that can be copied into Claude Code, +Codex, or another coding agent. + +#### V1.5 + +Keep the same local-first model, but remove more user friction around it: + +- pre-generate a better source-specific prompt +- include stronger example-app references +- make `Copy prompt` and `Reveal handoff files` extremely obvious +- anticipate common follow-up needs in the generated handoff +- keep the user anchored in source overview while doing this + +### Preferred first implementation + +Generate an agent-agnostic handoff bundle that can be copied into Claude Code, +Codex, or another coding agent. + +Flow: + +1. user clicks `Create app` +2. DataConnect opens source overview with app-creation UI active +3. DataConnect generates a source-specific handoff +4. user copies the prompt or opens the generated handoff files +5. the coding agent builds a starter app using the imported source + +### Why this approach + +- simplest implementation path +- keeps user data local-first +- avoids standing up remote job orchestration +- avoids ambiguous trust/privacy boundaries +- supports many agents instead of binding the product to one +- matches the "vibe code from my data" goal without pretending DataConnect is a + hosted app generator + +## Handoff artifact + +The first version should generate a small local artifact bundle: + +- a prompt file and/or skill-like instruction file +- path to the exported data file/folder +- source summary: + - platform + - top-level schema hints + - export summary/counts if present +- link/reference to an example app if one exists + +Example: + +- LinkedIn import -> handoff includes: + - path to LinkedIn export JSON + - summary of key entities + - reference to the LinkedIn demo app pattern + - instruction to create a starter app against that local export + +## UX rules for app creation + +- `Create app` must never imply the app is already built +- label it as a guided generation/handoff action +- primary actions should be: + - `Copy prompt` + - `Reveal handoff files` +- v1.5 can add: + - `Copy prompt` + - `Reveal handoff files` + - `Open example app` + - `Open source folder` +- do not require DataConnect to detect a specific agent installation in v1 +- do not require cloud execution for v1 +- do not upload exported user data to remote infrastructure in v1 + +## Why hosted one-click generation is not v1 + +To make `Create app` become `press one button and get a deployed app`, the +product would need to own a much larger system: + +- authenticated user identity for app-generation jobs +- remote worker/runtime that can run coding agents safely +- secure upload or remote access to exported user data +- template/repo provisioning +- deployment-provider integration such as Vercel auth and project creation +- job progress, retry, failure, and cost controls +- trust/privacy language for sending personal exports to remote infrastructure + +That is a valid later direction, but it is materially bigger than the current +DataConnect scope. + +## Note on one-click deployment generation + +One-click `Create app -> deployed app` is probably not impossible. A credible +future version could look like: + +1. user clicks `Create app` +2. DataConnect provisions a starter repo from a source-specific template +3. an agent runs against the user's exported source data +4. the build is committed to the repo +5. Vercel deploys it +6. the user gets a live URL + +That could be a strong long-term product direction. It is explicitly not the +recommended current implementation because it requires a large jump in system +scope, trust model, and operations burden. + +## Non-goals for v1 + +- remote long-running app generation +- automatic app deployment/hosting +- one-click fully autonomous app generation inside DataConnect +- syncing app-generation job state back into DataConnect +- provider-specific setup such as Vercel account linking + +## Success criteria + +- a user can instantly see where to import more data +- a user can instantly see which imported sources are ready to use +- finishing an import creates a clear next step +- `View data` and `Create app` are both visible and understandable +- app creation has a privacy-preserving local-first path +- v1.5 makes the handoff feel smooth enough that most users do not get stuck on + "what do I do with this prompt/file?" + +## Open decisions + +- whether the source overview uses a sheet, dialog, or inline panel for the + app-creation handoff UI +- whether the generated handoff is a single markdown file, a skill folder, or + both +- whether example-app mappings live in source registry metadata diff --git a/docs/plans/260324-source-pipeline-home-and-local-app-creation-implementation-plan.md b/docs/plans/260324-source-pipeline-home-and-local-app-creation-implementation-plan.md new file mode 100644 index 00000000..0e2ce8d5 --- /dev/null +++ b/docs/plans/260324-source-pipeline-home-and-local-app-creation-implementation-plan.md @@ -0,0 +1,252 @@ +# 260324 Implementation Plan: Source Pipeline Home And Local App Creation + +## Goal + +Implement the Home/source-overview changes needed to: + +- recast Home as a source pipeline +- make imported sources actionable +- add a local-first `Create app` handoff flow + +This plan intentionally covers `V1` and `V1.5`, and explicitly excludes hosted +one-click deployment. + +## Confirmed decisions + +- use a two-column Home board, not tabs +- preserve current import blocking/background behavior +- route `Create app` into source overview +- use URL search params to trigger app-creation UI state +- make app creation local-first and agent-agnostic +- do not attempt hosted one-click deployment in this phase + +## Scope + +### V1 + +- Home shows: + - `Available / Importing` + - `Imported / Ready to use` +- imported-source cards expose: + - `View data` + - `Create app` + - `Fetch latest` +- import-complete toast exposes: + - `View data` + - `Create app` +- source overview can open app-creation UI based on URL intent +- source overview can generate/copy/reveal an agent handoff artifact + +### V1.5 + +- improve handoff quality and clarity +- add example-app references where available +- anticipate user next steps in the generated handoff +- make source overview feel like a polished launch point for app creation + +## Out of scope + +- remote agent execution +- user account linking to Vercel +- repo provisioning and deployment automation +- job queue/progress system for app generation +- cloud transfer of exported user data + +## Proposed UX + +### Home + +Left column: + +- source cards for importable sources +- active imports stay here while running +- preserve current spinner/backgrounding semantics + +Right column: + +- imported-source cards +- each card includes: + - last updated + - `View data` + - `Create app` + - `Fetch latest` + +### Import completion + +- global toast: + - `{Source} import complete` + - `View data` + - `Create app` + +Routes: + +- `View data` -> `/sources/:platformId` +- `Create app` -> `/sources/:platformId?intent=create-app` + +### Source overview + +Add app-creation entry point on the overview page: + +- sidebar action or top action area +- opens a sheet/dialog/panel if `intent=create-app` + +App-creation UI should offer: + +- `Copy prompt` +- `Reveal handoff files` + +V1.5 can add: + +- `Open example app` +- `Open source folder` + +## Technical slices + +### Slice 1: Home IA refactor + +Files likely touched: + +- `src/pages/home/index.tsx` +- `src/pages/home/components/connected-sources-list.tsx` +- `src/pages/home/components/available-sources-list.tsx` +- new Home layout components + +Work: + +- replace current stacked sections with a two-column layout +- keep current orchestration in `index.tsx` +- introduce a dumb layout layer that takes prepared props and callback handlers +- avoid overloading the new layout with run/state derivation logic +- adapt imported-source list so each row/card can expose `Create app` +- keep current import-state policy logic intact + +Suggested component organization: + +- keep current functional/orchestration components working during migration +- add new layout-first components rather than mutating the current ones in place + +Suggested split: + +- `index.tsx` + - owns data fetching, route navigation, callbacks, debug wiring + - builds view-model props for presentation +- `home-source-pipeline-layout.tsx` + - dumb two-column shell + - receives: + - page title + - left column content props + - right column content props +- `home-available-sources-section.tsx` + - dumb presentational section for the left column + - receives cards + callbacks +- `home-imported-sources-section.tsx` + - dumb presentational section for the right column + - receives rows/cards + callbacks + +Migration rule: + +- prefer creating new layout/presentation components +- leave current `ConnectedSourcesList` and `AvailableSourcesList` intact until + the new structure proves cleaner +- reuse existing lower-level primitives where possible + +Reason: + +- this keeps layout agnostic from business logic +- this avoids turning existing mixed-responsibility components into harder-to-read + transition code +- this gives us a clearer cut line between orchestration and presentation + +### Slice 2: Completion CTA plumbing + +Files likely touched: + +- Home import orchestration files +- global toast usage points + +Work: + +- detect terminal success transition for a source import +- show one completion toast per successful run +- wire toast actions to source overview routes + +### Slice 3: Source overview intent handling + +Files likely touched: + +- `src/pages/source/index.tsx` +- `src/pages/source/use-source-overview-page.ts` +- source overview components + +Work: + +- read `intent=create-app` from URL search params +- open app-creation UI state on first render for that intent +- keep route canonical to the source page, not a new app-builder route + +### Slice 4: Local handoff generation + +Files likely touched: + +- source overview hook/components +- new helper(s) for handoff generation + +Work: + +- derive source summary from existing export preview/meta +- generate agent-agnostic prompt text +- expose `Copy prompt` +- expose `Reveal handoff files` + +Potential artifact formats: + +- markdown prompt file +- small folder with prompt + source metadata + +### Slice 5: V1.5 smoothing + +Work: + +- add example-app mapping support +- enrich prompts with source-specific guidance +- make likely next actions obvious in source overview + +## Data/product rules + +- `Create app` is always explicit user intent +- never imply the app is already being built +- never upload source export data remotely in this phase +- do not depend on detecting Claude/Codex/etc. +- handoff must be useful even if the user only copies plain text + +## Suggested implementation order + +1. Home two-column layout +2. `Create app` CTA on imported sources +3. source overview URL-intent handling +4. completion toast CTA wiring +5. local prompt/handoff generation +6. v1.5 prompt quality and example-app refinement + +## Testing + +- Home layout tests for available/imported split +- tests covering import success -> toast CTA visibility +- source overview tests for `intent=create-app` +- runtime-branch tests for local file reveal/open helpers where needed +- regression test that `View data` and `Create app` routes stay source-scoped + +## Risks + +- Home can become more complex if imported-source actions are overstuffed +- source overview could feel overloaded if the app-creation UI is too large +- generated prompts may be weak until example-app mappings and schema summaries + improve + +## Exit criteria + +- Home clearly reads as a source pipeline +- imported sources feel actionable +- users can reach app creation from both Home and source overview +- app-creation handoff is local-first and usable without a hosted backend +- the plan remains explicitly short of one-click deployed generation From 3560332aa86c76ac0414639f362890991ff77ddc Mon Sep 17 00:00:00 2001 From: Callum Flack Date: Wed, 25 Mar 2026 15:36:34 +1000 Subject: [PATCH 2/4] docs(app-quickstart): add spec and planning checkpoints Archive the March 24 ideation plan and add the App Quickstart handoff, invariants, evals, product spec, and refreshed implementation plan as the new docs source of truth. --- ...-local-app-creation-implementation-plan.md | 131 +++++ ...60325-app-quickstart-invariants-evals.yaml | 362 ++++++++++++ .../260325-app-quickstart-spec.md | 522 ++++++++++++++++++ .../260325-builder-flow-mermaid.md | 176 ++++++ .../260325-builder-redesign-handoff.md | 319 +++++++++++ .../260325-builder-redesign-invariants.md | 194 +++++++ .../260325-builder-redesign-kickoff-prompt.md | 65 +++ ...0325-app-quickstart-implementation-plan.md | 237 ++++++++ 8 files changed, 2006 insertions(+) rename docs/{plans => _archive}/260324-source-pipeline-home-and-local-app-creation-implementation-plan.md (65%) create mode 100644 docs/builder-flow/260325-app-quickstart-invariants-evals.yaml create mode 100644 docs/builder-flow/260325-app-quickstart-spec.md create mode 100644 docs/builder-flow/260325-builder-flow-mermaid.md create mode 100644 docs/builder-flow/260325-builder-redesign-handoff.md create mode 100644 docs/builder-flow/260325-builder-redesign-invariants.md create mode 100644 docs/builder-flow/260325-builder-redesign-kickoff-prompt.md create mode 100644 docs/plans/260325-app-quickstart-implementation-plan.md diff --git a/docs/plans/260324-source-pipeline-home-and-local-app-creation-implementation-plan.md b/docs/_archive/260324-source-pipeline-home-and-local-app-creation-implementation-plan.md similarity index 65% rename from docs/plans/260324-source-pipeline-home-and-local-app-creation-implementation-plan.md rename to docs/_archive/260324-source-pipeline-home-and-local-app-creation-implementation-plan.md index 0e2ce8d5..b46e452e 100644 --- a/docs/plans/260324-source-pipeline-home-and-local-app-creation-implementation-plan.md +++ b/docs/_archive/260324-source-pipeline-home-and-local-app-creation-implementation-plan.md @@ -1,5 +1,12 @@ # 260324 Implementation Plan: Source Pipeline Home And Local App Creation +> [!WARNING] +> SUPERSEDED — archived on 2026-03-25. +> This document is retained for early ideation/reference only and is no longer +> an active source of truth. +> Use `docs/builder-flow/260325-app-quickstart-spec.md` and +> `docs/plans/260325-app-quickstart-implementation-plan.md` instead. + ## Goal Implement the Home/source-overview changes needed to: @@ -157,6 +164,130 @@ Reason: transition code - this gives us a clearer cut line between orchestration and presentation +### Concrete prop interface sketch + +The target shape is: + +- route/container builds view models +- layout components render only +- sections take plain data + callbacks +- lower-level item components stay small and dumb + +Example sketch: + +```ts +type HomePipelineColumnKey = "available" | "imported" + +interface HomeSourcePipelineLayoutProps { + title: string + leftColumn: HomePipelineColumnProps + rightColumn: HomePipelineColumnProps +} + +interface HomePipelineColumnProps { + key: HomePipelineColumnKey + heading: string + description?: string + emptyState?: { + title: string + description?: string + } + children: ReactNode +} + +interface HomeAvailableSourcesSectionProps { + heading: string + description?: string + addYourOwnHref: string + sources: HomeAvailableSourceCardProps[] + onConnect: (platformId: string) => void + onStopImport: (runId: string) => void +} + +interface HomeAvailableSourceCardProps { + id: string + name: string + iconName: string + iconImageSrc?: string + availability: "available" | "running" | "blocked" | "coming-soon" + statusLine?: string + accountLine?: string + expectationLine?: string + helperLine?: string + runId?: string + canConnect: boolean + canStop: boolean +} + +interface HomeImportedSourcesSectionProps { + heading: string + description?: string + sources: HomeImportedSourceRowProps[] + onViewData: (platformId: string) => void + onCreateApp: (platformId: string) => void + onSync: (platformId: string) => void +} + +interface HomeImportedSourceRowProps { + id: string + name: string + iconName: string + lastUpdatedLabel?: string + syncState?: "idle" | "running" | "backgrounding" + syncDisabled?: boolean + createAppDisabled?: boolean +} +``` + +Container responsibility: + +- derive `availability` +- derive copy such as `statusLine`, `accountLine`, `expectationLine` +- derive whether actions are enabled +- map platform/run state to simple row/card props + +Presentation responsibility: + +- render headings and body copy with `Text` +- render layout spacing and column structure +- wire button clicks back through provided callbacks +- avoid reading router/store/hooks directly + +### Suggested file shape + +```text +src/pages/home/ + index.tsx + use-home-page.ts + components/ + home-source-pipeline-layout.tsx + home-pipeline-column.tsx + home-available-sources-section.tsx + home-available-source-card.tsx + home-imported-sources-section.tsx + home-imported-source-row.tsx +``` + +The point of `use-home-page.ts` is not to invent more abstraction. It is just a +clean place to hold Home orchestration once the route starts doing more: + +- derive available/imported view models +- expose callbacks +- handle import-success refresh/toast behavior +- keep `index.tsx` mostly composition + +### Layout notes from existing components + +Use the current page and source overview rhythm as the guide: + +- keep `PageContainer` at the route level +- keep headings/subcopy grouped in `space-y-1` or `space-y-gap` blocks +- keep `Text` as the default for visible copy +- prefer simple column wrappers over deeply nested section shells +- preserve the existing calm spacing patterns before trying to redesign them + +This should feel like the current app, just reorganized into a clearer pipeline. + ### Slice 2: Completion CTA plumbing Files likely touched: diff --git a/docs/builder-flow/260325-app-quickstart-invariants-evals.yaml b/docs/builder-flow/260325-app-quickstart-invariants-evals.yaml new file mode 100644 index 00000000..ec6dcad6 --- /dev/null +++ b/docs/builder-flow/260325-app-quickstart-invariants-evals.yaml @@ -0,0 +1,362 @@ +evals: + - id: HC-EXPERIENCE-001 + section: Experience + source_order: 1 + source_ref: "260325-builder-redesign-invariants.md > Product Invariants > Experience" + source_span: "lines 67-68" + classification: hard_constraint + evidence_target: test + hookability: harness_gate + check: App Quickstart starts only from explicit user intent and must not auto-open after import completion. + pass_if: + - Quickstart opens only after a user-triggered `Create app` action. + - Import completion UI may offer `Create app`, but does not auto-open the quickstart surface. + fail_if: + - Completing an import automatically opens the quickstart dialog, sheet, or route. + - Visiting an imported-source screen auto-starts quickstart without an explicit `Create app` action. + counterexample: + - A completed import toast immediately opens the quickstart dialog before the user clicks anything. + hook_suggestion: + kind: test_gate + reject_if: + - Route or interaction tests show quickstart opening without a user-triggered `Create app` action. + + - id: HC-EXPERIENCE-002 + section: Experience + source_order: 2 + source_ref: "260325-builder-redesign-invariants.md > Product Invariants > Experience" + source_span: "lines 69-69" + classification: hard_constraint + evidence_target: manual + hookability: manual_only + check: Collect the user's app idea once before handoff generation and do not require the same intent to be re-entered later in the flow. + pass_if: + - The flow has one required free-text app-idea input before generation. + - Later screens focus on generated handoff review, editing, or export rather than restating the same idea. + fail_if: + - A later quickstart step requires the user to re-enter the app concept, description, or equivalent intent field. + counterexample: + - The user writes an app idea in the first screen and is later forced to fill another required product-description field with the same concept. + hook_suggestion: + kind: manual_only + reject_if: + - Manual review finds multiple required free-text app-intent entries in the same quickstart flow. + + - id: HC-EXPERIENCE-003 + section: Experience + source_order: 3 + source_ref: "260325-builder-redesign-invariants.md > Product Invariants > Experience" + source_span: "lines 74-75" + classification: hard_constraint + evidence_target: dom + hookability: harness_gate + check: The completion state must provide a usable handoff artifact and must not imply the app is already built, deployed, or published. + pass_if: + - The completion state exposes a generated artifact the user can preview, copy, reveal, or otherwise take away immediately. + - UI copy describes the outcome as a handoff, prompt, quickstart, or next-step artifact rather than a built or live app. + fail_if: + - The flow ends on a checklist or congratulatory screen without a usable artifact. + - UI copy claims the app was built, deployed, published, or made live when the feature only generated a handoff. + counterexample: + - A success modal says "Your app is live" but only offers a list of manual next steps and no generated artifact. + hook_suggestion: + kind: dom_audit + reject_if: + - The completion state lacks artifact actions such as preview, copy, or reveal. + - Completion copy claims the app already exists as a built or deployed product without such a feature being implemented. + + - id: HC-STRUCTURE-001 + section: Structure + source_order: 4 + source_ref: "260325-builder-redesign-invariants.md > Product Invariants > Structure" + source_span: "lines 79-81" + classification: hard_constraint + evidence_target: test + hookability: harness_gate + check: App Quickstart remains source-scoped and is launched within source-overview context rather than a separate builder dashboard. + pass_if: + - "`Create app` routes or UI state remain tied to a specific imported source." + - Quickstart opens inside source overview context or an equivalent source-scoped surface. + fail_if: + - "`Create app` launches a global builder dashboard detached from the current source." + - The quickstart path loses the source context needed to understand which imported data is in use. + counterexample: + - Clicking `Create app` on a source routes to `/build` with no source context and no source overview state. + hook_suggestion: + kind: test_gate + reject_if: + - Routing tests show `Create app` leaving source context instead of opening source-scoped quickstart UI. + + - id: HC-RELIABILITY-001 + section: Reliability + source_order: 5 + source_ref: "260325-builder-redesign-invariants.md > Product Invariants > Reliability" + source_span: "lines 90-91" + classification: hard_constraint + evidence_target: test + hookability: harness_gate + check: AI generation failures degrade to a retryable, editable handoff state rather than a dead end. + pass_if: + - A failed generation keeps the user in a quickstart state where they can retry or edit the handoff inputs/artifact. + fail_if: + - A failed generation terminates the flow with no retry path, no editable state, or forced abandonment. + counterexample: + - The generation API fails and the UI only shows a blocking error screen with no retry and no editable prompt state. + hook_suggestion: + kind: test_gate + reject_if: + - Failure-path tests show no retry action and no editable recovery state after generation errors. + + - id: HC-RELIABILITY-002 + section: Reliability + source_order: 6 + source_ref: "260325-builder-redesign-invariants.md > Product Invariants > Reliability" + source_span: "lines 93-94; lines 123-139" + classification: hard_constraint + evidence_target: output + hookability: repo_hook + check: The generated handoff artifact includes actionable source context, including source identity, local data location or reveal path, and a meaningful preview or summary. + pass_if: + - The handoff artifact identifies the source. + - The handoff artifact tells the user or downstream tool where the local data lives or how to reveal it. + - The handoff artifact includes a non-trivial preview, summary, or similar source-aware context. + fail_if: + - The artifact is a generic app-building prompt with no source identity. + - The artifact omits the local data path or equivalent reveal instruction. + - The artifact lacks any source-aware preview or summary content. + counterexample: + - The copied prompt says only "Build a starter app from user data" and provides no source name, no local path, and no preview context. + hook_suggestion: + kind: output_gate + reject_if: + - Generated quickstart artifacts omit source identity, local data location or reveal instruction, or any preview/summary field. + + - id: HC-PRIVACY-001 + section: Privacy And Trust + source_order: 7 + source_ref: "260325-builder-redesign-invariants.md > Product Invariants > Privacy And Trust" + source_span: "lines 103-104" + classification: hard_constraint + evidence_target: diff + hookability: repo_hook + check: V1 quickstart remains local-first and does not upload exported source data to remote infrastructure. + pass_if: + - Quickstart generation and artifact handling do not send exported source contents or files to non-local services in v1. + fail_if: + - Quickstart uploads exported data, previews, or source files to a remote API, worker, or hosted generation service. + counterexample: + - Clicking `Create app` posts the full exported JSON to a hosted endpoint to generate the starter app prompt. + hook_suggestion: + kind: diff_gate + reject_if: + - Quickstart code paths send exported source file contents, previews, or file uploads to non-local endpoints. + + - id: HC-PRIVACY-002 + section: Privacy And Trust + source_order: 8 + source_ref: "260325-builder-redesign-invariants.md > Product Invariants > Privacy And Trust" + source_span: "lines 105-105" + classification: hard_constraint + evidence_target: diff + hookability: repo_hook + check: V1 quickstart must not require detecting a specific coding agent installation before producing a usable artifact. + pass_if: + - The user can complete quickstart and obtain a usable artifact without installation checks for Claude, Codex, Lovable, Bolt, v0, or similar tools. + fail_if: + - Quickstart is blocked until the app confirms a specific external tool is installed or connected. + counterexample: + - The `Create app` flow is disabled unless Codex Desktop is detected on the machine. + hook_suggestion: + kind: diff_gate + reject_if: + - Quickstart availability or artifact generation is gated on detecting or authenticating a specific coding agent tool. + + - id: HC-SCOPE-001 + section: Scope + source_order: 9 + source_ref: "260325-builder-redesign-invariants.md > Product Invariants > Scope" + source_span: "lines 111-112; 260325-builder-redesign-handoff.md lines 228-230" + classification: hard_constraint + evidence_target: diff + hookability: repo_hook + check: The quickstart happy path must not depend on current on-chain mechanics or registration steps before the user has a valid artifact. + pass_if: + - Quickstart can complete artifact generation, preview, copy, or reveal without on-chain registration or equivalent protocol ceremony. + fail_if: + - Quickstart invokes on-chain registration, wallet generation, relayer setup, or equivalent before the artifact exists. + counterexample: + - The user cannot copy the quickstart prompt until a grantee is registered on-chain. + hook_suggestion: + kind: diff_gate + reject_if: + - Quickstart happy-path code performs on-chain registration or related protocol setup before artifact generation is complete. + + - id: HC-SCOPE-002 + section: Scope + source_order: 10 + source_ref: "260325-builder-redesign-invariants.md > Product Invariants > Scope" + source_span: "lines 112-112; lines 168-170" + classification: hard_constraint + evidence_target: diff + hookability: repo_hook + check: The quickstart happy path must not require CMS persistence before the user can access the generated artifact. + pass_if: + - The artifact can be previewed, copied, or revealed without saving to Sanity or another CMS. + fail_if: + - Quickstart blocks on CMS save success before showing the artifact. + counterexample: + - The flow hides the generated prompt until a remote draft record is persisted. + hook_suggestion: + kind: diff_gate + reject_if: + - Quickstart completion depends on remote CMS persistence before artifact actions are available. + + - id: HC-SCOPE-003 + section: Scope + source_order: 11 + source_ref: "260325-builder-redesign-invariants.md > Product Invariants > Scope" + source_span: "lines 114-115; lines 165-170" + classification: hard_constraint + evidence_target: diff + hookability: repo_hook + check: V1 quickstart must not become hosted one-click app generation, deployment, or app management. + pass_if: + - V1 quickstart ends with a local-first handoff artifact rather than a hosted generation job, deployment, or management dashboard. + fail_if: + - V1 quickstart provisions remote jobs, auto-deploys an app, or merges into app-management workflows as its primary outcome. + counterexample: + - Clicking `Create app` starts a hosted build-and-deploy job and then routes the user into a generated-app dashboard. + hook_suggestion: + kind: diff_gate + reject_if: + - V1 quickstart code provisions hosted app-generation jobs, deployment automation, or app-management surfaces as part of the primary flow. + + - id: HC-FIRST-SLICE-001 + section: First Build Slice + source_order: 12 + source_ref: "260325-builder-redesign-invariants.md > First Build Slice" + source_span: "lines 154-156" + classification: hard_constraint + evidence_target: dom + hookability: harness_gate + check: Imported-source surfaces expose `Create app`, and that action enters source-overview quickstart UI. + pass_if: + - Imported-source rows, cards, or equivalent surfaces expose a `Create app` action. + - Activating that action opens or routes into source-overview quickstart UI for the current source. + fail_if: + - Imported sources lack a `Create app` action. + - "`Create app` bypasses source overview and opens an unrelated flow." + counterexample: + - Imported-source cards show `View data` only, with no way to start quickstart from the imported source. + hook_suggestion: + kind: dom_audit + reject_if: + - Imported-source surfaces do not render a `Create app` action tied to the current source. + + - id: HC-FIRST-SLICE-002 + section: First Build Slice + source_order: 13 + source_ref: "260325-builder-redesign-invariants.md > First Build Slice" + source_span: "lines 157-160" + classification: hard_constraint + evidence_target: dom + hookability: harness_gate + check: The source-overview quickstart UI exposes a prompt preview, `Copy prompt`, and `Reveal handoff files`. + pass_if: + - The quickstart surface shows the generated prompt or equivalent artifact preview. + - The quickstart surface includes direct actions for `Copy prompt` and `Reveal handoff files`. + fail_if: + - The artifact is hidden until export. + - The quickstart surface omits either preview, copy, or reveal actions. + counterexample: + - The quickstart dialog shows only a single "Continue" button and no prompt preview or reveal-files action. + hook_suggestion: + kind: dom_audit + reject_if: + - Quickstart UI lacks a visible prompt preview or is missing `Copy prompt` or `Reveal handoff files` actions. + + - id: HC-ADJACENT-001 + section: Adjacent Thread / Existing Starter Apps + source_order: 14 + source_ref: "260325-builder-redesign-handoff.md > Recommended DataConnect Product Shape > Adjacent Thread: Existing Starter Apps" + source_span: "lines 123-140" + classification: hard_constraint + evidence_target: dom + hookability: manual_only + check: When both quickstart and existing starter-app paths are surfaced, the UI clearly distinguishes generating a new app handoff from opening an existing app, template, or example. + pass_if: + - "`Create app` is reserved for generating a new quickstart handoff." + - Existing app/template/example actions use distinct copy and intent from `Create app`. + fail_if: + - Existing starter apps are surfaced using `Create app` copy or otherwise blur the difference between generation and opening an existing app. + counterexample: + - A source-overview panel labels an existing starter app link as `Create app`, making it unclear whether the user is generating something new or opening something prebuilt. + hook_suggestion: + kind: manual_only + reject_if: + - Manual UX review finds starter-app actions and quickstart generation actions conflated in copy or interaction intent. + + - id: SS-EXPERIENCE-001 + section: Experience + source_order: 15 + source_ref: "260325-builder-redesign-invariants.md > Product Invariants > Experience" + source_span: "lines 72-73" + classification: soft_score + evidence_target: dom + hookability: manual_only + check: Prefer a generated handoff as the default path instead of a long empty form. + pass_if: + - The default quickstart path quickly produces a generated artifact or draft the user can react to. + fail_if: + - The main quickstart flow presents a large empty form as the primary experience. + counterexample: + - After clicking `Create app`, the user lands on a multi-field blank wizard before any generated handoff appears. + + - id: SS-EXPERIENCE-002 + section: Experience + source_order: 16 + source_ref: "260325-builder-redesign-invariants.md > Product Invariants > Experience" + source_span: "lines 73-73; 260325-builder-redesign-handoff.md lines 94-95" + classification: soft_score + evidence_target: dom + hookability: manual_only + check: Keep advanced configuration collapsed or visually secondary by default. + pass_if: + - Advanced prompt or data-processing controls are hidden, collapsed, or clearly secondary on first render. + fail_if: + - Advanced controls dominate the first quickstart view or compete with the primary happy path. + counterexample: + - The quickstart opens with a full advanced data-processing editor expanded above the main prompt preview. + + - id: SS-RELIABILITY-001 + section: Reliability + source_order: 17 + source_ref: "260325-builder-redesign-invariants.md > Product Invariants > Reliability" + source_span: "lines 96-97; 260325-builder-redesign-handoff.md lines 193-195" + classification: soft_score + evidence_target: dom + hookability: manual_only + check: Advanced prompt inspection should be available as an optional layer without becoming a mandatory step in the happy path. + pass_if: + - Users can reach artifact generation and export actions without editing advanced prompt controls. + - Advanced inspection exists for users who need it. + fail_if: + - The user must open, edit, or acknowledge advanced prompt controls to complete quickstart. + counterexample: + - The flow blocks `Copy prompt` until the user visits and confirms an advanced prompt step. + + - id: SS-DEFINITION-001 + section: Definition Of Success + source_order: 18 + source_ref: "260325-builder-redesign-invariants.md > Definition Of Success" + source_span: "lines 189-194; 260325-builder-redesign-handoff.md lines 314-319" + classification: soft_score + evidence_target: output + hookability: manual_only + check: The generated handoff should feel source-specific and make the next step obvious. + pass_if: + - The artifact reads as tailored to the current source and clearly tells the user what to do next. + fail_if: + - The artifact feels generic enough to work for any source and leaves the next step ambiguous. + counterexample: + - The prompt could apply equally to any export and does not tell the user how to proceed with the local source data. diff --git a/docs/builder-flow/260325-app-quickstart-spec.md b/docs/builder-flow/260325-app-quickstart-spec.md new file mode 100644 index 00000000..d736b6b9 --- /dev/null +++ b/docs/builder-flow/260325-app-quickstart-spec.md @@ -0,0 +1,522 @@ +# 260325 Product Spec: App Quickstart + +## Purpose + +Define the source-of-truth UI and behavior contract for DataConnect's +`Create app` feature. + +This spec converts the App Quickstart handoff, invariants, and evals into one +implementation-ready contract. + +It is intentionally scoped to: + +- source overview as the primary surface +- Home/import-complete entry points +- adjacent starter-app actions when there is a strong source match + +It does not redesign the Data Apps page in detail. + +## Product Stance And Terminology + +### Product stance + +DataConnect should give imported sources an immediate `build something from +this` payoff by generating a local-first starter-app handoff that gets the user +to a working demo fast. + +### Terms + +- `Create app` + The user-facing CTA. This always means generating a new quickstart handoff + from the current source. +- `App Quickstart` + The feature/system concept behind `Create app`. +- `Starter app` + A pre-existing app, template, or example app that already fits a source and + can be opened directly. +- `Data Apps` + The broader catalog/discovery surface for existing apps. It is not the primary + App Quickstart surface. + +### Naming rule + +`Create app` must never be used for starter-app actions. +Starter-app actions must use distinct copy such as `Open {AppName}` or +`Open starter app`. + +## Information Architecture And Entry Points + +### Primary surface + +The primary App Quickstart surface is a modal dialog anchored to source overview. + +Route shape: + +- source overview route remains canonical: `/sources/:platformId` +- quickstart open state is controlled by URL search params: + `/sources/:platformId?intent=create-app` + +Closing the dialog removes only `intent=create-app` and leaves the user on the +same source route. + +### Entry points + +V1 entry points are: + +1. Home imported-source CTA + - `Create app` navigates to `/sources/:platformId?intent=create-app` +2. import-complete toast CTA + - `Create app` navigates to `/sources/:platformId?intent=create-app` +3. source overview CTA + - `Create app` opens the same dialog in-place + +### Data Apps relationship + +Data Apps remains the broader catalog/discovery surface for existing apps. +This spec only requires that source overview may surface one adjacent starter-app +action when there is a strong match for the current source. + +No detailed Data Apps redesign is part of this spec. + +## Source Overview Quickstart Contract + +### Surface shape + +V1 uses a source-overview modal dialog, not a separate dashboard or wizard +route. + +The dialog contains, in order: + +1. header +2. source context block +3. optional starter-app block +4. app-idea input +5. quickstart artifact panel +6. action row + +### Header contract + +- title: `Create app from {SourceName}` +- support copy: one short paragraph explaining that DataConnect can prepare a + local-first quickstart from the current source + +### Source context block + +Always show: + +- source name +- local data location or reveal hint +- short source summary or preview summary + +This block exists in every quickstart state. + +### App-idea input contract + +There is exactly one required free-text input before generation. + +- label: `What do you want to make?` +- control: multiline text input +- required for generation +- no secondary required form fields in v1 + +The input should be preserved while the user remains on the same source page in +the same session, even if the dialog closes and reopens. +It resets when the source changes or the page reloads. + +## Quickstart States + +### Interface + +```ts +type QuickstartGenerationState = + | { status: "idle" } + | { status: "generating" } + | { status: "generated"; artifact: AppQuickstartArtifact } + | { + status: "fallback-ready" + artifact: AppQuickstartArtifact + reason: "ai-unavailable" | "ai-failed" | "ai-invalid" + } + | { + status: "error-with-retry" + message: string + } +``` + +### `idle` + +Visible UI: + +- source context block +- optional starter-app block +- empty app-idea input +- empty artifact panel with short guidance + +Primary action: + +- `Generate quickstart` + +Secondary actions: + +- `Close` + +### `generating` + +Visible UI: + +- source context block remains visible +- starter-app block remains visible if present +- app-idea input remains visible but disabled +- artifact panel shows loading state + +Primary action label: + +- `Generating quickstart…` + +### `generated` + +Visible UI: + +- source context block +- starter-app block if present +- app-idea input with current value +- artifact panel showing the generated handoff + +Primary action: + +- `Copy prompt` + +Secondary actions: + +- `Reveal handoff files` +- `Generate again` +- `Close` + +### `fallback-ready` + +This state is used when AI generation is unavailable, fails, or returns invalid +output, but deterministic local fallback artifact generation succeeds. + +Visible UI: + +- same as `generated` +- inline note that the current artifact is the local quickstart fallback + +Primary action: + +- `Copy prompt` + +Secondary actions: + +- `Reveal handoff files` +- `Retry with AI` +- `Close` + +### `error-with-retry` + +This state is used only when the system cannot build either an AI artifact or a +deterministic fallback artifact. + +Visible UI: + +- source context block +- starter-app block if present +- app-idea input with preserved value +- inline error message in the artifact panel + +Primary action: + +- `Retry generation` + +Secondary actions: + +- `Close` + +This must be a rare exception path, not the normal failure mode. + +## Starter-App Adjacent Path + +### Product rule + +Starter apps are adjacent to App Quickstart, not a replacement for it. + +- `Create app` generates something new +- starter-app actions open something that already exists + +### Matching rule + +V1 derives a `StarterAppMatch` from the existing app registry. + +```ts +type StarterAppMatch = { + sourceId: string + appLabel: string + destinationUrl: string + actionLabel: string +} +``` + +High-confidence match rule: + +- source id comes from the current source overview route +- candidate apps come from registry entries with `status: "live"` +- a candidate is a match when `app.dataRequired` contains the current source id + as its `token` +- show an adjacent starter-app slot only when there is exactly one matching live + app + +If there are zero matches: + +- show no starter-app slot + +If there are multiple matches: + +- show no starter-app slot in v1 +- leave discovery to the Data Apps catalog + +There is no ranking or recommendation logic in v1. + +### Starter-app block contract + +When a `StarterAppMatch` exists, show a clearly separate block inside the +quickstart dialog. + +Visible content: + +- heading: `Starter app` +- app label +- one-sentence app description if available from registry +- CTA: `Open {AppLabel}` + +Behavior: + +- opens the external starter app URL +- does not mutate quickstart state +- does not replace `Create app` + +## Artifact Contract + +### Interface + +```ts +type AppQuickstartArtifact = { + source: { + id: string + label: string + schemaId?: number + localDataLocation: string + sourceSummary: string + } + intent: { + appIdea: string + } + handoff: { + title: string + summary: string + prompt: string + nextStep: string + exampleAppLabel?: string + exampleAppHref?: string + } + advanced?: { + dataProcessingPrompt?: string + } + generation: { + mode: "ai-generated" | "fallback" + providerLabel?: string + } +} +``` + +### Minimal required contents + +The artifact must always include: + +- source identity +- local data location or a reveal-safe local path hint +- source summary +- the user's app idea +- a prompt that can be copied on its own +- one explicit next-step string +- generation mode + +### Artifact preview contract + +The dialog preview must show: + +- title +- summary +- prompt preview +- source summary +- local data location +- generation mode note only when relevant to user understanding + +### Handoff files contract + +`Reveal handoff files` opens a dedicated local quickstart folder under the +DataConnect app-data root, not the source export directory itself. + +V1 folder contents: + +- `app-quickstart.md` + - title + - summary + - prompt + - next step +- `source-context.json` + - source identity + - local data location + - source summary + - generation mode metadata + +If the files do not exist yet for the current artifact, create them first, then +open the folder. + +## Generation Contract + +### Product requirement + +The product contract is AI-assisted when available, but deterministic fallback +is mandatory. + +The quickstart must remain usable even when AI is unavailable. + +### Privacy rule + +V1 must remain local-first: + +- do not upload exported source files +- do not upload exported source preview payloads +- do not require a remote generation job + +This means: + +- remote providers may only be used with inputs that do not violate the above + rule +- any richer source-aware generation using export preview data must stay local + in v1 + +### AI generation input contract + +The provider-agnostic AI adapter may consume: + +- app idea +- source id +- source label +- stable product instructions + +If a local-only provider exists, it may additionally consume richer local source +summary inputs. + +### Validation rule + +AI output must be validated before it becomes the active artifact. + +Minimum valid output must include: + +- handoff title +- handoff summary +- prompt +- next step + +If validation fails, switch to deterministic fallback. + +### Deterministic fallback contract + +Fallback generation is local and synchronous from: + +- source name/id +- local data location +- existing source preview/summary data already available in the app +- the user's app idea + +Fallback output must still satisfy the full `AppQuickstartArtifact` contract. + +### Retry contract + +- `Retry with AI` and `Retry generation` both preserve source context and app + idea +- retry attempts do not clear the last available fallback artifact until a new + valid artifact replaces it +- if retry fails and a fallback artifact already exists, remain in + `fallback-ready` with an updated reason rather than dropping the user into a + dead end + +## Copy And Label Contract + +Required labels: + +- entry CTA: `Create app` +- input label: `What do you want to make?` +- idle primary action: `Generate quickstart` +- generating action: `Generating quickstart…` +- artifact action: `Copy prompt` +- artifact action: `Reveal handoff files` +- fallback retry: `Retry with AI` +- generic retry: `Retry generation` +- starter-app heading: `Starter app` +- starter-app action: `Open {AppLabel}` + +Forbidden copy patterns: + +- do not label starter-app actions as `Create app` +- do not claim the app is built, deployed, live, or published +- do not describe the artifact as a finished product + +## V1 Out Of Scope + +- hosted one-click app generation +- automatic deployment or hosting +- on-chain registration in the happy path +- CMS persistence requirements in the happy path +- coding-agent installation detection +- Data Apps page redesign +- starter-app ranking when multiple apps match a source +- app-management dashboards + +## Acceptance Criteria + +The implementation is complete only when the spec satisfies these hard +constraints from the eval set: + +- `HC-EXPERIENCE-001` + - quickstart starts only from explicit `Create app` intent +- `HC-EXPERIENCE-003` + - completion exposes a usable artifact and does not imply a built app +- `HC-STRUCTURE-001` + - quickstart stays source-scoped in source overview +- `HC-RELIABILITY-001` + - AI failures degrade to retryable or fallback states +- `HC-RELIABILITY-002` + - artifacts include source identity, local data location, and source summary +- `HC-PRIVACY-001` + - no remote upload of exported source data in v1 +- `HC-PRIVACY-002` + - no specific coding-agent detection requirement +- `HC-SCOPE-001` + - no on-chain dependency before artifact generation +- `HC-SCOPE-002` + - no CMS dependency before artifact access +- `HC-SCOPE-003` + - no hosted one-click generation/deployment/app management in v1 +- `HC-FIRST-SLICE-001` + - imported-source surfaces route into source-overview quickstart +- `HC-FIRST-SLICE-002` + - quickstart surface exposes preview, `Copy prompt`, and `Reveal handoff files` +- `HC-ADJACENT-001` + - starter-app actions are clearly distinct from `Create app` + +### Required end-to-end scenarios + +The spec must support these scenarios: + +1. import complete -> `Create app` CTA -> source-overview quickstart opens +2. source overview -> enter app idea -> AI-assisted generation succeeds +3. source overview -> AI unavailable -> fallback artifact is still usable +4. source overview -> AI request fails -> fallback artifact remains available and + `Retry with AI` works +5. exactly one starter-app match exists -> adjacent starter-app action is shown +6. zero or multiple starter-app matches exist -> no adjacent starter-app slot is + shown +7. completion state offers artifact actions and never claims the app is already + built or deployed diff --git a/docs/builder-flow/260325-builder-flow-mermaid.md b/docs/builder-flow/260325-builder-flow-mermaid.md new file mode 100644 index 00000000..76b1d522 --- /dev/null +++ b/docs/builder-flow/260325-builder-flow-mermaid.md @@ -0,0 +1,176 @@ +# Builder Flow Diagrams + +This document breaks the builder workflow into smaller Mermaid diagrams. +The goal is readability first, not completeness in a single canvas. + +## 0. FigJam-Friendly Overview + +```mermaid +flowchart LR + A["Open /build"] --> B{"Checks pass?"} + B -->|"No"| C["Resolve auth, wallet, or Google Drive connection"] + B -->|"Yes"| D["Choose path"] + + D --> E["Start from app seed input"] + D --> F["Open user apps dashboard"] + + E --> G["Describe app idea"] + G --> H["Select data source"] + H --> I["Analyze seed prompt"] + I --> J["Prefill app creation store"] + J --> K["Go to /build/create"] + K --> L["Complete create wizard"] + L --> M["Show success page"] + M --> N["Copy Lovable prompt"] + N --> O["Finish app manually in Lovable"] + + F --> P["Open existing app"] + F --> Q["Edit existing app"] + F --> R["Delete existing app"] + F --> S["Submit for hackathon or listing"] +``` + +## 1. High-Level Builder Flow + +```mermaid +flowchart TD + A["/build"] --> B["Connection checks + auth + wallet + Google Drive"] + B --> C["App Seed Input"] + B --> D["User Apps Dashboard"] + + C --> E["Describe app idea"] + E --> F["Select data source"] + F --> G["Analyze seed prompt"] + G --> H["Prefill app creation store"] + H --> I["Go to /build/create"] + + I --> J["Create wizard"] + J --> K["Success page"] + K --> L["Copy Lovable prompt"] + L --> M["Finish app manually in Lovable"] + + D --> N["Open existing app"] + D --> O["Edit existing app"] + D --> P["Delete existing app"] + D --> Q["Submit for hackathon/listing"] +``` + +## 2. App Creation Wizard + +```mermaid +flowchart TD + A["Step 1: App Info"] --> B["Validate marketing fields + name + tagline + description + branding"] + B --> C["Write values to app creation store"] + + C --> D["Step 2: Data Prompt"] + D --> E["Load sample data for selected schema"] + E --> F["Run prompt against sandbox sample"] + F --> G["Refine dataProcessingPrompt"] + + G --> H["Step 3: Product Description"] + H --> I["Generate app wallet"] + I --> J["Fetch relayer public key"] + J --> K["Encrypt app private key"] + K --> L["Require connected builder wallet"] + L --> M["Register grantee onchain"] + M --> N["Receive granteeId"] + N --> O["Save app draft to Sanity CMS"] + + O --> P["Step 4: Success"] + P --> Q["Generate Lovable seed prompt"] +``` + +## 3. Seed Analysis + Prefill + +```mermaid +sequenceDiagram + participant U as User + participant B as /build page + participant S as AppSeedInput + participant API as /api/apps/analyze-seed + participant AI as Gemini + participant Store as AppCreationStore + + U->>B: Open /build + B->>S: Show seed input + U->>S: Enter app idea + choose data source + S->>Store: initializeApp() + S->>Store: save seedPrompt + schema + S->>API: POST analyze-seed + API->>AI: Ask for suggested app config + AI-->>API: Suggested fields + API-->>S: JSON suggestions + S->>Store: save suggested name, descriptions, prompts + S->>B: Navigate to /build/create +``` + +## 4. Prompt Sandbox Flow + +```mermaid +sequenceDiagram + participant U as User + participant Step as StepDataPrompt + participant Sandbox as SchemaSubpromptSandbox + participant Samples as /api/sandbox/fetch-samples + participant Run as /api/sandbox/run-prompt + + U->>Step: Open Data Prompt step + Step->>Sandbox: Inject selected schema + base prompt + Sandbox->>Samples: Fetch sample data for schema + Samples-->>Sandbox: Return sample JSON + U->>Sandbox: Edit prompt and sample if needed + U->>Sandbox: Click Run Analysis + Sandbox->>Run: POST enhanced prompt with sample data + Run-->>Sandbox: Return JSON result + Sandbox-->>U: Show analysis output +``` + +## 5. Onchain Registration + Save + +```mermaid +sequenceDiagram + participant U as User + participant Step as StepProductDescription + participant Key as /api/app-creation/relayer-key + participant Chain as /api/build/register-grantee + participant CMS as /api/apps + + U->>Step: Submit product description + Step->>Step: Generate app wallet + Step->>Key: GET relayer public key + Key-->>Step: publicKey + Step->>Step: Encrypt app private key + Step->>Chain: POST walletAddress + publicKey + Chain-->>Step: granteeId + tx hash + Step->>CMS: POST app draft + CMS-->>Step: saved Sanity document id + Step-->>U: Success page +``` + +## 6. Existing Apps Flow + +```mermaid +flowchart TD + A["User Apps Dashboard"] --> B["Fetch my apps"] + B --> C["List draft / existing apps"] + + C --> D["Open app URL"] + C --> E["Edit app"] + C --> F["Delete app"] + C --> G["Share app"] + C --> H["Submit for hackathon / listing"] + + E --> I["/build/edit/[id]"] + I --> J["Load app via /api/apps/[id]?build=true"] + J --> K["PATCH /api/apps/[id]"] + + F --> L["DELETE /api/apps/[id]"] +``` + +## Notes + +- The current live flow chooses the data source in the seed input on `/build`. +- There is a `step-data-source.tsx` file in the repo, but it is not wired into the current wizard steps. +- The success page does not fully publish the app. It hands off to Lovable for the final manual build/publish flow. diff --git a/docs/builder-flow/260325-builder-redesign-handoff.md b/docs/builder-flow/260325-builder-redesign-handoff.md new file mode 100644 index 00000000..74859cf9 --- /dev/null +++ b/docs/builder-flow/260325-builder-redesign-handoff.md @@ -0,0 +1,319 @@ +# App Quickstart Handoff + +## Purpose + +This document packages the product/design learnings from the March 25 builder +flow exploration and reframes them for DataConnect's `Create app` feature. + +It is intentionally product-first. + +The goal is not to port the old builder implementation. +The goal is to preserve the useful know-how, avoid the old UX failures, and use +that learning to shape a cleaner `App Quickstart` inside DataConnect. + +`App Quickstart` is the preferred feature name in this document because +`Builder` already has protocol meaning elsewhere in DataConnect. + +## What We Produced + +### Diagrams And Prototype Material + +- Mermaid source: `apps/web/app/build/_docs/260325-builder-flow-mermaid.md` +- FigJam overview board: + `https://www.figma.com/board/4BJ1BRNEqJOpvn1KTBuMEu/Builder-Flow-Overview` +- Prototype-inspired ideal flow lives on that board as: + `Zoom 3: Prototype-Based Ideal Flow` + +### Prototype Direction + +We reviewed a cleaner prototype flow with these screens: + +1. choose source + describe app +2. AI expands context into editable draft fields +3. review advanced data-processing prompt +4. confirm generated app instructions +5. create prompt / create record +6. copy or open in external tool + +That direction is materially better than the old builder code flow, even though +the new DataConnect product should be framed as a fast quickstart rather than a +full prompt studio. + +## Short Product Conclusion + +The old builder behaved like an internal process. +DataConnect should behave like a fast local-first quickstart. + +The right mental model is: + +`import data -> click Create app -> get a working demo handoff fast` + +And, when useful: + +`already have a relevant starter app -> open that as a separate path` + +Not: + +`seed input -> separate wizard steps -> infra ceremony -> manual checklist` + +## Product Stance + +DataConnect should give imported sources an immediate `build something from +this` payoff by generating a local-first starter-app handoff that gets the user +to a working demo fast. + +The user-facing CTA should stay: + +- `Create app` + +## What Was Wrong With The Old Builder + +### Core Problems + +- The user was asked for the same intent multiple times. +- Creation, gating, and app management were mixed on one surface. +- On-chain registration, encryption, and CMS save sat directly on the happy + path. +- "Success" was really a manual checklist, not a completed artifact. +- The flow was form-heavy where it should have been draft/handoff-heavy. + +### Practical UX Failures + +- The seed input already inferred useful app metadata, but the user then had to + re-enter overlapping details later. +- The `/build` page mixed checks, creation, and dashboard concerns. +- The product-description step did expensive and fragile work at the wrong time. +- Lovable handoff appeared too late and too awkwardly. + +## What The Prototype Got Right + +- It asks once. +- It keeps source selection lightweight. +- It uses AI expansion to create a draft instead of making the user fill long + forms. +- It keeps `Advanced` secondary. +- It makes the export artifact explicit. + +These are still the right cues for App Quickstart. + +## Recommended DataConnect Product Shape + +### Primary Thread: App Quickstart + +1. start from an imported source +2. click `Create app` +3. describe the app idea once +4. generate an AI-assisted handoff +5. optionally inspect advanced data-processing guidance +6. copy the prompt and/or reveal local handoff files +7. continue in a coding environment of choice + +### Adjacent Thread: Existing Starter Apps + +There is a second, distinct path that already exists in the product ecosystem: +some sources may have existing starter apps, templates, or example apps that +the user can run locally or fork immediately. + +Example shape: + +- LinkedIn data -> open an existing app such as `linkedin-to-readcv` + +This path is adjacent to App Quickstart, not a replacement for it. + +The UI should account for both threads: + +- `Create app` means generate a new quickstart handoff from the user's data. +- starter app/example app actions mean open something that already exists. + +Those actions should be clearly distinct in copy and intent. + +### Information Architecture Implication + +App Quickstart should stay source-scoped and source-aware. +Starter apps belong to the discover/catalog/distribution side of the product, +but can be surfaced from source-overview when there is a clear match for the +current source. + +The important thing is not to blur: + +- generating a new app handoff +- opening an existing app/template/example + +## What To Carry Forward From The Existing Builder Work + +### Carry Forward + +- AI seed analysis as a prefill mechanism +- schema-aware prompt generation +- prompt sandbox/testing as an advanced layer +- explicit final export artifact +- failure knowledge from auth-fetch and prompt execution + +### Do Not Carry Forward As-Is + +- the old wizard scaffolding +- Lovable-specific URL/ID generation as the core model +- on-chain registration inside the main quickstart path +- mixed creation and dashboard entry page +- heavy reliance on mutable wizard state as the main mental model + +## Important Existing Nuances + +These are worth preserving as knowledge even if implementation changes +substantially. + +### 1. Seed Analysis Already Solved The Right Problem + +`apps/web/lib/services/app-seed-analysis.service.ts` + +The old system had the right instinct: +take one seed prompt and expand it into name, tagline, description, branding, +data prompt, and product description. + +That should still inform App Quickstart. +The difference is that the output now serves a fast handoff, not a longer wizard +journey. + +### 2. Prompt Generation Was Already Treated As A Real Artifact + +`apps/web/lib/utils/app-seed-prompt.ts` + +The old system generated a large structured prompt artifact for external AI app +builders. +That was directionally correct. + +The export artifact should remain first-class, even if the exact format changes. + +### 3. Data Prompt Testing Already Exists + +- `apps/web/app/build/create/steps/step-data-prompt.tsx` +- `apps/web/components/sandbox/schema-subprompt-sandbox.tsx` +- `apps/web/app/api/sandbox/run-prompt/route.ts` + +This is valuable. +In DataConnect, it should survive as an advanced inspection layer, not a +mandatory step in the happy path. + +### 4. The Old Success State Was Not Real Success + +`apps/web/app/build/create/steps/step-success.tsx` + +The old success page was really a manual Lovable checklist. +That should not be copied. + +For App Quickstart, success should mean: + +- a real handoff artifact exists +- the user can copy it or reveal it locally +- the next step is immediately understandable + +### 5. The Old Infra Path Was Too Heavy + +- `apps/web/app/build/create/steps/step-product-description.tsx` +- `apps/web/app/api/app-creation/relayer-key/route.ts` +- `apps/web/app/api/build/register-grantee/route.ts` +- `apps/web/lib/services/app-cms.service.ts` + +The old product-description step bundled: + +- app-wallet generation +- relayer key fetch +- encryption +- on-chain registration +- chain confirmation waiting +- CMS persistence + +That was exactly the wrong place for it. + +If record-creation or publish steps exist in the future, they should happen +after the user already has a valid quickstart artifact, not before first +success. + +### 6. Source Overview Is The Right Anchor In DataConnect + +The newer source-pipeline thinking in this repo is correct: +the source overview page is the right place to turn imported data into action. + +That means: + +- Home can create momentum into `Create app` +- source overview can host the quickstart UI +- the user stays anchored on the actual imported data while deciding what to do + +## Suggested Canonical Artifact + +The flow should revolve around one coherent handoff artifact, conceptually +similar to: + +```ts +type AppQuickstartArtifact = { + source: { + id: string; + label: string; + schemaId?: number; + exportPath?: string; + }; + intent: string; + handoff: { + title: string; + summary: string; + prompt: string; + sourceSummary?: string; + sampleOutput?: string; + exampleAppLabel?: string; + exampleAppHref?: string; + }; + advanced?: { + dataProcessingPrompt?: string; + }; +}; +``` + +This is not a required schema. +It is a good directional model because it gives the product one clear artifact +to preview, copy, and optionally write to local files. + +## First Product Slice + +The first implementation should include: + +- `Create app` CTA from imported-source surfaces +- source-overview quickstart UI +- short app-idea input +- AI-assisted handoff generation +- prompt preview +- `Copy prompt` +- `Reveal handoff files` +- optional advanced prompt inspection + +The first implementation should not include: + +- hosted generation jobs +- automatic deployment/hosting +- app-management dashboards +- on-chain registration in the happy path +- CMS persistence requirements +- provider-specific generation as the core flow + +## Open Product Questions Worth Tracking + +- when a source has a known starter app, where should that be surfaced: + Home, source overview, data-app catalog, or some combination? +- should the quickstart UI show starter apps as adjacent actions only when a + high-confidence source match exists? +- should the handoff artifact embed starter-app references when relevant, or + should those stay visually separate in UI? + +## Definition Of Success + +App Quickstart succeeds when a user can go from imported data to a credible +starter-app handoff with very little friction. + +That means: + +- the source context is already clear +- the handoff feels specific rather than generic +- the user can copy or reveal it immediately +- the next step is obvious +- the path feels fast enough that importing data naturally leads into trying to + make something with it diff --git a/docs/builder-flow/260325-builder-redesign-invariants.md b/docs/builder-flow/260325-builder-redesign-invariants.md new file mode 100644 index 00000000..3c539adb --- /dev/null +++ b/docs/builder-flow/260325-builder-redesign-invariants.md @@ -0,0 +1,194 @@ +# App Quickstart Invariants + +## Purpose + +This document reframes the March 25 builder-flow learnings for DataConnect's +`Create app` feature. + +It keeps the useful know-how from the earlier builder product, but narrows the +goal to a faster, more honest outcome inside DataConnect: + +`import data -> click Create app -> get a working demo handoff fast` + +The term `App Quickstart` replaces `builder` in this document because +`Builder` already has protocol meaning elsewhere in DataConnect. + +## Product Stance + +DataConnect should give imported sources an immediate `build something from +this` payoff by generating a local-first starter-app handoff that gets the user +to a working demo fast. + +The user-facing CTA should remain: + +- `Create app` + +The feature/system concept should be: + +- `App Quickstart` + +## Goal + +Build a source-scoped quickstart flow that helps a user: + +1. start from an imported source +2. describe the app idea once +3. receive an AI-assisted starter handoff +4. optionally inspect advanced data-processing guidance +5. copy or reveal the handoff artifact +6. continue in the coding environment of their choice + +## Prior Knowledge To Heed + +These points come from the previous builder implementation and the newer +source-pipeline docs. They should shape the quickstart invariants directly. + +### Carry forward + +- Single-prompt seed analysis is the right instinct. +- Schema-aware prompt generation is valuable. +- Prompt sandbox/testing is useful as an advanced layer. +- Exporting a real artifact is better than ending on a checklist. +- Source overview is the right anchor for app creation in DataConnect. +- Import completion should create momentum into `Create app`. + +### Do not carry forward as-is + +- The old step-by-step wizard structure. +- Mixing creation, management, and infrastructure on one surface. +- On-chain registration inside the main quickstart path. +- CMS persistence as a requirement for first success. +- Lovable-specific behavior as the product model. + +## Product Invariants + +### Experience + +- `Create app` is always explicit user intent. Never auto-start quickstart after + import. +- Ask for the user's app idea once. +- Optimize the default path for time to first working demo, not maximum + completeness. +- Prefer a generated handoff over empty forms. +- Keep advanced configuration collapsed by default. +- The success state must mean the user now has a usable handoff artifact. +- Never imply the app is already built, deployed, or published. + +### Structure + +- App Quickstart is source-scoped and starts from imported data. +- App Quickstart belongs in the source-overview context, not a separate builder + dashboard. +- Creation/quickstart must stay separate from any future app-management surface. +- The flow should revolve around one canonical handoff artifact, not scattered + wizard state. +- Source selection belongs at the start of the quickstart path when needed, but + in DataConnect the ideal trigger is usually an already-imported source. + +### Reliability + +- AI generation failures must degrade to a retryable, editable handoff state, + not a dead end. +- The handoff must still be useful if the user only copies plain text. +- The handoff must include enough source context to be actionable: + local data location, source identity, and a meaningful preview/summary. +- Expensive or fragile operations must not block first quickstart success. +- Advanced prompt inspection/testing should be available without becoming a + mandatory step. +- The user must be able to leave the quickstart with a real artifact even if no + downstream tool integration exists. + +### Privacy And Trust + +- Keep the quickstart local-first in v1. +- Do not upload exported source data to remote infrastructure in v1. +- Do not require detecting a specific coding agent installation. +- External tools are destinations for the handoff, not the system of record. + +### Scope + +- Do not rebuild the old builder wizard. +- Do not couple quickstart to current on-chain mechanics. +- Do not couple quickstart to Sanity CMS. +- Do not make Lovable/Bolt/v0-specific behavior the core model. +- Do not turn quickstart into hosted one-click app generation in v1. +- Do not turn quickstart into app management. + +## Canonical Artifact + +The quickstart flow should revolve around one source of truth, conceptually +similar to: + +```ts +type AppQuickstartArtifact = { + source: { + id: string; + label: string; + schemaId?: number; + exportPath?: string; + }; + intent: string; + handoff: { + title: string; + summary: string; + prompt: string; + sourceSummary?: string; + sampleOutput?: string; + exampleAppLabel?: string; + exampleAppHref?: string; + }; + advanced?: { + dataProcessingPrompt?: string; + }; +}; +``` + +This is not a locked schema. +The important point is that quickstart produces one coherent handoff artifact +that can be previewed, copied, and optionally written to local files. + +## First Build Slice + +The first implementation should include: + +- `Create app` entry from imported-source surfaces +- source-overview quickstart UI +- short app-idea input +- AI-assisted handoff generation +- prompt preview +- `Copy prompt` +- `Reveal handoff files` +- optional advanced prompt section + +The first implementation should not include: + +- hosted app generation jobs +- automatic deployment/hosting +- app-management dashboards +- on-chain registration on the happy path +- CMS persistence requirements +- provider-specific quickstart logic as the core flow + +## Design Cues From The Prototype + +These cues are still correct and should survive the rename to App Quickstart: + +- ask once +- edit a generated draft instead of filling long forms +- keep advanced inspection secondary +- make the exported artifact explicit +- keep the path understandable without exposing infrastructure details + +## Definition Of Success + +App Quickstart succeeds when a user can go from imported data to a credible +starter-app handoff with very little friction. + +That means: + +- the source context is already clear +- the prompt/handoff feels specific rather than generic +- the user can copy or reveal it immediately +- the user understands that the next step is to build from the local export +- the payoff feels fast enough that importing data naturally leads into trying + to make something with it diff --git a/docs/builder-flow/260325-builder-redesign-kickoff-prompt.md b/docs/builder-flow/260325-builder-redesign-kickoff-prompt.md new file mode 100644 index 00000000..3e97d5ca --- /dev/null +++ b/docs/builder-flow/260325-builder-redesign-kickoff-prompt.md @@ -0,0 +1,65 @@ +# Kickoff Prompt For App Quickstart Work + +Use this to start the next architecture/spec conversation for DataConnect's +`Create app` feature. + +```md +We are designing and building `App Quickstart` in this repository. + +This is not a port of the old builder. +Treat the old implementation as reference material for useful ideas, failure +cases, and product lessons. + +Please read these documents first: + +1. `260325-builder-redesign-handoff.md` +2. `260325-builder-redesign-invariants.md` +3. `260325-builder-flow-mermaid.md` +4. `260324-source-pipeline-home-and-local-app-creation-spec.md` + +Context: + +- The old builder mixed creation, management, infra ceremony, and manual + handoff. +- We want a much faster local-first path inside DataConnect. +- The preferred product stance is: + `import data -> click Create app -> get a working demo handoff fast` +- The user-facing CTA remains `Create app`. +- The feature/system concept is `App Quickstart`. +- We are deliberately not carrying forward the old on-chain/CMS flow as the + happy path. +- Existing starter apps/templates are an adjacent thread: + opening something that already exists is distinct from generating a new + quickstart handoff. + +Your first task: + +1. inspect the repo +2. propose the minimum architecture for App Quickstart in DataConnect +3. define the canonical quickstart artifact +4. recommend the first implementation slice +5. call out anything missing or risky in the invariants +6. explain how UI should distinguish: + - `Create app` (generate a new quickstart handoff) + - starter app/example app actions (open something that already exists) + +Important constraints: + +- optimize for product simplicity and speed to first useful result +- avoid wizard-heavy UX +- prefer generated handoffs over manual form entry +- keep advanced configuration collapsed by default +- stay local-first in v1 +- do not design around legacy infra assumptions unless clearly justified + +Useful prior-art references from the old builder: + +- seed analysis service +- schema-aware prompt generation +- prompt sandbox +- known auth/prompt failure modes + +Do not begin implementation immediately. +First, provide a concise architecture and delivery plan for App Quickstart based +on these invariants. +``` diff --git a/docs/plans/260325-app-quickstart-implementation-plan.md b/docs/plans/260325-app-quickstart-implementation-plan.md new file mode 100644 index 00000000..5766e766 --- /dev/null +++ b/docs/plans/260325-app-quickstart-implementation-plan.md @@ -0,0 +1,237 @@ +# 260325 Implementation Plan: App Quickstart + +## Goal + +Implement the first App Quickstart slice in DataConnect using the new product +spec as the source of truth. + +This plan replaces the earlier March 24 ideation plan and is organized around +the concrete v1 contract: + +- source-overview quickstart +- explicit `Create app` entry points +- deterministic local handoff artifact +- optional AI-assisted generation through the same contract +- adjacent starter-app action when there is exactly one strong source match + +## Decision Anchors + +- `Create app` always means generate a new quickstart handoff from the current + source. +- source overview is the primary quickstart surface. +- starter-app actions are separate from quickstart and appear only when there is + exactly one live registry match for the current source. +- v1 must remain local-first and must not upload exported source data remotely. +- deterministic fallback artifact is mandatory even if AI generation exists. +- on-chain, CMS, deployment, and app-management concerns remain out of scope. + +## Thin Vertical Slice + +The first end-to-end implementation slice is: + +1. imported source exposes `Create app` +2. user lands on source overview with quickstart open +3. user enters one app idea +4. DataConnect generates a usable fallback-backed quickstart artifact +5. user can `Copy prompt` +6. user can `Reveal handoff files` +7. if exactly one starter app matches, user also sees a separate `Open {App}` + action + +AI-assisted generation plugs into this same slice, but the slice is not allowed +to depend on AI availability for usability. + +## Implementation Slices + +### Slice 1: Entry points and routing + +Work: + +- keep Home imported-source `Create app` CTA routing to + `/sources/:platformId?intent=create-app` +- keep import-complete toast `Create app` CTA routing to the same URL +- keep source-overview `Create app` CTA opening the same quickstart state +- closing quickstart removes only `intent=create-app` + +Expected files: + +- `src/pages/home/*` +- `src/pages/source/index.tsx` + +### Slice 2: Source-overview quickstart state machine + +Work: + +- replace the current simple prompt-preview dialog behavior with the spec state + machine: + - `idle` + - `generating` + - `generated` + - `fallback-ready` + - `error-with-retry` +- keep one required multiline app-idea input +- preserve source context and app idea through retries +- preserve quickstart state while the user remains on the same source page in + the same session +- reset quickstart state when the source changes or the page reloads + +Expected files: + +- `src/pages/source/use-source-overview-page.ts` +- `src/pages/source/components/source-create-app-dialog.tsx` +- source-page-local quickstart helper/types file(s) + +### Slice 3: Artifact generation and validation + +Work: + +- define app-local interfaces matching the spec: + - `AppQuickstartArtifact` + - `StarterAppMatch` + - `QuickstartGenerationState` +- implement deterministic fallback artifact generation from: + - source id/label + - local data location + - preview/summary data already available in the app + - user app idea +- validate AI-generated artifacts before activation +- if AI is unavailable, fails, or returns invalid output, switch to + `fallback-ready` +- if both AI and fallback fail, use `error-with-retry` + +Important rule: + +- no exported source file contents or preview payloads may be sent to remote + services in this slice + +### Slice 4: Handoff files + +Work: + +- create a dedicated quickstart folder under the DataConnect app-data root +- write: + - `app-quickstart.md` + - `source-context.json` +- wire `Reveal handoff files` to create-if-needed then open the folder +- keep this separate from the source export directory + +Expected files: + +- source quickstart helper(s) +- Tauri path/open-resource helpers as needed + +### Slice 5: Starter-app adjacent slot + +Work: + +- derive a starter-app match from the existing app registry +- use the current source id as the matching token +- consider only `status: "live"` apps +- consider an app a strong match when `dataRequired` contains the source token +- show an adjacent starter-app slot only when there is exactly one live match +- open the matched app via its existing external URL behavior + +Do not implement in v1: + +- multi-app ranking +- carousel/list of starter apps +- Data Apps page redesign + +Expected files: + +- `src/apps/*` helper for source-to-app matching +- `src/pages/source/*` quickstart UI state + +### Slice 6: AI adapter hook-in + +Work: + +- expose one provider-agnostic generation interface behind the quickstart state + machine +- keep the adapter optional from the product-availability perspective +- if an approved local or privacy-safe provider exists, connect it here +- if no approved provider exists yet, the fallback path still ships as the + usable implementation + +This slice must not change the UI contract. + +## Test Plan + +### Route and UI behavior + +- Home imported-source `Create app` routes to source overview with + `intent=create-app` +- import-complete toast `Create app` routes to source overview with the same + intent +- source-overview `Create app` opens quickstart and closing clears only the + intent param + +### Quickstart state machine + +- idle state renders source context and app-idea input +- generating state disables generation controls and preserves context +- AI success enters `generated` +- AI unavailable enters `fallback-ready` +- AI failure after fallback generation keeps fallback visible and offers retry +- total failure enters `error-with-retry` + +### Artifact contract + +- generated/fallback artifacts include: + - source id/label + - local data location + - source summary + - app idea + - prompt + - next step +- `Copy prompt` copies the prompt string +- `Reveal handoff files` creates and opens the quickstart folder + +### Starter-app path + +- exactly one live registry match shows one starter-app slot +- zero matches show no slot +- multiple matches show no slot +- starter-app action copy is distinct from `Create app` + +### Regression coverage + +- completion states never claim the app is built/deployed/live +- no quickstart happy-path code depends on on-chain or CMS calls +- no quickstart path is gated on coding-agent installation detection + +## Acceptance Gates + +Implementation is not complete until the following eval-backed gates pass: + +- `HC-EXPERIENCE-001` +- `HC-EXPERIENCE-003` +- `HC-STRUCTURE-001` +- `HC-RELIABILITY-001` +- `HC-RELIABILITY-002` +- `HC-PRIVACY-001` +- `HC-PRIVACY-002` +- `HC-SCOPE-001` +- `HC-SCOPE-002` +- `HC-SCOPE-003` +- `HC-FIRST-SLICE-001` +- `HC-FIRST-SLICE-002` +- `HC-ADJACENT-001` + +## Risks + +- source-overview quickstart can become too large if starter-app UI competes + with artifact UI +- fallback artifact quality may feel generic until source summaries improve +- adding AI too early can pressure the privacy boundary if the adapter contract + is not enforced strictly + +## Exit Criteria + +- imported sources consistently offer a source-scoped `Create app` path +- quickstart produces a usable local-first artifact without depending on AI +- starter-app actions appear only when there is exactly one strong match and are + clearly distinct from quickstart +- users can copy the prompt and reveal handoff files from source overview +- the shipped slice still excludes hosted generation, deployment, on-chain happy + path work, CMS dependency, and Data Apps redesign From 26c8e5c626f2c70581c43cb5c5872e7375bd7080 Mon Sep 17 00:00:00 2001 From: Callum Flack Date: Wed, 25 Mar 2026 16:11:19 +1000 Subject: [PATCH 3/4] feat(app-quickstart): add source-scoped create app flow --- src-tauri/src/commands/file_ops.rs | 223 ++++++++--- src-tauri/src/lib.rs | 17 +- src/components/ui/dialog.tsx | 155 ++++++++ src/lib/tauri-paths.ts | 13 + .../components/connected-sources-list.tsx | 91 +++-- src/pages/home/index.test.tsx | 185 ++++++++- src/pages/home/index.tsx | 58 ++- src/pages/source/app-quickstart-ai.ts | 28 ++ src/pages/source/app-quickstart.ts | 212 ++++++++++ .../source-app-quickstart-dialog.tsx | 372 ++++++++++++++++++ .../source/components/source-preview-card.tsx | 19 +- src/pages/source/index.test.tsx | 353 ++++++++++------- src/pages/source/index.tsx | 69 +++- src/pages/source/types.ts | 66 ++++ .../source/use-source-overview-page.test.ts | 143 ++++++- src/pages/source/use-source-overview-page.ts | 231 ++++++++++- 16 files changed, 1916 insertions(+), 319 deletions(-) create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/pages/source/app-quickstart-ai.ts create mode 100644 src/pages/source/app-quickstart.ts create mode 100644 src/pages/source/components/source-app-quickstart-dialog.tsx diff --git a/src-tauri/src/commands/file_ops.rs b/src-tauri/src/commands/file_ops.rs index f9d74900..7120f50c 100644 --- a/src-tauri/src/commands/file_ops.rs +++ b/src-tauri/src/commands/file_ops.rs @@ -1,9 +1,9 @@ +use dirs::home_dir; use serde::{Deserialize, Serialize}; use std::fs::{self, File}; use std::io::Read; use std::path::{Path, PathBuf}; use tauri::{AppHandle, Manager}; -use dirs::home_dir; #[derive(Debug, Serialize, Deserialize)] pub struct FileInfo { @@ -128,10 +128,9 @@ fn scan_latest_json_in_tree(dir: &Path, latest: &mut Option<(PathBuf, i64)>) -> } let timestamp = json_timestamp(&path); - if latest - .as_ref() - .map_or(true, |(_, current_timestamp)| timestamp > *current_timestamp) - { + if latest.as_ref().map_or(true, |(_, current_timestamp)| { + timestamp > *current_timestamp + }) { *latest = Some((path, timestamp)); } } @@ -156,8 +155,8 @@ fn find_latest_source_json( } fn read_export_content(path: &Path) -> Result { - let content = fs::read_to_string(path) - .map_err(|e| format!("Failed to read export file: {}", e))?; + let content = + fs::read_to_string(path).map_err(|e| format!("Failed to read export file: {}", e))?; let data = serde_json::from_str::(&content) .map_err(|e| format!("Failed to parse export file: {}", e))?; match data.get("content") { @@ -184,6 +183,25 @@ fn sanitize_path_component(input: &str) -> String { } } +fn slugify_path_component(input: &str) -> String { + let sanitized = sanitize_path_component(input) + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '-' }) + .collect::(); + + let collapsed = sanitized + .split('-') + .filter(|segment| !segment.is_empty()) + .collect::>() + .join("-"); + + if collapsed.is_empty() { + "quickstart".to_string() + } else { + collapsed.to_lowercase() + } +} + fn truncate_utf8_by_bytes(input: &str, max_bytes: usize) -> (String, bool) { if input.len() <= max_bytes { return (input.to_string(), false); @@ -205,8 +223,8 @@ fn truncate_utf8_by_bytes(input: &str, max_bytes: usize) -> (String, bool) { } fn read_raw_export_preview(path: &Path, max_bytes: usize) -> Result<(String, bool), String> { - let mut file = File::open(path) - .map_err(|e| format!("Failed to open export file for preview: {}", e))?; + let mut file = + File::open(path).map_err(|e| format!("Failed to open export file for preview: {}", e))?; let mut buffer = vec![0u8; max_bytes.saturating_add(1)]; let bytes_read = file .read(&mut buffer) @@ -220,7 +238,10 @@ fn read_raw_export_preview(path: &Path, max_bytes: usize) -> Result<(String, boo end -= 1; } - Ok((String::from_utf8_lossy(&slice[..end]).to_string(), is_truncated)) + Ok(( + String::from_utf8_lossy(&slice[..end]).to_string(), + is_truncated, + )) } fn build_source_export_preview( @@ -283,7 +304,7 @@ pub async fn write_export_data( platform_id: String, company: String, name: Option, // Optional display name from frontend - data: String, // JSON string from frontend + data: String, // JSON string from frontend ) -> Result { let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -291,8 +312,8 @@ pub async fn write_export_data( .as_secs(); // Parse the JSON string to get the content - let content: serde_json::Value = serde_json::from_str(&data) - .map_err(|e| format!("Failed to parse data: {}", e))?; + let content: serde_json::Value = + serde_json::from_str(&data).map_err(|e| format!("Failed to parse data: {}", e))?; // Use provided name, or try to extract from content, or default to platform_id let name = name.unwrap_or_else(|| { @@ -379,21 +400,30 @@ pub async fn load_runs(app: AppHandle) -> Result, String> { let mut runs = Vec::new(); // Walk through company directories - for company_entry in fs::read_dir(&data_dir).map_err(|e| e.to_string())?.flatten() { + for company_entry in fs::read_dir(&data_dir) + .map_err(|e| e.to_string())? + .flatten() + { if !company_entry.path().is_dir() { continue; } let company = company_entry.file_name().to_string_lossy().to_string(); // Walk through platform directories - for platform_entry in fs::read_dir(company_entry.path()).map_err(|e| e.to_string())?.flatten() { + for platform_entry in fs::read_dir(company_entry.path()) + .map_err(|e| e.to_string())? + .flatten() + { if !platform_entry.path().is_dir() { continue; } let platform_name = platform_entry.file_name().to_string_lossy().to_string(); // Walk through run directories - for run_entry in fs::read_dir(platform_entry.path()).map_err(|e| e.to_string())?.flatten() { + for run_entry in fs::read_dir(platform_entry.path()) + .map_err(|e| e.to_string())? + .flatten() + { if !run_entry.path().is_dir() { continue; } @@ -402,14 +432,20 @@ pub async fn load_runs(app: AppHandle) -> Result, String> { // Find JSON files in the run directory let mut latest_json: Option<(PathBuf, u64)> = None; - for file_entry in fs::read_dir(&run_path).map_err(|e| e.to_string())?.flatten() { + for file_entry in fs::read_dir(&run_path) + .map_err(|e| e.to_string())? + .flatten() + { let path = file_entry.path(); if path.extension().map_or(false, |ext| ext == "json") { // Extract timestamp from filename (format: platformId_timestamp.json) let filename = path.file_stem().unwrap_or_default().to_string_lossy(); if let Some(ts_str) = filename.split('_').last() { if let Ok(ts) = ts_str.parse::() { - if latest_json.as_ref().map_or(true, |(_, prev_ts)| ts > *prev_ts) { + if latest_json + .as_ref() + .map_or(true, |(_, prev_ts)| ts > *prev_ts) + { latest_json = Some((path.clone(), ts)); } } @@ -422,7 +458,8 @@ pub async fn load_runs(app: AppHandle) -> Result, String> { if let Ok(content) = fs::read_to_string(&json_path) { if let Ok(data) = serde_json::from_str::(&content) { // Check if this export was synced to personal server - let synced = data.get("syncedToPersonalServer") + let synced = data + .get("syncedToPersonalServer") .and_then(|v| v.as_bool()) .unwrap_or(false); let scope = data @@ -434,7 +471,10 @@ pub async fn load_runs(app: AppHandle) -> Result, String> { // (content was stripped during sync). For unsynced, parse from content. let (items_exported, item_label) = if synced { let items = data.get("itemsExported").and_then(|v| v.as_i64()); - let label = data.get("itemLabel").and_then(|v| v.as_str()).map(|s| s.to_string()); + let label = data + .get("itemLabel") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); (items, label) } else { // Extract items count and label from the content @@ -443,7 +483,8 @@ pub async fn load_runs(app: AppHandle) -> Result, String> { let content_data = content.and_then(|c| c.get("data")); // Try exportSummary at content.exportSummary or content.data.exportSummary - let export_summary = content.and_then(|c| c.get("exportSummary")) + let export_summary = content + .and_then(|c| c.get("exportSummary")) .or_else(|| content_data.and_then(|d| d.get("exportSummary"))); let items = export_summary @@ -451,12 +492,30 @@ pub async fn load_runs(app: AppHandle) -> Result, String> { .or_else(|| { let sources = [content, content_data]; for src in sources.iter().flatten() { - let count = src.get("totalConversations").and_then(|v| v.as_i64()) - .or_else(|| src.get("totalPosts").and_then(|v| v.as_i64())) - .or_else(|| src.get("conversations").and_then(|v| v.as_array()).map(|a| a.len() as i64)) - .or_else(|| src.get("posts").and_then(|v| v.as_array()).map(|a| a.len() as i64)) - .or_else(|| src.get("memories").and_then(|v| v.as_array()).map(|a| a.len() as i64)) - .or_else(|| src.get("media_count").and_then(|v| v.as_i64())); + let count = src + .get("totalConversations") + .and_then(|v| v.as_i64()) + .or_else(|| { + src.get("totalPosts").and_then(|v| v.as_i64()) + }) + .or_else(|| { + src.get("conversations") + .and_then(|v| v.as_array()) + .map(|a| a.len() as i64) + }) + .or_else(|| { + src.get("posts") + .and_then(|v| v.as_array()) + .map(|a| a.len() as i64) + }) + .or_else(|| { + src.get("memories") + .and_then(|v| v.as_array()) + .map(|a| a.len() as i64) + }) + .or_else(|| { + src.get("media_count").and_then(|v| v.as_i64()) + }); if count.is_some() { return count; } @@ -465,11 +524,17 @@ pub async fn load_runs(app: AppHandle) -> Result, String> { }); let label = export_summary - .and_then(|s| s.get("label").and_then(|v| v.as_str()).map(|s| s.to_string())) + .and_then(|s| { + s.get("label") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + }) .or_else(|| { let sources = [content, content_data]; for src in sources.iter().flatten() { - if src.get("posts").is_some() || src.get("media_count").is_some() { + if src.get("posts").is_some() + || src.get("media_count").is_some() + { return Some("posts".to_string()); } else if src.get("conversations").is_some() { return Some("conversations".to_string()); @@ -526,7 +591,10 @@ pub async fn load_runs(app: AppHandle) -> Result, String> { /// Load detailed export data for a specific run (conversations, posts, etc.) #[tauri::command] -pub async fn load_run_export_data(_run_id: String, export_path: String) -> Result { +pub async fn load_run_export_data( + _run_id: String, + export_path: String, +) -> Result { let run_path = PathBuf::from(&export_path); if !run_path.exists() { @@ -605,8 +673,7 @@ pub async fn load_latest_source_export_full( name: String, scope: Option, ) -> Result, String> { - let Some((json_path, _)) = - find_latest_source_json(&app, &company, &name, scope.as_deref())? + let Some((json_path, _)) = find_latest_source_json(&app, &company, &name, scope.as_deref())? else { return Ok(None); }; @@ -632,8 +699,7 @@ pub async fn open_platform_export_folder( } } - let candidate_dirs = - build_source_data_candidates(&app, &company, &name, scope.as_deref())?; + let candidate_dirs = build_source_data_candidates(&app, &company, &name, scope.as_deref())?; let existing_dir = candidate_dirs.into_iter().find(|dir| dir.exists()); let data_dir = existing_dir.ok_or_else(|| "Export folder does not exist".to_string())?; super::download::open_folder(data_dir.to_string_lossy().to_string()).await @@ -649,9 +715,15 @@ pub async fn open_personal_server_scope_folder(scope: String) -> Result<(), Stri let home = home_dir().ok_or_else(|| "Failed to get home directory".to_string())?; let roots = [ - home.join("data-connect").join("personal-server").join("data"), - home.join("dev.dataconnect").join("personal-server").join("data"), - home.join(".dataconnect").join("personal-server").join("data"), + home.join("data-connect") + .join("personal-server") + .join("data"), + home.join("dev.dataconnect") + .join("personal-server") + .join("data"), + home.join(".dataconnect") + .join("personal-server") + .join("data"), ]; for root in roots { @@ -670,6 +742,38 @@ pub async fn open_personal_server_scope_folder(scope: String) -> Result<(), Stri )) } +/// Write app quickstart handoff files into a dedicated folder under app-data. +#[tauri::command] +pub async fn write_app_quickstart_files( + app: AppHandle, + source_id: String, + app_idea: String, + markdown_content: String, + source_context_json: String, +) -> Result { + let app_data_dir = app + .path() + .app_data_dir() + .map_err(|e| format!("Failed to get app data dir: {}", e))?; + + let source_segment = sanitize_path_component(&source_id); + let idea_segment = slugify_path_component(&app_idea); + let folder_path = app_data_dir + .join("app_quickstarts") + .join(source_segment) + .join(idea_segment); + + fs::create_dir_all(&folder_path) + .map_err(|e| format!("Failed to create app quickstart folder: {}", e))?; + + fs::write(folder_path.join("app-quickstart.md"), markdown_content) + .map_err(|e| format!("Failed to write app quickstart markdown: {}", e))?; + fs::write(folder_path.join("source-context.json"), source_context_json) + .map_err(|e| format!("Failed to write app quickstart source context: {}", e))?; + + Ok(folder_path.to_string_lossy().to_string()) +} + /// Mark an export as synced after successful delivery to the personal server. /// Preserves the full content payload alongside synced metadata. #[tauri::command] @@ -706,12 +810,18 @@ pub async fn mark_export_synced( .join("exported_data"); if !dir_path.starts_with(&data_dir) { - return Err(format!("Refusing to modify path outside exported_data: {}", export_path)); + return Err(format!( + "Refusing to modify path outside exported_data: {}", + export_path + )); } // Find the JSON file in the run directory let mut json_path: Option = None; - for entry in fs::read_dir(&dir_path).map_err(|e| e.to_string())?.flatten() { + for entry in fs::read_dir(&dir_path) + .map_err(|e| e.to_string())? + .flatten() + { let path = entry.path(); if path.extension().map_or(false, |ext| ext == "json") { json_path = Some(path); @@ -719,13 +829,14 @@ pub async fn mark_export_synced( } } - let json_path = json_path.ok_or_else(|| "No JSON file found in export directory".to_string())?; + let json_path = + json_path.ok_or_else(|| "No JSON file found in export directory".to_string())?; // Read existing JSON, strip content, add synced metadata - let raw = fs::read_to_string(&json_path) - .map_err(|e| format!("Failed to read export file: {}", e))?; - let mut data: serde_json::Value = serde_json::from_str(&raw) - .map_err(|e| format!("Failed to parse export file: {}", e))?; + let raw = + fs::read_to_string(&json_path).map_err(|e| format!("Failed to read export file: {}", e))?; + let mut data: serde_json::Value = + serde_json::from_str(&raw).map_err(|e| format!("Failed to parse export file: {}", e))?; let synced_at = chrono::Utc::now().to_rfc3339(); @@ -744,10 +855,13 @@ pub async fn mark_export_synced( let updated = serde_json::to_string_pretty(&data) .map_err(|e| format!("Failed to serialize export: {}", e))?; - fs::write(&json_path, &updated) - .map_err(|e| format!("Failed to write export: {}", e))?; + fs::write(&json_path, &updated).map_err(|e| format!("Failed to write export: {}", e))?; - log::info!("Marked export as synced for run {} ({})", run_id, json_path.display()); + log::info!( + "Marked export as synced for run {} ({})", + run_id, + json_path.display() + ); Ok(()) } @@ -775,8 +889,8 @@ pub async fn delete_exported_run(app: AppHandle, export_path: String) -> Result< let canonical_data_dir = fs::canonicalize(&data_dir) .map_err(|e| format!("Failed to resolve exported_data path: {}", e))?; - let canonical_dir_path = fs::canonicalize(&dir_path) - .map_err(|e| format!("Failed to resolve export path: {}", e))?; + let canonical_dir_path = + fs::canonicalize(&dir_path).map_err(|e| format!("Failed to resolve export path: {}", e))?; if !canonical_dir_path.starts_with(&canonical_data_dir) { return Err(format!( @@ -856,8 +970,7 @@ pub async fn get_app_config() -> Result { let content = fs::read_to_string(&config_path) .map_err(|e| format!("Failed to read config file: {}", e))?; - serde_json::from_str(&content) - .map_err(|e| format!("Failed to parse config file: {}", e)) + serde_json::from_str(&content).map_err(|e| format!("Failed to parse config file: {}", e)) } /// Set app configuration to ~/.dataconnect/config.json @@ -874,8 +987,7 @@ pub async fn set_app_config(config: AppConfig) -> Result<(), String> { let json = serde_json::to_string_pretty(&config) .map_err(|e| format!("Failed to serialize config: {}", e))?; - fs::write(&config_path, json) - .map_err(|e| format!("Failed to write config file: {}", e))?; + fs::write(&config_path, json).map_err(|e| format!("Failed to write config file: {}", e))?; log::info!("App config saved to: {:?}", config_path); Ok(()) @@ -978,7 +1090,10 @@ mod tests { "null content should fall back to root export object" ); assert_eq!(content["content"], serde_json::Value::Null); - assert_eq!(content["syncedToPersonalServer"], serde_json::Value::Bool(true)); + assert_eq!( + content["syncedToPersonalServer"], + serde_json::Value::Bool(true) + ); } #[test] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0c8d010e..97339205 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,15 +4,15 @@ mod processors; use commands::{ check_browser_available, check_connected_platforms, check_connector_updates, cleanup_personal_server, cleanup_playwright_processes, clear_browser_session, - clear_personal_server_data, - debug_connector_paths, delete_exported_run, download_browser, download_chromium_rust, - download_connector, get_app_config, get_installed_connectors, get_log_path, - get_personal_server_data_path, get_personal_server_status, get_platforms, get_registry_url, get_run_files, - get_user_data_path, handle_download, list_browser_sessions, open_personal_server_scope_folder, + clear_personal_server_data, debug_connector_paths, delete_exported_run, download_browser, + download_chromium_rust, download_connector, get_app_config, get_installed_connectors, + get_log_path, get_personal_server_data_path, get_personal_server_status, get_platforms, + get_registry_url, get_run_files, get_user_data_path, handle_download, list_browser_sessions, load_latest_source_export_full, load_latest_source_export_preview, load_run_export_data, - load_runs, mark_export_synced, open_folder, open_platform_export_folder, set_app_config, - start_connector_run, start_personal_server, stop_connector_run, stop_personal_server, - test_nodejs, write_export_data, + load_runs, mark_export_synced, open_folder, open_personal_server_scope_folder, + open_platform_export_folder, set_app_config, start_connector_run, start_personal_server, + stop_connector_run, stop_personal_server, test_nodejs, write_app_quickstart_files, + write_export_data, }; use tauri::{Listener, Manager}; @@ -92,6 +92,7 @@ pub fn run() { load_run_export_data, load_latest_source_export_preview, load_latest_source_export_full, + write_app_quickstart_files, delete_exported_run, check_connector_updates, download_connector, diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 00000000..62db0d5b --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,155 @@ +import * as React from "react" +import { Dialog as DialogPrimitive } from "radix-ui" +import { XIcon } from "lucide-react" +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton ? ( + + + + ) : null} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogTrigger, + DialogPortal, + DialogOverlay, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/src/lib/tauri-paths.ts b/src/lib/tauri-paths.ts index 95cf548c..cf282aef 100644 --- a/src/lib/tauri-paths.ts +++ b/src/lib/tauri-paths.ts @@ -47,3 +47,16 @@ export const loadLatestSourceExportFull = ( export const deleteExportedRun = (exportPath: string) => invoke("delete_exported_run", { exportPath }) + +export const writeAppQuickstartFiles = ( + sourceId: string, + appIdea: string, + markdownContent: string, + sourceContextJson: string +) => + invoke("write_app_quickstart_files", { + sourceId, + appIdea, + markdownContent, + sourceContextJson, + }) diff --git a/src/pages/home/components/connected-sources-list.tsx b/src/pages/home/components/connected-sources-list.tsx index 4e6a23c5..33728d24 100644 --- a/src/pages/home/components/connected-sources-list.tsx +++ b/src/pages/home/components/connected-sources-list.tsx @@ -11,6 +11,7 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" +import { Button } from "@/components/ui/button" import { ROUTES } from "@/config/routes" import { cn } from "@/lib/classes" import { getLastRunLabel } from "@/lib/platform/ui" @@ -24,6 +25,7 @@ interface ConnectedSourcesListProps { runs: Run[] headline?: string onOpenRuns?: (platform: Platform) => void + onCreateApp?: (platform: Platform) => void onSyncSource?: (platform: Platform) => void } @@ -43,6 +45,7 @@ export function ConnectedSourcesList({ runs, headline = "Your sources at the moment.", onOpenRuns, + onCreateApp, onSyncSource, }: ConnectedSourcesListProps) { const inFlightSyncPlatformIdsRef = useRef>(new Set()) @@ -184,41 +187,56 @@ export function ConnectedSourcesList({ ariaLabel: `Open ${platform.name}`, }} middleSlot={ - - - triggerSyncFeedback(platform) - : undefined - } - disabled={isSyncDisabled} - aria-label={`Fetch latest data for ${platform.name}`} +
+ {onCreateApp ? ( + + ) : null} + + + triggerSyncFeedback(platform) + : undefined + } + disabled={isSyncDisabled} + aria-label={`Fetch latest data for ${platform.name}`} + > + {syncFeedbackState ? ( + + {syncFeedbackState === "running" + ? "Fetching..." + : "Backgrounding..."} + + ) : null} + + + + + {syncTooltipCopy} + + +
} endSlotClassName="[&_svg:not([class*='size-']):not([data-slot=spinner])]:size-7!" surface="list-item" @@ -287,10 +305,7 @@ function PersonalServerOnboardingCopy({ return ( {copy.beforeServerLink} - + {copy.serverLinkText} {copy.afterServerLink} diff --git a/src/pages/home/index.test.tsx b/src/pages/home/index.test.tsx index b9752f52..829597fd 100644 --- a/src/pages/home/index.test.tsx +++ b/src/pages/home/index.test.tsx @@ -1,6 +1,16 @@ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest" -import { render, waitFor, cleanup, fireEvent, screen } from "@testing-library/react" -import { createMemoryRouter, MemoryRouter, RouterProvider } from "react-router-dom" +import { + render, + waitFor, + cleanup, + fireEvent, + screen, +} from "@testing-library/react" +import { + createMemoryRouter, + MemoryRouter, + RouterProvider, +} from "react-router-dom" import { ROUTES } from "@/config/routes" import { TooltipProvider } from "@/components/ui/tooltip" import { Home } from "./index" @@ -10,6 +20,7 @@ const mockStartImport = vi.fn() const mockStopExport = vi.fn() const mockNavigate = vi.fn() const mockRefreshConnectedStatus = vi.fn() +const mockToast = vi.fn() let mockConnectedPlatforms: Record = {} let mockRuns: Array<{ id: string @@ -27,9 +38,8 @@ let mockRuns: Array<{ }> = [] vi.mock("react-router-dom", async () => { - const actual = await vi.importActual( - "react-router-dom" - ) + const actual = + await vi.importActual("react-router-dom") return { ...actual, useNavigate: () => mockNavigate, @@ -47,6 +57,10 @@ vi.mock("@/hooks/useConnector", () => ({ }), })) +vi.mock("sonner", () => ({ + toast: (...args: unknown[]) => mockToast(...args), +})) + vi.mock("@/hooks/useConnectedApps", () => ({ useConnectedApps: () => ({ connectedApps: [], @@ -103,6 +117,7 @@ describe("Home", () => { mockStopExport.mockReset() mockNavigate.mockReset() mockRefreshConnectedStatus.mockReset() + mockToast.mockReset() mockConnectedPlatforms = {} mockRuns = [] mockStartImport.mockResolvedValue("run-1") @@ -125,17 +140,13 @@ describe("Home", () => { const { getByRole } = renderHome() expect(getByRole("heading", { level: 1, name: /your data/i })).toBeTruthy() - expect( - getByRole("heading", { name: /your imported data/i }) - ).toBeTruthy() + expect(getByRole("heading", { name: /your imported data/i })).toBeTruthy() }) it("does not render the connected apps tab or surface", () => { const { container } = renderHome() - expect( - screen.queryByRole("tab", { name: /connected apps/i }) - ).toBeNull() + expect(screen.queryByRole("tab", { name: /connected apps/i })).toBeNull() expect( container.querySelector('[data-component="connected-apps-list"]') ).toBeNull() @@ -264,7 +275,9 @@ describe("Home", () => { expect( screen.queryByRole("button", { name: /connect chatgpt/i }) ).toBeNull() - expect(screen.getAllByRole("button", { name: /open chatgpt/i }).length).toBeGreaterThan(0) + expect( + screen.getAllByRole("button", { name: /open chatgpt/i }).length + ).toBeGreaterThan(0) }) it("shows connected source from persisted run even when connected status map is stale", async () => { @@ -313,7 +326,9 @@ describe("Home", () => { expect( screen.queryByRole("button", { name: /connect chatgpt/i }) ).toBeNull() - expect(screen.getAllByRole("button", { name: /open chatgpt/i }).length).toBeGreaterThan(0) + expect( + screen.getAllByRole("button", { name: /open chatgpt/i }).length + ).toBeGreaterThan(0) }) it("syncs a connected source from the home list", () => { @@ -353,7 +368,8 @@ describe("Home", () => { company: "OpenAI", name: "ChatGPT", logs: "", - exportPath: "/tmp/dataconnect/exported_data/OpenAI/ChatGPT/run-chatgpt-1", + exportPath: + "/tmp/dataconnect/exported_data/OpenAI/ChatGPT/run-chatgpt-1", }, ] @@ -370,6 +386,135 @@ describe("Home", () => { ) }) + it("routes create app from a connected source to source quickstart", () => { + const chatgpt = { + id: "chatgpt", + company: "OpenAI", + name: "ChatGPT", + filename: "chatgpt", + description: "ChatGPT export", + isUpdated: false, + logoURL: "", + needsConnection: true, + connectURL: null, + connectSelector: null, + exportFrequency: null, + vectorize_config: null, + runtime: "playwright", + } + mockConnectedPlatforms = { chatgpt: true } + mockUsePlatforms.mockReturnValue({ + platforms: [chatgpt], + connectedPlatforms: mockConnectedPlatforms, + loadPlatforms: vi.fn(), + refreshConnectedStatus: vi.fn(), + getPlatformById: vi.fn(), + isPlatformConnected: vi.fn(id => Boolean(mockConnectedPlatforms[id])), + }) + mockRuns = [ + { + id: "run-chatgpt-1", + platformId: "chatgpt", + filename: "chatgpt", + isConnected: true, + startDate: new Date().toISOString(), + status: "success", + url: "", + company: "OpenAI", + name: "ChatGPT", + logs: "", + exportPath: + "/tmp/dataconnect/exported_data/OpenAI/ChatGPT/run-chatgpt-1", + }, + ] + + renderHome() + + fireEvent.click( + screen.getByRole("button", { name: /create app from chatgpt data/i }) + ) + + expect(mockNavigate).toHaveBeenCalledWith( + "/sources/chatgpt?intent=create-app" + ) + }) + + it("offers create app from the import complete toast without auto-opening quickstart", async () => { + const platform = { + id: "chatgpt", + company: "OpenAI", + name: "ChatGPT", + filename: "chatgpt", + description: "ChatGPT export", + isUpdated: false, + logoURL: "", + needsConnection: true, + connectURL: null, + connectSelector: null, + exportFrequency: null, + vectorize_config: null, + runtime: "playwright", + } + mockUsePlatforms.mockReturnValue({ + platforms: [platform], + connectedPlatforms: mockConnectedPlatforms, + loadPlatforms: vi.fn(), + refreshConnectedStatus: mockRefreshConnectedStatus, + getPlatformById: vi.fn(), + isPlatformConnected: vi.fn(id => Boolean(mockConnectedPlatforms[id])), + }) + + const view = render( + + + + + + ) + + mockRuns = [ + { + id: "run-chatgpt-1", + platformId: "chatgpt", + filename: "chatgpt", + isConnected: true, + startDate: new Date().toISOString(), + status: "success", + url: "", + company: "OpenAI", + name: "ChatGPT", + logs: "", + exportPath: + "/tmp/dataconnect/exported_data/OpenAI/ChatGPT/run-chatgpt-1", + }, + ] + + view.rerender( + + + + + + ) + + await waitFor(() => { + expect(mockRefreshConnectedStatus).toHaveBeenCalledTimes(1) + expect(mockToast).toHaveBeenCalledTimes(1) + }) + + const toastOptions = mockToast.mock.calls[0]?.[1] as { + action?: { label: string; onClick: () => void } + } + expect(toastOptions.action?.label).toBe("Create app") + expect(mockNavigate).not.toHaveBeenCalled() + + toastOptions.action?.onClick() + + expect(mockNavigate).toHaveBeenCalledWith( + "/sources/chatgpt?intent=create-app" + ) + }) + it("disables home sync while another run is waiting for sign-in", () => { const chatgpt = { id: "chatgpt", @@ -429,9 +574,11 @@ describe("Home", () => { renderHome() expect( - screen.getByRole("button", { - name: /fetch latest data for chatgpt/i, - }).hasAttribute("disabled") + screen + .getByRole("button", { + name: /fetch latest data for chatgpt/i, + }) + .hasAttribute("disabled") ).toBe(true) }) @@ -628,7 +775,9 @@ describe("Home", () => { renderHome() - const spotifyButton = screen.getByRole("button", { name: /connect spotify/i }) + const spotifyButton = screen.getByRole("button", { + name: /connect spotify/i, + }) expect(spotifyButton.hasAttribute("disabled")).toBe(false) fireEvent.click(spotifyButton) diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 81aa7a83..bd1ee1b4 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useLocation, useNavigate } from "react-router-dom" import { useSelector } from "react-redux" +import { toast } from "sonner" import { usePlatforms } from "@/hooks/usePlatforms" import { useConnector } from "@/hooks/useConnector" import type { Platform, RootState } from "@/types" @@ -34,6 +35,16 @@ import { isAppUpdateUiDebugEnabled, } from "@/hooks/app-update/app-update-ui-debug" +const getQuickstartRoute = (platform: { + id: string + name?: string + company?: string +}) => { + const platformEntry = getPlatformRegistryEntry(platform) + const sourceId = platformEntry?.id ?? platform.id + return `${ROUTES.source.replace(":platformId", sourceId)}?intent=create-app` +} + export function Home() { const homeDebugScenarioLabel: Record = { "blocking-waiting": "blocking-waiting", @@ -80,9 +91,8 @@ export function Home() { const displayPlatforms = platforms useEffect(() => { - const successfulRunIds = runs - .filter(run => run.status === "success") - .map(run => run.id) + const successfulRuns = runs.filter(run => run.status === "success") + const successfulRunIds = successfulRuns.map(run => run.id) if (knownSuccessfulRunIdsRef.current === null) { knownSuccessfulRunIdsRef.current = new Set(successfulRunIds) @@ -90,16 +100,38 @@ export function Home() { } const knownSuccessfulRunIds = knownSuccessfulRunIdsRef.current - const hasNewSuccess = successfulRunIds.some(runId => { - if (knownSuccessfulRunIds.has(runId)) return false - knownSuccessfulRunIds.add(runId) + const newSuccessfulRuns = successfulRuns.filter(run => { + if (knownSuccessfulRunIds.has(run.id)) return false + knownSuccessfulRunIds.add(run.id) return true }) - if (hasNewSuccess) { - void refreshConnectedStatus() + if (newSuccessfulRuns.length === 0) { + return } - }, [refreshConnectedStatus, runs]) + + void refreshConnectedStatus() + + newSuccessfulRuns.forEach(run => { + const platformEntry = getPlatformRegistryEntry({ + id: run.platformId, + name: run.name, + company: run.company, + }) + if (!platformEntry) return + + toast("Import complete", { + id: `app-quickstart-${run.id}`, + description: `${platformEntry.displayName} data is ready for a quickstart.`, + action: { + label: "Create app", + onClick: () => { + navigate(getQuickstartRoute(run)) + }, + }, + }) + }) + }, [navigate, refreshConnectedStatus, runs]) const handleImportSource = useCallback( async (platform: Platform) => { @@ -240,6 +272,13 @@ export function Home() { [navigate] ) + const handleCreateApp = useCallback( + (platform: Platform) => { + navigate(getQuickstartRoute(platform)) + }, + [navigate] + ) + return (
@@ -251,6 +290,7 @@ export function Home() { runs={connectedSourcesRuns} headline="Your imported data" onOpenRuns={handleOpenRuns} + onCreateApp={handleCreateApp} onSyncSource={handleImportSource} /> { + return { + status: "unavailable", + } +} diff --git a/src/pages/source/app-quickstart.ts b/src/pages/source/app-quickstart.ts new file mode 100644 index 00000000..33ad4921 --- /dev/null +++ b/src/pages/source/app-quickstart.ts @@ -0,0 +1,212 @@ +import { getAppRegistryEntries } from "@/apps/registry" +import type { LiveAppRegistryEntry } from "@/apps/registry-types" +import type { AppQuickstartArtifact, StarterAppMatch } from "./types" + +const FALLBACK_LOCAL_PATH_HINT = "Reveal the source folder from DataConnect" +const MAX_SUMMARY_SNIPPET_CHARS = 280 +const MAX_PROMPT_SNIPPET_CHARS = 360 + +type BuildFallbackArtifactOptions = { + sourceId: string + sourceLabel: string + localDataLocation?: string | null + sourceSummary: string + appIdea: string + starterAppMatch: StarterAppMatch | null +} + +export function getStarterAppMatch(sourceId: string): StarterAppMatch | null { + const matches = getAppRegistryEntries().filter( + (app): app is LiveAppRegistryEntry => + app.status === "live" && + app.dataRequired.some(requirement => requirement.token === sourceId) + ) + + if (matches.length !== 1) { + return null + } + + const [match] = matches + return { + sourceId, + appLabel: match.name, + appDescription: match.description, + destinationUrl: match.externalUrl, + actionLabel: `Open ${match.name}`, + } +} + +export function buildSourceSummary( + sourceLabel: string, + previewJson: string, + options: { isTruncated?: boolean } = {} +): string { + const shapeSummary = describePreviewShape(previewJson, sourceLabel) + const snippet = truncateText(previewJson.trim().replace(/\s+/g, " ")) + const truncatedSuffix = options.isTruncated ? " Preview is truncated." : "" + + return `${shapeSummary}${truncatedSuffix} Preview snippet: ${snippet}` +} + +export function buildFallbackQuickstartArtifact({ + sourceId, + sourceLabel, + localDataLocation, + sourceSummary, + appIdea, + starterAppMatch, +}: BuildFallbackArtifactOptions): AppQuickstartArtifact { + const normalizedIdea = appIdea.trim() + const location = localDataLocation?.trim() || FALLBACK_LOCAL_PATH_HINT + const handoffTitle = `${normalizedIdea} quickstart from ${sourceLabel}` + const handoffSummary = `A local-first handoff for building ${normalizedIdea} from your ${sourceLabel} export.` + const nextStep = + "Paste this prompt into your coding tool, keep the export local, and build the first runnable demo against the source path above." + + const starterAppNote = starterAppMatch + ? `\nExisting example app:\n- ${starterAppMatch.appLabel}: ${starterAppMatch.destinationUrl}\nUse it for inspiration, but still generate something new for this quickstart.` + : "" + + const prompt = [ + `Build a local-first starter app from the user's ${sourceLabel} export.`, + "", + "App idea:", + normalizedIdea, + "", + "Source context:", + `- Source: ${sourceLabel} (${sourceId})`, + `- Local data location: ${location}`, + `- Source summary: ${sourceSummary}`, + "", + "Requirements:", + "1. Read the local export from the path above.", + "2. Do not upload raw exported data to remote services.", + `3. Build a small, useful demo that makes ${normalizedIdea} tangible quickly.`, + "4. Include a README with setup, run steps, and any schema assumptions.", + "5. Keep the first version understandable and easy to iterate on.", + starterAppNote, + ] + .filter(Boolean) + .join("\n") + + return { + source: { + id: sourceId, + label: sourceLabel, + localDataLocation: location, + sourceSummary, + }, + intent: { + appIdea: normalizedIdea, + }, + handoff: { + title: handoffTitle, + summary: handoffSummary, + prompt, + nextStep, + exampleAppLabel: starterAppMatch?.appLabel, + exampleAppHref: starterAppMatch?.destinationUrl, + }, + advanced: { + dataProcessingPrompt: `Inspect the ${sourceLabel} export in place, map the key entities, and note any transformations needed for ${normalizedIdea}.`, + }, + generation: { + mode: "fallback", + }, + } +} + +export function serializeAppQuickstartMarkdown( + artifact: AppQuickstartArtifact +): string { + const exampleSection = + artifact.handoff.exampleAppLabel && artifact.handoff.exampleAppHref + ? `\n## Example app\n\n- ${artifact.handoff.exampleAppLabel}: ${artifact.handoff.exampleAppHref}\n` + : "" + + return [ + `# ${artifact.handoff.title}`, + "", + artifact.handoff.summary, + "", + "## Source context", + "", + `- Source: ${artifact.source.label} (${artifact.source.id})`, + `- Local data location: ${artifact.source.localDataLocation}`, + `- Generation mode: ${artifact.generation.mode}`, + "", + "## Source summary", + "", + artifact.source.sourceSummary, + "", + "## Prompt", + "", + artifact.handoff.prompt, + "", + "## Next step", + "", + artifact.handoff.nextStep, + exampleSection.trimEnd(), + ] + .filter(Boolean) + .join("\n") +} + +export function serializeSourceContextJson( + artifact: AppQuickstartArtifact +): string { + return JSON.stringify( + { + source: artifact.source, + intent: artifact.intent, + generation: artifact.generation, + handoff: { + title: artifact.handoff.title, + summary: artifact.handoff.summary, + nextStep: artifact.handoff.nextStep, + exampleAppLabel: artifact.handoff.exampleAppLabel, + exampleAppHref: artifact.handoff.exampleAppHref, + }, + }, + null, + 2 + ) +} + +export function getPromptPreview(prompt: string): string { + return truncateText( + prompt.trim().replace(/\s+/g, " "), + MAX_PROMPT_SNIPPET_CHARS + ) +} + +function describePreviewShape( + sourcePreview: string, + sourceLabel: string +): string { + try { + const parsed = JSON.parse(sourcePreview) + if (Array.isArray(parsed)) { + return `${sourceLabel} preview is an array with ${parsed.length} top-level items.` + } + if (parsed && typeof parsed === "object") { + const keys = Object.keys(parsed) + if (keys.length === 0) { + return `${sourceLabel} preview is an empty object.` + } + const visibleKeys = keys.slice(0, 6).join(", ") + const moreSuffix = keys.length > 6 ? ", and more" : "" + return `${sourceLabel} preview includes top-level keys: ${visibleKeys}${moreSuffix}.` + } + return `${sourceLabel} preview is a JSON ${typeof parsed}.` + } catch { + return `${sourceLabel} preview is available as raw JSON text.` + } +} + +function truncateText(input: string, maxChars = MAX_SUMMARY_SNIPPET_CHARS) { + if (input.length <= maxChars) { + return input + } + return `${input.slice(0, maxChars).trimEnd()}...` +} diff --git a/src/pages/source/components/source-app-quickstart-dialog.tsx b/src/pages/source/components/source-app-quickstart-dialog.tsx new file mode 100644 index 00000000..36bfc997 --- /dev/null +++ b/src/pages/source/components/source-app-quickstart-dialog.tsx @@ -0,0 +1,372 @@ +import { WandSparklesIcon } from "lucide-react" +import { LoadingButton } from "@/components/elements/button-loading" +import { Text } from "@/components/typography/text" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { cn } from "@/lib/classes" +import { getPromptPreview } from "../app-quickstart" +import type { + AppQuickstartArtifact, + CopyStatus, + QuickstartGenerationState, + StarterAppMatch, +} from "../types" + +interface SourceAppQuickstartDialogProps { + open: boolean + sourceName: string + localDataLocation: string | null + sourceSummary: string + starterAppMatch: StarterAppMatch | null + quickstartIdea: string + quickstartState: QuickstartGenerationState + promptCopyStatus: CopyStatus + revealFilesStatus: CopyStatus + onOpenChange: (open: boolean) => void + onQuickstartIdeaChange: (value: string) => void + onGenerateQuickstart: () => Promise + onCopyPrompt: () => Promise + onRevealFiles: () => Promise + onOpenStarterApp: (match: StarterAppMatch) => void +} + +export function SourceAppQuickstartDialog({ + open, + sourceName, + localDataLocation, + sourceSummary, + starterAppMatch, + quickstartIdea, + quickstartState, + promptCopyStatus, + revealFilesStatus, + onOpenChange, + onQuickstartIdeaChange, + onGenerateQuickstart, + onCopyPrompt, + onRevealFiles, + onOpenStarterApp, +}: SourceAppQuickstartDialogProps) { + const trimmedIdea = quickstartIdea.trim() + const artifact = + quickstartState.status === "generated" || + quickstartState.status === "fallback-ready" + ? quickstartState.artifact + : null + const artifactIdea = artifact?.intent.appIdea.trim() ?? null + const isArtifactStale = Boolean( + artifact && artifactIdea && artifactIdea !== trimmedIdea + ) + const isGenerating = quickstartState.status === "generating" + const isFallbackReady = quickstartState.status === "fallback-ready" + const showCopyPromptAction = Boolean( + artifact && !isArtifactStale && !isGenerating + ) + const showGenerateAction = + !showCopyPromptAction || quickstartState.status === "error-with-retry" + + return ( + + + + Create app from {sourceName} + + DataConnect can prepare a local-first quickstart from this source so + you can get to a working demo quickly. + + + +
+
+
+ + + Source context + +
+
+
+ + Source + + + {sourceName} + +
+
+ + Local data location + + + {localDataLocation ?? + "Reveal the source folder from DataConnect"} + +
+
+ + Source summary + + + {sourceSummary} + +
+
+
+ + {starterAppMatch ? ( +
+
+
+ + Starter app + + + {starterAppMatch.appLabel} + + {starterAppMatch.appDescription ? ( + + {starterAppMatch.appDescription} + + ) : null} +
+ +
+
+ ) : null} + +
+
+ +