diff --git a/.agents/skills/skill-to-evals/PROJECT_NOTES.md b/.agents/skills/skill-to-evals/PROJECT_NOTES.md new file mode 100644 index 00000000..e90bb01e --- /dev/null +++ b/.agents/skills/skill-to-evals/PROJECT_NOTES.md @@ -0,0 +1,302 @@ +# Skill To Evals Project Notes + +## Purpose + +This document records how the `skill-to-evals` work evolved, which inputs shaped it, and what we learned from building `v1` and `v2`. + +## Sources + +### Repos and local references + +- `data-connect` + - `.agents/skills/skill-to-evals/SKILL.md` + - `.agents/skills/skill-to-evals-v2/SKILL.md` + - `.agents/skills/skill-creator-anthropic/SKILL.md` + - `.agents/skills/skill-creator-anthropic/references/schemas.md` + - `.agents/skills/skill-creator-anthropic/agents/grader.md` + - `.agents/skills/skill-creator-anthropic/eval-viewer/generate_review.py` +- system skill creator + - `/Users/cflack/.codex/skills/.system/skill-creator/SKILL.md` + - `/Users/cflack/.codex/skills/.system/skill-creator/scripts/quick_validate.py` +- external repo + - `https://github.com/ehmo/platform-design-skills` + - especially `skills/web/SKILL.md`, `skills/web/AGENTS.md`, and `skills/web/rules/_sections.md` + +### Notes and links provided in conversation + +- the eval-first notes starting from: + - `Started: plan -> implement -> review -> fix` + - `Now: evals -> prod spec -> ...` +- the follow-up notes about: + - letting models draft the eval outline + - turning evals into hooks when possible + - collecting guides and papers, then converting them into skills and evals + - using autoresearch when a predictable outcome exists + - revisiting older projects after improving evals +- the example references to Russ Cox: + - `https://swtch.com/~rsc/worknotes/` + - `https://research.swtch.com/names` +- the Notion page: + - `https://www.notion.so/callum/I-now-essentially-spend-90-of-time-working-on-evals-32dc1b8a6eed8024b714fc7649db6a19?source=copy_link` + +## How We Approached It + +### 1. Initial framing + +The starting task was to create a skill that takes an existing skill document and compiles it into executable evals. + +The first useful framing choice was: + +- treat the output as eval artifacts, not as a rewritten skill +- convert each rule into a testable check +- split hard constraints from soft quality signals +- include hook-style enforcement ideas for hard constraints +- require at least one concrete failing example for each strong emitted eval + +This produced the first draft of `skill-to-evals`. + +### 2. Correction on source of truth + +An important course correction happened early: + +- use the system `skill-creator` as the main authoring skill +- do not treat the repo-local `skill-creator` as the canonical creation workflow for this task + +That tightened the skill structure and reduced accidental drift. + +### 3. Learning from the Anthropic skill-creator + +The repo-local Anthropic variant added an important idea: + +- evals are part of a system, not just a list + +The key pieces were: + +- `evals/evals.json` +- explicit expectations/assertions +- grader evidence +- benchmark aggregation +- viewer-based review + +That changed the project in a meaningful way. The skill needed to produce checks that a grader could actually verify with evidence, not just checks that looked neat in YAML. + +This pushed `v1` toward: + +- binary checks +- evidence-friendly `pass_if` and `fail_if` +- omission of untestable guidance +- stronger rejection of shallow compliance + +### 4. Learning from the eval-first notes + +The conversation notes shifted the mental model again. + +The strongest ideas were: + +- evals come before product specs +- stronger guardrails produce clearer specs and better outputs +- good evals often become hooks +- evals should be revised continuously and backtested on old projects + +This implied that `skill-to-evals` should not merely emit checks. It should compile guidance into something closer to a guardrail layer. + +### 5. Learning from `platform-design-skills` + +The `ehmo/platform-design-skills` repo showed a useful structure for that compiler model: + +- long-form guidance exists, but the important unit is the atomic rule +- sectioning matters +- priority labels matter +- "never do" rules are naturally hook-ready +- a flat list loses too much information + +The most useful file for this was `skills/web/rules/_sections.md`, because it looks more like compilation-ready source material than like prose. + +That led to `skill-to-evals-v2`. + +## What Changed Between V1 and V2 + +### V1 + +`v1` focuses on converting rules into concise machine-checkable evals. + +Core ideas: + +- hard vs soft classification +- explicit `pass_if` / `fail_if` +- hook suggestion for hard constraints +- omit non-testable source material +- prefer discriminating checks over shallow ones + +This is a good rule-to-check compiler. + +### V2 + +`v2` treats the task more like guardrail compilation. + +New ideas added in `v2`: + +- stable IDs instead of simple sequence numbers +- preserved `section` +- preserved `priority` +- preserved `source_order` +- explicit `source_ref` +- explicit `evidence_target` +- stronger emphasis on repo-hook and harness-gate compatibility +- explicit source spans when available +- concrete counterexamples +- first-class hookability + +This is closer to a practical eval system compiler. + +## Main Learnings So Far + +### 1. A skill is not automatically an eval source + +A large `SKILL.md` is usually too mixed: + +- explanation +- examples +- rationale +- norms +- actual rules + +The first job is normalization, not direct conversion. + +### 2. Atomic rules are the real input + +The best source material for eval generation looks like: + +- sectioned +- prioritized +- imperative +- independently testable + +This is why the `platform-design-skills` rule files were so informative. + +The next improvement on top of this is to make the normalized rule IR explicit: + +- source guidance is not the same thing as a compiled rule +- evals should be emitted from normalized rules, not directly from prose +- that IR is the stable layer for regeneration, auditing, and diffing + +### 3. Hard constraints should be hook-first + +If a hard rule cannot plausibly become: + +- a repo hook +- a harness gate +- a lint-style rejection +- a deterministic test gate + +then it is probably too weak, too vague, or misclassified. + +The important refinement is to separate: + +- hookability: can this realistically be enforced, and where +- hook suggestion: what is the narrowest concrete mechanism + +### 4. Good evals are discriminating + +A weak check passes when the output is superficially compliant. + +A useful check fails: + +- wrong structure +- wrong behavior +- wrong content +- coincidental matches + +This came directly from the Anthropic grader model and from the eval-first notes. + +A simple way to raise the discrimination bar is to require one explicit counterexample for each emitted eval. + +### 5. Evidence matters as much as the rule + +An eval without a clear evidence target is hard to grade and hard to trust. + +Useful evidence targets include: + +- output +- diff +- transcript +- AST +- DOM +- deterministic tests + +This is why `v2` carries `evidence_target` explicitly. + +The same logic applies to provenance: + +- exact source spans are better than vague section references +- exact spans make regeneration and source-diff review much easier + +### 6. Flattening loses too much information + +A flat list of anonymous checks throws away: + +- where the rule came from +- how important it is +- what section it belongs to +- whether it should block or merely score + +That makes regeneration, auditing, and backtesting harder. + +### 7. The destination is a guardrail system, not a YAML file + +The most important lesson from the notes is that the output artifact matters less than the enforcement model. + +The path is: + +- collect rules +- normalize them +- compile them into evals +- turn the strong ones into hooks +- benchmark and review the rest +- revise and backtest + +Backtesting should be treated as a first-class system component, not a later nice-to-have: + +- each eval compiler should have a small pass/fail corpus +- strong hard constraints should be checked against old examples +- regressions in the compiler are easier to spot when the corpus is stable + +## Working Model Emerging From This Project + +The emerging compiler pipeline is: + +1. ingest source guidance +2. normalize into atomic rules +3. preserve section, priority, provenance, and exact source spans +4. classify each rule as hard or soft +5. choose the strongest observable evidence target +6. attach a concrete counterexample +7. record hookability and then attach the narrowest viable hook suggestion +8. emit guardrail-ready evals +9. promote strong hard constraints into hooks where possible +10. backtest and benchmark the remainder over time + +## Current Open Questions + +- Should the next version emit both guardrails and benchmark assertions as separate outputs? +- Should the normalized-rule IR be optionally emitted for auditability, or remain internal by default? +- Should backtest corpus management live beside the skill, or in a separate eval package? + +## Current Artifacts + +- v1: `.agents/skills/skill-to-evals/SKILL.md` +- v2: `.agents/skills/skill-to-evals-v2/SKILL.md` + +## Summary + +The project started as "convert rules into checks." + +It has moved toward: + +- compile rule systems, not prose +- preserve structure and provenance +- make normalized rules the true compiler input +- optimize for enforcement, evidence, and backtesting +- treat hooks as the strongest form of eval when possible + +That shift is the main thing learned so far. diff --git a/.agents/skills/skill-to-evals/SKILL.md b/.agents/skills/skill-to-evals/SKILL.md new file mode 100644 index 00000000..5c453af0 --- /dev/null +++ b/.agents/skills/skill-to-evals/SKILL.md @@ -0,0 +1,273 @@ +--- +name: skill-to-evals-v2 +description: Convert a skill document, guide, paper summary, or rule set into guardrail-ready evals with stable IDs, section and priority preservation, explicit provenance, counterexamples, evidence targets, and hookability. Use when the user wants to turn rules into enforceable checks, repo hooks, harness gates, or benchmark assertions rather than prose guidance. +--- + +# Skill To Evals V2 + +## Goal + +Compile source guidance into eval artifacts that can drive: + +- repo hooks +- harness gates +- benchmark assertions +- review checklists + +Output evals only. + +- Do not restate or summarize the source. +- Do not give advice or remediation. +- Do not emit explanatory prose outside the output block. +- Omit source content that cannot become a discriminating check. + +## Compilation model + +Treat the source as raw material for a guardrail system, not as prose to paraphrase. + +Compile in this order: + +1. normalize the source into atomic rules internally +2. preserve source section, priority, and exact span when available +3. split mixed rules into independently testable units +4. classify each unit as hard or soft +5. choose the strongest observable evidence target +6. record one concrete counterexample for each emitted eval +7. attach hookability and the narrowest viable hook strategy + +## What to extract + +Convert only normative content: + +- required actions +- forbidden actions +- exact output contracts +- ordering constraints +- scope boundaries +- safety boundaries +- tool or file restrictions +- measurable thresholds stated by the source + +Do not convert: + +- introductions +- motivation +- background explanation +- duplicated guidance +- aspirational statements with no observable signal +- requirements that depend on hidden intent +- requirements that cannot be verified from output, diff, transcript, DOM, AST, or deterministic tests + +## Rule normalization + +Before emitting evals, construct an internal normalized-rule IR. + +The IR is not part of the final output unless the user explicitly asks for it. + +Each normalized rule should capture: + +- atomic rule text +- section +- priority +- source order +- source reference +- exact source span when available +- whether the rule is normative enough to emit + +Before emitting evals from that IR: + +- keep each rule atomic +- preserve section names +- preserve explicit priority labels such as `CRITICAL`, `HIGH`, `MEDIUM`, `LOW` +- preserve explicit source ordering within each section +- carry enough provenance to regenerate the eval from updated source material +- preserve exact heading and line or span references when available + +If the source mixes a hard prohibition with softer advice, split them into separate evals. + +## Eval quality bar + +Every eval must be: + +- atomic +- binary +- observable +- evidence-friendly +- discriminating +- backed by at least one concrete failing counterexample + +Reject weak conversions such as: + +- filename-only checks when file content matters +- presence-only checks when correctness matters +- text-match checks when structure or behavior matters +- process checks unless the source explicitly requires the process itself + +If a shallow or coincidental success would pass the check, tighten it or omit it. + +## Classification + +Use exactly one classification per eval: + +- `hard_constraint`: violation should reject the run +- `soft_score`: violation is a quality signal only + +Default `hard_constraint` for: + +- `must`, `must not`, `never`, `always`, `only`, `exactly` +- explicit bans +- safety and policy boundaries +- strict formatting or structural contracts +- explicit thresholds +- "never do" style rules + +Default `soft_score` for: + +- `prefer`, `avoid`, `keep`, `try`, `usually` +- style and quality heuristics +- guidance where multiple outputs could still be acceptable + +If ambiguous, use `soft_score`. + +## Evidence target + +Each eval must declare the strongest primary evidence target: + +- `output` +- `diff` +- `transcript` +- `ast` +- `dom` +- `test` +- `manual` + +Choose the strongest target that can actually verify the rule. + +- Prefer `ast` or `dom` over plain text when structure matters. +- Prefer `test` when runtime behavior is the real requirement. +- Prefer `output` over `transcript` unless the rule is explicitly about process. +- Use `manual` only when no reliable automated target exists. + +## Hook strategy + +Every eval must include `hookability`. + +Every `hard_constraint` must also include a hook suggestion. + +Allowed hookability values: + +- `repo_hook` +- `harness_gate` +- `manual_only` + +Allowed hook kinds: + +- `regex` +- `ast` +- `dom_audit` +- `diff_gate` +- `output_gate` +- `test_gate` +- `manual_only` + +Choose the narrowest hook that could reject a violation with low ambiguity. + +## Output contract + +Return exactly one fenced `yaml` block and nothing else. + +Use this schema exactly: + +```yaml +evals: + - id: HC-ACCESSIBILITY-001 + section: Accessibility / WCAG + priority: CRITICAL + source_order: 1 + source_ref: Accessibility / WCAG > Rule 1 + source_span: lines 12-18 + classification: hard_constraint + evidence_target: ast + hookability: repo_hook + check: Use semantic HTML instead of clickable non-semantic elements when a native control exists. + pass_if: + - Interactive controls are implemented with native semantic elements where applicable. + fail_if: + - A non-semantic element such as a clickable `div` is used where a native control would satisfy the same behavior. + counterexample: + - JSX uses a clickable `div` for button behavior without a valid semantic exception. + hook_suggestion: + kind: ast + reject_if: + - JSX or HTML contains click handlers on non-interactive elements for button-like behavior when no valid semantic exception is present. + - id: SS-TYPOGRAPHY-001 + section: Typography + priority: HIGH + source_order: 3 + source_ref: Typography > Rule 3 + source_span: lines 41-45 + classification: soft_score + evidence_target: output + hookability: manual_only + check: Keep body typography concise and readable. + pass_if: + - Body text style stays consistent and supports easy scanning. + fail_if: + - Body text style is inconsistent or clearly harms readability. + counterexample: + - Body text uses multiple inconsistent styles that make scanning noticeably harder. +``` + +## Formatting rules + +- Emit `hard_constraint` evals before `soft_score` evals. +- Preserve source order within each classification. +- IDs must be stable and derived from classification, section slug, and source order. +- Use uppercase priority labels exactly as they appear in the source when present. +- If the source has no explicit priority, omit `priority`. +- If the source has no meaningful section, use `General`. +- Include `source_span` when the source exposes exact headings, lines, or ranges. +- Do not include fields outside the schema. +- Do not include empty arrays or empty objects. + +## Conversion rules + +- Prefer outcome checks over process checks unless process compliance is itself the rule. +- Prefer content correctness over surface presence. +- Prefer structure and behavior over wording when those are the true requirement. +- Encode explicit numeric thresholds exactly as stated by the source. +- Do not invent thresholds, priorities, or policies not present or strongly implied in the source. +- If multiple evidence targets are possible, choose the one most suitable for automated enforcement. +- If the source contains a reusable hard prohibition, phrase it so it can become a repo or harness guardrail. +- If the source contains high-level guidance that cannot support a hard gate, keep it as a soft score or omit it. +- `counterexample` must describe a realistic failing case, not a paraphrase of `fail_if`. +- `hookability` should capture whether the rule is realistically enforceable in a repo hook, a harness gate, or only manual review. +- Prefer exact source spans over vague provenance when the source format allows it. + +## Omission rules + +Do not emit an eval when: + +- the rule is too vague to fail decisively +- the rule cannot produce concrete evidence +- the rule would reward shallow compliance +- the rule duplicates a stronger emitted eval + +## Quality test + +A good result is: + +- compact +- source-faithful +- guardrail-ready +- benchmark-ready +- easy to grade with evidence +- easy to backtest against known pass and fail examples + +A bad result is: + +- summary disguised as evals +- flattened output that loses section or priority +- checks that pass for coincidental compliance +- hard constraints with no realistic enforcement path +- evals with no concrete failing counterexample 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/_archive/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 new file mode 100644 index 00000000..b46e452e --- /dev/null +++ b/docs/_archive/260324-source-pipeline-home-and-local-app-creation-implementation-plan.md @@ -0,0 +1,383 @@ +# 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: + +- 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 + +### 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: + +- 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 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..06bb7218 --- /dev/null +++ b/docs/plans/260325-app-quickstart-implementation-plan.md @@ -0,0 +1,284 @@ +# 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 + +Current status: + +- the first implementation wedge is now complete in code +- the current next phase is artifact-quality and truthfulness work, not hosted + builder enablement + +## 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. +- the guaranteed completion contract is a copyable handoff artifact, not a + hosted app that is already runnable. +- the repo's Personal Server grant/tunnel flow is real infrastructure, but a + generic hosted builder is not yet packaged into App Quickstart's default + happy path. +- 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. + +The important boundary for this plan is: + +- local-first handoff is guaranteed +- hosted-builder execution is exploratory and must not be implied as already + working by default + +## 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. + +### Slice 7: Artifact quality and execution-boundary tightening + +Work: + +- strengthen the handoff artifact so it reads like a dense, constrained build + brief rather than a generic starter prompt +- make the prompt more explicit about the current local-first execution model +- keep completion copy truthful about what exists now: + - copyable handoff artifact now + - protocol-aware hosted-builder handshake later +- add tests that reject generic or overclaiming prompt content + +Do not implement in this slice: + +- generic "paste into Lovable and it just works" claims +- hidden dependency on hosted builder registration, manifest, or grant wiring +- any requirement that a hosted builder path be complete before the artifact is + useful + +## 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 +- prompt content is source-specific, non-generic, and explicit about local-first + constraints +- prompt content does not imply that a hosted builder integration is already + wired end-to-end + +### 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 +- no quickstart copy claims that paste-into-hosted-builder is guaranteed before + a protocol-aware hosted-builder contract exists + +## 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 +- implying hosted-builder readiness too early can make the handoff feel + misleading even if the underlying protocol pieces exist separately + +## 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 stays truthful about the current execution boundary: local + handoff is guaranteed; hosted-builder protocol integration is not yet the + default success contract +- the shipped slice still excludes hosted generation, deployment, on-chain happy + path work, CMS dependency, and Data Apps redesign diff --git a/docs/plans/260325-app-quickstart-status-handoff.md b/docs/plans/260325-app-quickstart-status-handoff.md new file mode 100644 index 00000000..19953448 --- /dev/null +++ b/docs/plans/260325-app-quickstart-status-handoff.md @@ -0,0 +1,264 @@ +# 260325 Status Handoff: App Quickstart + +## Purpose + +This file is the explicit handoff/status checkpoint for the current +`App Quickstart` workstream. + +Use it together with: + +- [260325 App Quickstart Spec](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/docs/builder-flow/260325-app-quickstart-spec.md) +- [260325 App Quickstart Implementation Plan](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/docs/plans/260325-app-quickstart-implementation-plan.md) +- [260325 App Quickstart Invariants Evals](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/docs/builder-flow/260325-app-quickstart-invariants-evals.yaml) + +This document exists because the feature has now moved from doc/spec work into +implemented code, and the next collaborator should not have to reconstruct the +current state from chat history. + +## Current Status + +The first implementation wedge is complete and committed. + +Latest relevant commits: + +- `3560332` + - docs checkpoint for App Quickstart framing/spec/planning +- `26c8e5c` + - first code slice for source-scoped App Quickstart + +Branch context: + +- currently on the PR branch used for this workstream: + `callum/source-pipeline-home-spec` + +## What Is Implemented + +The following parts of the plan are now implemented: + +### 1. Entry points and routing + +- Home imported-source surfaces now expose `Create app` +- successful import completion now offers a `Create app` toast CTA +- source overview still supports the same CTA +- quickstart is URL-backed via: + `/sources/:platformId?intent=create-app` +- closing the quickstart removes only `intent=create-app` + +### 2. Source-overview quickstart surface + +- source overview now opens a dedicated App Quickstart dialog +- the dialog uses a proper shadcn-style `Dialog` primitive, not `AlertDialog` +- the dialog is source-scoped and not a separate builder dashboard + +### 3. Quickstart state machine + +Implemented states: + +- `idle` +- `generating` +- `fallback-ready` +- `error-with-retry` + +Also structurally supported: + +- `generated` + +Important nuance: + +- the current AI adapter is a stub that returns `unavailable`, so the shipped + user path goes through deterministic fallback +- the state machine is already shaped to accept optional AI generation later, + but AI/provider work is not the current product boundary for this slice + +### 4. Artifact contract + +Implemented: + +- `AppQuickstartArtifact` +- deterministic fallback artifact generation +- source-aware prompt generation +- source summary generation +- generation mode metadata + +### 5. Handoff file flow + +Implemented: + +- `Copy prompt` +- `Reveal handoff files` +- dedicated quickstart folder under app-data +- handoff files: + - `app-quickstart.md` + - `source-context.json` + +Important product behavior: + +- handoff files are not written into the source export directory + +### 6. Starter-app adjacent path + +Implemented: + +- exact-one-match starter-app slot from registry +- distinct `Open {App}` action +- kept separate from `Create app` + +Current matching rule in code: + +- only show when exactly one live app matches the current source token +- if zero or multiple live matches exist, show nothing in v1 + +## Current Execution Boundary + +The current slice guarantees a dense, copyable local-first handoff artifact. + +This means: + +- `Copy prompt` is a truthful primary success action today +- the handoff is designed for tools that can run in the user's local + environment and access local files and/or the bundled Personal Server +- DataConnect does not yet guarantee that a generic hosted builder can complete + the Personal Server grant/tunnel handshake from the generated prompt alone + +Important nuance: + +- this is not a protocol limitation; the repo already has grant/tunnel + infrastructure for protocol-aware external apps +- the unresolved gap is packaging that infrastructure into a reusable hosted + builder quickstart contract + +## What Is Not Implemented Yet + +These are the next wedges, in order of cleanliness: + +### Next wedge: handoff artifact quality pass + +Current state: + +- the fallback artifact already satisfies the core evals and gives the user a + copyable handoff +- [app-quickstart-ai.ts](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/pages/source/app-quickstart-ai.ts) + remains a stub by design for now + +Needed: + +- sharper source summaries +- denser prompt structure with stronger build constraints +- a more explicit local-first execution story +- a more useful `nextStep` +- tests that gate for source-specific, truthful, non-generic artifact content + +### Later wedge: protocol-aware hosted-builder handshake + +Potential future work: + +- investigate how an external hosted builder could participate in the existing + Personal Server grant/tunnel flow +- define what would be required for a generated app to become a protocol-aware + external builder rather than just a copied prompt target + +Important rule: + +- this should be scoped separately from the artifact-quality pass +- do not imply that paste-into-Lovable is a guaranteed happy path before this + contract exists + +### Optional later wedge: starter-app matching UX + +Current behavior is intentionally strict: + +- exactly one match -> show starter app +- zero or multiple matches -> show nothing + +Possible future decision: + +- add a separate “See matching apps” path for multi-match cases + +### Separate cleanup track: Tauri build hygiene + +This is not an App Quickstart design problem, but it is a current repo friction: + +- `cargo check --manifest-path src-tauri/Cargo.toml` is blocked by an existing + build-script assumption about `../connectors/openai/**/*` + +That should be cleaned up separately from the product slice. + +## What Was Explicitly Deleted + +Yesterday's March 24 spike was intentionally removed before rebuilding this +slice. + +Deleted approach: + +- the early two-column Home/source-pipeline spike +- the old prompt-preview `AlertDialog` implementation +- the earlier lightweight “copy a generic prompt” version of create-app + +Reason: + +- it did not match the new App Quickstart spec/evals +- it used the wrong dialog primitive +- it blurred the older ideation with the now-defined product contract + +## Files To Start From + +If a new collaborator is continuing this work, the key files are: + +### Product docs + +- [260325 App Quickstart Spec](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/docs/builder-flow/260325-app-quickstart-spec.md) +- [260325 App Quickstart Implementation Plan](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/docs/plans/260325-app-quickstart-implementation-plan.md) +- [260325 App Quickstart Invariants Evals](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/docs/builder-flow/260325-app-quickstart-invariants-evals.yaml) + +### UI entry points + +- [home index](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/pages/home/index.tsx) +- [connected sources list](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/pages/home/components/connected-sources-list.tsx) +- [source overview index](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/pages/source/index.tsx) + +### Quickstart logic + +- [source hook](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/pages/source/use-source-overview-page.ts) +- [quickstart dialog](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/pages/source/components/source-app-quickstart-dialog.tsx) +- [artifact helpers](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/pages/source/app-quickstart.ts) +- [AI adapter seam](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/pages/source/app-quickstart-ai.ts) +- [quickstart types](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/pages/source/types.ts) +- [dialog primitive](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/components/ui/dialog.tsx) + +### Tauri handoff-file plumbing + +- [tauri path bindings](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/lib/tauri-paths.ts) +- [tauri file ops command](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src-tauri/src/commands/file_ops.rs) +- [tauri lib command registration](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src-tauri/src/lib.rs) + +### Tests + +- [home tests](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/pages/home/index.test.tsx) +- [connected sources list tests](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/pages/home/components/connected-sources-list.test.tsx) +- [source overview tests](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/pages/source/index.test.tsx) +- [source hook tests](/Users/cflack/Repos/vana-com/data-connect-source-pipeline/src/pages/source/use-source-overview-page.test.ts) + +## Verification Status + +For commit `26c8e5c`, the following passed: + +- `npx vitest run src/pages/source/index.test.tsx src/pages/source/use-source-overview-page.test.ts src/pages/home/index.test.tsx src/pages/home/components/connected-sources-list.test.tsx` +- `npm run typecheck` +- `git diff --check` + +The following did not complete due to an existing repo/environment issue: + +- `cargo check --manifest-path src-tauri/Cargo.toml` + - blocked by missing connector glob expectations unrelated to the new feature + +## Recommended Next Instruction + +If handing this to another collaborator, the cleanest next instruction is: + +`Strengthen the App Quickstart handoff artifact in src/pages/source/app-quickstart.ts so it is denser, more source-specific, and more truthful about the current local-first execution boundary, while preserving the current fallback path, state machine, and eval-backed tests.` + +## Notes + +- The checked-in implementation plan has not been rewritten line-by-line to mark + slices complete. +- This file is the explicit status checkpoint for where the process is now. 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} + +
+
+ +