From 6635978cec8e7869cafda8b7fd49f6f4748e6a39 Mon Sep 17 00:00:00 2001 From: Faisal Date: Sun, 26 Apr 2026 15:08:39 +0300 Subject: [PATCH] feat: add research space core --- README.md | 8 + ...2e-workspace-research-planning-e83335e2.md | 54 +++ ...an-workspace-research-planning-e83335e2.md | 349 ++++++++++++++++++ .../research-spaces/[rid]/enrich/route.ts | 28 ++ .../research-spaces/[rid]/promote/route.ts | 30 ++ .../[id]/research-spaces/[rid]/route.ts | 70 ++++ .../research-spaces/[rid]/synthesize/route.ts | 24 ++ .../workspaces/[id]/research-spaces/route.ts | 35 ++ src/app/workspace/[id]/research/page.tsx | 8 + src/components/research/command-input.tsx | 16 + .../research/import-export-menu.tsx | 10 + src/components/research/promote-menu.tsx | 22 ++ src/components/research/research-page.tsx | 69 ++++ src/components/research/space-sidebar.tsx | 27 ++ src/components/research/status-bar.tsx | 17 + src/components/research/synthesis-panel.tsx | 17 + src/components/research/template-picker.tsx | 13 + src/components/research/tile-card.tsx | 44 +++ src/components/research/tile-index.tsx | 14 + src/components/research/views/graph-view.tsx | 28 ++ src/components/research/views/kanban-view.tsx | 24 ++ src/components/research/views/tiling-view.tsx | 21 ++ src/components/workspace/dashboard.tsx | 27 +- src/hooks/use-research-autosave.ts | 47 +++ src/hooks/use-research-collaboration.ts | 41 ++ src/hooks/use-research-spaces.ts | 38 ++ src/lib/__tests__/research-ai.test.ts | 19 + .../__tests__/research-collaboration.test.ts | 20 + .../research-markdown-export.test.ts | 16 + .../__tests__/research-nodepad-format.test.ts | 19 + src/lib/__tests__/research-promotion.test.ts | 16 + src/lib/__tests__/research-schemas.test.ts | 18 + src/lib/__tests__/research-server.test.ts | 39 ++ src/lib/__tests__/research-templates.test.ts | 15 + src/lib/research/ai.ts | 88 +++++ src/lib/research/client.ts | 49 +++ src/lib/research/collaboration.ts | 47 +++ src/lib/research/content-types.ts | 11 + src/lib/research/detect-content-type.ts | 13 + src/lib/research/markdown-export.ts | 49 +++ src/lib/research/nodepad-format.ts | 65 ++++ src/lib/research/promotion.ts | 39 ++ src/lib/research/schemas.ts | 132 +++++++ src/lib/research/server.ts | 196 ++++++++++ src/lib/research/templates.ts | 64 ++++ src/lib/research/types.ts | 114 ++++++ src/store/research-store.ts | 71 ++++ 47 files changed, 2180 insertions(+), 1 deletion(-) create mode 100644 docs/tasks/workspace-research-planning-e83335e2/e2e-workspace-research-planning-e83335e2.md create mode 100644 docs/tasks/workspace-research-planning-e83335e2/plan-workspace-research-planning-e83335e2.md create mode 100644 src/app/api/workspaces/[id]/research-spaces/[rid]/enrich/route.ts create mode 100644 src/app/api/workspaces/[id]/research-spaces/[rid]/promote/route.ts create mode 100644 src/app/api/workspaces/[id]/research-spaces/[rid]/route.ts create mode 100644 src/app/api/workspaces/[id]/research-spaces/[rid]/synthesize/route.ts create mode 100644 src/app/api/workspaces/[id]/research-spaces/route.ts create mode 100644 src/app/workspace/[id]/research/page.tsx create mode 100644 src/components/research/command-input.tsx create mode 100644 src/components/research/import-export-menu.tsx create mode 100644 src/components/research/promote-menu.tsx create mode 100644 src/components/research/research-page.tsx create mode 100644 src/components/research/space-sidebar.tsx create mode 100644 src/components/research/status-bar.tsx create mode 100644 src/components/research/synthesis-panel.tsx create mode 100644 src/components/research/template-picker.tsx create mode 100644 src/components/research/tile-card.tsx create mode 100644 src/components/research/tile-index.tsx create mode 100644 src/components/research/views/graph-view.tsx create mode 100644 src/components/research/views/kanban-view.tsx create mode 100644 src/components/research/views/tiling-view.tsx create mode 100644 src/hooks/use-research-autosave.ts create mode 100644 src/hooks/use-research-collaboration.ts create mode 100644 src/hooks/use-research-spaces.ts create mode 100644 src/lib/__tests__/research-ai.test.ts create mode 100644 src/lib/__tests__/research-collaboration.test.ts create mode 100644 src/lib/__tests__/research-markdown-export.test.ts create mode 100644 src/lib/__tests__/research-nodepad-format.test.ts create mode 100644 src/lib/__tests__/research-promotion.test.ts create mode 100644 src/lib/__tests__/research-schemas.test.ts create mode 100644 src/lib/__tests__/research-server.test.ts create mode 100644 src/lib/__tests__/research-templates.test.ts create mode 100644 src/lib/research/ai.ts create mode 100644 src/lib/research/client.ts create mode 100644 src/lib/research/collaboration.ts create mode 100644 src/lib/research/content-types.ts create mode 100644 src/lib/research/detect-content-type.ts create mode 100644 src/lib/research/markdown-export.ts create mode 100644 src/lib/research/nodepad-format.ts create mode 100644 src/lib/research/promotion.ts create mode 100644 src/lib/research/schemas.ts create mode 100644 src/lib/research/server.ts create mode 100644 src/lib/research/templates.ts create mode 100644 src/lib/research/types.ts create mode 100644 src/store/research-store.ts diff --git a/README.md b/README.md index b2fe9eb..349066e 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,14 @@ Nexus is a visual workflow editor for designing, composing, and exporting AI wor - Export generated files as a ZIP or write them directly into a target folder - Include generated `run-.sh` and `run-.bat` helper scripts with exported workflow artifacts +### πŸ”Ž Workspace Research and Planning + +- Open a workspace-native Research surface from every workspace dashboard at `/workspace/[id]/research` +- Create research spaces from Research Brief, PRD, Implementation Plan, and Decision Log templates +- Capture tile-based notes, tasks, sources, relationships, syntheses, and promote curated findings into Brain +- Import/export portable `.nodepad` files and copy/export research as markdown +- Research editing stays local-first when optional AI connector features are disconnected + ### πŸ“ Content and Agent Authoring - Fullscreen editing for prompts and documents diff --git a/docs/tasks/workspace-research-planning-e83335e2/e2e-workspace-research-planning-e83335e2.md b/docs/tasks/workspace-research-planning-e83335e2/e2e-workspace-research-planning-e83335e2.md new file mode 100644 index 0000000..1c9c980 --- /dev/null +++ b/docs/tasks/workspace-research-planning-e83335e2/e2e-workspace-research-planning-e83335e2.md @@ -0,0 +1,54 @@ +# E2E: Workspace Research And Planning + +## User Story +Validate a workspace user can create and collaboratively use Research spaces, run/retry AI enrichment, switch views, synthesize, import/export, and promote to both Brain targets without regressing standalone `/editor`, workspace workflows, or Brain panel behavior. + +## Test Steps +Use `playwright-cli` only in the E2E pipeline (do not run from implementation validation): + +1. Start the app and create a new workspace from the landing page. +2. On the workspace dashboard, assert the `Workspace Research` entry is visible and open it. +3. Assert the route matches `/workspace/{id}/research` and the full-screen Research surface renders. +4. Create each planning template at least once, or use a minimal matrix that covers Research Brief, PRD, Implementation Plan, and Decision Log creation. +5. Add a freeform tile through the command input and edit its text. +6. Open a second browser context to the same Research URL, edit another tile, and verify live sync in the first context. +7. Trigger enrichment while the connector is unavailable; assert visible `AI not connected`, visible per-tile AI error, and visible `Re-enrich` control. +8. Click `Re-enrich` and assert the retry state/error remains visible instead of blocking note editing. +9. Switch between tiling, kanban, and graph views; assert the same tile content remains visible in each view. +10. Generate synthesis and assert the synthesis panel shows generated content with copy controls. +11. Export a `.nodepad` file, import it into a new/blank space, and assert core tiles and relationships are preserved. +12. Copy/export markdown and assert grouped research content appears in the exported text. +13. Promote selected notes to Workspace Brain and assert a successful promotion message. +14. Promote selected notes to Personal Brain and assert a successful promotion message. +15. Regression: open `/editor` and assert standalone workflow editing still renders. +16. Regression: create/open a workspace workflow and assert workflow saving/collaboration UI still renders. +17. Regression: open the Brain panel and assert promoted Brain documents are reachable. +18. Inspect storage/network where practical and assert there is no nodepad localStorage primary persistence dependency. + +## Success Criteria +- The Research dashboard entry is visible with text `Workspace Research` and opens `/workspace/{id}/research`. +- Blank Research page shows an empty-state message and template creation controls. +- Creating template spaces displays seeded tile text for each selected template. +- Two browser contexts show the same edited tile content after collaboration sync. +- Connector-unavailable enrichment displays exact visible text `AI not connected`. +- Per-tile AI errors are visible and include exact visible action text `Re-enrich`. +- Tiling, kanban, and graph controls switch views without losing tile data. +- Synthesis panel displays generated synthesis content and a copy control. +- `.nodepad` export/import preserves project name, blocks, annotations, relationships, pins, sources, and sub-tasks. +- Markdown copy/export includes headings, grouped notes, tasks, quotes, sources, and synthesis content. +- Workspace Brain promotion displays a success message and creates a Brain document. +- Personal Brain promotion displays a success message and creates a Brain document in the personal target. +- `/editor`, workspace workflow editing, and Brain panel still open successfully. +- No nodepad OpenRouter/OpenAI/Z.ai provider-key settings UI appears. +- No nodepad localStorage-only primary persistence dependency is required for research data. + +## Screenshot Capture Points +- Workspace dashboard Research entry. +- Blank Research page. +- Template-created space with seeded tiles. +- Two-browser sync state. +- AI error/retry state showing `AI not connected` and `Re-enrich`. +- Graph view. +- Synthesis panel. +- Promote menu. +- Brain document result after promotion. diff --git a/docs/tasks/workspace-research-planning-e83335e2/plan-workspace-research-planning-e83335e2.md b/docs/tasks/workspace-research-planning-e83335e2/plan-workspace-research-planning-e83335e2.md new file mode 100644 index 0000000..b800efa --- /dev/null +++ b/docs/tasks/workspace-research-planning-e83335e2/plan-workspace-research-planning-e83335e2.md @@ -0,0 +1,349 @@ +# feature: Workspace Research And Planning + +## Metadata +adw_id: `e83335e2` +document_description: `Workspace Research And Planning` + +## Description +The task adds a native, workspace-scoped research and planning surface to Nexus Workflow Studio. The new surface should live at `/workspace/[id]/research`, be launched from the workspace dashboard, and port the core user experience from the sibling `/media/falfaddaghi/extradrive2/repo/nodepad` clone into Nexus modules rather than hosting nodepad as a separate app. + +The V1 scope is broad and includes research spaces, note tiles, local-first collaboration, AI enrichment, inferred note relationships, tiling/kanban/graph views, synthesis, `.nodepad` import/export, markdown export/copy, planning templates, and promotion into both Workspace Brain and Personal Brain targets. Data must be persisted under the existing workspace data root at `{NEXUS_BRAIN_DATA_DIR}/workspaces/{workspaceId}/research/...`, and AI must reuse the existing Nexus connector path instead of introducing nodepad-local OpenRouter/OpenAI/Z.ai settings. + +Complexity assessment: `complex` because this work spans routing, dashboard UI, server persistence, schemas/types, collaboration/Yjs, AI integration, import/export, Brain promotion, package dependencies, and broad unit/E2E coverage. + +## Objective +Implement a fully integrated workspace Research page that gives each Nexus workspace collaborative nodepad-like research spaces with server-backed persistence, AI-assisted enrichment/synthesis through Nexus connectors, planning templates, import/export, and Brain promotion, while preserving existing standalone `/editor`, workspace workflow editing, and Brain panel behavior. + +## Problem Statement +Nexus currently centers on workflow editing and Brain documents but lacks a workspace-native surface for collecting research notes, organizing planning artifacts, collaboratively synthesizing information, and promoting curated research into the Brain. The sibling nodepad app has the desired interaction model, but running or embedding it separately would fragment routing, storage, styling, auth/connector behavior, collaboration, and data portability. + +## Solution Statement +Port the relevant nodepad concepts into a first-class `research` namespace inside Nexus. Add workspace API routes and a file-backed research store mirroring the existing workspace/Brain manifest patterns. Add a client Research page composed from ported/adapted nodepad UI components styled with Nexus theme tokens. Add a research-specific Yjs/Hocuspocus adapter using stable room IDs (`nexus-research-{workspaceId}-{spaceId}`) and debounced autosave to the new API. Add AI helper modules that preserve nodepad enrichment/synthesis result shapes and robust JSON parsing while routing requests through Nexus/OpenCode connector state. Add template seeding, `.nodepad` and markdown import/export helpers, and promotion helpers that create versioned Knowledge Brain documents for workspace or personal targets. + +## Code Patterns to Follow +Reference implementations: +- `CLAUDE.md` β€” project coding rules: use Bun, `@/*` imports, `zod/v4`, dark-theme-first UI, and preserve browser/localStorage safeguards. +- `README.md` β€” current app behavior, scripts, `/editor` standalone expectations, and OpenCode optional/offline behavior. +- `docs/tasks/conditional_docs.md` β€” conditional docs used to identify workspace and Brain documentation requirements. +- `docs/tasks/feature-workspace-foundation-616005e8/doc-feature-workspace-foundation-616005e8.md` β€” workspace route, server file-store, dashboard, stable room ID, and autosave conventions. +- `docs/tasks/persistent-brain/doc-persistent-brain.md` β€” Brain persistence, versioning, Hocuspocus, and server-backed collaboration conventions. +- `/media/falfaddaghi/extradrive2/repo/NexusWorkflowStudio/docs/spec/spec-workspace-research-planning.md` β€” source spec referenced by the task document. Note: this spec exists in the parent repo checkout, not in the current task tree. +- `src/lib/workspace/server.ts`, `src/lib/workspace/types.ts`, `src/lib/workspace/schemas.ts` β€” mirror the workspace file-store manifest pattern, nanoid usage, JSON read/write helpers, and Zod route validation. +- `src/app/api/workspaces/[id]/workflows/route.ts` and `src/app/api/workspaces/[id]/workflows/[wid]/route.ts` β€” follow current Next App Router route handler structure and error response style. +- `src/lib/brain/server.ts`, `src/lib/brain/client.ts`, `src/lib/brain/schemas.ts`, `src/types/knowledge.ts` β€” follow Brain document shape, versioned save behavior, token/session distinction, and `associatedWorkflowIds` usage for promotion. +- `src/lib/collaboration/collab-doc.ts`, `src/lib/collaboration/config.ts`, `scripts/collab-server.ts` β€” follow existing Hocuspocus provider, connection status, seeding, and persisted room-state patterns, but do not couple research state to workflow node/edge state. +- `src/components/workspace/dashboard.tsx`, `src/components/workspace/workspace-header.tsx`, `src/components/workspace/workflow-card.tsx`, `src/app/workspace/[id]/page.tsx` β€” add a dashboard Research entry consistently with current workspace UI. +- `src/components/workflow/brain-panel/*` β€” follow visual language and Brain document UX where promotion touches Brain. +- `src/lib/opencode/*`, `src/store/opencode/*`, `src/hooks/use-models.ts`, `src/hooks/use-tools.ts` β€” reuse existing Nexus connector/OpenCode state; do not port `nodepad/lib/ai-settings.ts` provider-key settings. +- Nodepad visual/functional references to port/adapt: `/media/falfaddaghi/extradrive2/repo/nodepad/app/page.tsx`, `components/project-sidebar.tsx`, `components/status-bar.tsx`, `components/vim-input.tsx`, `components/tile-card.tsx`, `components/tile-index.tsx`, `components/tiling-area.tsx`, `components/kanban-area.tsx`, `components/graph-area.tsx`, `components/ghost-panel.tsx`. +- Nodepad data/AI/export references to port/adapt: `/media/falfaddaghi/extradrive2/repo/nodepad/lib/ai-enrich.ts`, `lib/ai-ghost.ts`, `lib/content-types.ts`, `lib/detect-content-type.ts`, `lib/export.ts`, `lib/nodepad-format.ts`, `lib/initial-data.ts`. + +Research notes from exhaustive greps: +- Existing Nexus workspace/Brain/OpenCode/collaboration references are spread across these files with counts and must be checked for integration impact: `src/app/api/brain/documents/route.ts` (3), `src/app/api/brain/documents/[id]/route.ts` (2), `src/app/api/brain/documents/[id]/feedback/route.ts` (2), `src/app/api/brain/documents/[id]/restore/route.ts` (2), `src/app/api/brain/documents/[id]/versions/route.ts` (2), `src/app/api/brain/documents/[id]/view/route.ts` (2), `src/app/api/brain/session/route.ts` (2), `src/components/workflow/brain-panel/constants.ts` (8), `src/components/workflow/brain-panel/doc-editor.tsx` (17), `src/components/workflow/brain-panel/panel.tsx` (7), `src/components/workflow/connect-dialog.tsx` (19), `src/components/workflow/floating-workflow-gen.tsx` (4), `src/components/workflow/generated-export-dialog.tsx` (4), `src/components/workflow/header/session-actions.tsx` (3), `src/components/workflow/header.tsx` (4), `src/components/workflow/header/use-header-controller.ts` (5), `src/components/workflow/project-switcher.tsx` (8), `src/components/workflow/shared-header-actions.tsx` (4), `src/components/workflow/workflow-editor.tsx` (4), `src/hooks/use-models.ts` (7), `src/hooks/use-tools.ts` (3), `src/hooks/use-workspace-autosave.ts` (1), `src/lib/brain/client.ts` (18), `src/lib/brain/schemas.ts` (1), `src/lib/brain/server.ts` (18), `src/lib/brain/types.ts` (7), `src/lib/collaboration/collab-doc.ts` (11), `src/lib/collaboration/config.ts` (3), `src/lib/collaboration/index.ts` (1), `src/lib/knowledge.ts` (10), `src/lib/opencode/client.ts` (4), `src/lib/opencode/config.ts` (3), `src/lib/opencode/errors.ts` (10), `src/lib/opencode/index.ts` (22), `src/lib/opencode/services/events.ts` (3), `src/lib/opencode/types.ts` (4), `src/lib/__tests__/brain-server.test.ts` (13), `src/store/knowledge/helpers.ts` (6), `src/store/knowledge/store.ts` (7), `src/store/knowledge/types.ts` (11), `src/store/opencode-store.ts` (3), `src/store/opencode/store.ts` (17), `src/types/knowledge.ts` (9). +- Nodepad-local patterns that must not be blindly ported include `localStorage`, `.nodepad`, `OpenRouter`, `OpenAI`, `Z.ai`, and `research`. The grep found relevant references in `/media/falfaddaghi/extradrive2/repo/nodepad/app/page.tsx` (22), `app/layout.tsx` (9), `components/about-panel.tsx` (18), `components/project-sidebar.tsx` (3), `components/status-bar.tsx` (2), `components/vim-input.tsx` (2), `lib/ai-settings.ts` (17), `lib/ai-enrich.ts` (7), `lib/nodepad-format.ts` (8), `lib/export.ts` (5), plus smaller references in `app/api/fetch-url/route.ts`, `components/intro-modal.tsx`, `components/mobile-wall.tsx`, `components/tiling-area.tsx`, `lib/acp-client.ts`, and `lib/ai-ghost.ts`. Use these as a checklist to remove/replace localStorage-only and provider-key behavior in the Nexus port. + +## Relevant Files +Use these files to complete the task: + +- `CLAUDE.md` β€” mandatory project conventions and validation expectations. +- `.app_config.yaml` β€” app configuration and default validation commands. +- `README.md` β€” public behavior and scripts; update if Research becomes a user-facing feature documented in the main product overview. +- `package.json` / `bun.lock` β€” add direct dependencies if the ported UI keeps using `d3`, `framer-motion`, `cmdk`, `react-markdown`, and `remark-gfm`; validate scripts. +- `/media/falfaddaghi/extradrive2/repo/NexusWorkflowStudio/docs/spec/spec-workspace-research-planning.md` β€” source spec; ensure every FR/AC remains covered. +- `docs/tasks/conditional_docs.md` β€” conditional documentation rules; this task matches workspace and Brain conditions. +- `docs/tasks/feature-workspace-foundation-616005e8/doc-feature-workspace-foundation-616005e8.md` β€” workspace architecture reference. +- `docs/tasks/persistent-brain/doc-persistent-brain.md` β€” Brain and collaboration architecture reference. +- `src/app/workspace/[id]/page.tsx` β€” existing workspace dashboard route; add navigation path to Research through the dashboard component. +- `src/components/workspace/dashboard.tsx` β€” add Research card/entry and dashboard affordance that opens `/workspace/${workspaceId}/research`. +- `src/components/workspace/workspace-header.tsx` β€” consider adding a Research tab/button if dashboard navigation belongs in the header. +- `src/app/api/workspaces/[id]/route.ts` and `src/app/api/workspaces/[id]/workflows/**` β€” reference route conventions and workspace existence checks. +- `src/lib/workspace/config.ts` β€” use the existing workspace data root under `NEXUS_BRAIN_DATA_DIR`. +- `src/lib/workspace/server.ts` β€” extend or complement file persistence with research-specific storage under `workspaces/{workspaceId}/research`. +- `src/lib/workspace/types.ts` β€” keep workspace records unchanged; research types should live in `src/lib/research/types.ts` unless a shared workspace manifest extension is required. +- `src/lib/workspace/schemas.ts` β€” either add research route schemas here only if workspace-local, or prefer `src/lib/research/schemas.ts` for research namespace clarity. +- `src/lib/brain/server.ts`, `src/lib/brain/client.ts`, `src/lib/brain/schemas.ts`, `src/types/knowledge.ts` β€” implement Workspace Brain/Personal Brain promotion through existing Knowledge document conventions. +- `src/lib/knowledge.ts`, `src/store/knowledge/store.ts`, `src/store/knowledge/types.ts` β€” integrate personal Brain promotion if it uses the current browser/session Brain store APIs. +- `src/lib/collaboration/collab-doc.ts`, `src/lib/collaboration/config.ts`, `src/lib/collaboration/index.ts`, `scripts/collab-server.ts` β€” add research room id helpers and research-specific Yjs syncing without regressing workflow/Brain collaboration. +- `src/lib/opencode/index.ts`, `src/lib/opencode/client.ts`, `src/lib/opencode/types.ts`, `src/store/opencode-store.ts`, `src/store/opencode/store.ts`, `src/components/workflow/connect-dialog.tsx` β€” reuse existing connector/OpenCode status and calls for research AI. +- Nodepad reference files under `/media/falfaddaghi/extradrive2/repo/nodepad/components/*` and `/media/falfaddaghi/extradrive2/repo/nodepad/lib/*` listed above β€” port behavior, not app-level hosting or local provider settings. + +### New Files +- `src/app/workspace/[id]/research/page.tsx` β€” workspace Research route shell. +- `src/app/api/workspaces/[id]/research-spaces/route.ts` β€” `GET`/`POST` list and create research spaces. +- `src/app/api/workspaces/[id]/research-spaces/[rid]/route.ts` β€” `GET`/`PUT`/`PATCH`/`DELETE` single research space operations. +- `src/app/api/workspaces/[id]/research-spaces/[rid]/promote/route.ts` β€” promote selected research content to Workspace Brain or Personal Brain. +- `src/app/api/workspaces/[id]/research-spaces/[rid]/enrich/route.ts` β€” optional server-side enrichment endpoint if connector calls should not run directly from the browser. +- `src/app/api/workspaces/[id]/research-spaces/[rid]/synthesize/route.ts` β€” optional synthesis endpoint if implemented separately from enrichment. +- `src/components/research/research-page.tsx` β€” main full-screen research container. +- `src/components/research/status-bar.tsx` β€” compact status bar with workspace/space status, sync/AI states, and view controls. +- `src/components/research/space-sidebar.tsx` β€” left spaces/settings/templates/import/export panel. +- `src/components/research/command-input.tsx` β€” bottom command input adapted from nodepad VimInput. +- `src/components/research/tile-card.tsx` β€” note tile renderer with annotation, sources, errors, tasks, pinning, and `Re-enrich`. +- `src/components/research/tile-index.tsx` β€” index/search/filter panel. +- `src/components/research/views/tiling-view.tsx` β€” tiling note layout. +- `src/components/research/views/kanban-view.tsx` β€” kanban grouping by content type/category/status. +- `src/components/research/views/graph-view.tsx` β€” relationship graph view using `d3` if retained. +- `src/components/research/synthesis-panel.tsx` β€” generated synthesis output and copy/export controls. +- `src/components/research/promote-menu.tsx` β€” Workspace Brain default and Personal Brain secondary target with workflow linking. +- `src/components/research/template-picker.tsx` β€” Research Brief, PRD, Implementation Plan, and Decision Log template creation UI. +- `src/components/research/import-export-menu.tsx` β€” `.nodepad` import/export and markdown export/copy UI. +- `src/hooks/use-research-spaces.ts` β€” fetch/list/create/delete spaces for a workspace. +- `src/hooks/use-research-collaboration.ts` β€” start/stop research Yjs/Hocuspocus room, merge local/remote state, and expose connection status. +- `src/hooks/use-research-autosave.ts` β€” debounced snapshot saves via research API. +- `src/lib/research/types.ts` β€” `ResearchSpaceRecord`, `ResearchSpaceData`, `ResearchBlock`, `ResearchGhostNote`, `ResearchTemplateId`, `ResearchViewMode`, enrichment and synthesis types. +- `src/lib/research/schemas.ts` β€” Zod schemas using `zod/v4` for routes and import validation. +- `src/lib/research/server.ts` β€” file persistence under `workspaces/{workspaceId}/research/manifest.json` and `spaces/{spaceId}.json`. +- `src/lib/research/client.ts` β€” browser fetch helpers for the research API. +- `src/lib/research/templates.ts` β€” template seed data for Research Brief, PRD, Implementation Plan, Decision Log. +- `src/lib/research/nodepad-format.ts` β€” Nexus-adapted `.nodepad` parse/serialize helpers preserving nodepad portability. +- `src/lib/research/markdown-export.ts` β€” Nexus-adapted research-logical markdown export/copy helper. +- `src/lib/research/ai.ts` β€” enrichment/synthesis prompt construction, robust JSON parsing, and connector calls. +- `src/lib/research/content-types.ts` and `src/lib/research/detect-content-type.ts` β€” content-type taxonomy and local classifier adapted from nodepad. +- `src/lib/research/promotion.ts` β€” convert selected research content to versioned Knowledge Brain documents. +- `src/lib/research/collaboration.ts` β€” `buildResearchRoomId(workspaceId, spaceId)` and Yjs snapshot helpers. +- `src/store/research-store.ts` or `src/store/research/*` β€” client state for active space, blocks, view mode, panels, selection, AI states. +- `src/lib/__tests__/research-server.test.ts` β€” persistence and API helper unit tests. +- `src/lib/__tests__/research-schemas.test.ts` β€” schema validation tests. +- `src/lib/__tests__/research-nodepad-format.test.ts` β€” `.nodepad` import/export tests. +- `src/lib/__tests__/research-markdown-export.test.ts` β€” markdown grouping/export tests. +- `src/lib/__tests__/research-ai.test.ts` β€” robust JSON parsing and prompt behavior tests. +- `src/lib/__tests__/research-templates.test.ts` β€” planning template seed tests. +- `src/lib/__tests__/research-promotion.test.ts` β€” Workspace Brain/Personal Brain promotion conversion tests. +- `src/lib/__tests__/research-collaboration.test.ts` β€” room id, snapshot seeding, Yjs round-trip, and autosave helper tests. +- `docs/tasks/workspace-research-planning-e83335e2/e2e-workspace-research-planning-e83335e2.md` β€” E2E spec to be created during implementation, not during planning. + +## Implementation Plan +### Phase 1: Foundation +Create the research domain model, schemas, file-backed persistence, API routes, client fetch helpers, package dependencies, and dashboard route entry. Establish a research-specific room ID and snapshot format before building UI so all components share stable types. + +### Phase 2: Core Implementation +Port/adapt nodepad UI and behavior into `src/components/research` and `src/lib/research`. Implement spaces, blocks, notes, planning templates, views, index, command input, synthesis, `.nodepad`/markdown import/export, AI enrichment with visible retry/error states, and Brain promotion conversion. + +### Phase 3: Integration +Wire Research into workspace routing, existing connector state, Hocuspocus collaboration, autosave, and Brain APIs. Add tests for persistence, schemas, AI parsing, import/export, templates, collaboration, and promotion. Add the separate E2E specification covering browser interactions but do not execute E2E in validation commands. + +## Step by Step Tasks +IMPORTANT: Execute every step in order, top to bottom. + +### 1. Confirm Scope, Reference Behavior, and Dependencies +- Re-read the task source JSON and the spec file at `/media/falfaddaghi/extradrive2/repo/NexusWorkflowStudio/docs/spec/spec-workspace-research-planning.md` before coding. +- Re-open `CLAUDE.md`, `README.md`, `docs/tasks/conditional_docs.md`, `docs/tasks/feature-workspace-foundation-616005e8/doc-feature-workspace-foundation-616005e8.md`, and `docs/tasks/persistent-brain/doc-persistent-brain.md`. +- Inspect the nodepad reference files in `/media/falfaddaghi/extradrive2/repo/nodepad`, especially `app/page.tsx`, `components/*`, `lib/ai-enrich.ts`, `lib/ai-ghost.ts`, `lib/export.ts`, and `lib/nodepad-format.ts`. +- Add direct package dependencies with Bun only if retained by the ported implementation: `bun add d3 framer-motion cmdk react-markdown remark-gfm`. If any are not used, do not add them. +- Do not port `nodepad/lib/ai-settings.ts` settings UI or nodepad-local provider-key storage. + +### 2. Define Research Types, Schemas, and Seed Data +- Create `src/lib/research/types.ts` with at least: + - `ResearchSpaceRecord` + - `ResearchSpaceData` + - `ResearchBlock` + - `ResearchGhostNote` + - `ResearchTemplateId` + - `ResearchViewMode` + - enrichment result shape containing `contentType`, `category`, `annotation`, `confidence`, `influencedByIndices`, `isUnrelated`, `mergeWithIndex`, and optional `sources` +- Include fields needed for collaboration and persistence: stable `id`, `workspaceId`, `name`, `createdAt`, `updatedAt`, `createdBy`/`lastModifiedBy`, `blocks`, `collapsedIds`, `ghostNotes`, `syntheses`, `templateId`, `associatedWorkflowIds`, and view/UI state where appropriate. +- Create `src/lib/research/schemas.ts` using `zod/v4` for create/update/save/promote/import payloads. +- Create `src/lib/research/templates.ts` with deterministic starter tiles for: + - `research-brief` + - `prd` + - `implementation-plan` + - `decision-log` +- Ensure template seed blocks are structured enough to be useful but not tied to any single workspace. + +### 3. Implement File-Backed Research Persistence +- Create `src/lib/research/server.ts` modeled after `src/lib/workspace/server.ts`. +- Store data exactly under: + - `{NEXUS_BRAIN_DATA_DIR}/workspaces/{workspaceId}/research/manifest.json` + - `{NEXUS_BRAIN_DATA_DIR}/workspaces/{workspaceId}/research/spaces/{spaceId}.json` +- Ensure `manifest.json` tracks version, `workspaceId`, `spaces: ResearchSpaceRecord[]`, and updated timestamps. +- Add helpers: + - `listResearchSpaces(workspaceId)` + - `createResearchSpace(workspaceId, input)` + - `getResearchSpace(workspaceId, spaceId)` + - `saveResearchSpace(workspaceId, spaceId, data, lastModifiedBy)` + - `updateResearchSpaceMeta(workspaceId, spaceId, updates)` + - `deleteResearchSpace(workspaceId, spaceId)` +- Validate that the parent workspace exists via existing workspace helpers before creating research data. +- Make writes atomic enough for local server use by writing complete JSON snapshots and ensuring directories exist. +- Strip transient UI-only fields such as `isEnriching`, temporary status text, drag state, and local errors before saving unless errors are intentionally persisted as visible per-tile AI error state. + +### 4. Add Research API Routes +- Add `src/app/api/workspaces/[id]/research-spaces/route.ts` with: + - `GET` returning `{ spaces }` + - `POST` accepting name/template and returning `{ space }` with status `201` +- Add `src/app/api/workspaces/[id]/research-spaces/[rid]/route.ts` with: + - `GET` returning the full `ResearchSpaceData` + - `PUT` saving a complete snapshot + - `PATCH` updating metadata such as name/template/workflow links + - `DELETE` returning `204` +- Add `src/app/api/workspaces/[id]/research-spaces/[rid]/promote/route.ts` accepting selected block/synthesis/task/source IDs, target (`workspace` default or `personal`), and optional `associatedWorkflowIds`. +- If needed for connector safety, add enrichment/synthesis API routes under the same research-space route namespace rather than calling provider details directly from UI code. +- Match the existing route style: `export const dynamic = "force-dynamic"`, `NextResponse.json`, `params: Promise<...>`, `safeParse`, `400` for validation, `404` for missing workspace/space, and `500` for unexpected failures. + +### 5. Add Client API Helpers and State Store +- Create `src/lib/research/client.ts` for typed fetch wrappers around all research API routes. +- Create `src/store/research-store.ts` or a `src/store/research/` module for active space state, block operations, active view mode, panel open state, selection, synthesis state, and AI status. +- Keep store mutations local-first so note creation/editing works while AI is disconnected or failing. +- Avoid localStorage as the primary research persistence. If localStorage is used at all, limit it to safe UI preferences such as last selected view mode or panel open/closed state. + +### 6. Implement Research Collaboration and Autosave +- Add `buildResearchRoomId(workspaceId, spaceId): string` returning exactly `nexus-research-{workspaceId}-{spaceId}` in `src/lib/research/collaboration.ts` or `src/lib/collaboration/index.ts`. +- Implement Yjs snapshot serialization/deserialization for research space data. Prefer a Y.Map keyed by stable block IDs plus maps/text for metadata and syntheses; keep stable block IDs as the merge target for AI results. +- Create `src/hooks/use-research-collaboration.ts` using `HocuspocusProvider` and `getCollabServerUrl()`; follow `CollabDoc` connect/disconnect/awareness patterns without reusing workflow node/edge store internals. +- Seed the room from the saved `ResearchSpaceData` only when the Yjs room is empty. +- Merge remote edits into the research store and pause local-to-remote subscribers during remote application to avoid feedback loops. +- Add `src/hooks/use-research-autosave.ts` that debounces snapshot saves to `PUT /api/workspaces/[id]/research-spaces/[rid]` and performs a best-effort final save on unload when possible. +- Ensure failed enrichment does not block collaboration; AI results merge back by stable `block.id` as ordinary collaborative state. + +### 7. Port and Adapt Nodepad UI into Nexus Research Components +- Create `src/app/workspace/[id]/research/page.tsx` as a client route shell resolving `params` and rendering `ResearchPage`. +- Create `src/components/research/research-page.tsx` as the full-screen page with Nexus dark theme tokens. +- Port/adapt these nodepad UI pieces: + - compact status bar + - left space/settings/templates panel + - bottom command input + - tiling view + - kanban view + - graph view + - tile index panel + - synthesis panel + - visible per-tile `Re-enrich` retry controls +- Preserve the nodepad layout closely while adapting class names/colors to Nexus patterns (`src/lib/theme` where useful). +- Add empty/loading/error states for no spaces, missing workspace, missing space, API failures, collaboration disconnected, and AI disconnected. +- Do not include nodepad intro/about/provider settings unless they are adapted to Nexus UX and still needed. + +### 8. Add Workspace Dashboard Entry +- Update `src/components/workspace/dashboard.tsx` to show a Research entry on every workspace dashboard. +- The entry should route to `/workspace/${workspaceId}/research`. +- If the workspace has no workflows, still show the Research entry along with the existing empty state or adjust `EmptyState` so users can access Research without first creating a workflow. +- Optionally add a secondary Research tab/button in `src/components/workspace/workspace-header.tsx` if it fits existing navigation. +- Ensure existing workflow creation, workflow cards, workspace rename, and recent workspace tracking remain unchanged. + +### 9. Implement Note Blocks, Relationships, Views, and Synthesis +- Implement create/edit/delete/pin/collapse operations for `ResearchBlock`. +- Add inferred relationship fields using stable block IDs, converting nodepad's `influencedByIndices` into stable IDs at merge time. +- Implement tiling, kanban, and graph views over the same collaborative block state. +- Add synthesis generation and a synthesis panel that can store multiple synthesis records in `ResearchSpaceData`. +- Ensure synthesis output can be copied/exported and promoted to Brain alongside selected notes/tasks/sources. +- Include per-block sub-task support if ported from nodepad; tasks should be eligible for Brain promotion. + +### 10. Implement AI Enrichment Through Nexus Connector +- Create `src/lib/research/ai.ts` by adapting nodepad's prompt intent and robust JSON parsing. +- Preserve the enrichment result shape exactly: `contentType`, `category`, `annotation`, `confidence`, `influencedByIndices`, `isUnrelated`, `mergeWithIndex`, optional `sources`. +- Route calls through existing Nexus connector/OpenCode mechanisms. If a connector is not available, return a controlled error state such as `AI not connected` rather than blocking note creation. +- Show AI errors visibly on each tile with an explicit `Re-enrich` action. +- Keep note creation/editing/collaboration fully functional when AI is disconnected, times out, returns invalid JSON, or returns partial data. +- Add retry parsing behavior similar to nodepad's `parseEnrichResult`/`coerceLooseEnrichResult`, but do not include nodepad provider-key settings or OpenRouter/OpenAI/Z.ai-specific UI. + +### 11. Implement `.nodepad` Import/Export and Markdown Export/Copy +- Create `src/lib/research/nodepad-format.ts` adapted from nodepad's `lib/nodepad-format.ts`. +- Preserve `.nodepad` compatibility with `version`, `exportedAt`, project name, blocks, collapsed IDs, ghost notes, AI annotations, connections, confidence, sources, pins, and sub-tasks. +- On import, assign fresh Nexus research space IDs and preserve stable imported block IDs only when safe; otherwise remap relationships consistently. +- Create `src/lib/research/markdown-export.ts` adapted from nodepad's research-logical grouping. +- Add UI controls for `.nodepad` import, `.nodepad` export, markdown file export, and markdown copy. +- Validate malformed imports safely and show user-visible errors. + +### 12. Implement Brain Promotion +- Create `src/lib/research/promotion.ts` to convert selected tiles, syntheses, tasks, sources, template metadata, and `associatedWorkflowIds` into `KnowledgeDoc`/Brain save inputs. +- Workspace Brain must be the default target from workspace research. +- Personal Brain must be available as a secondary target in the promote menu. +- For Workspace Brain, write a versioned Brain document through existing Brain server/session patterns or a clear workspace target helper. +- For Personal Brain, use the current user/browser Brain session path without requiring workspace Brain to be selected. +- Include selected workflow links in `associatedWorkflowIds` when provided. +- Ensure promotion failures do not lose research edits and are surfaced to the user. + +### 13. Add Unit and Integration Tests +- Add schema tests in `src/lib/__tests__/research-schemas.test.ts` for valid/invalid blocks, templates, save payloads, and promotion payloads. +- Add persistence tests in `src/lib/__tests__/research-server.test.ts` using a temp data dir to verify manifest creation, space CRUD, missing workspace handling, snapshot save, delete, and path layout. +- Add `.nodepad` import/export tests in `src/lib/__tests__/research-nodepad-format.test.ts` for full-fidelity round trips and malformed input. +- Add markdown export tests in `src/lib/__tests__/research-markdown-export.test.ts` for type grouping, claims/tasks/quotes, sources, and empty spaces. +- Add AI tests in `src/lib/__tests__/research-ai.test.ts` for fenced JSON, loose/truncated JSON fallback, invalid JSON errors, connector-unavailable behavior, and confidence clamping. +- Add template tests in `src/lib/__tests__/research-templates.test.ts` for all four V1 templates and deterministic starter tiles. +- Add promotion tests in `src/lib/__tests__/research-promotion.test.ts` for Workspace Brain default, Personal Brain target, selected content, syntheses, tasks, sources, template metadata, and `associatedWorkflowIds`. +- Add collaboration helper tests in `src/lib/__tests__/research-collaboration.test.ts` for exact room id generation, initial snapshot seeding, Yjs state round-trip, and autosave snapshot preparation. +- Update existing Brain/workspace/collaboration tests if shared helpers change. + +### 14. Create the Separate E2E Test Specification File +- Create `docs/tasks/workspace-research-planning-e83335e2/e2e-workspace-research-planning-e83335e2.md` during implementation. +- Do not execute this E2E file in the implementation validation commands; a separate pipeline runs it. +- The E2E spec must contain these sections: + - `User Story`: validate a workspace user can create and collaboratively use Research spaces, run/retry AI enrichment, switch views, synthesize, import/export, and promote to both Brain targets. + - `Test Steps`: browser interactions using `playwright-cli` only, including creating a workspace, opening the Research dashboard entry, creating each required planning template at least once or using a minimal matrix, adding/editing tiles in two browser contexts, verifying live sync, triggering enrichment and `Re-enrich`, switching tiling/kanban/graph views, generating synthesis, exporting `.nodepad`, importing `.nodepad`, copying/exporting markdown, promoting to Workspace Brain and Personal Brain, and regression checks for `/editor`, workspace workflows, and Brain panel. + - `Success Criteria`: exact UI text/values to assert, including route `/workspace/{id}/research`, visible `AI not connected` when connector unavailable, visible per-tile AI errors, visible `Re-enrich`, successful promotion messages, and no nodepad localStorage dependency. + - `Screenshot Capture Points`: dashboard Research entry, blank Research page, template-created space, two-browser sync state, AI error/retry state, graph view, synthesis panel, promote menu, and Brain document result. + +### 15. Update Documentation Where User-Facing Behavior Changes +- Update `README.md` to mention workspace Research if project maintainers expect new user-facing features to be documented there. +- If configuration changes are needed for collaboration/Brain, update `.env.example`, Docker/start scripts, and docs consistently; avoid adding new env vars unless existing `NEXUS_BRAIN_DATA_DIR`/collab vars cannot satisfy the requirement. +- Add a task summary document under this task directory after implementation if the project convention requires documenting major features. + +### 16. Run Validation Commands +- Run all commands listed in the `Validation Commands` section. +- Fix every type, lint, test, and build failure before considering the task complete. +- Do not run browser/E2E commands here. + +## Testing Strategy +### Unit Tests +- Research schemas validate all route payloads, block shapes, enrichment result shapes, template IDs, view modes, and promotion target values. +- Research server tests verify exact filesystem layout, manifest updates, CRUD operations, missing workspace/space behavior, safe deletes, and snapshot persistence. +- `.nodepad` import/export tests verify full-fidelity serialization, transient-state stripping, relationship preservation/remapping, name conflict handling, and invalid file errors. +- Markdown export tests verify research-logical grouping order, claims table, task lists, quote formatting, sources, front matter, and empty export behavior. +- AI parsing tests verify strict JSON, fenced JSON, loose/truncated JSON fallback, invalid JSON errors, unavailable connector state, and confidence clamping. +- Template tests verify Research Brief, PRD, Implementation Plan, and Decision Log seed the expected starter tiles. +- Promotion tests verify Workspace Brain default, Personal Brain secondary target, selected content only, syntheses/tasks/sources inclusion, template metadata, workflow associations, and versioned document creation. +- Collaboration tests verify `nexus-research-{workspaceId}-{spaceId}` room IDs, Yjs snapshot round-trip, initial seeding only into empty docs, stable block ID merging, and autosave serialization. + +### Edge Cases +- Workspace does not exist when listing/creating research spaces. +- Research manifest exists but a space file is missing or malformed. +- Two clients create/edit/delete the same block while autosave is pending. +- AI connector unavailable, disconnected mid-request, times out, returns provider errors, returns invalid JSON, or returns relationship indices that no longer map to existing blocks. +- Imported `.nodepad` file has duplicate block IDs, unknown content types, missing optional fields, old/newer versions, malformed JSON, or broken relationship references. +- Markdown export of empty spaces, blocks with pipes/newlines, long annotations, missing categories, missing confidence, and source URLs with unusual characters. +- Promotion with no selected tiles, deleted selected tiles, missing Brain session, duplicate document title, invalid `associatedWorkflowIds`, and partial Brain save failure. +- Collaboration server unreachable; page should still allow local editing and show visible sync/AI states. +- Existing `/editor` standalone route, workspace workflows, workspace collaboration, and Brain panel continue to work. + +## Acceptance Criteria +- Workspace dashboard includes a Research entry that opens `/workspace/[id]/research`. +- `/workspace/[id]/research` renders a native Nexus full-screen research surface, not a separately hosted nodepad app. +- Research spaces persist under `{NEXUS_BRAIN_DATA_DIR}/workspaces/{workspaceId}/research/manifest.json` and `spaces/{spaceId}.json`. +- API routes exist and work for all required methods: list/create/get/save/update/delete/promote. +- Types exist for `ResearchSpaceRecord`, `ResearchSpaceData`, `ResearchBlock`, `ResearchGhostNote`, `ResearchTemplateId`, and `ResearchViewMode`. +- Planning templates exist for Research Brief, PRD, Implementation Plan, and Decision Log, each seeding structured starter tiles. +- Research collaboration uses room IDs exactly matching `nexus-research-{workspaceId}-{spaceId}`. +- Saved snapshots seed empty Yjs rooms, collaborative edits sync live, and debounced autosave writes back through the research API. +- Tiling, kanban, and graph views render the same research space state. +- Index and synthesis panels are available in the Research page. +- AI enrichment uses Nexus connector/OpenCode paths and does not add nodepad-local OpenRouter/OpenAI/Z.ai key settings. +- Notes save and collaborate when AI is disconnected, with a visible `AI not connected` state. +- Per-tile AI errors remain visible and include an explicit `Re-enrich` action. +- Enrichment result shape preserves `contentType`, `category`, `annotation`, `confidence`, `influencedByIndices`, `isUnrelated`, `mergeWithIndex`, and optional `sources`. +- `.nodepad` import/export works for project portability. +- Markdown export/copy uses nodepad's research-logical grouping adapted to Nexus. +- Brain promotion supports Workspace Brain as the default and Personal Brain as a secondary target. +- Promotion writes a versioned Brain document including selected tiles, syntheses, tasks, sources, template metadata, and optional `associatedWorkflowIds`. +- Unit tests cover schemas, persistence, import/export, markdown export, AI parsing, template seeding, Brain promotion, and collaboration helpers. +- A separate E2E spec file is created at `docs/tasks/workspace-research-planning-e83335e2/e2e-workspace-research-planning-e83335e2.md` with the required structure and browser flow, but is not executed by validation commands. +- Regression checks confirm `/editor` standalone still works, workspace workflows still save/collaborate, Brain panel still opens, and no nodepad localStorage primary persistence dependency is introduced. + +## Validation Commands +Execute every command to validate the work is complete with zero regressions. + +From `.app_config.yaml` and project scripts: +- `npm run typecheck` +- `npm run lint` +- `bun run test` +- `npm run build` + +Notes: +- `.app_config.yaml` provides `npm run typecheck`, `npm run lint`, and `npm run build`; `commands.test` is unset, but the task/spec and `package.json` require `bun run test` for this feature. +- No browser, Playwright, `playwright-cli`, or HTTP UI probing commands belong in this section; those belong to the separate E2E spec/pipeline. + +## Notes +- Use the project harness/skills described in `CLAUDE.md` for non-trivial implementation; this task is complex and multi-file. +- Treat the sibling nodepad repository as reference/source code only. Do not iframe, proxy, start, or separately host nodepad. +- Prefer a research namespace (`src/lib/research`, `src/components/research`, `src/store/research`) to avoid coupling workflow-node code to research tiles. +- Preserve existing sanitization and validation patterns. Avoid new `any` unless isolated to compatibility parsing with follow-up typed normalization. +- Be careful with browser-only APIs (`window`, `localStorage`, `navigator.clipboard`, `Blob`, file inputs) and keep them in client components/helpers. +- Keep OpenCode/AI optional: offline research editing is a first-class path. diff --git a/src/app/api/workspaces/[id]/research-spaces/[rid]/enrich/route.ts b/src/app/api/workspaces/[id]/research-spaces/[rid]/enrich/route.ts new file mode 100644 index 0000000..9daa158 --- /dev/null +++ b/src/app/api/workspaces/[id]/research-spaces/[rid]/enrich/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server"; +import { parseEnrichResult, ResearchAiError } from "@/lib/research/ai"; +import { ResearchBlockSchema } from "@/lib/research/schemas"; +import { getResearchSpace } from "@/lib/research/server"; + +export const dynamic = "force-dynamic"; + +type RouteParams = { params: Promise<{ id: string; rid: string }> }; + +export async function POST(request: Request, { params }: RouteParams) { + try { + const { id, rid } = await params; + const space = await getResearchSpace(id, rid); + if (!space) return NextResponse.json({ error: "Research space not found" }, { status: 404 }); + const body = await request.json().catch(() => ({})); + const block = ResearchBlockSchema.safeParse(body.block); + if (!block.success) return NextResponse.json({ error: "Invalid block" }, { status: 400 }); + if (typeof body.rawResult !== "string") { + return NextResponse.json({ error: "AI not connected" }, { status: 503 }); + } + const result = parseEnrichResult(body.rawResult, block.data.content); + return NextResponse.json({ result }); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to enrich research"; + const status = error instanceof ResearchAiError ? 422 : 500; + return NextResponse.json({ error: message }, { status }); + } +} diff --git a/src/app/api/workspaces/[id]/research-spaces/[rid]/promote/route.ts b/src/app/api/workspaces/[id]/research-spaces/[rid]/promote/route.ts new file mode 100644 index 0000000..1bc5092 --- /dev/null +++ b/src/app/api/workspaces/[id]/research-spaces/[rid]/promote/route.ts @@ -0,0 +1,30 @@ +import { NextResponse } from "next/server"; +import { getBrainStore } from "@/lib/brain/server"; +import { PromoteResearchSchema } from "@/lib/research/schemas"; +import { getResearchSpace } from "@/lib/research/server"; +import { buildResearchPromotionDoc } from "@/lib/research/promotion"; + +export const dynamic = "force-dynamic"; + +type RouteParams = { params: Promise<{ id: string; rid: string }> }; + +export async function POST(request: Request, { params }: RouteParams) { + try { + const { id, rid } = await params; + const parsed = PromoteResearchSchema.safeParse(await request.json().catch(() => ({}))); + if (!parsed.success) { + return NextResponse.json({ error: parsed.error.issues[0]?.message ?? "Invalid promote payload" }, { status: 400 }); + } + const space = await getResearchSpace(id, rid); + if (!space) return NextResponse.json({ error: "Research space not found" }, { status: 404 }); + + const targetWorkspaceId = parsed.data.target === "personal" + ? request.headers.get("x-brain-workspace-id") ?? id + : id; + const doc = await getBrainStore().saveDoc(targetWorkspaceId, buildResearchPromotionDoc(space, parsed.data)); + return NextResponse.json({ doc, target: parsed.data.target }); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to promote research"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/workspaces/[id]/research-spaces/[rid]/route.ts b/src/app/api/workspaces/[id]/research-spaces/[rid]/route.ts new file mode 100644 index 0000000..8f3e161 --- /dev/null +++ b/src/app/api/workspaces/[id]/research-spaces/[rid]/route.ts @@ -0,0 +1,70 @@ +import { NextResponse } from "next/server"; +import { ResearchSpaceDataSchema, SaveResearchSpaceSchema, UpdateResearchSpaceMetaSchema } from "@/lib/research/schemas"; +import { deleteResearchSpace, getResearchSpace, saveResearchSpace, updateResearchSpaceMeta } from "@/lib/research/server"; + +export const dynamic = "force-dynamic"; + +type RouteParams = { params: Promise<{ id: string; rid: string }> }; + +export async function GET(_request: Request, { params }: RouteParams) { + try { + const { id, rid } = await params; + const space = await getResearchSpace(id, rid); + if (!space) return NextResponse.json({ error: "Research space not found" }, { status: 404 }); + return NextResponse.json(space); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to read research space"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} + +export async function PUT(request: Request, { params }: RouteParams) { + try { + const { id, rid } = await params; + const body = await request.json().catch(() => ({})); + const parsed = SaveResearchSpaceSchema.safeParse(body); + const fallbackParsed = ResearchSpaceDataSchema.safeParse(body); + if (!parsed.success && !fallbackParsed.success) { + return NextResponse.json({ error: parsed.error?.issues[0]?.message ?? "Invalid save payload" }, { status: 400 }); + } + const data = parsed.success ? parsed.data.data : fallbackParsed.success ? fallbackParsed.data : null; + if (!data) { + return NextResponse.json({ error: "Invalid save payload" }, { status: 400 }); + } + const lastModifiedBy = parsed.success ? parsed.data.lastModifiedBy : "anonymous"; + const space = await saveResearchSpace(id, rid, data, lastModifiedBy); + if (!space) return NextResponse.json({ error: "Research space not found" }, { status: 404 }); + return NextResponse.json({ saved: true, space }); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to save research space"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} + +export async function PATCH(request: Request, { params }: RouteParams) { + try { + const { id, rid } = await params; + const parsed = UpdateResearchSpaceMetaSchema.safeParse(await request.json().catch(() => ({}))); + if (!parsed.success) { + return NextResponse.json({ error: parsed.error.issues[0]?.message ?? "Invalid update payload" }, { status: 400 }); + } + const space = await updateResearchSpaceMeta(id, rid, parsed.data); + if (!space) return NextResponse.json({ error: "Research space not found" }, { status: 404 }); + return NextResponse.json({ space }); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to update research space"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} + +export async function DELETE(_request: Request, { params }: RouteParams) { + try { + const { id, rid } = await params; + const deleted = await deleteResearchSpace(id, rid); + if (!deleted) return NextResponse.json({ error: "Research space not found" }, { status: 404 }); + return new NextResponse(null, { status: 204 }); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to delete research space"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/workspaces/[id]/research-spaces/[rid]/synthesize/route.ts b/src/app/api/workspaces/[id]/research-spaces/[rid]/synthesize/route.ts new file mode 100644 index 0000000..e2f95ee --- /dev/null +++ b/src/app/api/workspaces/[id]/research-spaces/[rid]/synthesize/route.ts @@ -0,0 +1,24 @@ +import { NextResponse } from "next/server"; +import { synthesizeResearch } from "@/lib/research/ai"; +import { getResearchSpace, saveResearchSpace } from "@/lib/research/server"; + +export const dynamic = "force-dynamic"; + +type RouteParams = { params: Promise<{ id: string; rid: string }> }; + +export async function POST(request: Request, { params }: RouteParams) { + try { + const { id, rid } = await params; + const space = await getResearchSpace(id, rid); + if (!space) return NextResponse.json({ error: "Research space not found" }, { status: 404 }); + const body = await request.json().catch(() => ({})); + const selectedIds = Array.isArray(body.blockIds) ? new Set(body.blockIds) : null; + const blocks = selectedIds ? space.blocks.filter((block) => selectedIds.has(block.id)) : space.blocks; + const synthesis = synthesizeResearch(blocks, body.title ?? "Research Synthesis", body.createdBy ?? "research"); + const saved = await saveResearchSpace(id, rid, { ...space, syntheses: [synthesis, ...space.syntheses] }, body.createdBy ?? "research"); + return NextResponse.json({ synthesis, space: saved }); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to synthesize research"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/workspaces/[id]/research-spaces/route.ts b/src/app/api/workspaces/[id]/research-spaces/route.ts new file mode 100644 index 0000000..a1f3e0c --- /dev/null +++ b/src/app/api/workspaces/[id]/research-spaces/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from "next/server"; +import { CreateResearchSpaceSchema } from "@/lib/research/schemas"; +import { createResearchSpace, listResearchSpaces } from "@/lib/research/server"; + +export const dynamic = "force-dynamic"; + +type RouteParams = { params: Promise<{ id: string }> }; + +export async function GET(_request: Request, { params }: RouteParams) { + try { + const { id } = await params; + const spaces = await listResearchSpaces(id); + if (!spaces) return NextResponse.json({ error: "Workspace not found" }, { status: 404 }); + return NextResponse.json({ spaces }); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to list research spaces"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} + +export async function POST(request: Request, { params }: RouteParams) { + try { + const { id } = await params; + const parsed = CreateResearchSpaceSchema.safeParse(await request.json().catch(() => ({}))); + if (!parsed.success) { + return NextResponse.json({ error: parsed.error.issues[0]?.message ?? "Invalid research space payload" }, { status: 400 }); + } + const space = await createResearchSpace(id, parsed.data); + if (!space) return NextResponse.json({ error: "Workspace not found" }, { status: 404 }); + return NextResponse.json({ space }, { status: 201 }); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to create research space"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/workspace/[id]/research/page.tsx b/src/app/workspace/[id]/research/page.tsx new file mode 100644 index 0000000..86d727d --- /dev/null +++ b/src/app/workspace/[id]/research/page.tsx @@ -0,0 +1,8 @@ +import { ResearchPage } from "@/components/research/research-page"; + +export const dynamic = "force-dynamic"; + +export default async function WorkspaceResearchRoute({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + return ; +} diff --git a/src/components/research/command-input.tsx b/src/components/research/command-input.tsx new file mode 100644 index 0000000..27f8349 --- /dev/null +++ b/src/components/research/command-input.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { useState } from "react"; +import { Send } from "lucide-react"; + +export function CommandInput({ onSubmit }: { onSubmit: (text: string) => void }) { + const [value, setValue] = useState(""); + return ( +
{ event.preventDefault(); const text = value.trim(); if (text) { onSubmit(text); setValue(""); } }} className="border-t border-zinc-800 bg-zinc-950 p-4"> +
+ setValue(event.target.value)} placeholder="Add a tile, question, source URL, quote, or task…" className="flex-1 bg-transparent text-sm text-zinc-100 outline-none placeholder:text-zinc-600" /> + +
+
+ ); +} diff --git a/src/components/research/import-export-menu.tsx b/src/components/research/import-export-menu.tsx new file mode 100644 index 0000000..6be5f0e --- /dev/null +++ b/src/components/research/import-export-menu.tsx @@ -0,0 +1,10 @@ +"use client"; + +export function ImportExportMenu() { + return ( +
+

Import / Export

+

Use the action bar to export .nodepad and markdown. Malformed imports are rejected by API helpers.

+
+ ); +} diff --git a/src/components/research/promote-menu.tsx b/src/components/research/promote-menu.tsx new file mode 100644 index 0000000..3e21f01 --- /dev/null +++ b/src/components/research/promote-menu.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { promoteResearchClient } from "@/lib/research/client"; +import type { ResearchSpaceData } from "@/lib/research/types"; + +export function PromoteMenu({ workspaceId, space }: { workspaceId: string; space: ResearchSpaceData | null }) { + const promote = async (target: "workspace" | "personal") => { + if (!space) return; + try { + await promoteResearchClient(workspaceId, space.id, { target, blockIds: space.selectedBlockIds }); + window.alert(`Promoted to ${target === "workspace" ? "Workspace Brain" : "Personal Brain"}`); + } catch (error) { + window.alert(error instanceof Error ? error.message : "Promotion failed"); + } + }; + return ( +
+ + +
+ ); +} diff --git a/src/components/research/research-page.tsx b/src/components/research/research-page.tsx new file mode 100644 index 0000000..ab790d0 --- /dev/null +++ b/src/components/research/research-page.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useEffect } from "react"; +import { ArrowLeft, Download, FileText } from "lucide-react"; +import Link from "next/link"; +import { getResearchSpaceClient, saveResearchSpaceClient } from "@/lib/research/client"; +import { exportResearchMarkdown } from "@/lib/research/markdown-export"; +import { serializeNodepad } from "@/lib/research/nodepad-format"; +import type { ResearchTemplateId } from "@/lib/research/types"; +import { useResearchStore } from "@/store/research-store"; +import { useResearchSpaces } from "@/hooks/use-research-spaces"; +import { useResearchAutosave } from "@/hooks/use-research-autosave"; +import { useResearchCollaboration } from "@/hooks/use-research-collaboration"; +import { CommandInput } from "./command-input"; +import { PromoteMenu } from "./promote-menu"; +import { SpaceSidebar } from "./space-sidebar"; +import { StatusBar } from "./status-bar"; +import { SynthesisPanel } from "./synthesis-panel"; +import { TileIndex } from "./tile-index"; +import { GraphView } from "./views/graph-view"; +import { KanbanView } from "./views/kanban-view"; +import { TilingView } from "./views/tiling-view"; + +export function ResearchPage({ workspaceId }: { workspaceId: string }) { + const { spaces, isLoading, error, createSpace, deleteSpace } = useResearchSpaces(workspaceId); + const { activeSpace, setActiveSpace, viewMode, setViewMode, addBlock, updateBlock, deleteBlock, selectedBlockIds, toggleSelected, aiStatus } = useResearchStore(); + useResearchAutosave(workspaceId, activeSpace); + const collab = useResearchCollaboration(workspaceId, activeSpace, setActiveSpace); + + useEffect(() => { + if (!activeSpace && spaces[0]) void getResearchSpaceClient(workspaceId, spaces[0].id).then(setActiveSpace).catch(() => undefined); + }, [activeSpace, spaces, workspaceId, setActiveSpace]); + + const openSpace = async (id: string) => setActiveSpace(await getResearchSpaceClient(workspaceId, id)); + const createBlank = async () => setActiveSpace(await createSpace("Untitled Research Space")); + const createTemplate = async (templateId: ResearchTemplateId) => setActiveSpace(await createSpace(templateId.replaceAll("-", " "), templateId)); + const saveNow = async () => { if (activeSpace) setActiveSpace(await saveResearchSpaceClient(workspaceId, activeSpace)); }; + const copyMarkdown = async () => { if (activeSpace) await navigator.clipboard?.writeText(exportResearchMarkdown(activeSpace)); }; + const downloadText = (name: string, content: string) => { + const url = URL.createObjectURL(new Blob([content], { type: "text/plain" })); + const a = document.createElement("a"); a.href = url; a.download = name; a.click(); URL.revokeObjectURL(url); + }; + + return ( +
+ +
+ void openSpace(id)} onCreate={() => void createBlank()} onCreateTemplate={(id) => void createTemplate(id)} onDelete={(id) => void deleteSpace(id)} /> +
+
+

{activeSpace?.name ?? "Workspace Research"}

AI not connected β€” local-first notes, collaboration, import/export, and Brain promotion remain available.

+
+
+
+ {isLoading &&

Loading research spaces…

} + {error &&

{error}

} + {!isLoading && !activeSpace &&
No spaces yet. Create a blank space or choose a planning template.
} + {activeSpace && viewMode === "tiling" && } + {activeSpace && viewMode === "kanban" && } + {activeSpace && viewMode === "graph" && } +
+ {activeSpace && } + +
+ {activeSpace && } +
+
+ ); +} diff --git a/src/components/research/space-sidebar.tsx b/src/components/research/space-sidebar.tsx new file mode 100644 index 0000000..896ada8 --- /dev/null +++ b/src/components/research/space-sidebar.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { Plus, Trash2 } from "lucide-react"; +import type { ResearchSpaceRecord, ResearchTemplateId } from "@/lib/research/types"; +import { TemplatePicker } from "./template-picker"; +import { ImportExportMenu } from "./import-export-menu"; + +export function SpaceSidebar({ spaces, activeId, onSelect, onCreate, onCreateTemplate, onDelete }: { spaces: ResearchSpaceRecord[]; activeId?: string; onSelect: (id: string) => void; onCreate: () => void; onCreateTemplate: (id: ResearchTemplateId) => void; onDelete: (id: string) => void }) { + return ( + + ); +} diff --git a/src/components/research/status-bar.tsx b/src/components/research/status-bar.tsx new file mode 100644 index 0000000..9ac09f4 --- /dev/null +++ b/src/components/research/status-bar.tsx @@ -0,0 +1,17 @@ +"use client"; + +import type { ResearchViewMode } from "@/lib/research/types"; + +export function StatusBar({ workspaceId, spaceName, syncStatus, aiStatus, viewMode, onViewMode }: { workspaceId: string; spaceName: string; syncStatus: string; aiStatus: string; viewMode: ResearchViewMode; onViewMode: (mode: ResearchViewMode) => void }) { + return ( +
+
Workspace {workspaceId}/{spaceName}
+
+ Sync: {syncStatus}AI: {aiStatus === "error" ? "AI not connected" : aiStatus} +
+ {(["tiling", "kanban", "graph"] as ResearchViewMode[]).map((mode) => )} +
+
+
+ ); +} diff --git a/src/components/research/synthesis-panel.tsx b/src/components/research/synthesis-panel.tsx new file mode 100644 index 0000000..1cda1ba --- /dev/null +++ b/src/components/research/synthesis-panel.tsx @@ -0,0 +1,17 @@ +"use client"; + +import type { ResearchSynthesis } from "@/lib/research/types"; + +export function SynthesisPanel({ syntheses }: { syntheses: ResearchSynthesis[] }) { + return ( +
+

Synthesis

+ {syntheses.length === 0 ?

No synthesis generated yet.

: syntheses.map((item) => ( +
+

{item.title}

+
{item.content}
+
+ ))} +
+ ); +} diff --git a/src/components/research/template-picker.tsx b/src/components/research/template-picker.tsx new file mode 100644 index 0000000..78f7448 --- /dev/null +++ b/src/components/research/template-picker.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { RESEARCH_TEMPLATE_IDS, getResearchTemplateName } from "@/lib/research/templates"; +import type { ResearchTemplateId } from "@/lib/research/types"; + +export function TemplatePicker({ onCreate }: { onCreate: (templateId: ResearchTemplateId) => void }) { + return ( +
+

Planning templates

+ {RESEARCH_TEMPLATE_IDS.map((id) => )} +
+ ); +} diff --git a/src/components/research/tile-card.tsx b/src/components/research/tile-card.tsx new file mode 100644 index 0000000..72ee4f3 --- /dev/null +++ b/src/components/research/tile-card.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { AlertCircle, Pin, RefreshCw, Trash2 } from "lucide-react"; +import type { ResearchBlock } from "@/lib/research/types"; + +interface TileCardProps { + block: ResearchBlock; + selected?: boolean; + onChange: (patch: Partial) => void; + onDelete: () => void; + onSelect: () => void; +} + +export function TileCard({ block, selected, onChange, onDelete, onSelect }: TileCardProps) { + return ( +
+
+ +
+ + +
+
+