Tiger's Macbook Air commited on
Commit
3f76ff4
·
1 Parent(s): 4015524

Build agentic PM demo app

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +4 -0
  2. .gitignore +46 -0
  3. AGENTS.md +29 -0
  4. Dockerfile +16 -0
  5. README.md +31 -2
  6. agentic_pm_demo_codex_plans/.gitignore +6 -0
  7. agentic_pm_demo_codex_plans/AGENTS.md +57 -0
  8. agentic_pm_demo_codex_plans/README.md +61 -0
  9. agentic_pm_demo_codex_plans/app/globals.css +1007 -0
  10. agentic_pm_demo_codex_plans/app/layout.tsx +19 -0
  11. agentic_pm_demo_codex_plans/app/page.tsx +5 -0
  12. agentic_pm_demo_codex_plans/components/AgenticPmWorkbench.tsx +826 -0
  13. agentic_pm_demo_codex_plans/data/work-packages.seed.json +338 -0
  14. agentic_pm_demo_codex_plans/eslint.config.mjs +11 -0
  15. agentic_pm_demo_codex_plans/lib/command-parser.ts +86 -0
  16. agentic_pm_demo_codex_plans/lib/mock-agent.ts +177 -0
  17. agentic_pm_demo_codex_plans/lib/work-package-types.ts +105 -0
  18. agentic_pm_demo_codex_plans/next.config.ts +10 -0
  19. agentic_pm_demo_codex_plans/package-lock.json +0 -0
  20. agentic_pm_demo_codex_plans/package.json +24 -0
  21. agentic_pm_demo_codex_plans/plans/01-product-vision-and-scope.md +59 -0
  22. agentic_pm_demo_codex_plans/plans/02-ux-layout-and-interaction.md +110 -0
  23. agentic_pm_demo_codex_plans/plans/03-work-package-data-model.md +124 -0
  24. agentic_pm_demo_codex_plans/plans/04-work-package-catalog.md +298 -0
  25. agentic_pm_demo_codex_plans/plans/05-reference-command-and-execution.md +108 -0
  26. agentic_pm_demo_codex_plans/plans/06-llm-api-and-prompt-contract.md +87 -0
  27. agentic_pm_demo_codex_plans/plans/07-technical-architecture-and-file-structure.md +100 -0
  28. agentic_pm_demo_codex_plans/plans/08-local-run-and-huggingface-deployment.md +83 -0
  29. agentic_pm_demo_codex_plans/prompts/work-package-system-prompt.md +103 -0
  30. agentic_pm_demo_codex_plans/tsconfig.json +33 -0
  31. app/api/chat/route.ts +361 -0
  32. app/api/test-connection/route.ts +61 -0
  33. app/favicon.ico +0 -0
  34. app/globals.css +204 -0
  35. app/layout.tsx +36 -0
  36. app/page.tsx +5 -0
  37. components.json +25 -0
  38. components/AgentLogPanel.tsx +73 -0
  39. components/AppShell.tsx +629 -0
  40. components/ChatPanel.test.tsx +33 -0
  41. components/ChatPanel.tsx +258 -0
  42. components/MarkdownContent.tsx +14 -0
  43. components/WorkPackageBoard.tsx +108 -0
  44. components/WorkPackageCard.tsx +228 -0
  45. components/WorkPackageDetail.tsx +300 -0
  46. components/WorkPackageOutput.tsx +66 -0
  47. components/ui/badge.tsx +49 -0
  48. components/ui/button.tsx +67 -0
  49. components/ui/card.tsx +103 -0
  50. components/ui/dropdown-menu.tsx +269 -0
.env.example ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ LLM_API_BASE_URL=https://api.openai.com/v1
2
+ LLM_API_KEY=
3
+ LLM_MODEL=gpt-4.1-mini
4
+
.gitignore ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+ !.env.example
36
+
37
+ # local tool/cache state
38
+ .npm-cache/
39
+ .claude/
40
+
41
+ # vercel
42
+ .vercel
43
+
44
+ # typescript
45
+ *.tsbuildinfo
46
+ next-env.d.ts
AGENTS.md ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+ <!-- END:nextjs-agent-rules -->
6
+
7
+ # Agentic PM Demo (Codex)
8
+
9
+ ## Product
10
+
11
+ Build an independent web demo called **Agentic PM Demo** (not a Codex plugin).
12
+
13
+ The UI is split:
14
+
15
+ - Left: chat workspace
16
+ - Right: structured IoT product-development work packages
17
+
18
+ ## MVP Rules
19
+
20
+ 1. Seed work packages from `agentic_pm_demo_codex_plans/data/work-packages.seed.json`.
21
+ 2. Support commands:
22
+ - `@WorkPackageName ask ...`
23
+ - `@WorkPackageName plan ...`
24
+ - `@WorkPackageName change ...`
25
+ - `@WorkPackageName execute ...`
26
+ 3. All `execute` tasks are simulated.
27
+ 4. Every simulated output must include this disclaimer verbatim:
28
+
29
+ > This is a simulated execution result generated for demo purposes. No real external tool, engineering review, certification approval, user database, image generation service, patent search, or test system was executed.
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package*.json ./
6
+ RUN npm ci
7
+
8
+ COPY . .
9
+
10
+ RUN npm run build
11
+
12
+ EXPOSE 3000
13
+
14
+ ENV PORT=3000
15
+ CMD ["npm", "start"]
16
+
README.md CHANGED
@@ -6,7 +6,36 @@ colorTo: gray
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
- short_description: work with Agent to co-develope Hardware products
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
+ short_description: Agentic PM demo for hardware product development
10
  ---
11
 
12
+ # Agentic PM Demo
13
+
14
+ Two-column demo app:
15
+
16
+ - Left: chat with an AI assistant
17
+ - Right: structured IoT product-development work packages
18
+
19
+ ## Local run
20
+
21
+ ```bash
22
+ npm install
23
+ npm run dev
24
+ ```
25
+
26
+ Open `http://localhost:3000`.
27
+
28
+ ## Environment
29
+
30
+ Copy `.env.example` to `.env.local` and set:
31
+
32
+ - `LLM_API_BASE_URL` (OpenAI-compatible)
33
+ - `LLM_API_KEY` (optional; app runs in mock mode without it)
34
+ - `LLM_MODEL`
35
+
36
+ ## Docker (Hugging Face Spaces)
37
+
38
+ ```bash
39
+ docker build -t agentic-pm-demo .
40
+ docker run --rm -p 3000:3000 agentic-pm-demo
41
+ ```
agentic_pm_demo_codex_plans/.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .next
2
+ node_modules
3
+ out
4
+ .env*.local
5
+ .npm-cache
6
+ npm-debug.log*
agentic_pm_demo_codex_plans/AGENTS.md ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AGENTS.md — Codex Instructions
2
+
3
+ ## Project
4
+
5
+ Build an independent web demo called **Agentic PM Demo**.
6
+
7
+ The application demonstrates an AI-native product management workflow for IoT product development.
8
+
9
+ ## Development Principles
10
+
11
+ 1. Keep the MVP small and working.
12
+ 2. Build UI first with mock data.
13
+ 3. Add LLM integration only after the local UI works.
14
+ 4. All tool execution must be simulated in MVP.
15
+ 5. Never imply simulated execution is real.
16
+ 6. Keep data models explicit and typed.
17
+ 7. Use OpenAI-compatible API format.
18
+ 8. Make the app deployable to Hugging Face Spaces with Docker.
19
+
20
+ ## Required App Features
21
+
22
+ 1. Two-column layout: left chat, right work package board.
23
+ 2. Seed work packages from `data/work-packages.seed.json`.
24
+ 3. Support commands:
25
+ - `@WorkPackageName ask ...`
26
+ - `@WorkPackageName plan ...`
27
+ - `@WorkPackageName change ...`
28
+ - `@WorkPackageName execute ...`
29
+ 4. For `ask`, do not change the board.
30
+ 5. For `plan`, update tasks or next steps.
31
+ 6. For `change`, update work package fields.
32
+ 7. For `execute`, generate simulated outputs and append them to the selected package.
33
+ 8. Every simulated output must show the disclaimer.
34
+
35
+ ## Recommended Implementation Order
36
+
37
+ 1. Create Next.js project scaffold.
38
+ 2. Add static two-column layout.
39
+ 3. Add seed work package board.
40
+ 4. Add chat input and message history.
41
+ 5. Add command parser.
42
+ 6. Add local mock agent fallback.
43
+ 7. Add LLM API route.
44
+ 8. Add prompt contract and structured JSON parsing.
45
+ 9. Add Dockerfile for Hugging Face Spaces.
46
+ 10. Polish UX.
47
+
48
+ ## Avoid in MVP
49
+
50
+ - No login.
51
+ - No database.
52
+ - No real external tool execution.
53
+ - No real patent search.
54
+ - No real certification validation.
55
+ - No real user-data reading.
56
+ - No real image generation.
57
+ - No complex drag-and-drop kanban.
agentic_pm_demo_codex_plans/README.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agentic PM Demo — Codex Development Pack
2
+
3
+ This pack contains Codex-ready planning documents for an independent **Agentic PM Demo** web app.
4
+
5
+ ## Product Direction
6
+
7
+ Build an independent web demo, not a Codex plugin. Codex is used as the development assistant; the product itself is a web app.
8
+
9
+ ## Demo Concept
10
+
11
+ A user chats with an AI assistant on the left side. The right side shows structured product-development work packages. The user can reference a work package and ask the AI to `ask`, `plan`, `change`, or `execute` its contents.
12
+
13
+ ## MVP Rule
14
+
15
+ All `execute` tasks are simulated. The app must clearly label all execution outputs as fake/simulated.
16
+
17
+ Default disclaimer:
18
+
19
+ > This is a simulated execution result generated for demo purposes. No real external tool, engineering review, certification approval, user database, image generation service, patent search, or test system was executed.
20
+
21
+ ## Recommended Stack
22
+
23
+ - Next.js
24
+ - React
25
+ - TypeScript
26
+ - Tailwind CSS
27
+ - shadcn/ui
28
+ - OpenAI-compatible LLM API
29
+ - Docker for Hugging Face Spaces
30
+
31
+ ## Documents
32
+
33
+ - `AGENTS.md`
34
+ - `plans/01-product-vision-and-scope.md`
35
+ - `plans/02-ux-layout-and-interaction.md`
36
+ - `plans/03-work-package-data-model.md`
37
+ - `plans/04-work-package-catalog.md`
38
+ - `plans/05-reference-command-and-execution.md`
39
+ - `plans/06-llm-api-and-prompt-contract.md`
40
+ - `plans/07-technical-architecture-and-file-structure.md`
41
+ - `plans/08-local-run-and-huggingface-deployment.md`
42
+ - `prompts/work-package-system-prompt.md`
43
+ - `data/work-packages.seed.json`
44
+
45
+ ## Local UI Prototype
46
+
47
+ This folder now also contains a small Next.js demo UI for the Agentic PM workspace.
48
+
49
+ ```bash
50
+ npm install
51
+ npm run dev
52
+ ```
53
+
54
+ Open `http://localhost:3000` and try:
55
+
56
+ ```text
57
+ @SRS plan Break this into verification tasks.
58
+ @Design FMEA execute Generate a risk table for a tightening-quality IoT product.
59
+ ```
60
+
61
+ The UX pattern is intentionally Cursor/Codex-like: chat stays on the left, work packages stay on the right, and every package section can be cited into the composer with `@Package#Context` references.
agentic_pm_demo_codex_plans/app/globals.css ADDED
@@ -0,0 +1,1007 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --page-bg: #f6f9fd;
3
+ --page-tint: rgba(85, 132, 214, 0.08);
4
+ --surface: rgba(255, 255, 255, 0.96);
5
+ --surface-strong: #ffffff;
6
+ --surface-muted: #eef4fb;
7
+ --surface-accent: #f4f8fd;
8
+ --ink: #18202b;
9
+ --muted: #637182;
10
+ --line: rgba(24, 32, 43, 0.1);
11
+ --line-strong: rgba(24, 32, 43, 0.2);
12
+ --accent: #3d6fd6;
13
+ --accent-soft: rgba(61, 111, 214, 0.12);
14
+ --success: #2f7a56;
15
+ --warning: #b97b18;
16
+ --shadow: 0 16px 36px rgba(42, 73, 122, 0.08);
17
+ --font-sans: "Avenir Next", "IBM Plex Sans", "Segoe UI", sans-serif;
18
+ --font-display: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", serif;
19
+ }
20
+
21
+ * {
22
+ box-sizing: border-box;
23
+ }
24
+
25
+ html,
26
+ body {
27
+ height: 100%;
28
+ min-height: 100%;
29
+ overflow: hidden;
30
+ }
31
+
32
+ body {
33
+ margin: 0;
34
+ background:
35
+ radial-gradient(circle at top left, rgba(255, 255, 255, 0.9), transparent 28rem),
36
+ linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(246, 249, 253, 0.98)),
37
+ var(--page-bg);
38
+ color: var(--ink);
39
+ font-family: var(--font-sans);
40
+ overscroll-behavior: none;
41
+ }
42
+
43
+ body::before {
44
+ background:
45
+ linear-gradient(120deg, transparent 0%, var(--page-tint) 42%, transparent 72%),
46
+ repeating-linear-gradient(
47
+ 90deg,
48
+ transparent 0,
49
+ transparent 23px,
50
+ rgba(31, 27, 22, 0.018) 23px,
51
+ rgba(31, 27, 22, 0.018) 24px
52
+ );
53
+ content: "";
54
+ inset: 0;
55
+ pointer-events: none;
56
+ position: fixed;
57
+ }
58
+
59
+ button,
60
+ textarea {
61
+ font: inherit;
62
+ }
63
+
64
+ button {
65
+ cursor: pointer;
66
+ }
67
+
68
+ svg {
69
+ display: block;
70
+ flex: 0 0 auto;
71
+ height: 1rem;
72
+ width: 1rem;
73
+ }
74
+
75
+ .pm-shell {
76
+ display: grid;
77
+ gap: 12px;
78
+ grid-template-columns: minmax(280px, 320px) minmax(0, 1fr);
79
+ height: 100vh;
80
+ min-height: 760px;
81
+ overflow: hidden;
82
+ padding: 12px;
83
+ position: relative;
84
+ }
85
+
86
+ .chat-rail,
87
+ .workspace-canvas,
88
+ .agent-log-zone {
89
+ backdrop-filter: blur(14px);
90
+ background: var(--surface);
91
+ border: 1px solid var(--line);
92
+ box-shadow: var(--shadow);
93
+ }
94
+
95
+ .chat-rail {
96
+ border-radius: 12px;
97
+ display: grid;
98
+ grid-template-rows: auto minmax(0, 1fr) auto;
99
+ min-height: 0;
100
+ overflow: hidden;
101
+ }
102
+
103
+ .project-strip {
104
+ align-items: center;
105
+ border-bottom: 1px solid var(--line);
106
+ display: grid;
107
+ gap: 12px;
108
+ grid-template-columns: 52px 1fr;
109
+ padding: 12px;
110
+ }
111
+
112
+ .project-mark {
113
+ align-items: center;
114
+ background: linear-gradient(145deg, #ffffff, #eef4fb);
115
+ border: 1px solid var(--line);
116
+ border-radius: 8px;
117
+ color: var(--accent);
118
+ display: flex;
119
+ height: 52px;
120
+ justify-content: center;
121
+ }
122
+
123
+ .project-meta p,
124
+ .workspace-title p,
125
+ .panel-kicker span,
126
+ .composer-topline span:last-child,
127
+ .agent-log-header p,
128
+ .phase-column header small,
129
+ .context-section-header span,
130
+ .stat-card span,
131
+ .agent-summary-card span,
132
+ .agent-summary-card small,
133
+ .empty-copy,
134
+ .chat-bubble strong {
135
+ color: var(--muted);
136
+ }
137
+
138
+ .project-meta p {
139
+ font-size: 0.72rem;
140
+ letter-spacing: 0.12em;
141
+ margin: 0 0 4px;
142
+ text-transform: uppercase;
143
+ }
144
+
145
+ .project-meta h1,
146
+ .workspace-title h2,
147
+ .detail-hero-copy h1,
148
+ .context-section h3,
149
+ .agent-log-header h2 {
150
+ font-family: var(--font-display);
151
+ font-weight: 600;
152
+ letter-spacing: -0.02em;
153
+ margin: 0;
154
+ }
155
+
156
+ .project-meta h1 {
157
+ font-size: 1.1rem;
158
+ }
159
+
160
+ .project-meta span {
161
+ color: var(--muted);
162
+ display: block;
163
+ font-size: 0.88rem;
164
+ margin-top: 2px;
165
+ }
166
+
167
+ .chat-history {
168
+ display: flex;
169
+ flex-direction: column;
170
+ min-height: 0;
171
+ }
172
+
173
+ .panel-kicker {
174
+ align-items: center;
175
+ border-bottom: 1px solid var(--line);
176
+ display: flex;
177
+ gap: 8px;
178
+ padding: 10px 14px;
179
+ }
180
+
181
+ .panel-kicker span {
182
+ font-size: 0.82rem;
183
+ font-weight: 600;
184
+ }
185
+
186
+ .message-stack {
187
+ display: flex;
188
+ flex: 1;
189
+ flex-direction: column;
190
+ gap: 10px;
191
+ overflow: auto;
192
+ padding: 12px;
193
+ }
194
+
195
+ .chat-bubble {
196
+ background: rgba(255, 255, 255, 0.84);
197
+ border: 1px solid var(--line);
198
+ border-radius: 8px;
199
+ max-width: 92%;
200
+ padding: 10px 11px;
201
+ }
202
+
203
+ .chat-bubble.user {
204
+ align-self: flex-end;
205
+ background: rgba(61, 111, 214, 0.08);
206
+ border-color: rgba(61, 111, 214, 0.22);
207
+ }
208
+
209
+ .chat-bubble strong {
210
+ display: block;
211
+ font-size: 0.72rem;
212
+ margin-bottom: 4px;
213
+ text-transform: uppercase;
214
+ }
215
+
216
+ .chat-bubble p {
217
+ font-size: 0.92rem;
218
+ line-height: 1.45;
219
+ margin: 0;
220
+ white-space: pre-wrap;
221
+ }
222
+
223
+ .chat-input-zone {
224
+ border-top: 1px solid var(--line);
225
+ padding: 12px;
226
+ }
227
+
228
+ .composer-topline,
229
+ .quick-action-row,
230
+ .reference-row,
231
+ .input-toolbar,
232
+ .workspace-tabs,
233
+ .workspace-title,
234
+ .package-card-topline,
235
+ .package-card-actions,
236
+ .floating-cite-bar,
237
+ .detail-hero-actions,
238
+ .simple-tags,
239
+ .simple-task,
240
+ .context-section-header,
241
+ .simple-output-header,
242
+ .agent-log-header,
243
+ .agent-log-content {
244
+ align-items: center;
245
+ display: flex;
246
+ gap: 8px;
247
+ }
248
+
249
+ .composer-topline {
250
+ justify-content: space-between;
251
+ margin-bottom: 8px;
252
+ }
253
+
254
+ .composer-topline span {
255
+ font-size: 0.8rem;
256
+ }
257
+
258
+ .reference-row {
259
+ flex-wrap: wrap;
260
+ margin-bottom: 8px;
261
+ }
262
+
263
+ .reference-chip {
264
+ background: var(--surface-strong);
265
+ border: 1px solid var(--line);
266
+ border-radius: 999px;
267
+ color: var(--ink);
268
+ font-size: 0.75rem;
269
+ padding: 6px 9px;
270
+ }
271
+
272
+ .reference-chip.removable {
273
+ color: #8f5530;
274
+ }
275
+
276
+ .quick-action-row {
277
+ flex-wrap: wrap;
278
+ margin-bottom: 10px;
279
+ }
280
+
281
+ .quick-action-button,
282
+ .workspace-tabs button,
283
+ .phase-nav-button,
284
+ .context-section-header button,
285
+ .detail-hero-actions button,
286
+ .floating-cite-bar button,
287
+ .selection-cite button,
288
+ .back-button,
289
+ .send-button,
290
+ .ghost-button {
291
+ align-items: center;
292
+ background: var(--surface-strong);
293
+ border: 1px solid var(--line);
294
+ border-radius: 8px;
295
+ color: var(--ink);
296
+ display: inline-flex;
297
+ gap: 8px;
298
+ justify-content: center;
299
+ padding: 8px 12px;
300
+ transition:
301
+ transform 160ms ease,
302
+ border-color 160ms ease,
303
+ background-color 160ms ease;
304
+ }
305
+
306
+ .quick-action-button:hover,
307
+ .workspace-tabs button:hover,
308
+ .phase-nav-button:hover,
309
+ .context-section-header button:hover,
310
+ .detail-hero-actions button:hover,
311
+ .floating-cite-bar button:hover,
312
+ .selection-cite button:hover,
313
+ .back-button:hover,
314
+ .send-button:hover,
315
+ .ghost-button:hover,
316
+ .package-card:hover {
317
+ border-color: rgba(61, 111, 214, 0.34);
318
+ transform: translateY(-1px);
319
+ }
320
+
321
+ .quick-action-button {
322
+ font-size: 0.8rem;
323
+ padding: 7px 10px;
324
+ }
325
+
326
+ .quick-action-button svg,
327
+ .detail-hero-actions button svg,
328
+ .context-section-header button svg,
329
+ .floating-cite-bar button svg,
330
+ .selection-cite button svg {
331
+ color: var(--accent);
332
+ }
333
+
334
+ .composer-wrap {
335
+ position: relative;
336
+ }
337
+
338
+ textarea {
339
+ background: rgba(255, 255, 255, 0.82);
340
+ border: 1px solid var(--line);
341
+ border-radius: 8px;
342
+ color: var(--ink);
343
+ min-height: 124px;
344
+ outline: 0;
345
+ padding: 12px 14px;
346
+ resize: none;
347
+ width: 100%;
348
+ }
349
+
350
+ textarea::placeholder {
351
+ color: #8b9ab0;
352
+ }
353
+
354
+ textarea:focus {
355
+ border-color: rgba(61, 111, 214, 0.32);
356
+ box-shadow: 0 0 0 4px rgba(61, 111, 214, 0.08);
357
+ }
358
+
359
+ .mention-popover {
360
+ background: var(--surface-strong);
361
+ border: 1px solid var(--line);
362
+ border-radius: 8px;
363
+ bottom: calc(100% + 8px);
364
+ box-shadow: var(--shadow);
365
+ display: grid;
366
+ gap: 5px;
367
+ left: 0;
368
+ padding: 8px;
369
+ position: absolute;
370
+ width: 100%;
371
+ z-index: 3;
372
+ }
373
+
374
+ .mention-popover button {
375
+ background: transparent;
376
+ border: 0;
377
+ border-radius: 6px;
378
+ display: grid;
379
+ gap: 2px;
380
+ padding: 8px;
381
+ text-align: left;
382
+ }
383
+
384
+ .mention-popover button:hover {
385
+ background: var(--surface-muted);
386
+ }
387
+
388
+ .mention-popover span {
389
+ font-weight: 700;
390
+ }
391
+
392
+ .mention-popover small {
393
+ color: var(--muted);
394
+ }
395
+
396
+ .input-toolbar {
397
+ justify-content: space-between;
398
+ margin-top: 10px;
399
+ }
400
+
401
+ .ghost-button,
402
+ .send-button {
403
+ border-radius: 8px;
404
+ height: 38px;
405
+ padding: 0;
406
+ width: 38px;
407
+ }
408
+
409
+ .ghost-button svg,
410
+ .send-button svg {
411
+ height: 0.95rem;
412
+ width: 0.95rem;
413
+ }
414
+
415
+ .send-button {
416
+ background: var(--accent);
417
+ border-color: var(--accent);
418
+ color: #f8fbff;
419
+ }
420
+
421
+ .main-workspace {
422
+ display: grid;
423
+ gap: 14px;
424
+ grid-template-rows: minmax(0, 1fr) 164px;
425
+ min-width: 0;
426
+ }
427
+
428
+ .workspace-canvas {
429
+ border-radius: 12px;
430
+ display: grid;
431
+ grid-template-rows: auto minmax(0, 1fr);
432
+ min-height: 0;
433
+ overflow: hidden;
434
+ position: relative;
435
+ }
436
+
437
+ .workspace-toolbar {
438
+ align-items: center;
439
+ border-bottom: 1px solid var(--line);
440
+ display: flex;
441
+ justify-content: space-between;
442
+ padding: 12px 16px;
443
+ }
444
+
445
+ .workspace-title svg,
446
+ .panel-kicker svg,
447
+ .agent-log-header svg {
448
+ color: var(--accent);
449
+ }
450
+
451
+ .workspace-title h2,
452
+ .agent-log-header h2 {
453
+ font-size: 1.05rem;
454
+ }
455
+
456
+ .workspace-title p,
457
+ .agent-log-header p {
458
+ font-size: 0.86rem;
459
+ margin: 2px 0 0;
460
+ }
461
+
462
+ .workspace-tabs button {
463
+ font-size: 0.82rem;
464
+ padding: 8px 11px;
465
+ }
466
+
467
+ .workspace-tabs button.active {
468
+ background: var(--accent-soft);
469
+ border-color: rgba(61, 111, 214, 0.2);
470
+ color: #315aa9;
471
+ }
472
+
473
+ .overview-layout {
474
+ display: grid;
475
+ grid-template-columns: 170px minmax(0, 1fr);
476
+ min-height: 0;
477
+ overflow: hidden;
478
+ }
479
+
480
+ .phase-rail {
481
+ border-right: 1px solid var(--line);
482
+ display: flex;
483
+ flex-direction: column;
484
+ gap: 8px;
485
+ overflow: hidden;
486
+ padding: 14px;
487
+ }
488
+
489
+ .phase-rail p {
490
+ color: var(--muted);
491
+ font-size: 0.78rem;
492
+ font-weight: 700;
493
+ letter-spacing: 0.08em;
494
+ margin: 0 0 2px;
495
+ text-transform: uppercase;
496
+ }
497
+
498
+ .phase-nav-button {
499
+ justify-content: space-between;
500
+ text-align: left;
501
+ }
502
+
503
+ .phase-nav-button span {
504
+ font-size: 0.88rem;
505
+ }
506
+
507
+ .phase-nav-button small {
508
+ color: var(--muted);
509
+ font-size: 0.76rem;
510
+ }
511
+
512
+ .overview-content {
513
+ display: flex;
514
+ flex-direction: column;
515
+ min-height: 0;
516
+ overflow: auto;
517
+ padding: 12px 14px 58px;
518
+ }
519
+
520
+ .stat-row {
521
+ display: grid;
522
+ gap: 8px;
523
+ grid-template-columns: repeat(3, minmax(0, 1fr));
524
+ margin-bottom: 14px;
525
+ }
526
+
527
+ .stat-card {
528
+ background: linear-gradient(160deg, rgba(255, 255, 255, 0.96), rgba(244, 248, 253, 0.96));
529
+ border: 1px solid var(--line);
530
+ border-radius: 8px;
531
+ padding: 10px 12px;
532
+ }
533
+
534
+ .stat-card span {
535
+ display: block;
536
+ font-size: 0.8rem;
537
+ margin-bottom: 10px;
538
+ }
539
+
540
+ .stat-card strong {
541
+ font-family: var(--font-display);
542
+ font-size: 1.55rem;
543
+ }
544
+
545
+ .phase-map {
546
+ display: grid;
547
+ flex: 1 1 auto;
548
+ gap: 12px;
549
+ }
550
+
551
+ .phase-column header {
552
+ align-items: baseline;
553
+ display: flex;
554
+ justify-content: space-between;
555
+ margin-bottom: 10px;
556
+ }
557
+
558
+ .phase-column header span {
559
+ font-size: 0.82rem;
560
+ font-weight: 700;
561
+ letter-spacing: 0.09em;
562
+ text-transform: uppercase;
563
+ }
564
+
565
+ .package-card-grid {
566
+ display: grid;
567
+ gap: 10px;
568
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
569
+ }
570
+
571
+ .package-card {
572
+ background: linear-gradient(160deg, rgba(255, 255, 255, 0.98), rgba(244, 248, 253, 0.96));
573
+ border: 1px solid var(--line);
574
+ border-radius: 8px;
575
+ padding: 12px;
576
+ text-align: left;
577
+ transition:
578
+ transform 160ms ease,
579
+ border-color 160ms ease,
580
+ box-shadow 160ms ease;
581
+ }
582
+
583
+ .package-card.selected {
584
+ border-color: rgba(61, 111, 214, 0.4);
585
+ box-shadow: inset 0 0 0 1px rgba(61, 111, 214, 0.16);
586
+ }
587
+
588
+ .package-card-topline {
589
+ justify-content: space-between;
590
+ margin-bottom: 10px;
591
+ }
592
+
593
+ .package-short {
594
+ color: #315aa9;
595
+ font-size: 0.78rem;
596
+ font-weight: 700;
597
+ letter-spacing: 0.08em;
598
+ text-transform: uppercase;
599
+ }
600
+
601
+ .status-pill {
602
+ border-radius: 999px;
603
+ font-size: 0.72rem;
604
+ padding: 5px 8px;
605
+ }
606
+
607
+ .status-pill.todo {
608
+ background: #edf3f8;
609
+ color: #59697d;
610
+ }
611
+
612
+ .status-pill.in_progress {
613
+ background: #f5e7c9;
614
+ color: var(--warning);
615
+ }
616
+
617
+ .status-pill.done {
618
+ background: #deefe6;
619
+ color: var(--success);
620
+ }
621
+
622
+ .package-card h3 {
623
+ font-size: 1rem;
624
+ margin: 0 0 8px;
625
+ }
626
+
627
+ .package-card p {
628
+ color: var(--muted);
629
+ font-size: 0.84rem;
630
+ line-height: 1.35;
631
+ margin: 0 0 10px;
632
+ }
633
+
634
+ .package-card-actions {
635
+ justify-content: space-between;
636
+ }
637
+
638
+ .package-card-actions > span,
639
+ .package-card-actions button {
640
+ align-items: center;
641
+ display: inline-flex;
642
+ gap: 6px;
643
+ }
644
+
645
+ .package-card-actions > span {
646
+ color: var(--muted);
647
+ font-size: 0.78rem;
648
+ }
649
+
650
+ .package-card-actions button {
651
+ background: transparent;
652
+ border: 0;
653
+ border-radius: 999px;
654
+ color: var(--accent);
655
+ padding: 4px;
656
+ }
657
+
658
+ .floating-cite-bar {
659
+ background: rgba(255, 255, 255, 0.96);
660
+ border-top: 1px solid var(--line);
661
+ inset: auto 0 0 0;
662
+ justify-content: space-between;
663
+ padding: 10px 14px;
664
+ position: absolute;
665
+ }
666
+
667
+ .floating-cite-bar span {
668
+ color: var(--muted);
669
+ font-size: 0.85rem;
670
+ }
671
+
672
+ .floating-cite-bar button {
673
+ padding: 8px 10px;
674
+ }
675
+
676
+ .detail-layout {
677
+ min-height: 0;
678
+ overflow: auto;
679
+ padding: 14px;
680
+ }
681
+
682
+ .detail-hero {
683
+ background: linear-gradient(140deg, rgba(255, 255, 255, 0.98), rgba(244, 248, 253, 0.96));
684
+ border: 1px solid var(--line);
685
+ border-radius: 8px;
686
+ display: grid;
687
+ gap: 18px;
688
+ grid-template-columns: 170px minmax(0, 1fr) auto;
689
+ margin-bottom: 14px;
690
+ padding: 14px;
691
+ }
692
+
693
+ .back-button {
694
+ align-self: start;
695
+ justify-self: start;
696
+ }
697
+
698
+ .detail-hero-copy {
699
+ min-width: 0;
700
+ }
701
+
702
+ .status-label {
703
+ color: #315aa9;
704
+ display: inline-block;
705
+ font-size: 0.76rem;
706
+ font-weight: 700;
707
+ letter-spacing: 0.08em;
708
+ margin-bottom: 10px;
709
+ text-transform: uppercase;
710
+ }
711
+
712
+ .detail-hero-copy h1 {
713
+ font-size: clamp(1.6rem, 2.4vw, 2.4rem);
714
+ margin-bottom: 8px;
715
+ }
716
+
717
+ .detail-hero-copy p {
718
+ color: var(--muted);
719
+ font-size: 1rem;
720
+ line-height: 1.55;
721
+ margin: 0;
722
+ max-width: 48rem;
723
+ }
724
+
725
+ .detail-hero-actions {
726
+ align-items: flex-start;
727
+ flex-direction: column;
728
+ }
729
+
730
+ .detail-hero-actions button {
731
+ justify-content: flex-start;
732
+ width: 132px;
733
+ }
734
+
735
+ .detail-content-grid {
736
+ display: grid;
737
+ gap: 12px;
738
+ grid-template-columns: repeat(2, minmax(0, 1fr));
739
+ }
740
+
741
+ .context-section {
742
+ background: rgba(255, 255, 255, 0.78);
743
+ border: 1px solid var(--line);
744
+ border-radius: 8px;
745
+ padding: 14px;
746
+ }
747
+
748
+ .context-section-header {
749
+ justify-content: space-between;
750
+ margin-bottom: 12px;
751
+ }
752
+
753
+ .context-section h3 {
754
+ font-size: 1.02rem;
755
+ margin-bottom: 3px;
756
+ }
757
+
758
+ .context-section-header span {
759
+ display: block;
760
+ font-size: 0.8rem;
761
+ }
762
+
763
+ .context-section-header button {
764
+ padding: 8px 10px;
765
+ }
766
+
767
+ .context-section p {
768
+ color: var(--muted);
769
+ font-size: 0.92rem;
770
+ line-height: 1.5;
771
+ margin: 0;
772
+ }
773
+
774
+ .simple-tags {
775
+ flex-wrap: wrap;
776
+ }
777
+
778
+ .simple-tags span {
779
+ background: var(--surface-strong);
780
+ border: 1px solid var(--line);
781
+ border-radius: 6px;
782
+ font-size: 0.8rem;
783
+ padding: 7px 10px;
784
+ }
785
+
786
+ .simple-task-list {
787
+ display: grid;
788
+ gap: 12px;
789
+ }
790
+
791
+ .simple-task {
792
+ align-items: flex-start;
793
+ }
794
+
795
+ .task-status-dot {
796
+ background: #b8a895;
797
+ border-radius: 999px;
798
+ height: 10px;
799
+ margin-top: 4px;
800
+ width: 10px;
801
+ }
802
+
803
+ .task-status-dot.in_progress {
804
+ background: var(--warning);
805
+ }
806
+
807
+ .task-status-dot.done {
808
+ background: var(--success);
809
+ }
810
+
811
+ .simple-task strong {
812
+ display: block;
813
+ margin-bottom: 3px;
814
+ }
815
+
816
+ .simple-output {
817
+ background: var(--surface-strong);
818
+ border: 1px solid var(--line);
819
+ border-radius: 8px;
820
+ padding: 12px;
821
+ }
822
+
823
+ .simple-output + .simple-output {
824
+ margin-top: 10px;
825
+ }
826
+
827
+ .simple-output-header {
828
+ justify-content: space-between;
829
+ margin-bottom: 10px;
830
+ }
831
+
832
+ .simple-output-header span {
833
+ color: #315aa9;
834
+ font-size: 0.76rem;
835
+ font-weight: 700;
836
+ letter-spacing: 0.08em;
837
+ text-transform: uppercase;
838
+ }
839
+
840
+ pre {
841
+ color: var(--ink);
842
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
843
+ font-size: 0.83rem;
844
+ line-height: 1.5;
845
+ margin: 0 0 10px;
846
+ overflow: auto;
847
+ white-space: pre-wrap;
848
+ }
849
+
850
+ .selection-cite {
851
+ background: rgba(255, 255, 255, 0.98);
852
+ border: 1px solid rgba(61, 111, 214, 0.28);
853
+ border-radius: 8px;
854
+ bottom: 0;
855
+ justify-content: space-between;
856
+ margin-top: 14px;
857
+ padding: 12px 14px;
858
+ position: sticky;
859
+ }
860
+
861
+ .selection-cite span {
862
+ color: var(--muted);
863
+ font-size: 0.88rem;
864
+ }
865
+
866
+ .selection-cite button {
867
+ padding: 8px 12px;
868
+ }
869
+
870
+ .agent-log-zone {
871
+ border-radius: 12px;
872
+ display: grid;
873
+ grid-template-rows: auto minmax(0, 1fr);
874
+ overflow: hidden;
875
+ padding: 12px 14px 14px;
876
+ }
877
+
878
+ .agent-log-header {
879
+ justify-content: space-between;
880
+ margin-bottom: 12px;
881
+ }
882
+
883
+ .agent-focus-pill {
884
+ background: var(--accent-soft);
885
+ border: 1px solid rgba(61, 111, 214, 0.18);
886
+ border-radius: 8px;
887
+ color: #315aa9;
888
+ font-size: 0.82rem;
889
+ padding: 7px 11px;
890
+ }
891
+
892
+ .agent-log-content {
893
+ align-items: stretch;
894
+ gap: 12px;
895
+ }
896
+
897
+ .agent-summary-card,
898
+ .agent-log-list {
899
+ background: rgba(255, 255, 255, 0.68);
900
+ border: 1px solid var(--line);
901
+ border-radius: 8px;
902
+ }
903
+
904
+ .agent-summary-card {
905
+ display: flex;
906
+ flex-direction: column;
907
+ justify-content: center;
908
+ min-width: 240px;
909
+ padding: 12px 14px;
910
+ }
911
+
912
+ .agent-summary-card strong {
913
+ font-size: 1rem;
914
+ margin: 6px 0 3px;
915
+ }
916
+
917
+ .agent-log-list {
918
+ display: grid;
919
+ gap: 8px;
920
+ list-style: decimal;
921
+ margin: 0;
922
+ padding: 14px 18px 14px 34px;
923
+ width: 100%;
924
+ }
925
+
926
+ .agent-log-list li {
927
+ color: var(--muted);
928
+ font-size: 0.88rem;
929
+ line-height: 1.45;
930
+ }
931
+
932
+ @media (max-width: 1180px) {
933
+ .pm-shell {
934
+ grid-template-columns: 1fr;
935
+ height: auto;
936
+ min-height: 100vh;
937
+ overflow: visible;
938
+ }
939
+
940
+ html,
941
+ body {
942
+ height: auto;
943
+ overflow: auto;
944
+ }
945
+
946
+ .chat-rail {
947
+ min-height: 680px;
948
+ }
949
+
950
+ .main-workspace {
951
+ grid-template-rows: minmax(640px, auto) auto;
952
+ }
953
+
954
+ .overview-layout,
955
+ .detail-hero,
956
+ .detail-content-grid,
957
+ .agent-log-content {
958
+ grid-template-columns: 1fr;
959
+ }
960
+
961
+ .phase-rail {
962
+ border-bottom: 1px solid var(--line);
963
+ border-right: 0;
964
+ }
965
+
966
+ .phase-nav-button {
967
+ justify-content: flex-start;
968
+ }
969
+
970
+ .detail-hero-actions {
971
+ flex-direction: row;
972
+ flex-wrap: wrap;
973
+ }
974
+
975
+ .detail-hero-actions button {
976
+ width: auto;
977
+ }
978
+ }
979
+
980
+ @media (max-width: 760px) {
981
+ .pm-shell {
982
+ padding: 10px;
983
+ }
984
+
985
+ .workspace-toolbar,
986
+ .floating-cite-bar,
987
+ .selection-cite,
988
+ .agent-log-header,
989
+ .agent-log-content,
990
+ .context-section-header,
991
+ .composer-topline {
992
+ align-items: flex-start;
993
+ flex-direction: column;
994
+ }
995
+
996
+ .stat-row {
997
+ grid-template-columns: 1fr;
998
+ }
999
+
1000
+ .workspace-tabs {
1001
+ width: 100%;
1002
+ }
1003
+
1004
+ .detail-layout {
1005
+ padding: 12px;
1006
+ }
1007
+ }
agentic_pm_demo_codex_plans/app/layout.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "Agentic PM Demo",
6
+ description: "A Cursor-like workspace for AI-native product management work packages.",
7
+ };
8
+
9
+ export default function RootLayout({
10
+ children,
11
+ }: Readonly<{
12
+ children: React.ReactNode;
13
+ }>) {
14
+ return (
15
+ <html lang="en">
16
+ <body>{children}</body>
17
+ </html>
18
+ );
19
+ }
agentic_pm_demo_codex_plans/app/page.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { AgenticPmWorkbench } from "@/components/AgenticPmWorkbench";
2
+
3
+ export default function Page() {
4
+ return <AgenticPmWorkbench />;
5
+ }
agentic_pm_demo_codex_plans/components/AgenticPmWorkbench.tsx ADDED
@@ -0,0 +1,826 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useMemo, useRef, useState } from "react";
4
+ import seedWorkPackages from "@/data/work-packages.seed.json";
5
+ import { getPackageSuggestions } from "@/lib/command-parser";
6
+ import { runMockAgentTurn } from "@/lib/mock-agent";
7
+ import type {
8
+ ChatMessage,
9
+ CitationReference,
10
+ CommandMode,
11
+ WorkPackage,
12
+ WorkPackagePhase,
13
+ WorkPackageStatus,
14
+ } from "@/lib/work-package-types";
15
+
16
+ type WorkspaceMode = "overview" | "detail";
17
+
18
+ const phases: WorkPackagePhase[] = [
19
+ "Stakeholder Needs",
20
+ "Specify Product",
21
+ "Design Product",
22
+ "Verify and Validate Product",
23
+ ];
24
+
25
+ const statusLabels: Record<WorkPackageStatus, string> = {
26
+ todo: "Todo",
27
+ in_progress: "In progress",
28
+ done: "Done",
29
+ };
30
+
31
+ const quickActions: Array<{ mode: CommandMode; label: string; icon: typeof SparkIcon }> = [
32
+ { mode: "ask", label: "Ask", icon: SparkIcon },
33
+ { mode: "plan", label: "Plan", icon: PlanIcon },
34
+ { mode: "change", label: "Change", icon: TuneIcon },
35
+ { mode: "execute", label: "Execute", icon: PlayIcon },
36
+ ];
37
+
38
+ export function AgenticPmWorkbench() {
39
+ const [workPackages, setWorkPackages] = useState<WorkPackage[]>(() => seedWorkPackages as WorkPackage[]);
40
+ const [selectedPackageId, setSelectedPackageId] = useState(workPackages[0]?.id ?? "");
41
+ const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>("overview");
42
+ const [inputValue, setInputValue] = useState("");
43
+ const [activeReferences, setActiveReferences] = useState<CitationReference[]>([]);
44
+ const [selectedQuote, setSelectedQuote] = useState("");
45
+ const idCounterRef = useRef(0);
46
+ const inputRef = useRef<HTMLTextAreaElement>(null);
47
+
48
+ const [messages, setMessages] = useState<ChatMessage[]>([
49
+ {
50
+ id: "welcome",
51
+ role: "assistant",
52
+ createdAt: new Date().toISOString(),
53
+ content:
54
+ "Workspace ready. Pick a work package, cite the exact context you want, then ask the agent to plan, change, or execute a simulated deliverable.",
55
+ },
56
+ ]);
57
+
58
+ const [agentLogs, setAgentLogs] = useState([
59
+ "System initialized with the seeded IPD package map.",
60
+ "Agent standby: ready for package-specific planning or execution.",
61
+ ]);
62
+
63
+ const selectedPackage =
64
+ workPackages.find((workPackage) => workPackage.id === selectedPackageId) ?? workPackages[0];
65
+ const suggestions = getPackageSuggestions(inputValue, workPackages);
66
+
67
+ const groupedPackages = useMemo(() => {
68
+ return phases.map((phase) => ({
69
+ phase,
70
+ workPackages: workPackages.filter((workPackage) => workPackage.phase === phase),
71
+ }));
72
+ }, [workPackages]);
73
+
74
+ const boardStats = useMemo(() => {
75
+ const done = workPackages.filter((workPackage) => workPackage.status === "done").length;
76
+ const inProgress = workPackages.filter((workPackage) => workPackage.status === "in_progress").length;
77
+
78
+ return {
79
+ total: workPackages.length,
80
+ done,
81
+ inProgress,
82
+ };
83
+ }, [workPackages]);
84
+
85
+ function openPackage(workPackage: WorkPackage) {
86
+ setSelectedPackageId(workPackage.id);
87
+ setWorkspaceMode("detail");
88
+ }
89
+
90
+ function handleQuickAction(workPackage: WorkPackage, mode: CommandMode) {
91
+ setSelectedPackageId(workPackage.id);
92
+ setInputValue(`@${workPackage.shortName} ${mode} `);
93
+ inputRef.current?.focus();
94
+ }
95
+
96
+ function addReference(workPackage: WorkPackage, label: string, quote?: string) {
97
+ const reference: CitationReference = {
98
+ id: `ref-${workPackage.id}-${idCounterRef.current++}`,
99
+ packageId: workPackage.id,
100
+ packageShortName: workPackage.shortName,
101
+ packageTitle: workPackage.title,
102
+ label,
103
+ quote,
104
+ };
105
+
106
+ setSelectedPackageId(workPackage.id);
107
+ setActiveReferences((references) => [...references, reference]);
108
+ setInputValue((currentValue) => {
109
+ const token = quote
110
+ ? `@${workPackage.shortName}:"${quote.slice(0, 72)}"`
111
+ : `@${workPackage.shortName}#${label.replace(/\s+/g, "-")}`;
112
+
113
+ return currentValue.trim() ? `${currentValue.trimEnd()} ${token} ` : `${token} `;
114
+ });
115
+ inputRef.current?.focus();
116
+ }
117
+
118
+ function citeSelectedQuote() {
119
+ if (!selectedPackage || !selectedQuote) {
120
+ return;
121
+ }
122
+
123
+ addReference(selectedPackage, "Selected wording", selectedQuote);
124
+ setSelectedQuote("");
125
+ }
126
+
127
+ function captureSelectedText() {
128
+ const selection = window.getSelection()?.toString().replace(/\s+/g, " ").trim() ?? "";
129
+
130
+ if (selection.length > 2) {
131
+ setSelectedQuote(selection.slice(0, 180));
132
+ }
133
+ }
134
+
135
+ function applySuggestion(workPackage: WorkPackage) {
136
+ setInputValue((currentValue) => currentValue.replace(/@[A-Za-z\s-]*$/, `@${workPackage.shortName} `));
137
+ setSelectedPackageId(workPackage.id);
138
+ inputRef.current?.focus();
139
+ }
140
+
141
+ function handleSend() {
142
+ const content = inputValue.trim();
143
+
144
+ if (!content) {
145
+ return;
146
+ }
147
+
148
+ const userMessage: ChatMessage = {
149
+ id: `message-user-${idCounterRef.current++}`,
150
+ role: "user",
151
+ content,
152
+ createdAt: new Date().toISOString(),
153
+ references: activeReferences,
154
+ };
155
+ const result = runMockAgentTurn(content, workPackages);
156
+ const assistantMessage: ChatMessage = {
157
+ id: `message-assistant-${idCounterRef.current++}`,
158
+ role: "assistant",
159
+ content: result.assistantMessage,
160
+ createdAt: new Date().toISOString(),
161
+ };
162
+ const selectedAfterTurn = result.workPackages.find(
163
+ (workPackage) => workPackage.id === (result.selectedPackageId ?? selectedPackageId),
164
+ );
165
+
166
+ setMessages((currentMessages) => [...currentMessages, userMessage, assistantMessage]);
167
+ setWorkPackages(result.workPackages);
168
+ setSelectedPackageId(result.selectedPackageId ?? selectedPackageId);
169
+ setWorkspaceMode(result.selectedPackageId ? "detail" : workspaceMode);
170
+ setAgentLogs((logs) => [
171
+ `Package ${selectedAfterTurn?.shortName ?? "General"} handled request: ${content.slice(0, 78)}.`,
172
+ `Board state is now ${selectedAfterTurn ? statusLabels[selectedAfterTurn.status].toLowerCase() : "unchanged"}.`,
173
+ ...logs,
174
+ ]);
175
+ setInputValue("");
176
+ setActiveReferences([]);
177
+ }
178
+
179
+ return (
180
+ <main className="pm-shell">
181
+ <aside className="chat-rail" aria-label="All chat and history">
182
+ <header className="project-strip">
183
+ <div className="project-mark">
184
+ <ProjectGlyph />
185
+ </div>
186
+ <div className="project-meta">
187
+ <p>Agentic PM</p>
188
+ <div>
189
+ <h1>Factory Sensor Platform</h1>
190
+ <span>Project 042, IPD Sandbox</span>
191
+ </div>
192
+ </div>
193
+ </header>
194
+
195
+ <section className="chat-history">
196
+ <div className="panel-kicker">
197
+ <ChatIcon />
198
+ <span>Chat and history</span>
199
+ </div>
200
+ <div className="message-stack">
201
+ {messages.map((message) => (
202
+ <article className={`chat-bubble ${message.role}`} key={message.id}>
203
+ <strong>{message.role === "assistant" ? "Agent" : "You"}</strong>
204
+ <p>{message.content}</p>
205
+ {!!message.references?.length && (
206
+ <div className="reference-row">
207
+ {message.references.map((reference) => (
208
+ <span className="reference-chip" key={reference.id}>
209
+ @{reference.packageShortName} · {reference.label}
210
+ </span>
211
+ ))}
212
+ </div>
213
+ )}
214
+ </article>
215
+ ))}
216
+ </div>
217
+ </section>
218
+
219
+ <section className="chat-input-zone" aria-label="User input chat zone">
220
+ <div className="composer-topline">
221
+ <span>Composer</span>
222
+ <span>{selectedPackage ? `Context: ${selectedPackage.shortName}` : "No package selected"}</span>
223
+ </div>
224
+
225
+ {!!activeReferences.length && (
226
+ <div className="reference-row">
227
+ {activeReferences.map((reference) => (
228
+ <button
229
+ className="reference-chip removable"
230
+ key={reference.id}
231
+ onClick={() =>
232
+ setActiveReferences((references) =>
233
+ references.filter((existingReference) => existingReference.id !== reference.id),
234
+ )
235
+ }
236
+ type="button"
237
+ >
238
+ @{reference.packageShortName} · {reference.label}
239
+ </button>
240
+ ))}
241
+ </div>
242
+ )}
243
+
244
+ <div className="quick-action-row">
245
+ {quickActions.map(({ icon: Icon, label, mode }) => (
246
+ <button
247
+ className="quick-action-button"
248
+ key={mode}
249
+ onClick={() => selectedPackage && handleQuickAction(selectedPackage, mode)}
250
+ type="button"
251
+ >
252
+ <Icon />
253
+ <span>{label}</span>
254
+ </button>
255
+ ))}
256
+ </div>
257
+
258
+ <div className="composer-wrap">
259
+ {suggestions.length > 0 && (
260
+ <div className="mention-popover">
261
+ {suggestions.map((workPackage) => (
262
+ <button key={workPackage.id} onClick={() => applySuggestion(workPackage)} type="button">
263
+ <span>@{workPackage.shortName}</span>
264
+ <small>{workPackage.title}</small>
265
+ </button>
266
+ ))}
267
+ </div>
268
+ )}
269
+ <textarea
270
+ aria-label="Chat input"
271
+ onChange={(event) => setInputValue(event.target.value)}
272
+ onKeyDown={(event) => {
273
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
274
+ handleSend();
275
+ }
276
+ }}
277
+ placeholder="Ask the current package for a plan, cite a section, or execute a simulated output."
278
+ ref={inputRef}
279
+ value={inputValue}
280
+ />
281
+ </div>
282
+
283
+ <div className="input-toolbar">
284
+ <button className="ghost-button" type="button">
285
+ <PlusIcon />
286
+ </button>
287
+ <button className="ghost-button" type="button">
288
+ <BoltIcon />
289
+ </button>
290
+ <button className="ghost-button" type="button">
291
+ <DeckIcon />
292
+ </button>
293
+ <button className="ghost-button" type="button">
294
+ <MoreIcon />
295
+ </button>
296
+ <button className="send-button" onClick={handleSend} type="button">
297
+ <SendIcon />
298
+ </button>
299
+ </div>
300
+ </section>
301
+ </aside>
302
+
303
+ <section className="main-workspace">
304
+ {workspaceMode === "overview" ? (
305
+ <WorkPackageOverview
306
+ boardStats={boardStats}
307
+ groupedPackages={groupedPackages}
308
+ onCite={addReference}
309
+ onOpen={openPackage}
310
+ onSwitchMode={setWorkspaceMode}
311
+ selectedPackage={selectedPackage}
312
+ selectedPackageId={selectedPackageId}
313
+ />
314
+ ) : (
315
+ selectedPackage && (
316
+ <WorkPackageDetail
317
+ onBack={() => setWorkspaceMode("overview")}
318
+ onCaptureSelection={captureSelectedText}
319
+ onCite={addReference}
320
+ onQuickAction={handleQuickAction}
321
+ onSelectedQuoteCite={citeSelectedQuote}
322
+ onSwitchMode={setWorkspaceMode}
323
+ selectedPackage={selectedPackage}
324
+ selectedQuote={selectedQuote}
325
+ />
326
+ )
327
+ )}
328
+
329
+ <AgentLog logs={agentLogs} selectedPackage={selectedPackage} />
330
+ </section>
331
+ </main>
332
+ );
333
+ }
334
+
335
+ function WorkPackageOverview({
336
+ boardStats,
337
+ groupedPackages,
338
+ onCite,
339
+ onOpen,
340
+ onSwitchMode,
341
+ selectedPackage,
342
+ selectedPackageId,
343
+ }: {
344
+ boardStats: { total: number; done: number; inProgress: number };
345
+ groupedPackages: Array<{ phase: WorkPackagePhase; workPackages: WorkPackage[] }>;
346
+ onCite: (workPackage: WorkPackage, label: string, quote?: string) => void;
347
+ onOpen: (workPackage: WorkPackage) => void;
348
+ onSwitchMode: (mode: WorkspaceMode) => void;
349
+ selectedPackage: WorkPackage;
350
+ selectedPackageId: string;
351
+ }) {
352
+ return (
353
+ <section className="workspace-canvas">
354
+ <header className="workspace-toolbar">
355
+ <div className="workspace-title">
356
+ <CompassIcon />
357
+ <div>
358
+ <h2>Work package map</h2>
359
+ <p>Overview of the IPD package landscape and current package status.</p>
360
+ </div>
361
+ </div>
362
+
363
+ <div className="workspace-tabs">
364
+ <button className="active" onClick={() => onSwitchMode("overview")} type="button">
365
+ Overview
366
+ </button>
367
+ <button onClick={() => onSwitchMode("detail")} type="button">
368
+ Detail
369
+ </button>
370
+ </div>
371
+ </header>
372
+
373
+ <div className="overview-layout">
374
+ <aside className="phase-rail">
375
+ <p>Phases</p>
376
+ {groupedPackages.map((group) => (
377
+ <button className="phase-nav-button" key={group.phase} type="button">
378
+ <span>{group.phase}</span>
379
+ <small>{group.workPackages.length}</small>
380
+ </button>
381
+ ))}
382
+ </aside>
383
+
384
+ <div className="overview-content">
385
+ <div className="stat-row">
386
+ <StatCard label="Total packages" value={String(boardStats.total)} />
387
+ <StatCard label="In progress" value={String(boardStats.inProgress)} />
388
+ <StatCard label="Done" value={String(boardStats.done)} />
389
+ </div>
390
+
391
+ <div className="phase-map">
392
+ {groupedPackages.map((group) => (
393
+ <section className="phase-column" key={group.phase}>
394
+ <header>
395
+ <span>{group.phase}</span>
396
+ <small>{group.workPackages.length} packages</small>
397
+ </header>
398
+ <div className="package-card-grid">
399
+ {group.workPackages.map((workPackage) => (
400
+ <article
401
+ className={`package-card ${workPackage.id === selectedPackageId ? "selected" : ""}`}
402
+ key={workPackage.id}
403
+ onClick={() => onOpen(workPackage)}
404
+ onKeyDown={(event) => {
405
+ if (event.key === "Enter" || event.key === " ") {
406
+ event.preventDefault();
407
+ onOpen(workPackage);
408
+ }
409
+ }}
410
+ role="button"
411
+ tabIndex={0}
412
+ >
413
+ <div className="package-card-topline">
414
+ <span className="package-short">{workPackage.shortName}</span>
415
+ <span className={`status-pill ${workPackage.status}`}>
416
+ {statusLabels[workPackage.status]}
417
+ </span>
418
+ </div>
419
+ <h3>{workPackage.title}</h3>
420
+ <p>{workPackage.objective}</p>
421
+ <div className="package-card-actions">
422
+ <span>
423
+ <FileIcon />
424
+ Open
425
+ </span>
426
+ <button
427
+ onClick={(event) => {
428
+ event.stopPropagation();
429
+ onCite(workPackage, "Package");
430
+ }}
431
+ type="button"
432
+ >
433
+ <QuoteIcon />
434
+ </button>
435
+ </div>
436
+ </article>
437
+ ))}
438
+ </div>
439
+ </section>
440
+ ))}
441
+ </div>
442
+ </div>
443
+ </div>
444
+
445
+ <div className="floating-cite-bar">
446
+ <span>Selected package: {selectedPackage.shortName}</span>
447
+ <button onClick={() => onCite(selectedPackage, "Package")} type="button">
448
+ <QuoteIcon />
449
+ Cite package
450
+ </button>
451
+ </div>
452
+ </section>
453
+ );
454
+ }
455
+
456
+ function WorkPackageDetail({
457
+ onBack,
458
+ onCaptureSelection,
459
+ onCite,
460
+ onQuickAction,
461
+ onSelectedQuoteCite,
462
+ onSwitchMode,
463
+ selectedPackage,
464
+ selectedQuote,
465
+ }: {
466
+ onBack: () => void;
467
+ onCaptureSelection: () => void;
468
+ onCite: (workPackage: WorkPackage, label: string, quote?: string) => void;
469
+ onQuickAction: (workPackage: WorkPackage, mode: CommandMode) => void;
470
+ onSelectedQuoteCite: () => void;
471
+ onSwitchMode: (mode: WorkspaceMode) => void;
472
+ selectedPackage: WorkPackage;
473
+ selectedQuote: string;
474
+ }) {
475
+ return (
476
+ <section className="workspace-canvas">
477
+ <header className="workspace-toolbar">
478
+ <div className="workspace-title">
479
+ <FileStackIcon />
480
+ <div>
481
+ <h2>{selectedPackage.title}</h2>
482
+ <p>{selectedPackage.phase}</p>
483
+ </div>
484
+ </div>
485
+
486
+ <div className="workspace-tabs">
487
+ <button onClick={() => onSwitchMode("overview")} type="button">
488
+ Overview
489
+ </button>
490
+ <button className="active" onClick={() => onSwitchMode("detail")} type="button">
491
+ Detail
492
+ </button>
493
+ </div>
494
+ </header>
495
+
496
+ <article className="detail-layout" onMouseUp={onCaptureSelection}>
497
+ <div className="detail-hero">
498
+ <button className="back-button" onClick={onBack} type="button">
499
+ <ArrowLeftIcon />
500
+ <span>Back to map</span>
501
+ </button>
502
+ <div className="detail-hero-copy">
503
+ <span className="status-label">{statusLabels[selectedPackage.status]}</span>
504
+ <h1>{selectedPackage.title}</h1>
505
+ <p>{selectedPackage.objective}</p>
506
+ </div>
507
+ <div className="detail-hero-actions">
508
+ {quickActions.map(({ icon: Icon, label, mode }) => (
509
+ <button key={mode} onClick={() => onQuickAction(selectedPackage, mode)} type="button">
510
+ <Icon />
511
+ <span>{label}</span>
512
+ </button>
513
+ ))}
514
+ </div>
515
+ </div>
516
+
517
+ <div className="detail-content-grid">
518
+ <ContextSection
519
+ label="Expected outputs"
520
+ meta={`${selectedPackage.outputFiles.length} deliverables`}
521
+ onCite={() => onCite(selectedPackage, "Expected outputs", selectedPackage.outputFiles.join(", "))}
522
+ >
523
+ <div className="simple-tags">
524
+ {selectedPackage.outputFiles.map((outputFile) => (
525
+ <span key={outputFile}>{outputFile}</span>
526
+ ))}
527
+ </div>
528
+ </ContextSection>
529
+
530
+ <ContextSection
531
+ label="Tasks"
532
+ meta={`${selectedPackage.tasks.length} tasks`}
533
+ onCite={() =>
534
+ onCite(
535
+ selectedPackage,
536
+ "Tasks",
537
+ selectedPackage.tasks.map((task) => task.title).join(", "),
538
+ )
539
+ }
540
+ >
541
+ <div className="simple-task-list">
542
+ {selectedPackage.tasks.map((task) => (
543
+ <div className="simple-task" key={task.id}>
544
+ <span className={`task-status-dot ${task.status}`} />
545
+ <div>
546
+ <strong>{task.title}</strong>
547
+ <p>{task.description}</p>
548
+ </div>
549
+ </div>
550
+ ))}
551
+ </div>
552
+ </ContextSection>
553
+
554
+ {!!selectedPackage.coreSections.length && (
555
+ <ContextSection
556
+ label="Added context"
557
+ meta={`${selectedPackage.coreSections.length} notes`}
558
+ onCite={() => onCite(selectedPackage, "Added context", selectedPackage.coreSections.join(", "))}
559
+ >
560
+ <div className="simple-tags">
561
+ {selectedPackage.coreSections.map((section) => (
562
+ <span key={section}>{section}</span>
563
+ ))}
564
+ </div>
565
+ </ContextSection>
566
+ )}
567
+
568
+ <ContextSection
569
+ label="Simulated outputs"
570
+ meta={`${selectedPackage.outputs.length} saved`}
571
+ onCite={() => onCite(selectedPackage, "Simulated outputs")}
572
+ >
573
+ {selectedPackage.outputs.length ? (
574
+ selectedPackage.outputs.map((output) => (
575
+ <div className="simple-output" key={output.id}>
576
+ <div className="simple-output-header">
577
+ <strong>{output.title}</strong>
578
+ <span>{output.type}</span>
579
+ </div>
580
+ <pre>{output.content}</pre>
581
+ <p>{output.disclaimer}</p>
582
+ </div>
583
+ ))
584
+ ) : (
585
+ <p className="empty-copy">No outputs yet. Use execute to generate a simulated result.</p>
586
+ )}
587
+ </ContextSection>
588
+ </div>
589
+
590
+ {selectedQuote && (
591
+ <div className="selection-cite">
592
+ <span>Selected text ready to cite: “{selectedQuote}”</span>
593
+ <button onClick={onSelectedQuoteCite} type="button">
594
+ <QuoteIcon />
595
+ <span>Cite selected wording</span>
596
+ </button>
597
+ </div>
598
+ )}
599
+ </article>
600
+ </section>
601
+ );
602
+ }
603
+
604
+ function AgentLog({
605
+ logs,
606
+ selectedPackage,
607
+ }: {
608
+ logs: string[];
609
+ selectedPackage?: WorkPackage;
610
+ }) {
611
+ return (
612
+ <section className="agent-log-zone" aria-label="Agent logging zone">
613
+ <div className="agent-log-header">
614
+ <div className="workspace-title">
615
+ <PulseIcon />
616
+ <div>
617
+ <h2>Agent activity</h2>
618
+ <p>Live execution, package status, and process notes.</p>
619
+ </div>
620
+ </div>
621
+ <div className="agent-focus-pill">{selectedPackage?.shortName ?? "No package selected"}</div>
622
+ </div>
623
+
624
+ <div className="agent-log-content">
625
+ <div className="agent-summary-card">
626
+ <span>Active package</span>
627
+ <strong>{selectedPackage?.title ?? "None"}</strong>
628
+ <small>{selectedPackage ? statusLabels[selectedPackage.status] : "Waiting"}</small>
629
+ </div>
630
+ <ol className="agent-log-list">
631
+ {logs.slice(0, 3).map((log) => (
632
+ <li key={log}>{log}</li>
633
+ ))}
634
+ </ol>
635
+ </div>
636
+ </section>
637
+ );
638
+ }
639
+
640
+ function ContextSection({
641
+ children,
642
+ label,
643
+ meta,
644
+ onCite,
645
+ }: {
646
+ children: React.ReactNode;
647
+ label: string;
648
+ meta: string;
649
+ onCite: () => void;
650
+ }) {
651
+ return (
652
+ <section className="context-section">
653
+ <div className="context-section-header">
654
+ <div>
655
+ <h3>{label}</h3>
656
+ <span>{meta}</span>
657
+ </div>
658
+ <button onClick={onCite} type="button">
659
+ <QuoteIcon />
660
+ <span>Cite</span>
661
+ </button>
662
+ </div>
663
+ {children}
664
+ </section>
665
+ );
666
+ }
667
+
668
+ function StatCard({ label, value }: { label: string; value: string }) {
669
+ return (
670
+ <div className="stat-card">
671
+ <span>{label}</span>
672
+ <strong>{value}</strong>
673
+ </div>
674
+ );
675
+ }
676
+
677
+ function ProjectGlyph() {
678
+ return (
679
+ <svg aria-hidden="true" viewBox="0 0 24 24">
680
+ <path d="M5 5h7v7H5zM12 12h7v7h-7zM14 5h5v5h-5zM5 14h5v5H5z" fill="currentColor" />
681
+ </svg>
682
+ );
683
+ }
684
+
685
+ function ChatIcon() {
686
+ return (
687
+ <svg aria-hidden="true" viewBox="0 0 24 24">
688
+ <path
689
+ d="M5 6.5h14a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H9l-4 3v-3H5a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2Z"
690
+ fill="none"
691
+ stroke="currentColor"
692
+ strokeWidth="1.7"
693
+ />
694
+ </svg>
695
+ );
696
+ }
697
+
698
+ function CompassIcon() {
699
+ return (
700
+ <svg aria-hidden="true" viewBox="0 0 24 24">
701
+ <circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" strokeWidth="1.7" />
702
+ <path d="m10 14 1.5-4 4-1.5-1.5 4Z" fill="currentColor" />
703
+ </svg>
704
+ );
705
+ }
706
+
707
+ function FileStackIcon() {
708
+ return (
709
+ <svg aria-hidden="true" viewBox="0 0 24 24">
710
+ <path d="M7 5h10v10H7z" fill="none" stroke="currentColor" strokeWidth="1.7" />
711
+ <path d="M5 8h10v10H5z" fill="none" stroke="currentColor" strokeWidth="1.7" />
712
+ </svg>
713
+ );
714
+ }
715
+
716
+ function SparkIcon() {
717
+ return (
718
+ <svg aria-hidden="true" viewBox="0 0 24 24">
719
+ <path d="M12 3 14 9l6 3-6 3-2 6-2-6-6-3 6-3Z" fill="currentColor" />
720
+ </svg>
721
+ );
722
+ }
723
+
724
+ function PlanIcon() {
725
+ return (
726
+ <svg aria-hidden="true" viewBox="0 0 24 24">
727
+ <path d="M6 7h12M6 12h8M6 17h10" fill="none" stroke="currentColor" strokeWidth="1.7" />
728
+ </svg>
729
+ );
730
+ }
731
+
732
+ function TuneIcon() {
733
+ return (
734
+ <svg aria-hidden="true" viewBox="0 0 24 24">
735
+ <path d="M5 7h14M8 7v10M12 12h7M5 17h14M16 17V7" fill="none" stroke="currentColor" strokeWidth="1.7" />
736
+ </svg>
737
+ );
738
+ }
739
+
740
+ function PlayIcon() {
741
+ return (
742
+ <svg aria-hidden="true" viewBox="0 0 24 24">
743
+ <path d="m8 6 10 6-10 6Z" fill="currentColor" />
744
+ </svg>
745
+ );
746
+ }
747
+
748
+ function QuoteIcon() {
749
+ return (
750
+ <svg aria-hidden="true" viewBox="0 0 24 24">
751
+ <path
752
+ d="M7 10h4v4H7v4H3v-4c0-4 2-6 4-7Zm10 0h4v4h-4v4h-4v-4c0-4 2-6 4-7Z"
753
+ fill="currentColor"
754
+ />
755
+ </svg>
756
+ );
757
+ }
758
+
759
+ function ArrowLeftIcon() {
760
+ return (
761
+ <svg aria-hidden="true" viewBox="0 0 24 24">
762
+ <path d="m11 6-6 6 6 6M6 12h13" fill="none" stroke="currentColor" strokeWidth="1.7" />
763
+ </svg>
764
+ );
765
+ }
766
+
767
+ function SendIcon() {
768
+ return (
769
+ <svg aria-hidden="true" viewBox="0 0 24 24">
770
+ <path d="m4 12 15-7-3 7 3 7Z" fill="none" stroke="currentColor" strokeWidth="1.7" />
771
+ <path d="M4 12h12" fill="none" stroke="currentColor" strokeWidth="1.7" />
772
+ </svg>
773
+ );
774
+ }
775
+
776
+ function PulseIcon() {
777
+ return (
778
+ <svg aria-hidden="true" viewBox="0 0 24 24">
779
+ <path d="M3 12h4l2-4 4 8 2-4h6" fill="none" stroke="currentColor" strokeWidth="1.7" />
780
+ </svg>
781
+ );
782
+ }
783
+
784
+ function PlusIcon() {
785
+ return (
786
+ <svg aria-hidden="true" viewBox="0 0 24 24">
787
+ <path d="M12 5v14M5 12h14" fill="none" stroke="currentColor" strokeWidth="1.7" />
788
+ </svg>
789
+ );
790
+ }
791
+
792
+ function BoltIcon() {
793
+ return (
794
+ <svg aria-hidden="true" viewBox="0 0 24 24">
795
+ <path d="M13 3 6 13h5l-1 8 8-11h-5l1-7Z" fill="currentColor" />
796
+ </svg>
797
+ );
798
+ }
799
+
800
+ function DeckIcon() {
801
+ return (
802
+ <svg aria-hidden="true" viewBox="0 0 24 24">
803
+ <path d="M5 7h14v10H5z" fill="none" stroke="currentColor" strokeWidth="1.7" />
804
+ <path d="M8 10h8M8 14h5" fill="none" stroke="currentColor" strokeWidth="1.7" />
805
+ </svg>
806
+ );
807
+ }
808
+
809
+ function MoreIcon() {
810
+ return (
811
+ <svg aria-hidden="true" viewBox="0 0 24 24">
812
+ <circle cx="6" cy="12" r="1.8" fill="currentColor" />
813
+ <circle cx="12" cy="12" r="1.8" fill="currentColor" />
814
+ <circle cx="18" cy="12" r="1.8" fill="currentColor" />
815
+ </svg>
816
+ );
817
+ }
818
+
819
+ function FileIcon() {
820
+ return (
821
+ <svg aria-hidden="true" viewBox="0 0 24 24">
822
+ <path d="M7 4h7l4 4v12H7z" fill="none" stroke="currentColor" strokeWidth="1.7" />
823
+ <path d="M14 4v4h4" fill="none" stroke="currentColor" strokeWidth="1.7" />
824
+ </svg>
825
+ );
826
+ }
agentic_pm_demo_codex_plans/data/work-packages.seed.json ADDED
@@ -0,0 +1,338 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "wp-crs",
4
+ "title": "Customer Requirements Specification",
5
+ "shortName": "CRS",
6
+ "phase": "Stakeholder Needs",
7
+ "objective": "Capture and structure customer requirements.",
8
+ "inputFiles": [],
9
+ "outputFiles": [
10
+ "Customer Requirement List",
11
+ "User Stories",
12
+ "Requirement Classification",
13
+ "CRS-to-SRS Traceability Input"
14
+ ],
15
+ "coreSections": [],
16
+ "tasks": [
17
+ {
18
+ "id": "task-crs-execute",
19
+ "title": "Generate simulated output",
20
+ "description": "Generate a simulated MVP output for this work package.",
21
+ "type": "generation",
22
+ "executable": true,
23
+ "status": "todo"
24
+ }
25
+ ],
26
+ "outputs": [],
27
+ "status": "todo",
28
+ "priority": "high"
29
+ },
30
+ {
31
+ "id": "wp-final-concept",
32
+ "title": "Final Concept",
33
+ "shortName": "Final Concept",
34
+ "phase": "Stakeholder Needs",
35
+ "objective": "Transform CRS into a product concept with insights, benefits, reasons to believe, validation, and visual brief.",
36
+ "inputFiles": [],
37
+ "outputFiles": [
38
+ "Final Product Concept",
39
+ "Insight-Benefit-RTB Matrix",
40
+ "Validation of Verbal Concept",
41
+ "2D Concept Visual Brief"
42
+ ],
43
+ "coreSections": [],
44
+ "tasks": [
45
+ {
46
+ "id": "task-final-concept-execute",
47
+ "title": "Generate simulated output",
48
+ "description": "Generate a simulated MVP output for this work package.",
49
+ "type": "generation",
50
+ "executable": true,
51
+ "status": "todo"
52
+ }
53
+ ],
54
+ "outputs": [],
55
+ "status": "todo",
56
+ "priority": "medium"
57
+ },
58
+ {
59
+ "id": "wp-srs",
60
+ "title": "System Requirements Specification",
61
+ "shortName": "SRS",
62
+ "phase": "Specify Product",
63
+ "objective": "Translate CRS into traceable system-level requirements, components, interfaces, specifications, and verification methods.",
64
+ "inputFiles": [],
65
+ "outputFiles": [
66
+ "System Requirements Specification",
67
+ "CRS-to-SRS Traceability Matrix",
68
+ "Component Requirement List",
69
+ "Interface Requirement List"
70
+ ],
71
+ "coreSections": [],
72
+ "tasks": [
73
+ {
74
+ "id": "task-srs-execute",
75
+ "title": "Generate simulated output",
76
+ "description": "Generate a simulated MVP output for this work package.",
77
+ "type": "generation",
78
+ "executable": true,
79
+ "status": "todo"
80
+ }
81
+ ],
82
+ "outputs": [],
83
+ "status": "todo",
84
+ "priority": "high"
85
+ },
86
+ {
87
+ "id": "wp-safety-of-products",
88
+ "title": "Safety of Products",
89
+ "shortName": "Safety",
90
+ "phase": "Specify Product",
91
+ "objective": "Identify product safety hazards, assess initial and residual risks, and define risk reduction measures.",
92
+ "inputFiles": [],
93
+ "outputFiles": [
94
+ "Product Safety Risk Assessment",
95
+ "Hazard List",
96
+ "Risk Matrix",
97
+ "Risk Reduction Measures"
98
+ ],
99
+ "coreSections": [],
100
+ "tasks": [
101
+ {
102
+ "id": "task-safety-of-products-execute",
103
+ "title": "Generate simulated output",
104
+ "description": "Generate a simulated MVP output for this work package.",
105
+ "type": "generation",
106
+ "executable": true,
107
+ "status": "todo"
108
+ }
109
+ ],
110
+ "outputs": [],
111
+ "status": "todo",
112
+ "priority": "medium"
113
+ },
114
+ {
115
+ "id": "wp-product-certification",
116
+ "title": "Product Certification",
117
+ "shortName": "Certification",
118
+ "phase": "Specify Product",
119
+ "objective": "Identify market access requirements, standards, certification documents, testing needs, approval milestones, and action items.",
120
+ "inputFiles": [],
121
+ "outputFiles": [
122
+ "Product Certification Briefing",
123
+ "Product Certification Plan",
124
+ "Certification Checklist",
125
+ "Applicable Standards List"
126
+ ],
127
+ "coreSections": [],
128
+ "tasks": [
129
+ {
130
+ "id": "task-product-certification-execute",
131
+ "title": "Generate simulated output",
132
+ "description": "Generate a simulated MVP output for this work package.",
133
+ "type": "generation",
134
+ "executable": true,
135
+ "status": "todo"
136
+ }
137
+ ],
138
+ "outputs": [],
139
+ "status": "todo",
140
+ "priority": "medium"
141
+ },
142
+ {
143
+ "id": "wp-test-management",
144
+ "title": "Test Management",
145
+ "shortName": "Test",
146
+ "phase": "Specify Product",
147
+ "objective": "Plan product verification and reliability validation from CRS, SRS, safety, and certification requirements.",
148
+ "inputFiles": [],
149
+ "outputFiles": [
150
+ "Test Plan",
151
+ "Requirement-to-Test Matrix",
152
+ "Reliability Validation Plan",
153
+ "Sample Size Calculation"
154
+ ],
155
+ "coreSections": [],
156
+ "tasks": [
157
+ {
158
+ "id": "task-test-management-execute",
159
+ "title": "Generate simulated output",
160
+ "description": "Generate a simulated MVP output for this work package.",
161
+ "type": "generation",
162
+ "executable": true,
163
+ "status": "todo"
164
+ }
165
+ ],
166
+ "outputs": [],
167
+ "status": "todo",
168
+ "priority": "high"
169
+ },
170
+ {
171
+ "id": "wp-decompose-system",
172
+ "title": "Decompose System",
173
+ "shortName": "Decompose",
174
+ "phase": "Design Product",
175
+ "objective": "Decompose the product/system into components, modules, standardization opportunities, technical focus areas, and risks.",
176
+ "inputFiles": [],
177
+ "outputFiles": [
178
+ "Technology Filter",
179
+ "Focus Area Analysis",
180
+ "Technical Complexity Category",
181
+ "Project Risk Class"
182
+ ],
183
+ "coreSections": [],
184
+ "tasks": [
185
+ {
186
+ "id": "task-decompose-system-execute",
187
+ "title": "Generate simulated output",
188
+ "description": "Generate a simulated MVP output for this work package.",
189
+ "type": "generation",
190
+ "executable": true,
191
+ "status": "todo"
192
+ }
193
+ ],
194
+ "outputs": [],
195
+ "status": "todo",
196
+ "priority": "medium"
197
+ },
198
+ {
199
+ "id": "wp-industrial-design",
200
+ "title": "Industrial Design",
201
+ "shortName": "Industrial Design",
202
+ "phase": "Design Product",
203
+ "objective": "Translate product concept and requirements into product form, visual direction, interaction layout, CMF, and concept design direction.",
204
+ "inputFiles": [],
205
+ "outputFiles": [
206
+ "Industrial Design Brief",
207
+ "2D Concept Design Brief",
208
+ "CMF Proposal",
209
+ "Design Review Checklist"
210
+ ],
211
+ "coreSections": [],
212
+ "tasks": [
213
+ {
214
+ "id": "task-industrial-design-execute",
215
+ "title": "Generate simulated output",
216
+ "description": "Generate a simulated MVP output for this work package.",
217
+ "type": "generation",
218
+ "executable": true,
219
+ "status": "todo"
220
+ }
221
+ ],
222
+ "outputs": [],
223
+ "status": "todo",
224
+ "priority": "medium"
225
+ },
226
+ {
227
+ "id": "wp-patent-check",
228
+ "title": "Patent Check",
229
+ "shortName": "Patent",
230
+ "phase": "Design Product",
231
+ "objective": "Identify patent-related risks and possible patentable innovation points before engineering design is finalized.",
232
+ "inputFiles": [],
233
+ "outputFiles": [
234
+ "Patent Search Topic List",
235
+ "FTO Risk Assumptions",
236
+ "Patentable Invention Points",
237
+ "IP Review Action Items"
238
+ ],
239
+ "coreSections": [],
240
+ "tasks": [
241
+ {
242
+ "id": "task-patent-check-execute",
243
+ "title": "Generate simulated output",
244
+ "description": "Generate a simulated MVP output for this work package.",
245
+ "type": "generation",
246
+ "executable": true,
247
+ "status": "todo"
248
+ }
249
+ ],
250
+ "outputs": [],
251
+ "status": "todo",
252
+ "priority": "medium"
253
+ },
254
+ {
255
+ "id": "wp-design-fmea",
256
+ "title": "Design FMEA",
257
+ "shortName": "Design FMEA",
258
+ "phase": "Design Product",
259
+ "objective": "Identify design failure modes, effects, causes, controls, risk priority, recommended actions, and residual risk.",
260
+ "inputFiles": [],
261
+ "outputFiles": [
262
+ "Design FMEA Table",
263
+ "High-Risk Failure Mode List",
264
+ "Recommended Design Actions",
265
+ "Residual Risk Summary"
266
+ ],
267
+ "coreSections": [],
268
+ "tasks": [
269
+ {
270
+ "id": "task-design-fmea-execute",
271
+ "title": "Generate simulated output",
272
+ "description": "Generate a simulated MVP output for this work package.",
273
+ "type": "generation",
274
+ "executable": true,
275
+ "status": "todo"
276
+ }
277
+ ],
278
+ "outputs": [],
279
+ "status": "todo",
280
+ "priority": "high"
281
+ },
282
+ {
283
+ "id": "wp-final-engineering-concept",
284
+ "title": "Final Engineering Concept",
285
+ "shortName": "Engineering Concept",
286
+ "phase": "Design Product",
287
+ "objective": "Finalize hardware BOM, software SBOM, and feature list.",
288
+ "inputFiles": [],
289
+ "outputFiles": [
290
+ "Hardware BOM",
291
+ "Software SBOM",
292
+ "Feature List",
293
+ "Traceability Mapping"
294
+ ],
295
+ "coreSections": [],
296
+ "tasks": [
297
+ {
298
+ "id": "task-final-engineering-concept-execute",
299
+ "title": "Generate simulated output",
300
+ "description": "Generate a simulated MVP output for this work package.",
301
+ "type": "generation",
302
+ "executable": true,
303
+ "status": "todo"
304
+ }
305
+ ],
306
+ "outputs": [],
307
+ "status": "todo",
308
+ "priority": "high"
309
+ },
310
+ {
311
+ "id": "wp-service-ability-review",
312
+ "title": "Service Ability Review",
313
+ "shortName": "Service Review",
314
+ "phase": "Design Product",
315
+ "objective": "Use a quality gate questionnaire to confirm serviceability readiness before next phase, tooling, or production preparation.",
316
+ "inputFiles": [],
317
+ "outputFiles": [
318
+ "Service Quality Gate Questionnaire",
319
+ "Service Readiness Score",
320
+ "Go/Conditional Go/No-Go Recommendation",
321
+ "Open Service Action Items"
322
+ ],
323
+ "coreSections": [],
324
+ "tasks": [
325
+ {
326
+ "id": "task-service-ability-review-execute",
327
+ "title": "Generate simulated output",
328
+ "description": "Generate a simulated MVP output for this work package.",
329
+ "type": "generation",
330
+ "executable": true,
331
+ "status": "todo"
332
+ }
333
+ ],
334
+ "outputs": [],
335
+ "status": "todo",
336
+ "priority": "medium"
337
+ }
338
+ ]
agentic_pm_demo_codex_plans/eslint.config.mjs ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
9
+ ]);
10
+
11
+ export default eslintConfig;
agentic_pm_demo_codex_plans/lib/command-parser.ts ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { CommandMode, ParsedCommand, WorkPackage } from "./work-package-types";
2
+
3
+ const commandModes: CommandMode[] = ["ask", "plan", "change", "execute"];
4
+
5
+ export function parseWorkPackageCommand(
6
+ input: string,
7
+ workPackages: WorkPackage[],
8
+ ): ParsedCommand | null {
9
+ const trimmedInput = input.trim();
10
+
11
+ if (!trimmedInput.startsWith("@")) {
12
+ return null;
13
+ }
14
+
15
+ const mentionBody = trimmedInput.slice(1);
16
+ const candidates = workPackages
17
+ .flatMap((workPackage) => [
18
+ { name: workPackage.title, workPackage },
19
+ { name: workPackage.shortName, workPackage },
20
+ ])
21
+ .sort((left, right) => right.name.length - left.name.length);
22
+
23
+ for (const candidate of candidates) {
24
+ const packageName = candidate.name.toLowerCase();
25
+ const inputName = mentionBody.toLowerCase();
26
+
27
+ if (!inputName.startsWith(packageName)) {
28
+ continue;
29
+ }
30
+
31
+ const boundaryCharacter = mentionBody[candidate.name.length];
32
+ const isBoundary =
33
+ boundaryCharacter === undefined ||
34
+ boundaryCharacter === " " ||
35
+ boundaryCharacter === "#" ||
36
+ boundaryCharacter === ":";
37
+
38
+ if (!isBoundary) {
39
+ continue;
40
+ }
41
+
42
+ const remainingText = mentionBody.slice(candidate.name.length).trim();
43
+ const [maybeMode, ...instructionParts] = remainingText.split(/\s+/);
44
+ const mode = commandModes.includes(maybeMode as CommandMode)
45
+ ? (maybeMode as CommandMode)
46
+ : "ask";
47
+
48
+ return {
49
+ referencedPackageName: candidate.name,
50
+ workPackageId: candidate.workPackage.id,
51
+ mode,
52
+ instruction:
53
+ mode === maybeMode
54
+ ? instructionParts.join(" ").trim()
55
+ : remainingText.replace(/^#\S+\s*/, "").trim(),
56
+ };
57
+ }
58
+
59
+ return {
60
+ mode: "ask",
61
+ instruction: trimmedInput,
62
+ };
63
+ }
64
+
65
+ export function getPackageSuggestions(input: string, workPackages: WorkPackage[]) {
66
+ const match = input.match(/@([A-Za-z\s-]*)$/);
67
+
68
+ if (!match) {
69
+ return [];
70
+ }
71
+
72
+ const query = match[1].toLowerCase().trim();
73
+
74
+ return workPackages
75
+ .filter((workPackage) => {
76
+ if (!query) {
77
+ return true;
78
+ }
79
+
80
+ return (
81
+ workPackage.title.toLowerCase().includes(query) ||
82
+ workPackage.shortName.toLowerCase().includes(query)
83
+ );
84
+ })
85
+ .slice(0, 6);
86
+ }
agentic_pm_demo_codex_plans/lib/mock-agent.ts ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { parseWorkPackageCommand } from "./command-parser";
2
+ import {
3
+ SIMULATED_EXECUTION_DISCLAIMER,
4
+ type CommandMode,
5
+ type OutputType,
6
+ type WorkPackage,
7
+ type WorkPackageTask,
8
+ } from "./work-package-types";
9
+
10
+ type AgentTurnResult = {
11
+ assistantMessage: string;
12
+ workPackages: WorkPackage[];
13
+ selectedPackageId?: string;
14
+ };
15
+
16
+ export function runMockAgentTurn(input: string, workPackages: WorkPackage[]): AgentTurnResult {
17
+ const command = parseWorkPackageCommand(input, workPackages);
18
+
19
+ if (!command?.workPackageId) {
20
+ return {
21
+ assistantMessage:
22
+ "I can help shape that into product work. Try citing a package like `@CRS ask what should we capture first?`, or click a Cite button in the board to pull exact context into the chat.",
23
+ workPackages,
24
+ };
25
+ }
26
+
27
+ const selectedPackage = workPackages.find((workPackage) => workPackage.id === command.workPackageId);
28
+
29
+ if (!selectedPackage) {
30
+ return {
31
+ assistantMessage:
32
+ "I could not find that work package. Use one of the package references from the board, such as `@CRS`, `@SRS`, or `@Design FMEA`.",
33
+ workPackages,
34
+ };
35
+ }
36
+
37
+ const instruction = command.instruction || "Review this work package.";
38
+ const nextPackages = workPackages.map((workPackage) => {
39
+ if (workPackage.id !== selectedPackage.id) {
40
+ return workPackage;
41
+ }
42
+
43
+ return updatePackageForMode(workPackage, command.mode, instruction);
44
+ });
45
+
46
+ return {
47
+ assistantMessage: makeAssistantMessage(selectedPackage, command.mode, instruction),
48
+ workPackages: nextPackages,
49
+ selectedPackageId: selectedPackage.id,
50
+ };
51
+ }
52
+
53
+ function updatePackageForMode(
54
+ workPackage: WorkPackage,
55
+ mode: CommandMode,
56
+ instruction: string,
57
+ ): WorkPackage {
58
+ if (mode === "ask") {
59
+ return workPackage;
60
+ }
61
+
62
+ if (mode === "plan") {
63
+ const plannedTasks = makePlannedTasks(workPackage, instruction);
64
+
65
+ return {
66
+ ...workPackage,
67
+ status: "in_progress",
68
+ tasks: [...workPackage.tasks, ...plannedTasks],
69
+ };
70
+ }
71
+
72
+ if (mode === "change") {
73
+ const changeNote = `Change request: ${instruction}`;
74
+
75
+ return {
76
+ ...workPackage,
77
+ status: "in_progress",
78
+ coreSections: workPackage.coreSections.includes(changeNote)
79
+ ? workPackage.coreSections
80
+ : [...workPackage.coreSections, changeNote],
81
+ };
82
+ }
83
+
84
+ const taskToClose = workPackage.tasks.find((task) => task.executable);
85
+
86
+ return {
87
+ ...workPackage,
88
+ status: "done",
89
+ tasks: workPackage.tasks.map((task) =>
90
+ task.id === taskToClose?.id ? { ...task, status: "done" } : task,
91
+ ),
92
+ outputs: [
93
+ {
94
+ id: `output-${workPackage.id}-${Date.now()}`,
95
+ title: `Simulated ${workPackage.shortName} Output`,
96
+ type: inferOutputType(workPackage),
97
+ content: makeSimulatedOutput(workPackage, instruction),
98
+ createdAt: new Date().toISOString(),
99
+ sourceTaskId: taskToClose?.id,
100
+ executionMode: "simulated",
101
+ disclaimer: SIMULATED_EXECUTION_DISCLAIMER,
102
+ },
103
+ ...workPackage.outputs,
104
+ ],
105
+ };
106
+ }
107
+
108
+ function makePlannedTasks(workPackage: WorkPackage, instruction: string): WorkPackageTask[] {
109
+ const normalizedInstruction = instruction.replace(/\s+/g, " ").trim();
110
+
111
+ return [
112
+ {
113
+ id: `task-${workPackage.id}-context-${Date.now()}`,
114
+ title: "Clarify cited context",
115
+ description: normalizedInstruction || `Review the current ${workPackage.shortName} context.`,
116
+ type: "planning",
117
+ executable: false,
118
+ status: "todo",
119
+ },
120
+ {
121
+ id: `task-${workPackage.id}-draft-${Date.now() + 1}`,
122
+ title: "Draft package-specific deliverable",
123
+ description: `Create a first-pass ${workPackage.outputFiles[0] ?? "deliverable"} using cited assumptions.`,
124
+ type: "generation",
125
+ executable: true,
126
+ status: "todo",
127
+ },
128
+ ];
129
+ }
130
+
131
+ function makeAssistantMessage(
132
+ workPackage: WorkPackage,
133
+ mode: CommandMode,
134
+ instruction: string,
135
+ ) {
136
+ if (mode === "ask") {
137
+ return `${workPackage.shortName} is the right context for this. I would inspect its objective, expected outputs, and downstream dependencies first. For your request — “${instruction}” — the next useful answer is to separate requirement intent from the evidence needed to validate it.`;
138
+ }
139
+
140
+ if (mode === "plan") {
141
+ return `I added a lightweight plan to ${workPackage.shortName}: clarify the cited context, then draft the first package-specific deliverable. The board is now marked in progress.`;
142
+ }
143
+
144
+ if (mode === "change") {
145
+ return `I recorded that change request inside ${workPackage.shortName} as package context, so future asks/plans/executions can cite it.`;
146
+ }
147
+
148
+ return `I generated a simulated ${workPackage.shortName} output and saved it back to the package. It is explicitly marked as simulated, with the required demo disclaimer.`;
149
+ }
150
+
151
+ function inferOutputType(workPackage: WorkPackage): OutputType {
152
+ const title = workPackage.title.toLowerCase();
153
+
154
+ if (title.includes("fmea")) return "risk_table";
155
+ if (title.includes("certification")) return "checklist";
156
+ if (title.includes("test")) return "test_case";
157
+ if (title.includes("industrial design")) return "design_brief";
158
+ if (title.includes("engineering")) return "bom";
159
+ if (title.includes("patent")) return "text";
160
+
161
+ return "table";
162
+ }
163
+
164
+ function makeSimulatedOutput(workPackage: WorkPackage, instruction: string) {
165
+ const deliverables = workPackage.outputFiles.slice(0, 4).map((outputFile) => `- ${outputFile}`);
166
+
167
+ return [
168
+ `Instruction: ${instruction}`,
169
+ "",
170
+ "Generated package content:",
171
+ ...deliverables,
172
+ "",
173
+ "Recommended next review:",
174
+ `- Confirm assumptions with the ${workPackage.phase} owner.`,
175
+ "- Convert accepted items into traceable requirements or action items.",
176
+ ].join("\n");
177
+ }
agentic_pm_demo_codex_plans/lib/work-package-types.ts ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const SIMULATED_EXECUTION_DISCLAIMER =
2
+ "This is a simulated execution result generated for demo purposes. No real external tool, engineering review, certification approval, user database, image generation service, patent search, or test system was executed.";
3
+
4
+ export type WorkPackagePhase =
5
+ | "Stakeholder Needs"
6
+ | "Specify Product"
7
+ | "Design Product"
8
+ | "Verify and Validate Product";
9
+
10
+ export type WorkPackageStatus = "todo" | "in_progress" | "done";
11
+ export type WorkPackagePriority = "low" | "medium" | "high";
12
+
13
+ export type TaskType =
14
+ | "research"
15
+ | "planning"
16
+ | "generation"
17
+ | "testing"
18
+ | "design"
19
+ | "analysis"
20
+ | "review"
21
+ | "compliance"
22
+ | "risk";
23
+
24
+ export type OutputType =
25
+ | "text"
26
+ | "table"
27
+ | "code"
28
+ | "image_prompt"
29
+ | "design_brief"
30
+ | "test_case"
31
+ | "checklist"
32
+ | "risk_table"
33
+ | "bom"
34
+ | "sbom"
35
+ | "feature_list";
36
+
37
+ export type ExecutionMode = "simulated" | "real";
38
+
39
+ export type WorkPackageTask = {
40
+ id: string;
41
+ title: string;
42
+ description: string;
43
+ type: TaskType;
44
+ executable: boolean;
45
+ status: WorkPackageStatus;
46
+ };
47
+
48
+ export type WorkPackageOutput = {
49
+ id: string;
50
+ title: string;
51
+ type: OutputType;
52
+ content: string;
53
+ createdAt: string;
54
+ sourceTaskId?: string;
55
+ executionMode: ExecutionMode;
56
+ disclaimer: string;
57
+ };
58
+
59
+ export type WorkPackageDeliverable = {
60
+ name: string;
61
+ required: boolean | string;
62
+ description?: string;
63
+ };
64
+
65
+ export type WorkPackage = {
66
+ id: string;
67
+ title: string;
68
+ shortName: string;
69
+ phase: WorkPackagePhase;
70
+ objective: string;
71
+ inputFiles: string[];
72
+ outputFiles: string[];
73
+ coreSections: string[];
74
+ deliverables?: WorkPackageDeliverable[];
75
+ tasks: WorkPackageTask[];
76
+ outputs: WorkPackageOutput[];
77
+ status: WorkPackageStatus;
78
+ priority: WorkPackagePriority;
79
+ };
80
+
81
+ export type CommandMode = "ask" | "plan" | "change" | "execute";
82
+
83
+ export type ParsedCommand = {
84
+ referencedPackageName?: string;
85
+ workPackageId?: string;
86
+ mode: CommandMode;
87
+ instruction: string;
88
+ };
89
+
90
+ export type CitationReference = {
91
+ id: string;
92
+ packageId: string;
93
+ packageShortName: string;
94
+ packageTitle: string;
95
+ label: string;
96
+ quote?: string;
97
+ };
98
+
99
+ export type ChatMessage = {
100
+ id: string;
101
+ role: "assistant" | "user";
102
+ content: string;
103
+ createdAt: string;
104
+ references?: CitationReference[];
105
+ };
agentic_pm_demo_codex_plans/next.config.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ allowedDevOrigins: ["127.0.0.1", "localhost"],
5
+ turbopack: {
6
+ root: process.cwd(),
7
+ },
8
+ };
9
+
10
+ export default nextConfig;
agentic_pm_demo_codex_plans/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
agentic_pm_demo_codex_plans/package.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "agentic-pm-demo",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev --webpack",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint ."
10
+ },
11
+ "dependencies": {
12
+ "next": "16.2.4",
13
+ "react": "19.2.4",
14
+ "react-dom": "19.2.4"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^20",
18
+ "@types/react": "^19",
19
+ "@types/react-dom": "^19",
20
+ "eslint": "^9",
21
+ "eslint-config-next": "16.2.4",
22
+ "typescript": "^5"
23
+ }
24
+ }
agentic_pm_demo_codex_plans/plans/01-product-vision-and-scope.md ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 01 Product Vision and Scope Plan
2
+
3
+ ## Project Name
4
+
5
+ Agentic PM Demo for IoT Product Planning
6
+
7
+ ## Goal
8
+
9
+ Build a lightweight web demo that shows how an AI assistant helps a product manager transform an ambiguous IoT product idea into structured product work packages.
10
+
11
+ The app should not behave like a normal chatbot only. It should demonstrate an agentic product management workflow: the user chats with AI on the left, and the AI creates, updates, and displays structured work packages on the right.
12
+
13
+ ## Target User
14
+
15
+ - Product manager
16
+ - Innovation manager
17
+ - Solution owner
18
+ - Technical product lead
19
+ - IoT / Industrial IoT product owner
20
+
21
+ ## First Demo Domain
22
+
23
+ IoT product planning, especially industrial IoT or smart device development.
24
+
25
+ ## MVP Scope
26
+
27
+ 1. Two-column web interface.
28
+ 2. Chat input and message history.
29
+ 3. Work package board.
30
+ 4. Seed work package catalog for IoT product development.
31
+ 5. LLM API route using OpenAI-compatible format.
32
+ 6. Work package command parser.
33
+ 7. Simulated execution outputs.
34
+ 8. Local development support.
35
+ 9. Docker deployment for Hugging Face Spaces.
36
+
37
+ ## Out of Scope for MVP
38
+
39
+ - User login
40
+ - Persistent database
41
+ - Multi-user collaboration
42
+ - Real tool execution
43
+ - Real patent database search
44
+ - Real certification/legal database lookup
45
+ - Real test execution
46
+ - Real user database access
47
+ - Real image generation
48
+ - Complex drag-and-drop kanban
49
+
50
+ ## Success Criteria
51
+
52
+ 1. User can run the app locally.
53
+ 2. User can enter an IoT product idea.
54
+ 3. App generates structured work packages.
55
+ 4. Board displays the packages clearly.
56
+ 5. User can reference packages using `@`.
57
+ 6. User can `ask`, `plan`, `change`, and `execute` package content.
58
+ 7. Simulated execution outputs are written back to the board.
59
+ 8. App can be deployed to Hugging Face Spaces with Docker.
agentic_pm_demo_codex_plans/plans/02-ux-layout-and-interaction.md ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 02 UX Layout and Interaction Plan
2
+
3
+ ## Layout
4
+
5
+ Use a two-column layout.
6
+
7
+ ### Left Column: Chat Workspace
8
+
9
+ - App title
10
+ - Short instruction
11
+ - Message history
12
+ - Chat input
13
+ - Send button
14
+ - Example prompts
15
+
16
+ ### Right Column: Work Package Board
17
+
18
+ - Phase sections
19
+ - Work package cards
20
+ - Work package status
21
+ - Inputs and outputs
22
+ - Tasks
23
+ - Simulated execution outputs
24
+ - Quick actions
25
+
26
+ ## Board Grouping
27
+
28
+ Group work packages by phase:
29
+
30
+ 1. Stakeholder Needs
31
+ 2. Specify Product
32
+ 3. Design Product
33
+ 4. Verify and Validate Product
34
+
35
+ ## Work Package Card
36
+
37
+ Each card should show:
38
+
39
+ - Title
40
+ - Short name
41
+ - Phase
42
+ - Objective
43
+ - Status
44
+ - Priority
45
+ - Input files
46
+ - Output files
47
+ - Key sections
48
+ - Tasks
49
+ - Outputs
50
+ - Quick actions
51
+
52
+ ## Quick Actions
53
+
54
+ Each card should include:
55
+
56
+ - Ask
57
+ - Plan
58
+ - Change
59
+ - Execute
60
+ - Copy Reference
61
+
62
+ Clicking a quick action should pre-fill the chat input, for example:
63
+
64
+ ```text
65
+ @CRS ask
66
+ @SRS plan
67
+ @Design FMEA change
68
+ @Test Management execute
69
+ ```
70
+
71
+ ## Interaction Examples
72
+
73
+ Free-form product idea:
74
+
75
+ ```text
76
+ I want to build an IoT product for production-line tightening quality monitoring.
77
+ ```
78
+
79
+ Reference command:
80
+
81
+ ```text
82
+ @SRS ask Which components are needed to implement CRS-001?
83
+ ```
84
+
85
+ Plan command:
86
+
87
+ ```text
88
+ @Test Management plan Break this into detailed test planning tasks.
89
+ ```
90
+
91
+ Change command:
92
+
93
+ ```text
94
+ @Product Certification change Add EU Data Act as a checklist item.
95
+ ```
96
+
97
+ Execute command:
98
+
99
+ ```text
100
+ @Design FMEA execute Generate a Design FMEA for this IoT product.
101
+ ```
102
+
103
+ ## Simulated Execution UX Rule
104
+
105
+ Every simulated output card must show:
106
+
107
+ ```text
108
+ Simulated Execution
109
+ This is a simulated execution result generated for demo purposes. No real external tool or formal review was executed.
110
+ ```
agentic_pm_demo_codex_plans/plans/03-work-package-data-model.md ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 03 Work Package Data Model
2
+
3
+ ## TypeScript Types
4
+
5
+ ```ts
6
+ export type WorkPackagePhase =
7
+ | "Stakeholder Needs"
8
+ | "Specify Product"
9
+ | "Design Product"
10
+ | "Verify and Validate Product";
11
+
12
+ export type WorkPackageStatus = "todo" | "in_progress" | "done";
13
+ export type WorkPackagePriority = "low" | "medium" | "high";
14
+
15
+ export type TaskType =
16
+ | "research"
17
+ | "planning"
18
+ | "generation"
19
+ | "testing"
20
+ | "design"
21
+ | "analysis"
22
+ | "review"
23
+ | "compliance"
24
+ | "risk";
25
+
26
+ export type OutputType =
27
+ | "text"
28
+ | "table"
29
+ | "code"
30
+ | "image_prompt"
31
+ | "design_brief"
32
+ | "test_case"
33
+ | "checklist"
34
+ | "risk_table"
35
+ | "bom"
36
+ | "sbom"
37
+ | "feature_list";
38
+
39
+ export type ExecutionMode = "simulated" | "real";
40
+
41
+ export type WorkPackageTask = {
42
+ id: string;
43
+ title: string;
44
+ description: string;
45
+ type: TaskType;
46
+ executable: boolean;
47
+ status: WorkPackageStatus;
48
+ };
49
+
50
+ export type WorkPackageOutput = {
51
+ id: string;
52
+ title: string;
53
+ type: OutputType;
54
+ content: string;
55
+ createdAt: string;
56
+ sourceTaskId?: string;
57
+ executionMode: ExecutionMode;
58
+ disclaimer: string;
59
+ };
60
+
61
+ export type WorkPackageDeliverable = {
62
+ name: string;
63
+ required: boolean | string;
64
+ description?: string;
65
+ };
66
+
67
+ export type WorkPackage = {
68
+ id: string;
69
+ title: string;
70
+ shortName: string;
71
+ phase: WorkPackagePhase;
72
+ objective: string;
73
+ inputFiles: string[];
74
+ outputFiles: string[];
75
+ coreSections: string[];
76
+ deliverables?: WorkPackageDeliverable[];
77
+ tasks: WorkPackageTask[];
78
+ outputs: WorkPackageOutput[];
79
+ status: WorkPackageStatus;
80
+ priority: WorkPackagePriority;
81
+ };
82
+ ```
83
+
84
+ ## Command Model
85
+
86
+ ```ts
87
+ export type CommandMode = "ask" | "plan" | "change" | "execute";
88
+
89
+ export type ParsedCommand = {
90
+ referencedPackageName?: string;
91
+ mode?: CommandMode;
92
+ instruction: string;
93
+ };
94
+ ```
95
+
96
+ ## Command Pattern
97
+
98
+ ```text
99
+ @WorkPackageName [ask | plan | change | execute] user instruction
100
+ ```
101
+
102
+ ## Board Update Rules
103
+
104
+ ### ask
105
+
106
+ - Do not update work package data.
107
+ - Return only chat response.
108
+
109
+ ### plan
110
+
111
+ - May add or refine tasks.
112
+ - May update status to `in_progress`.
113
+
114
+ ### change
115
+
116
+ - May update objective, tasks, input files, output files, core sections, or deliverables.
117
+ - Explain what changed.
118
+
119
+ ### execute
120
+
121
+ - Must create a `WorkPackageOutput`.
122
+ - Must set `executionMode` to `simulated`.
123
+ - Must include disclaimer.
124
+ - May mark related task as `done`.
agentic_pm_demo_codex_plans/plans/04-work-package-catalog.md ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 04 Work Package Catalog
2
+
3
+ ## Phase Overview
4
+
5
+ ```text
6
+ Stakeholder Needs
7
+ 1. CRS
8
+ 2. Final Concept
9
+
10
+ Specify Product
11
+ 3. SRS
12
+ 4. Safety of Products
13
+ 5. Product Certification
14
+ 6. Test Management
15
+
16
+ Design Product
17
+ 7. Decompose System
18
+ 8. Pre-Development — skipped in MVP
19
+ 9. Industrial Design
20
+ 10. Patent Check
21
+ 11. Design FMEA
22
+ 12. Final Engineering Concept
23
+ 13. Service Ability Review
24
+
25
+ Verify and Validate Product
26
+ 14. Build and Test A-Sample — future/optional
27
+ ```
28
+
29
+ ---
30
+
31
+ # 1. CRS — Customer Requirements Specification
32
+
33
+ ## Objective
34
+
35
+ Capture and structure customer, user, legal, safety, security, manufacturing, logistics, trade, and service requirements as the source for system requirements and product planning.
36
+
37
+ ## Inputs
38
+
39
+ VOC notes, customer interviews, market feedback, user pain points, safety input, legal/certification input, security input, manufacturing constraints, logistics constraints, service feedback, trade/sales requirements.
40
+
41
+ In MVP, these inputs can be mocked.
42
+
43
+ ## Outputs
44
+
45
+ Customer Requirement List, User Stories, Requirement Classification, CRS-to-SRS Traceability Input, Revision History.
46
+
47
+ ## Core Sections
48
+
49
+ CRS-User, CRS-Safety, CRS-Legal, CRS-Security, CRS-Technology, CRS-Manufacturing, CRS-Logistics, CRS-Trade, CRS-Service.
50
+
51
+ ## Simulated Tasks
52
+
53
+ Generate customer requirements; classify requirements; convert VOC to user stories; prepare CRS-to-SRS traceability.
54
+
55
+ ---
56
+
57
+ # 2. Final Concept
58
+
59
+ ## Objective
60
+
61
+ Transform CRS into a product concept using user insights, benefits, reasons to believe, and verbal concept validation.
62
+
63
+ ## Outputs
64
+
65
+ Final Product Concept, Insight-Benefit-Reason-to-Believe Matrix, Validation of Verbal Concept, Concept Visual Brief, 2D Product Concept Image Prompt.
66
+
67
+ ## Core Sections
68
+
69
+ User group, user size, 3–5 insights, 3–5 benefits, 3–5 reasons to believe, validation of verbal concept, product concept visual brief.
70
+
71
+ ## Simulated Tasks
72
+
73
+ Generate final product concept from CRS; generate insights; generate benefit/RTB matrix; generate simulated verbal concept validation; generate 2D concept visual brief.
74
+
75
+ ---
76
+
77
+ # 3. SRS — System Requirements Specification
78
+
79
+ ## Objective
80
+
81
+ Translate CRS into traceable system-level requirements, including required functions, components, interfaces, specifications, and verification methods.
82
+
83
+ ## Key Concept
84
+
85
+ CRS explains what the customer needs. SRS explains what the system must contain and satisfy to realize the CRS.
86
+
87
+ ## Outputs
88
+
89
+ System Requirements Specification, CRS-to-SRS Traceability Matrix, Component Requirement List, Interface Requirement List, Verification Input for Test Management.
90
+
91
+ ## Core Sections
92
+
93
+ SRS ID, Related CRS ID, System Requirement Statement, Component / Module, Specification, Interface / Dependency, Verification Method, Acceptance Criteria.
94
+
95
+ ## Simulated Tasks
96
+
97
+ Generate system requirements from CRS; generate CRS-to-SRS traceability; identify required system components; generate verification methods.
98
+
99
+ ---
100
+
101
+ # 4. Safety of Products
102
+
103
+ ## Objective
104
+
105
+ Identify product safety hazards, assess initial and residual risks, define risk reduction measures, and document risk acceptance with evidence references.
106
+
107
+ ## Outputs
108
+
109
+ Product Safety Risk Assessment, Hazard List, Risk Matrix, Risk Reduction Measures, Residual Risk Assessment, Risk Acceptance Statement, Revision History.
110
+
111
+ ## Hazard Categories
112
+
113
+ Moving Elements, Accessible Parts, Thermal Hazards, Material/Substance Hazards, Ergonomic Hazards, Environment Hazards, Life Cycle Hazards.
114
+
115
+ ## Simulated Tasks
116
+
117
+ Generate intended use and foreseeable misuse; identify safety hazards; generate risk table; suggest risk reduction; generate residual risk acceptance summary.
118
+
119
+ ---
120
+
121
+ # 5. Product Certification
122
+
123
+ ## Objective
124
+
125
+ Identify applicable market access requirements, standards, certification documents, testing needs, approval milestones, and action items.
126
+
127
+ ## Outputs
128
+
129
+ Product Certification Briefing, Product Certification Plan, Certification Checklist, Applicable Standards List, Applicable Directives and Regulations List, EU DoC Input, Required Approval Document List, Certification Action Item List.
130
+
131
+ ## Certification Todo Topics
132
+
133
+ Radio module conformity assessment, radio approval test house, Bluetooth SIG/DID, North America questionnaire, battery/UN38.3, special attachments/accessories, risk assessment completion, button/coin battery, functional safety/SCF, Critical Component List, EU Data Act, label drawing review, test house vs in-house testing, required certification documents, sample condition, national approval handover.
134
+
135
+ ## Simulated Tasks
136
+
137
+ Generate product certification briefing; identify applicable standard categories; generate EU market access checklist; generate certification todo list; generate required document list.
138
+
139
+ ---
140
+
141
+ # 6. Test Management
142
+
143
+ ## Objective
144
+
145
+ Translate CRS, SRS, safety, and certification requirements into test objectives, test cases, sample size, testing time, acceptance criteria, and result assessment logic.
146
+
147
+ ## Outputs
148
+
149
+ Test Plan, Requirement-to-Test Traceability Matrix, Reliability Validation Plan, Sample Size Calculation, Test Time Calculation, Success Run Plan, WeiBayes Evaluation Summary, Test Cost Estimate.
150
+
151
+ ## Key Methods
152
+
153
+ Success Run basic, Success Run advanced, Weibull assumptions, WeiBayes evaluation, sample size estimation, test time estimation, test cost estimation.
154
+
155
+ ## Simulated Tasks
156
+
157
+ Generate requirement-to-test matrix; generate functional/performance test cases; generate reliability validation plan; estimate sample size and testing time; generate test cost and resource estimate.
158
+
159
+ ---
160
+
161
+ # 7. Decompose System
162
+
163
+ ## Objective
164
+
165
+ Decompose the product/system from the initial Engineering Concept into components, modules, standardization opportunities, technical focus areas, and critical pre-development topics.
166
+
167
+ ## Deliverables
168
+
169
+ - Technology Filter: required
170
+ - Focus Area Analysis: required if product category is `complicated-new` or `complex`
171
+ - Review of FAM: required if product category is `complex` or if FAM is used for technical risk management
172
+ - Technical Complexity Category: required
173
+ - Project Risk Class: required
174
+
175
+ ## Simulated Tasks
176
+
177
+ Generate system decomposition; generate Technology Filter; generate Focus Area Analysis; evaluate technical complexity; evaluate project risk class; identify pre-development topics.
178
+
179
+ ---
180
+
181
+ # 8. Pre-Development
182
+
183
+ Skipped in MVP.
184
+
185
+ ---
186
+
187
+ # 9. Industrial Design
188
+
189
+ ## Objective
190
+
191
+ Translate product concept, system requirements, safety, certification, and service needs into product form, visual direction, interaction layout, CMF, and concept design direction.
192
+
193
+ ## Outputs
194
+
195
+ Industrial Design Brief, 2D Concept Design Brief, CMF Proposal, HMI/Interaction Layout, Label/LED/Display/Button Layout, Product Usage Scenario Visual, Design Review Checklist.
196
+
197
+ ## Simulated Tasks
198
+
199
+ Generate industrial design brief; generate 2D concept visual prompt; generate CMF proposal; generate interaction checklist; generate design review checklist.
200
+
201
+ ---
202
+
203
+ # 10. Patent Check
204
+
205
+ ## Objective
206
+
207
+ Identify patent-related risks and possible patentable innovation points before engineering design is finalized.
208
+
209
+ ## Outputs
210
+
211
+ Patent Search Topic List, Potential Freedom-to-Operate Risk Summary, Competitor Patent Review Topics, Possible Patentable Invention Points, Design-Around Questions, IP Review Action Items.
212
+
213
+ ## MVP Rule
214
+
215
+ Must be simulated. Do not claim real patent search or legal opinion.
216
+
217
+ ---
218
+
219
+ # 11. Design FMEA
220
+
221
+ ## Objective
222
+
223
+ Identify potential design failure modes, effects, causes, controls, risk priority, recommended actions, and residual risk.
224
+
225
+ ## Outputs
226
+
227
+ Design FMEA Table, High-Risk Failure Mode List, Recommended Design Actions, Verification Action List, Residual Risk Summary, FMEA Revision History.
228
+
229
+ ## Core Fields
230
+
231
+ FMEA ID, Related CRS ID, Related SRS ID, System/Module, Function, Potential Failure Mode, Potential Effect, Severity, Cause, Occurrence, Prevention Control, Detection Control, Detection, Risk Priority, Recommended Action, Owner, Due Date, Status, Residual Risk, Verification Evidence.
232
+
233
+ ## MVP Scoring
234
+
235
+ Use simplified 1–5 scoring:
236
+
237
+ ```text
238
+ RPN = Severity × Occurrence × Detection
239
+ ```
240
+
241
+ Risk levels:
242
+
243
+ - 1–20: Low
244
+ - 21–60: Medium
245
+ - 61–125: High
246
+
247
+ ---
248
+
249
+ # 12. Final Engineering Concept
250
+
251
+ ## Objective
252
+
253
+ Finalize the engineering concept by generating the hardware BOM, software SBOM, and feature list.
254
+
255
+ ## Outputs
256
+
257
+ Hardware BOM, Software SBOM, Feature List, BOM-to-Feature Mapping, SBOM-to-Feature Mapping, Feature-to-SRS Traceability, Engineering Concept Summary, Open Engineering Decision List.
258
+
259
+ ## Hardware BOM Fields
260
+
261
+ BOM ID, Component Name, Category, Function, Related Feature, Related SRS ID, Specification, Source Type, Certification Impact, Safety Impact, Risk Note, Status.
262
+
263
+ ## Software SBOM Fields
264
+
265
+ SBOM ID, Component Name, Component Type, Function, Related Feature, Related SRS ID, Technology/Library, Version, License, Security Concern, Data Concern, Status.
266
+
267
+ ## Feature List Fields
268
+
269
+ Feature ID, Feature Name, Feature Description, User Value, Related CRS ID, Related SRS ID, Supported By Hardware, Supported By Software, Priority, Release Phase, Verification Method, Status.
270
+
271
+ ---
272
+
273
+ # 13. Service Ability Review
274
+
275
+ ## Objective
276
+
277
+ Use a quality gate questionnaire to confirm whether the product is serviceable enough to move into the next development phase, tooling investment, or production preparation.
278
+
279
+ ## Outputs
280
+
281
+ Service Ability Quality Gate Questionnaire, Service Readiness Score, Go / Conditional Go / No-Go Recommendation, Open Service Action Items, Replaceable Unit List, Spare Parts Assumptions, Diagnostic Requirement List, Service Documentation Requirement, Service Tool Requirement, Service Training Requirement.
282
+
283
+ ## Quality Gate Question Fields
284
+
285
+ Question ID, Area, Question, Related Component/Feature, Answer: Yes/No/Partial/N/A, Evidence, Risk if not closed, Required Action, Responsible Role, Due Date, Gate Impact: Blocker/Major/Minor, Status.
286
+
287
+ ## Suggested Gate Logic
288
+
289
+ - If any Blocker question is No: recommendation = No-Go
290
+ - If any Major question is No or Partial: recommendation = Conditional Go
291
+ - If all Blocker and Major questions are Yes or N/A: recommendation = Go
292
+ - Always list open actions.
293
+
294
+ ---
295
+
296
+ # 14. Build and Test A-Sample
297
+
298
+ Future/optional. This can be added later as the first verification work package after Design Product.
agentic_pm_demo_codex_plans/plans/05-reference-command-and-execution.md ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 05 Reference Command and Execution Plan
2
+
3
+ ## Goal
4
+
5
+ Support a Cursor-like interaction pattern for product work packages.
6
+
7
+ The user can reference a work package from chat and ask AI to reason about it, plan it, change it, or execute tasks inside it.
8
+
9
+ ## Command Syntax
10
+
11
+ ```text
12
+ @WorkPackageName [mode] user instruction
13
+ ```
14
+
15
+ Supported modes:
16
+
17
+ ```text
18
+ ask
19
+ plan
20
+ change
21
+ execute
22
+ ```
23
+
24
+ ## Mode Definitions
25
+
26
+ ### ask
27
+
28
+ Use when the user wants to ask a question about a selected work package.
29
+
30
+ Behavior: send selected work package as context to LLM; answer in chat; do not update board.
31
+
32
+ ### plan
33
+
34
+ Use when the user wants AI to further break down a work package.
35
+
36
+ Behavior: add/refine tasks, next steps, or deliverables.
37
+
38
+ ### change
39
+
40
+ Use when the user wants AI to directly modify a work package.
41
+
42
+ Behavior: update selected work package and explain what changed.
43
+
44
+ ### execute
45
+
46
+ Use when user wants AI to perform a task inside a work package.
47
+
48
+ MVP behavior: simulate execution, generate structured output, save output to selected work package, mark related task as done if appropriate, and show disclaimer.
49
+
50
+ ## Supported Simulated Execution Types
51
+
52
+ 1. Test case generation
53
+ 2. Automation test script generation
54
+ 3. Mock user information reading
55
+ 4. 2D design brief / image prompt generation
56
+ 5. Certification plan generation
57
+ 6. Patent check topic generation
58
+ 7. Design FMEA generation
59
+ 8. Hardware BOM / Software SBOM / Feature List generation
60
+ 9. Quality Gate questionnaire generation
61
+
62
+ ## Simulated Output Format
63
+
64
+ ```json
65
+ {
66
+ "title": "Simulated Output Title",
67
+ "type": "table | text | checklist | code | design_brief | image_prompt | risk_table | bom | sbom | feature_list",
68
+ "executionMode": "simulated",
69
+ "disclaimer": "This is a simulated execution result generated for demo purposes. No real external tool, engineering review, certification approval, user database, image generation service, patent search, or test system was executed.",
70
+ "content": "..."
71
+ }
72
+ ```
73
+
74
+ ## Command Parser Requirements
75
+
76
+ Input:
77
+
78
+ ```text
79
+ @Final Engineering Concept execute Generate BOM and SBOM.
80
+ ```
81
+
82
+ Output:
83
+
84
+ ```ts
85
+ {
86
+ referencedPackageName: "Final Engineering Concept",
87
+ mode: "execute",
88
+ instruction: "Generate BOM and SBOM."
89
+ }
90
+ ```
91
+
92
+ ## Matching Rules
93
+
94
+ Support exact title match, short name match, case-insensitive match, and trimmed spaces.
95
+
96
+ Examples:
97
+
98
+ - `@CRS`
99
+ - `@SRS`
100
+ - `@Design FMEA`
101
+ - `@Service Review`
102
+ - `@Final Engineering Concept`
103
+
104
+ ## Error Handling
105
+
106
+ If package is not found, ask user to select an existing package and show close matches if possible.
107
+
108
+ If mode is missing, default to `ask`.
agentic_pm_demo_codex_plans/plans/06-llm-api-and-prompt-contract.md ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 06 LLM API and Prompt Contract
2
+
3
+ ## Goal
4
+
5
+ Use an OpenAI-compatible API contract so the app can connect to OpenAI, OpenRouter, DeepSeek, Qwen, or other compatible providers.
6
+
7
+ ## Environment Variables
8
+
9
+ ```env
10
+ LLM_API_BASE_URL=https://api.openai.com/v1
11
+ LLM_API_KEY=your_api_key
12
+ LLM_MODEL=gpt-4.1-mini
13
+ ```
14
+
15
+ ## API Route
16
+
17
+ ```text
18
+ POST /api/chat
19
+ ```
20
+
21
+ ## Request Payload
22
+
23
+ ```ts
24
+ type ChatRequest = {
25
+ messages: ChatMessage[];
26
+ workPackages: WorkPackage[];
27
+ selectedWorkPackageId?: string;
28
+ parsedCommand?: ParsedCommand;
29
+ };
30
+ ```
31
+
32
+ ## Response Payload
33
+
34
+ ```ts
35
+ type ChatResponse = {
36
+ assistantMessage: string;
37
+ boardPatch?: WorkPackagePatch;
38
+ simulatedOutput?: WorkPackageOutput;
39
+ };
40
+ ```
41
+
42
+ ## Structured Output
43
+
44
+ Ask the LLM to return JSON in this shape:
45
+
46
+ ```json
47
+ {
48
+ "assistantMessage": "string",
49
+ "boardAction": {
50
+ "type": "none | create | update | append_output",
51
+ "workPackageId": "string",
52
+ "fields": {},
53
+ "output": {}
54
+ }
55
+ }
56
+ ```
57
+
58
+ ## Behavior Rules
59
+
60
+ ### Free-form Product Idea
61
+
62
+ When the user provides a product idea without `@` command:
63
+
64
+ 1. Interpret it as product planning request.
65
+ 2. Generate or refine the work package board.
66
+ 3. Use IoT work package catalog as default structure.
67
+ 4. Return concise explanation in chat.
68
+
69
+ ### ask
70
+
71
+ Answer based on referenced package. Do not modify board.
72
+
73
+ ### plan
74
+
75
+ Generate tasks or next steps. Update selected package tasks.
76
+
77
+ ### change
78
+
79
+ Modify selected package based on instruction. Return updated fields.
80
+
81
+ ### execute
82
+
83
+ Generate simulated output. Append to selected package outputs. Include disclaimer.
84
+
85
+ ## Critical Rule
86
+
87
+ The LLM must not claim real execution for MVP tasks.
agentic_pm_demo_codex_plans/plans/07-technical-architecture-and-file-structure.md ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 07 Technical Architecture and File Structure
2
+
3
+ ## Stack
4
+
5
+ - Next.js
6
+ - React
7
+ - TypeScript
8
+ - Tailwind CSS
9
+ - shadcn/ui
10
+ - OpenAI-compatible API
11
+ - Docker
12
+
13
+ ## Architecture
14
+
15
+ ```text
16
+ Browser
17
+
18
+ Next.js React UI
19
+
20
+ Client state management
21
+
22
+ Next.js API Route
23
+
24
+ OpenAI-compatible LLM API
25
+ ```
26
+
27
+ ## Suggested File Structure
28
+
29
+ ```text
30
+ agentic-pm-demo/
31
+ app/
32
+ page.tsx
33
+ api/
34
+ chat/
35
+ route.ts
36
+ components/
37
+ ChatPanel.tsx
38
+ WorkPackageBoard.tsx
39
+ WorkPackageCard.tsx
40
+ WorkPackageOutput.tsx
41
+ CommandInput.tsx
42
+ lib/
43
+ command-parser.ts
44
+ work-package-types.ts
45
+ work-package-utils.ts
46
+ llm-client.ts
47
+ board-actions.ts
48
+ data/
49
+ work-packages.seed.json
50
+ sample-user-profile.json
51
+ sample-interview-notes.md
52
+ sample-customer-scenario.md
53
+ prompts/
54
+ work-package-system-prompt.md
55
+ plans/
56
+ AGENTS.md
57
+ README.md
58
+ Dockerfile
59
+ package.json
60
+ .env.example
61
+ ```
62
+
63
+ ## Frontend Components
64
+
65
+ ### ChatPanel
66
+
67
+ Renders message history, handles user input, sends chat request, displays assistant replies.
68
+
69
+ ### WorkPackageBoard
70
+
71
+ Groups packages by phase, renders package cards, applies board patches.
72
+
73
+ ### WorkPackageCard
74
+
75
+ Shows package summary, tasks, outputs, and quick actions.
76
+
77
+ ### WorkPackageOutput
78
+
79
+ Renders simulated outputs with disclaimer and expand/collapse behavior.
80
+
81
+ ## Backend API
82
+
83
+ `POST /api/chat` should:
84
+
85
+ 1. Receive chat messages and current board state.
86
+ 2. Parse command if needed.
87
+ 3. Call LLM using OpenAI-compatible API.
88
+ 4. Return assistant message and board action.
89
+ 5. Validate simulated execution disclaimer for `execute` mode.
90
+
91
+ ## Mock Agent Fallback
92
+
93
+ If no API key exists, use local mock behavior:
94
+
95
+ - `execute`: generate mock output.
96
+ - `ask`: return generic explanation.
97
+ - `plan`: add sample tasks.
98
+ - `change`: update simple fields.
99
+
100
+ This lets the app run without API configuration.
agentic_pm_demo_codex_plans/plans/08-local-run-and-huggingface-deployment.md ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 08 Local Run and Hugging Face Deployment Plan
2
+
3
+ ## Local Setup
4
+
5
+ ```bash
6
+ npm install
7
+ npm run dev
8
+ ```
9
+
10
+ Open:
11
+
12
+ ```text
13
+ http://localhost:3000
14
+ ```
15
+
16
+ ## Environment Variables
17
+
18
+ Create `.env.local`:
19
+
20
+ ```env
21
+ LLM_API_BASE_URL=https://api.openai.com/v1
22
+ LLM_API_KEY=your_api_key
23
+ LLM_MODEL=gpt-4.1-mini
24
+ ```
25
+
26
+ ## Mock Mode
27
+
28
+ The app must run without `LLM_API_KEY`.
29
+
30
+ If no API key exists:
31
+
32
+ - Show UI badge: `Mock mode`
33
+ - Use local simulated responses
34
+ - Do not crash
35
+
36
+ ## Dockerfile
37
+
38
+ ```dockerfile
39
+ FROM node:20-alpine
40
+
41
+ WORKDIR /app
42
+
43
+ COPY package*.json ./
44
+ RUN npm install
45
+
46
+ COPY . .
47
+
48
+ RUN npm run build
49
+
50
+ EXPOSE 3000
51
+
52
+ ENV PORT=3000
53
+ CMD ["npm", "start"]
54
+ ```
55
+
56
+ ## package.json Scripts
57
+
58
+ ```json
59
+ {
60
+ "scripts": {
61
+ "dev": "next dev",
62
+ "build": "next build",
63
+ "start": "next start -p 3000",
64
+ "lint": "next lint"
65
+ }
66
+ }
67
+ ```
68
+
69
+ ## Hugging Face Spaces Deployment
70
+
71
+ Use a Docker Space.
72
+
73
+ Steps:
74
+
75
+ 1. Create a new Hugging Face Space.
76
+ 2. Select Docker as SDK.
77
+ 3. Push repository to the Space git repo.
78
+ 4. Add Space Secrets:
79
+ - `LLM_API_BASE_URL`
80
+ - `LLM_API_KEY`
81
+ - `LLM_MODEL`
82
+ 5. Wait for Space build.
83
+ 6. Open the running demo.
agentic_pm_demo_codex_plans/prompts/work-package-system-prompt.md ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Work Package System Prompt
2
+
3
+ You are an Agentic Product Management assistant for IoT product development.
4
+
5
+ You help the user transform product ideas into structured product development work packages.
6
+
7
+ ## Domain
8
+
9
+ The default domain is IoT product planning, especially industrial IoT, smart devices, connected tools, production-line monitoring, local dashboards, device connectivity, and system integration.
10
+
11
+ ## Work Package Catalog
12
+
13
+ Use this phase structure:
14
+
15
+ ```text
16
+ Stakeholder Needs
17
+ 1. CRS
18
+ 2. Final Concept
19
+
20
+ Specify Product
21
+ 3. SRS
22
+ 4. Safety of Products
23
+ 5. Product Certification
24
+ 6. Test Management
25
+
26
+ Design Product
27
+ 7. Decompose System
28
+ 8. Pre-Development — skipped in MVP
29
+ 9. Industrial Design
30
+ 10. Patent Check
31
+ 11. Design FMEA
32
+ 12. Final Engineering Concept
33
+ 13. Service Ability Review
34
+
35
+ Verify and Validate Product
36
+ 14. Build and Test A-Sample — future/optional
37
+ ```
38
+
39
+ ## Command Modes
40
+
41
+ The user may reference a package:
42
+
43
+ ```text
44
+ @WorkPackageName ask ...
45
+ @WorkPackageName plan ...
46
+ @WorkPackageName change ...
47
+ @WorkPackageName execute ...
48
+ ```
49
+
50
+ ## Behavior Rules
51
+
52
+ ### ask
53
+
54
+ Answer questions using the selected work package as context. Do not change the board.
55
+
56
+ ### plan
57
+
58
+ Break down the work package into more detailed tasks, questions, or deliverables. Update the package.
59
+
60
+ ### change
61
+
62
+ Modify the selected package according to the user instruction. Update the package.
63
+
64
+ ### execute
65
+
66
+ Generate a simulated output for the selected package. Append it to the package outputs.
67
+
68
+ ## Critical MVP Rule
69
+
70
+ All execution is simulated.
71
+
72
+ Never claim that a real test, real user database, real certification review, real patent search, real engineering review, real image generation, or real external tool was executed.
73
+
74
+ Every execution output must include:
75
+
76
+ ```text
77
+ This is a simulated execution result generated for demo purposes. No real external tool, engineering review, certification approval, user database, image generation service, patent search, or test system was executed.
78
+ ```
79
+
80
+ ## Output Format
81
+
82
+ Return JSON only in this shape:
83
+
84
+ ```json
85
+ {
86
+ "assistantMessage": "Short user-facing response.",
87
+ "boardAction": {
88
+ "type": "none | create | update | append_output",
89
+ "workPackageId": "string or null",
90
+ "fields": {},
91
+ "output": {
92
+ "id": "string",
93
+ "title": "string",
94
+ "type": "text | table | code | image_prompt | design_brief | test_case | checklist | risk_table | bom | sbom | feature_list",
95
+ "content": "string",
96
+ "createdAt": "ISO timestamp",
97
+ "sourceTaskId": "string or null",
98
+ "executionMode": "simulated",
99
+ "disclaimer": "This is a simulated execution result generated for demo purposes. No real external tool, engineering review, certification approval, user database, image generation service, patent search, or test system was executed."
100
+ }
101
+ }
102
+ }
103
+ ```
agentic_pm_demo_codex_plans/tsconfig.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": false,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts"
31
+ ],
32
+ "exclude": ["node_modules"]
33
+ }
app/api/chat/route.ts ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import type {
4
+ ParsedCommand,
5
+ WorkPackage,
6
+ } from "@/lib/work-package-types";
7
+ import { SIMULATED_EXECUTION_DISCLAIMER } from "@/lib/work-package-types";
8
+ import { runMockAgent } from "@/lib/mock-agent";
9
+ import type { ChatResponse } from "@/lib/board-actions";
10
+ import {
11
+ buildExecutionPrompt,
12
+ buildRealExecutionResponse,
13
+ REAL_AUTOMATION_DISCLAIMER,
14
+ } from "@/lib/automation-agent";
15
+ import { generateLlmText } from "@/lib/llm-client";
16
+ import {
17
+ DEFAULT_LLM_BASE_URL,
18
+ DEFAULT_LLM_MODEL,
19
+ isLlmConfigReady,
20
+ normalizeLlmConfig,
21
+ } from "@/lib/llm-config";
22
+ import { compactTracePreview } from "@/lib/chat-trace";
23
+ import { readFile } from "node:fs/promises";
24
+ import path from "node:path";
25
+
26
+ const ChatMessageSchema = z.object({
27
+ role: z.enum(["user", "assistant", "system"]),
28
+ content: z.string(),
29
+ });
30
+
31
+ const ParsedCommandSchema = z.object({
32
+ referencedPackageName: z.string().optional(),
33
+ mode: z.enum(["ask", "plan", "change", "execute"]).optional(),
34
+ instruction: z.string(),
35
+ });
36
+
37
+ const OutputTypeSchema = z.enum([
38
+ "text",
39
+ "table",
40
+ "code",
41
+ "image_prompt",
42
+ "design_brief",
43
+ "test_case",
44
+ "checklist",
45
+ "risk_table",
46
+ "bom",
47
+ "sbom",
48
+ "feature_list",
49
+ ]);
50
+
51
+ const WorkPackageOutputSchema = z.object({
52
+ id: z.string(),
53
+ title: z.string(),
54
+ type: OutputTypeSchema,
55
+ content: z.string(),
56
+ createdAt: z.string(),
57
+ sourceTaskId: z.string().nullable().optional(),
58
+ executionMode: z.enum(["simulated", "real"]),
59
+ disclaimer: z.string(),
60
+ });
61
+
62
+ const BoardActionSchema = z.object({
63
+ type: z.enum(["none", "create", "update", "append_output", "replace_all"]),
64
+ workPackageId: z.string().nullable(),
65
+ fields: z.record(z.string(), z.unknown()).optional(),
66
+ output: WorkPackageOutputSchema.optional(),
67
+ });
68
+
69
+ const LlmJsonSchema = z.object({
70
+ assistantMessage: z.string(),
71
+ boardAction: BoardActionSchema,
72
+ });
73
+
74
+ const ChatRequestSchema = z.object({
75
+ messages: z.array(ChatMessageSchema),
76
+ workPackages: z.array(z.any()),
77
+ selectedWorkPackageId: z.string().nullable().optional(),
78
+ parsedCommand: ParsedCommandSchema.optional(),
79
+ llmConfig: z
80
+ .object({
81
+ apiKey: z.string().optional(),
82
+ baseUrl: z.string().optional(),
83
+ model: z.string().optional(),
84
+ })
85
+ .optional(),
86
+ });
87
+
88
+ function safeJsonParse(text: string): unknown {
89
+ try {
90
+ return JSON.parse(text);
91
+ } catch {
92
+ // Try extracting the first JSON object.
93
+ const start = text.indexOf("{");
94
+ const end = text.lastIndexOf("}");
95
+ if (start !== -1 && end !== -1 && end > start) {
96
+ const slice = text.slice(start, end + 1);
97
+ return JSON.parse(slice);
98
+ }
99
+ throw new Error("Invalid JSON");
100
+ }
101
+ }
102
+
103
+ async function loadSystemPrompt(): Promise<string> {
104
+ // Prefer repo root prompt; fall back to the planning pack copy.
105
+ const rootPrompt = path.join(process.cwd(), "prompts/work-package-system-prompt.md");
106
+ try {
107
+ return await readFile(rootPrompt, "utf8");
108
+ } catch {
109
+ const packPrompt = path.join(
110
+ process.cwd(),
111
+ "agentic_pm_demo_codex_plans/prompts/work-package-system-prompt.md",
112
+ );
113
+ return await readFile(packPrompt, "utf8");
114
+ }
115
+ }
116
+
117
+ function ensureExecuteDisclaimer(payload: z.infer<typeof LlmJsonSchema>) {
118
+ const out = payload.boardAction?.output;
119
+ if (!out) return payload;
120
+ if (!out.disclaimer) {
121
+ out.disclaimer =
122
+ out.executionMode === "real"
123
+ ? REAL_AUTOMATION_DISCLAIMER
124
+ : SIMULATED_EXECUTION_DISCLAIMER;
125
+ }
126
+ return payload;
127
+ }
128
+
129
+ function resolveLlmConfig(
130
+ llmConfig?: { apiKey?: string; baseUrl?: string; model?: string } | null,
131
+ ) {
132
+ if (isLlmConfigReady(llmConfig)) {
133
+ return normalizeLlmConfig(llmConfig);
134
+ }
135
+
136
+ const fromEnv = normalizeLlmConfig({
137
+ apiKey: process.env.LLM_API_KEY,
138
+ baseUrl: process.env.LLM_API_BASE_URL || DEFAULT_LLM_BASE_URL,
139
+ model: process.env.LLM_MODEL || DEFAULT_LLM_MODEL,
140
+ });
141
+
142
+ return isLlmConfigReady(fromEnv) ? fromEnv : null;
143
+ }
144
+
145
+ function latestUserText(messages: Array<{ role: string; content: string }>) {
146
+ return [...messages].reverse().find((message) => message.role === "user")?.content;
147
+ }
148
+
149
+ export async function POST(req: Request) {
150
+ const json = await req.json().catch(() => null);
151
+ const parsedReq = ChatRequestSchema.safeParse(json);
152
+ if (!parsedReq.success) {
153
+ return NextResponse.json(
154
+ { assistantMessage: "Bad request.", boardAction: { type: "none", workPackageId: null } },
155
+ { status: 400 },
156
+ );
157
+ }
158
+
159
+ const {
160
+ messages,
161
+ workPackages,
162
+ selectedWorkPackageId,
163
+ parsedCommand,
164
+ llmConfig,
165
+ } = parsedReq.data;
166
+
167
+ const resolvedConfig = resolveLlmConfig(llmConfig);
168
+ const currentWorkPackages = workPackages as WorkPackage[];
169
+
170
+ // Mock-first: if no key is present, stay local.
171
+ if (!resolvedConfig) {
172
+ const resp = runMockAgent({
173
+ messages: messages.map((message, index) => ({
174
+ id: `server-msg-${index}`,
175
+ role: message.role === "system" ? "assistant" : message.role,
176
+ content: message.content,
177
+ createdAt: new Date().toISOString(),
178
+ })),
179
+ workPackages: workPackages as WorkPackage[],
180
+ selectedWorkPackageId: selectedWorkPackageId ?? undefined,
181
+ parsedCommand: parsedCommand as ParsedCommand | undefined,
182
+ });
183
+ return NextResponse.json({
184
+ ...resp,
185
+ trace: {
186
+ provider: "mock",
187
+ model: "mock-agent",
188
+ requestPreview: compactTracePreview(
189
+ latestUserText(messages) || "No user message.",
190
+ ),
191
+ responsePreview: compactTracePreview(resp.assistantMessage),
192
+ },
193
+ } satisfies ChatResponse);
194
+ }
195
+
196
+ const systemPrompt = await loadSystemPrompt();
197
+ const userText = latestUserText(messages) || "";
198
+ const selectedWp =
199
+ currentWorkPackages.find((wp) => wp.id === selectedWorkPackageId) ?? null;
200
+
201
+ if (parsedCommand?.mode === "execute") {
202
+ const targetPackage =
203
+ selectedWp ??
204
+ currentWorkPackages.find(
205
+ (wp) =>
206
+ wp.title.toLowerCase() ===
207
+ parsedCommand.referencedPackageName?.toLowerCase() ||
208
+ wp.shortName.toLowerCase() ===
209
+ parsedCommand.referencedPackageName?.toLowerCase(),
210
+ );
211
+
212
+ if (!targetPackage) {
213
+ return NextResponse.json(
214
+ {
215
+ assistantMessage: "No matching work package was found for execution.",
216
+ boardAction: { type: "none", workPackageId: null },
217
+ } satisfies ChatResponse,
218
+ { status: 200 },
219
+ );
220
+ }
221
+
222
+ try {
223
+ const llmResult = await generateLlmText({
224
+ config: resolvedConfig,
225
+ systemPrompt:
226
+ "You are an expert product development PM and systems engineer. Generate complete, practical, package-specific deliverables in markdown.",
227
+ userPrompt: buildExecutionPrompt({
228
+ workPackage: targetPackage,
229
+ workPackages: currentWorkPackages,
230
+ productIdea: userText,
231
+ instruction: parsedCommand.instruction,
232
+ }),
233
+ temperature: 0.2,
234
+ maxTokens: 2800,
235
+ });
236
+
237
+ return NextResponse.json(
238
+ {
239
+ ...buildRealExecutionResponse({
240
+ workPackage: targetPackage,
241
+ generatedContent: llmResult.text,
242
+ instruction: parsedCommand.instruction,
243
+ productIdea: userText,
244
+ }),
245
+ trace: {
246
+ provider: "live",
247
+ model: resolvedConfig.model,
248
+ endpoint: llmResult.endpoint,
249
+ requestPreview: llmResult.requestPreview,
250
+ responsePreview: llmResult.responsePreview,
251
+ },
252
+ } satisfies ChatResponse,
253
+ { status: 200 },
254
+ );
255
+ } catch (err) {
256
+ return NextResponse.json(
257
+ {
258
+ assistantMessage: `Live execution failed. (No board changes applied.)\n\n${String(err)}`,
259
+ boardAction: { type: "none", workPackageId: null },
260
+ trace: {
261
+ provider: "live",
262
+ model: resolvedConfig.model,
263
+ responsePreview: compactTracePreview(String(err)),
264
+ },
265
+ } satisfies ChatResponse,
266
+ { status: 200 },
267
+ );
268
+ }
269
+ }
270
+
271
+ const contextBlock = JSON.stringify(
272
+ {
273
+ selectedWorkPackageId: selectedWorkPackageId ?? null,
274
+ selectedWorkPackage: selectedWp,
275
+ parsedCommand: parsedCommand ?? null,
276
+ workPackages: currentWorkPackages,
277
+ },
278
+ null,
279
+ 2,
280
+ );
281
+
282
+ const conversationBlock = messages
283
+ .map((message) => `${message.role.toUpperCase()}: ${message.content}`)
284
+ .join("\n\n");
285
+
286
+ try {
287
+ const llmResult = await generateLlmText({
288
+ config: resolvedConfig,
289
+ systemPrompt,
290
+ userPrompt: [
291
+ "Return JSON ONLY that matches the required contract.",
292
+ "Use the context to decide whether and how the board should change.",
293
+ `Context:\n${contextBlock}`,
294
+ `Conversation:\n${conversationBlock}`,
295
+ ].join("\n\n"),
296
+ temperature: 0.2,
297
+ maxTokens: 2200,
298
+ });
299
+
300
+ if (!llmResult.text) {
301
+ return NextResponse.json(
302
+ {
303
+ assistantMessage: "LLM returned an empty response.",
304
+ boardAction: { type: "none", workPackageId: null },
305
+ trace: {
306
+ provider: "live",
307
+ model: resolvedConfig.model,
308
+ endpoint: llmResult.endpoint,
309
+ requestPreview: llmResult.requestPreview,
310
+ },
311
+ } satisfies ChatResponse,
312
+ { status: 200 },
313
+ );
314
+ }
315
+
316
+ const parsedJson = safeJsonParse(llmResult.text);
317
+ const validated = LlmJsonSchema.safeParse(parsedJson);
318
+ if (!validated.success) {
319
+ return NextResponse.json(
320
+ {
321
+ assistantMessage:
322
+ "LLM returned invalid JSON for the required contract. (No board changes applied.)",
323
+ boardAction: { type: "none", workPackageId: null },
324
+ trace: {
325
+ provider: "live",
326
+ model: resolvedConfig.model,
327
+ endpoint: llmResult.endpoint,
328
+ requestPreview: llmResult.requestPreview,
329
+ responsePreview: llmResult.responsePreview,
330
+ },
331
+ } satisfies ChatResponse,
332
+ { status: 200 },
333
+ );
334
+ }
335
+
336
+ const payload = ensureExecuteDisclaimer(validated.data);
337
+ return NextResponse.json({
338
+ ...payload,
339
+ trace: {
340
+ provider: "live",
341
+ model: resolvedConfig.model,
342
+ endpoint: llmResult.endpoint,
343
+ requestPreview: llmResult.requestPreview,
344
+ responsePreview: llmResult.responsePreview,
345
+ },
346
+ } satisfies ChatResponse);
347
+ } catch (err) {
348
+ return NextResponse.json(
349
+ {
350
+ assistantMessage: `Network or parsing error calling LLM. (No board changes applied.)\n\n${String(err)}`,
351
+ boardAction: { type: "none", workPackageId: null },
352
+ trace: {
353
+ provider: "live",
354
+ model: resolvedConfig.model,
355
+ responsePreview: compactTracePreview(String(err)),
356
+ },
357
+ } satisfies ChatResponse,
358
+ { status: 200 },
359
+ );
360
+ }
361
+ }
app/api/test-connection/route.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { generateLlmText } from "@/lib/llm-client";
4
+ import { isLlmConfigReady, normalizeLlmConfig } from "@/lib/llm-config";
5
+
6
+ const RequestSchema = z.object({
7
+ llmConfig: z.object({
8
+ apiKey: z.string().optional(),
9
+ baseUrl: z.string().optional(),
10
+ model: z.string().optional(),
11
+ }),
12
+ });
13
+
14
+ export async function POST(req: Request) {
15
+ const json = await req.json().catch(() => null);
16
+ const parsed = RequestSchema.safeParse(json);
17
+
18
+ if (!parsed.success) {
19
+ return NextResponse.json(
20
+ { ok: false, status: "error", message: "Bad request." },
21
+ { status: 400 },
22
+ );
23
+ }
24
+
25
+ if (!isLlmConfigReady(parsed.data.llmConfig)) {
26
+ return NextResponse.json({
27
+ ok: false,
28
+ status: "idle",
29
+ message: "Add API key, base URL, and model to test the connection.",
30
+ });
31
+ }
32
+
33
+ const config = normalizeLlmConfig(parsed.data.llmConfig);
34
+
35
+ try {
36
+ const result = await generateLlmText({
37
+ config,
38
+ systemPrompt:
39
+ "You are running a connection test. Reply with a short confirmation only.",
40
+ userPrompt:
41
+ "Connection test. Reply with exactly: Connected to the configured model.",
42
+ temperature: 0,
43
+ maxTokens: 60,
44
+ });
45
+
46
+ return NextResponse.json({
47
+ ok: true,
48
+ status: "connected",
49
+ message: result.text,
50
+ model: config.model,
51
+ endpoint: result.endpoint,
52
+ });
53
+ } catch (error) {
54
+ return NextResponse.json({
55
+ ok: false,
56
+ status: "error",
57
+ message: String(error),
58
+ model: config.model,
59
+ });
60
+ }
61
+ }
app/favicon.ico ADDED
app/globals.css ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "shadcn/tailwind.css";
4
+
5
+ @custom-variant dark (&:is(.dark *));
6
+
7
+ @theme inline {
8
+ --color-background: var(--background);
9
+ --color-foreground: var(--foreground);
10
+ --font-sans: var(--font-sans);
11
+ --font-mono: var(--font-geist-mono);
12
+ --font-heading: var(--font-sans);
13
+ --color-sidebar-ring: var(--sidebar-ring);
14
+ --color-sidebar-border: var(--sidebar-border);
15
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
16
+ --color-sidebar-accent: var(--sidebar-accent);
17
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
18
+ --color-sidebar-primary: var(--sidebar-primary);
19
+ --color-sidebar-foreground: var(--sidebar-foreground);
20
+ --color-sidebar: var(--sidebar);
21
+ --color-chart-5: var(--chart-5);
22
+ --color-chart-4: var(--chart-4);
23
+ --color-chart-3: var(--chart-3);
24
+ --color-chart-2: var(--chart-2);
25
+ --color-chart-1: var(--chart-1);
26
+ --color-ring: var(--ring);
27
+ --color-input: var(--input);
28
+ --color-border: var(--border);
29
+ --color-destructive: var(--destructive);
30
+ --color-accent-foreground: var(--accent-foreground);
31
+ --color-accent: var(--accent);
32
+ --color-muted-foreground: var(--muted-foreground);
33
+ --color-muted: var(--muted);
34
+ --color-secondary-foreground: var(--secondary-foreground);
35
+ --color-secondary: var(--secondary);
36
+ --color-primary-foreground: var(--primary-foreground);
37
+ --color-primary: var(--primary);
38
+ --color-popover-foreground: var(--popover-foreground);
39
+ --color-popover: var(--popover);
40
+ --color-card-foreground: var(--card-foreground);
41
+ --color-card: var(--card);
42
+ --radius-sm: calc(var(--radius) * 0.6);
43
+ --radius-md: calc(var(--radius) * 0.8);
44
+ --radius-lg: var(--radius);
45
+ --radius-xl: calc(var(--radius) * 1.4);
46
+ --radius-2xl: calc(var(--radius) * 1.8);
47
+ --radius-3xl: calc(var(--radius) * 2.2);
48
+ --radius-4xl: calc(var(--radius) * 2.6);
49
+ }
50
+
51
+ :root {
52
+ --background: oklch(1 0 0);
53
+ --foreground: oklch(0.145 0 0);
54
+ --card: oklch(1 0 0);
55
+ --card-foreground: oklch(0.145 0 0);
56
+ --popover: oklch(1 0 0);
57
+ --popover-foreground: oklch(0.145 0 0);
58
+ --primary: oklch(0.205 0 0);
59
+ --primary-foreground: oklch(0.985 0 0);
60
+ --secondary: oklch(0.97 0 0);
61
+ --secondary-foreground: oklch(0.205 0 0);
62
+ --muted: oklch(0.97 0 0);
63
+ --muted-foreground: oklch(0.556 0 0);
64
+ --accent: oklch(0.97 0 0);
65
+ --accent-foreground: oklch(0.205 0 0);
66
+ --destructive: oklch(0.577 0.245 27.325);
67
+ --border: oklch(0.922 0 0);
68
+ --input: oklch(0.922 0 0);
69
+ --ring: oklch(0.708 0 0);
70
+ --chart-1: oklch(0.87 0 0);
71
+ --chart-2: oklch(0.556 0 0);
72
+ --chart-3: oklch(0.439 0 0);
73
+ --chart-4: oklch(0.371 0 0);
74
+ --chart-5: oklch(0.269 0 0);
75
+ --radius: 0.625rem;
76
+ --sidebar: oklch(0.985 0 0);
77
+ --sidebar-foreground: oklch(0.145 0 0);
78
+ --sidebar-primary: oklch(0.205 0 0);
79
+ --sidebar-primary-foreground: oklch(0.985 0 0);
80
+ --sidebar-accent: oklch(0.97 0 0);
81
+ --sidebar-accent-foreground: oklch(0.205 0 0);
82
+ --sidebar-border: oklch(0.922 0 0);
83
+ --sidebar-ring: oklch(0.708 0 0);
84
+ }
85
+
86
+ .dark {
87
+ --background: oklch(0.145 0 0);
88
+ --foreground: oklch(0.985 0 0);
89
+ --card: oklch(0.205 0 0);
90
+ --card-foreground: oklch(0.985 0 0);
91
+ --popover: oklch(0.205 0 0);
92
+ --popover-foreground: oklch(0.985 0 0);
93
+ --primary: oklch(0.922 0 0);
94
+ --primary-foreground: oklch(0.205 0 0);
95
+ --secondary: oklch(0.269 0 0);
96
+ --secondary-foreground: oklch(0.985 0 0);
97
+ --muted: oklch(0.269 0 0);
98
+ --muted-foreground: oklch(0.708 0 0);
99
+ --accent: oklch(0.269 0 0);
100
+ --accent-foreground: oklch(0.985 0 0);
101
+ --destructive: oklch(0.704 0.191 22.216);
102
+ --border: oklch(1 0 0 / 10%);
103
+ --input: oklch(1 0 0 / 15%);
104
+ --ring: oklch(0.556 0 0);
105
+ --chart-1: oklch(0.87 0 0);
106
+ --chart-2: oklch(0.556 0 0);
107
+ --chart-3: oklch(0.439 0 0);
108
+ --chart-4: oklch(0.371 0 0);
109
+ --chart-5: oklch(0.269 0 0);
110
+ --sidebar: oklch(0.205 0 0);
111
+ --sidebar-foreground: oklch(0.985 0 0);
112
+ --sidebar-primary: oklch(0.488 0.243 264.376);
113
+ --sidebar-primary-foreground: oklch(0.985 0 0);
114
+ --sidebar-accent: oklch(0.269 0 0);
115
+ --sidebar-accent-foreground: oklch(0.985 0 0);
116
+ --sidebar-border: oklch(1 0 0 / 10%);
117
+ --sidebar-ring: oklch(0.556 0 0);
118
+ }
119
+
120
+ @layer base {
121
+ * {
122
+ @apply border-border outline-ring/50;
123
+ }
124
+ body {
125
+ @apply bg-background text-foreground;
126
+ }
127
+ html {
128
+ @apply font-sans;
129
+ }
130
+ }
131
+
132
+ @layer components {
133
+ .markdown-content h1,
134
+ .markdown-content h2,
135
+ .markdown-content h3,
136
+ .markdown-content h4 {
137
+ margin-top: 1rem;
138
+ margin-bottom: 0.5rem;
139
+ font-weight: 600;
140
+ line-height: 1.4;
141
+ }
142
+
143
+ .markdown-content p,
144
+ .markdown-content ul,
145
+ .markdown-content ol,
146
+ .markdown-content pre,
147
+ .markdown-content table,
148
+ .markdown-content blockquote {
149
+ margin-top: 0.5rem;
150
+ margin-bottom: 0.5rem;
151
+ }
152
+
153
+ .markdown-content ul,
154
+ .markdown-content ol {
155
+ padding-left: 1.25rem;
156
+ }
157
+
158
+ .markdown-content ul {
159
+ list-style: disc;
160
+ }
161
+
162
+ .markdown-content ol {
163
+ list-style: decimal;
164
+ }
165
+
166
+ .markdown-content code {
167
+ background: color-mix(in oklab, var(--muted) 70%, transparent);
168
+ border-radius: 0.375rem;
169
+ padding: 0.1rem 0.3rem;
170
+ font-size: 0.85em;
171
+ }
172
+
173
+ .markdown-content pre {
174
+ overflow-x: auto;
175
+ border-radius: 0.875rem;
176
+ background: color-mix(in oklab, var(--muted) 55%, transparent);
177
+ padding: 0.875rem;
178
+ }
179
+
180
+ .markdown-content pre code {
181
+ background: transparent;
182
+ padding: 0;
183
+ }
184
+
185
+ .markdown-content table {
186
+ width: 100%;
187
+ border-collapse: collapse;
188
+ font-size: 0.92em;
189
+ }
190
+
191
+ .markdown-content th,
192
+ .markdown-content td {
193
+ border: 1px solid var(--border);
194
+ padding: 0.5rem 0.625rem;
195
+ text-align: left;
196
+ vertical-align: top;
197
+ }
198
+
199
+ .markdown-content blockquote {
200
+ border-left: 3px solid var(--border);
201
+ padding-left: 0.875rem;
202
+ color: var(--muted-foreground);
203
+ }
204
+ }
app/layout.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import { TooltipProvider } from "@/components/ui/tooltip";
4
+ import "./globals.css";
5
+
6
+ const geistSans = Geist({
7
+ variable: "--font-geist-sans",
8
+ subsets: ["latin"],
9
+ });
10
+
11
+ const geistMono = Geist_Mono({
12
+ variable: "--font-geist-mono",
13
+ subsets: ["latin"],
14
+ });
15
+
16
+ export const metadata: Metadata = {
17
+ title: "Agentic PM Demo",
18
+ description: "Agentic product management demo for IoT product planning.",
19
+ };
20
+
21
+ export default function RootLayout({
22
+ children,
23
+ }: Readonly<{
24
+ children: React.ReactNode;
25
+ }>) {
26
+ return (
27
+ <html
28
+ lang="en"
29
+ className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
30
+ >
31
+ <body className="min-h-full flex flex-col">
32
+ <TooltipProvider>{children}</TooltipProvider>
33
+ </body>
34
+ </html>
35
+ );
36
+ }
app/page.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { AppShell } from "@/components/AppShell";
2
+
3
+ export default function Home() {
4
+ return <AppShell />;
5
+ }
components.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "radix-nova",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "rtl": false,
15
+ "aliases": {
16
+ "components": "@/components",
17
+ "utils": "@/lib/utils",
18
+ "ui": "@/components/ui",
19
+ "lib": "@/lib",
20
+ "hooks": "@/hooks"
21
+ },
22
+ "menuColor": "default",
23
+ "menuAccent": "subtle",
24
+ "registries": {}
25
+ }
components/AgentLogPanel.tsx ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import type { AgentLogEntry } from "@/lib/work-package-types";
4
+ import { ScrollArea } from "@/components/ui/scroll-area";
5
+ import { Badge } from "@/components/ui/badge";
6
+
7
+ function levelTone(level: AgentLogEntry["level"]) {
8
+ if (level === "success") return "text-emerald-700";
9
+ if (level === "warn") return "text-amber-700";
10
+ if (level === "error") return "text-rose-700";
11
+ return "text-foreground";
12
+ }
13
+
14
+ function formatLogTime(value: string) {
15
+ return value.slice(11, 19);
16
+ }
17
+
18
+ export function AgentLogPanel(props: {
19
+ logs: AgentLogEntry[];
20
+ busy: boolean;
21
+ }) {
22
+ const { logs, busy } = props;
23
+
24
+ return (
25
+ <div className="flex h-full min-h-0 flex-col rounded-2xl bg-muted/24 px-1">
26
+ <div className="px-3 py-2 md:px-4">
27
+ <div className="flex items-center justify-between gap-2">
28
+ <div>
29
+ <div className="text-sm font-semibold">Agent Log</div>
30
+ <div className="text-xs text-muted-foreground">
31
+ Background activity, command routing, and board updates.
32
+ </div>
33
+ </div>
34
+ <Badge variant={busy ? "secondary" : "outline"} className="text-[11px]">
35
+ {busy ? "Running" : "Idle"}
36
+ </Badge>
37
+ </div>
38
+ </div>
39
+
40
+ <ScrollArea className="flex-1">
41
+ <div className="px-3 py-1 font-mono text-[11px] leading-5 md:px-4">
42
+ {logs.length ? (
43
+ logs
44
+ .slice()
45
+ .reverse()
46
+ .map((log) => (
47
+ <div key={log.id} className="py-1 last:border-b-0">
48
+ <div className="flex items-start justify-between gap-2">
49
+ <div className={["min-w-0", levelTone(log.level)].join(" ")}>
50
+ <div className="font-semibold">{log.title}</div>
51
+ <div className="mt-0.5 whitespace-pre-wrap break-words text-muted-foreground/90">
52
+ {log.detail}
53
+ </div>
54
+ </div>
55
+ <div
56
+ className="shrink-0 text-[10px] text-muted-foreground"
57
+ suppressHydrationWarning
58
+ >
59
+ {formatLogTime(log.createdAt)}
60
+ </div>
61
+ </div>
62
+ </div>
63
+ ))
64
+ ) : (
65
+ <div className="text-muted-foreground">
66
+ No agent activity yet.
67
+ </div>
68
+ )}
69
+ </div>
70
+ </ScrollArea>
71
+ </div>
72
+ );
73
+ }
components/AppShell.tsx ADDED
@@ -0,0 +1,629 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import type {
5
+ AgentLogEntry,
6
+ ChatMessage,
7
+ ConnectionStatus,
8
+ WorkPackage,
9
+ } from "@/lib/work-package-types";
10
+ import type { LlmConfig } from "@/lib/llm-config";
11
+ import type { ModelTrace } from "@/lib/chat-trace";
12
+ import { parseCommand } from "@/lib/command-parser";
13
+ import { applyBoardAction, type ChatResponse } from "@/lib/board-actions";
14
+ import { runMockAgent } from "@/lib/mock-agent";
15
+ import { hydrateWorkPackages } from "@/lib/work-package-specs";
16
+ import { buildAutomationInstruction } from "@/lib/automation-agent";
17
+ import { extractProductTitle } from "@/lib/product-title";
18
+ import { AgentLogPanel } from "@/components/AgentLogPanel";
19
+ import { ChatPanel } from "@/components/ChatPanel";
20
+ import { WorkPackageBoard } from "@/components/WorkPackageBoard";
21
+ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
22
+ import {
23
+ isLlmConfigReady,
24
+ LLM_SETTINGS_STORAGE_KEY,
25
+ normalizeLlmConfig,
26
+ } from "@/lib/llm-config";
27
+
28
+ import seedWorkPackages from "@/agentic_pm_demo_codex_plans/data/work-packages.seed.json";
29
+
30
+ type ViewMode = "chat" | "board";
31
+
32
+ function nowIso() {
33
+ return new Date().toISOString();
34
+ }
35
+
36
+ function newId(prefix: string) {
37
+ return `${prefix}-${crypto.randomUUID()}`;
38
+ }
39
+
40
+ function welcomeMessage(live: boolean) {
41
+ return live
42
+ ? "Live mode is connected. Type a product idea to auto-run the full board, or use a work package command like:\n`@SRS ask ...` `@Design FMEA execute ...`"
43
+ : "Mock mode is active until you add an API key in Model Settings below.\n\nType a product idea to auto-run the full board, or use a work package command like:\n`@SRS ask ...` `@Design FMEA execute ...`";
44
+ }
45
+
46
+ export function AppShell() {
47
+ const [view, setView] = useState<ViewMode>("chat");
48
+ const [projectTitle, setProjectTitle] = useState("Agentic PM Demo");
49
+ const [workPackages, setWorkPackages] = useState<WorkPackage[]>(() =>
50
+ hydrateWorkPackages(seedWorkPackages as WorkPackage[]),
51
+ );
52
+ const [selectedWorkPackageId, setSelectedWorkPackageId] = useState<
53
+ string | undefined
54
+ >(() => (seedWorkPackages as WorkPackage[])[0]?.id);
55
+ const [detailOpen, setDetailOpen] = useState(false);
56
+ const [messages, setMessages] = useState<ChatMessage[]>(() => {
57
+ return [
58
+ {
59
+ id: "welcome-message",
60
+ role: "assistant",
61
+ content: welcomeMessage(false),
62
+ createdAt: nowIso(),
63
+ },
64
+ ];
65
+ });
66
+ const [agentLogs, setAgentLogs] = useState<AgentLogEntry[]>(() => [
67
+ {
68
+ id: newId("log"),
69
+ title: "Workspace ready",
70
+ detail:
71
+ "Workspace ready. Add an API key below for live automation, or stay in mock mode.",
72
+ level: "info",
73
+ createdAt: nowIso(),
74
+ },
75
+ ]);
76
+ const [draft, setDraft] = useState("");
77
+ const [busy, setBusy] = useState(false);
78
+ const [settingsLoaded, setSettingsLoaded] = useState(false);
79
+ const [connectionStatus, setConnectionStatus] =
80
+ useState<ConnectionStatus>("idle");
81
+ const [connectionMessage, setConnectionMessage] = useState(
82
+ "Add your model settings to enable live automation.",
83
+ );
84
+ const [activeWorkPackageId, setActiveWorkPackageId] = useState<
85
+ string | undefined
86
+ >();
87
+ const [currentActivity, setCurrentActivity] = useState(
88
+ "Ready for a product idea or a package command.",
89
+ );
90
+ const [llmConfig, setLlmConfig] = useState<LlmConfig>(() =>
91
+ normalizeLlmConfig(),
92
+ );
93
+ const lastConnectionLogKeyRef = useRef("");
94
+
95
+ useEffect(() => {
96
+ try {
97
+ const raw = window.localStorage.getItem(LLM_SETTINGS_STORAGE_KEY);
98
+ if (raw) {
99
+ const parsed = JSON.parse(raw) as Partial<LlmConfig>;
100
+ setLlmConfig(normalizeLlmConfig(parsed));
101
+ }
102
+ } catch {
103
+ // ignore malformed local storage
104
+ } finally {
105
+ setSettingsLoaded(true);
106
+ }
107
+ }, []);
108
+
109
+ useEffect(() => {
110
+ if (!settingsLoaded) return;
111
+ try {
112
+ window.localStorage.setItem(
113
+ LLM_SETTINGS_STORAGE_KEY,
114
+ JSON.stringify(llmConfig),
115
+ );
116
+ } catch {
117
+ // ignore persistence failures
118
+ }
119
+ }, [llmConfig, settingsLoaded]);
120
+
121
+ useEffect(() => {
122
+ if (typeof document === "undefined") return;
123
+ document.title =
124
+ projectTitle === "Agentic PM Demo"
125
+ ? projectTitle
126
+ : `${projectTitle} · Agentic PM Demo`;
127
+ }, [projectTitle]);
128
+
129
+ useEffect(() => {
130
+ setMessages((prev) => {
131
+ if (!prev.length || prev[0]?.id !== "welcome-message") return prev;
132
+ const next = [...prev];
133
+ next[0] = {
134
+ ...next[0],
135
+ content: welcomeMessage(connectionStatus === "connected"),
136
+ };
137
+ return next;
138
+ });
139
+ }, [connectionStatus]);
140
+
141
+ useEffect(() => {
142
+ if (!settingsLoaded) return;
143
+
144
+ if (!isLlmConfigReady(llmConfig)) {
145
+ setConnectionStatus("idle");
146
+ setConnectionMessage("Add API key, base URL, and model to enable live automation.");
147
+ return;
148
+ }
149
+
150
+ let cancelled = false;
151
+ const timeout = window.setTimeout(async () => {
152
+ setConnectionStatus("checking");
153
+ setConnectionMessage("Testing model connection...");
154
+
155
+ try {
156
+ const res = await fetch("/api/test-connection", {
157
+ method: "POST",
158
+ headers: { "content-type": "application/json" },
159
+ body: JSON.stringify({ llmConfig }),
160
+ });
161
+ const data = (await res.json()) as {
162
+ ok?: boolean;
163
+ status?: ConnectionStatus;
164
+ message?: string;
165
+ model?: string;
166
+ };
167
+
168
+ if (cancelled) return;
169
+
170
+ if (data.ok) {
171
+ setConnectionStatus("connected");
172
+ setConnectionMessage(data.message || "Connection verified.");
173
+ const logKey = `${llmConfig.baseUrl}|${llmConfig.model}|connected`;
174
+ if (lastConnectionLogKeyRef.current !== logKey) {
175
+ lastConnectionLogKeyRef.current = logKey;
176
+ addLog(
177
+ "Model connected",
178
+ `${llmConfig.model} is ready for live automation.`,
179
+ "success",
180
+ );
181
+ }
182
+ return;
183
+ }
184
+
185
+ setConnectionStatus(data.status === "error" ? "error" : "idle");
186
+ setConnectionMessage(data.message || "Connection test did not complete.");
187
+ const logKey = `${llmConfig.baseUrl}|${llmConfig.model}|${data.message}`;
188
+ if (data.status === "error" && lastConnectionLogKeyRef.current !== logKey) {
189
+ lastConnectionLogKeyRef.current = logKey;
190
+ addLog(
191
+ "Model connection failed",
192
+ data.message || "Connection test failed.",
193
+ "warn",
194
+ );
195
+ }
196
+ } catch (error) {
197
+ if (cancelled) return;
198
+ setConnectionStatus("error");
199
+ setConnectionMessage(String(error));
200
+ }
201
+ }, 700);
202
+
203
+ return () => {
204
+ cancelled = true;
205
+ window.clearTimeout(timeout);
206
+ };
207
+ }, [llmConfig, settingsLoaded]);
208
+
209
+ const isMockMode = useMemo(
210
+ () => connectionStatus !== "connected",
211
+ [connectionStatus],
212
+ );
213
+
214
+ function logModelTrace(trace: ModelTrace | undefined) {
215
+ if (!trace) return;
216
+
217
+ if (trace.requestPreview) {
218
+ addLog(
219
+ "Model request",
220
+ `${trace.model ?? "unknown"}${trace.endpoint ? ` @ ${trace.endpoint}` : ""} :: ${trace.requestPreview}`,
221
+ trace.provider === "live" ? "info" : "warn",
222
+ );
223
+ }
224
+
225
+ if (trace.responsePreview) {
226
+ addLog(
227
+ "Model response",
228
+ trace.responsePreview,
229
+ trace.provider === "live" ? "success" : "info",
230
+ );
231
+ }
232
+ }
233
+
234
+ function addLog(title: string, detail: string, level: AgentLogEntry["level"]) {
235
+ setAgentLogs((prev) => [
236
+ ...prev,
237
+ {
238
+ id: newId("log"),
239
+ title,
240
+ detail,
241
+ level,
242
+ createdAt: nowIso(),
243
+ },
244
+ ]);
245
+ }
246
+
247
+ async function requestChatResponse(args: {
248
+ nextMessages: ChatMessage[];
249
+ boardSnapshot: WorkPackage[];
250
+ selectedId?: string;
251
+ parsedCommand: {
252
+ referencedPackageName?: string;
253
+ mode?: "ask" | "plan" | "change" | "execute";
254
+ instruction: string;
255
+ };
256
+ }) {
257
+ const res = await fetch("/api/chat", {
258
+ method: "POST",
259
+ headers: { "content-type": "application/json" },
260
+ body: JSON.stringify({
261
+ messages: args.nextMessages.map((message) => ({
262
+ role: message.role,
263
+ content: message.content,
264
+ })),
265
+ workPackages: args.boardSnapshot,
266
+ selectedWorkPackageId: args.selectedId ?? null,
267
+ parsedCommand: args.parsedCommand,
268
+ llmConfig,
269
+ }),
270
+ });
271
+
272
+ if (!res.ok) {
273
+ throw new Error(`Request failed with status ${res.status}.`);
274
+ }
275
+
276
+ return (await res.json()) as ChatResponse;
277
+ }
278
+
279
+ async function automateBoardFromProductIdea(args: {
280
+ productIdea: string;
281
+ nextMessages: ChatMessage[];
282
+ fallbackToMock?: boolean;
283
+ }) {
284
+ const hydrated = hydrateWorkPackages(workPackages, args.productIdea);
285
+ let boardState = hydrated;
286
+ const completedShortNames: string[] = [];
287
+ const newTitle = extractProductTitle(args.productIdea);
288
+
289
+ setProjectTitle(newTitle);
290
+ setWorkPackages(hydrated);
291
+ setDetailOpen(false);
292
+ setActiveWorkPackageId(undefined);
293
+ setCurrentActivity(`Preparing board for ${newTitle}.`);
294
+ addLog(
295
+ "Board hydrated",
296
+ `Prepared ${hydrated.length} work packages from the product idea.`,
297
+ "info",
298
+ );
299
+
300
+ for (const workPackage of hydrated) {
301
+ setActiveWorkPackageId(workPackage.id);
302
+ setCurrentActivity(`Running ${workPackage.shortName} for ${newTitle}.`);
303
+ addLog(
304
+ "Package running",
305
+ `${workPackage.shortName} is running with ${
306
+ isMockMode ? "mock mode" : llmConfig.model
307
+ }.`,
308
+ "info",
309
+ );
310
+
311
+ const response = args.fallbackToMock
312
+ ? runMockAgent({
313
+ messages: args.nextMessages,
314
+ workPackages: boardState,
315
+ selectedWorkPackageId: workPackage.id,
316
+ parsedCommand: {
317
+ referencedPackageName: workPackage.title,
318
+ mode: "execute",
319
+ instruction: buildAutomationInstruction(workPackage),
320
+ },
321
+ })
322
+ : await requestChatResponse({
323
+ nextMessages: args.nextMessages,
324
+ boardSnapshot: boardState,
325
+ selectedId: workPackage.id,
326
+ parsedCommand: {
327
+ referencedPackageName: workPackage.title,
328
+ mode: "execute",
329
+ instruction: buildAutomationInstruction(workPackage),
330
+ },
331
+ });
332
+
333
+ if (response.boardAction) {
334
+ boardState = applyBoardAction(boardState, response.boardAction);
335
+ setWorkPackages(boardState);
336
+ }
337
+ logModelTrace(response.trace);
338
+
339
+ completedShortNames.push(workPackage.shortName);
340
+ addLog(
341
+ "Package completed",
342
+ `${workPackage.shortName} completed.`,
343
+ "success",
344
+ );
345
+ }
346
+
347
+ setActiveWorkPackageId(undefined);
348
+ setCurrentActivity(`Completed board automation for ${newTitle}.`);
349
+
350
+ setMessages((prev) => [
351
+ ...prev,
352
+ {
353
+ id: newId("m"),
354
+ role: "assistant",
355
+ content: isMockMode
356
+ ? `Completed all ${completedShortNames.length} work packages in mock mode.\n\nGenerated outputs: ${completedShortNames.join(", ")}`
357
+ : `Completed all ${completedShortNames.length} work packages with ${llmConfig.model}.\n\nGenerated outputs: ${completedShortNames.join(", ")}`,
358
+ createdAt: nowIso(),
359
+ },
360
+ ]);
361
+ addLog(
362
+ "Board automation finished",
363
+ `Generated outputs for ${completedShortNames.join(", ")}.`,
364
+ "success",
365
+ );
366
+ }
367
+
368
+ async function handleChatSend(rawText: string) {
369
+ const text = rawText.trim();
370
+ if (!text) return;
371
+
372
+ const userMsg: ChatMessage = {
373
+ id: newId("m"),
374
+ role: "user",
375
+ content: text,
376
+ createdAt: nowIso(),
377
+ };
378
+
379
+ const nextMessages = [...messages, userMsg];
380
+
381
+ setMessages(nextMessages);
382
+ setDraft("");
383
+
384
+ const parsed = parseCommand(text, workPackages);
385
+ if (parsed.matchedWorkPackageId) {
386
+ setSelectedWorkPackageId(parsed.matchedWorkPackageId);
387
+ setDetailOpen(true);
388
+ addLog(
389
+ "Package selected",
390
+ `Focused ${parsed.matchedWorkPackageTitle ?? parsed.parsed.referencedPackageName ?? "package"} from command context.`,
391
+ "info",
392
+ );
393
+ } else {
394
+ setDetailOpen(false);
395
+ }
396
+
397
+ if (parsed.error) {
398
+ const suggestionLine =
399
+ parsed.suggestions && parsed.suggestions.length
400
+ ? `\n\nDid you mean:\n${parsed.suggestions
401
+ .map((s) => `- ${s.shortName} (${s.title})`)
402
+ .join("\n")}`
403
+ : "";
404
+
405
+ setMessages((prev) => [
406
+ ...prev,
407
+ {
408
+ id: newId("m"),
409
+ role: "assistant",
410
+ content: `${parsed.error}${suggestionLine}`,
411
+ createdAt: nowIso(),
412
+ },
413
+ ]);
414
+ addLog("Command parse warning", parsed.error, "warn");
415
+ return;
416
+ }
417
+
418
+ // Mock-first: call local mock agent if the command is @... OR if the user is just brainstorming.
419
+ // Once the /api/chat route is present, we’ll prefer it for consistent behavior.
420
+ setBusy(true);
421
+ setCurrentActivity(
422
+ parsed.parsed.mode
423
+ ? `Running ${parsed.parsed.mode} on ${parsed.parsed.referencedPackageName ?? "selected package"}.`
424
+ : "Processing product idea and preparing the board.",
425
+ );
426
+ addLog(
427
+ "Agent started",
428
+ parsed.parsed.mode
429
+ ? `Running ${parsed.parsed.mode} on ${parsed.parsed.referencedPackageName ?? "board context"}.`
430
+ : "Processing free-form product planning request.",
431
+ "info",
432
+ );
433
+
434
+ try {
435
+ if (!parsed.parsed.mode && !parsed.parsed.referencedPackageName) {
436
+ await automateBoardFromProductIdea({
437
+ productIdea: text,
438
+ nextMessages,
439
+ });
440
+ return;
441
+ }
442
+
443
+ // Call server route first (it will fall back to mock mode automatically if no key).
444
+ if (parsed.parsed.mode === "execute") {
445
+ setActiveWorkPackageId(
446
+ parsed.matchedWorkPackageId ?? selectedWorkPackageId,
447
+ );
448
+ }
449
+ const response = await requestChatResponse({
450
+ nextMessages,
451
+ boardSnapshot: workPackages,
452
+ selectedId: parsed.matchedWorkPackageId ?? selectedWorkPackageId,
453
+ parsedCommand: parsed.parsed,
454
+ });
455
+ logModelTrace(response.trace);
456
+
457
+ if (response?.boardAction) {
458
+ setWorkPackages((prev) => applyBoardAction(prev, response?.boardAction));
459
+ if (response.boardAction.type === "replace_all") {
460
+ setDetailOpen(false);
461
+ addLog(
462
+ "Board refreshed",
463
+ "Hydrated the work package board from the product idea.",
464
+ "success",
465
+ );
466
+ } else if (response.boardAction.type !== "none") {
467
+ addLog(
468
+ "Board updated",
469
+ `Applied ${response.boardAction.type} to ${parsed.parsed.referencedPackageName ?? "work package"}.`,
470
+ "success",
471
+ );
472
+ }
473
+ }
474
+
475
+ setMessages((prev) => [
476
+ ...prev,
477
+ {
478
+ id: newId("m"),
479
+ role: "assistant",
480
+ content: response?.assistantMessage ?? "No response.",
481
+ createdAt: nowIso(),
482
+ },
483
+ ]);
484
+ addLog(
485
+ "Agent finished",
486
+ response?.assistantMessage ?? "No response.",
487
+ "success",
488
+ );
489
+ } catch (err) {
490
+ if (!parsed.parsed.mode && !parsed.parsed.referencedPackageName) {
491
+ addLog(
492
+ "Automation fallback",
493
+ "Live automation failed, continuing in mock mode.",
494
+ "warn",
495
+ );
496
+ await automateBoardFromProductIdea({
497
+ productIdea: text,
498
+ nextMessages,
499
+ fallbackToMock: true,
500
+ });
501
+ return;
502
+ }
503
+
504
+ const fallbackResponse = runMockAgent({
505
+ messages: nextMessages,
506
+ workPackages,
507
+ selectedWorkPackageId: parsed.matchedWorkPackageId ?? selectedWorkPackageId,
508
+ parsedCommand: parsed.parsed,
509
+ });
510
+
511
+ if (fallbackResponse.boardAction) {
512
+ setWorkPackages((prev) =>
513
+ applyBoardAction(prev, fallbackResponse.boardAction),
514
+ );
515
+ }
516
+ setMessages((prev) => [
517
+ ...prev,
518
+ {
519
+ id: newId("m"),
520
+ role: "assistant",
521
+ content:
522
+ fallbackResponse.assistantMessage ||
523
+ `Network error. (Mock mode)\n\n${String(err)}`,
524
+ createdAt: nowIso(),
525
+ },
526
+ ]);
527
+ addLog("Agent error", String(err), "error");
528
+ } finally {
529
+ setActiveWorkPackageId(undefined);
530
+ setCurrentActivity("Ready for the next step.");
531
+ setBusy(false);
532
+ }
533
+ }
534
+
535
+ return (
536
+ <div className="flex h-[100dvh] flex-col bg-muted/40">
537
+ <div className="bg-background/72 backdrop-blur-sm">
538
+ <div className="mx-auto w-full max-w-[1760px] px-4 py-3 md:px-5">
539
+ <div className="flex items-center justify-between gap-2">
540
+ <div className="min-w-0">
541
+ <div className="truncate text-sm font-semibold">
542
+ {projectTitle}
543
+ </div>
544
+ <div className="truncate text-xs text-muted-foreground">
545
+ {busy ? currentActivity : "Chat + automated IoT work packages"}
546
+ </div>
547
+ </div>
548
+ <div className="shrink-0 md:hidden">
549
+ <Tabs value={view} onValueChange={(v) => setView(v as ViewMode)}>
550
+ <TabsList>
551
+ <TabsTrigger value="chat">Chat</TabsTrigger>
552
+ <TabsTrigger value="board">Board</TabsTrigger>
553
+ </TabsList>
554
+ </Tabs>
555
+ </div>
556
+ <div className="hidden text-xs text-muted-foreground md:block">
557
+ {isMockMode ? "Mock mode" : `Live · ${llmConfig.model}`}
558
+ </div>
559
+ </div>
560
+ </div>
561
+ </div>
562
+
563
+ <div className="flex-1 overflow-hidden px-3 pb-3 md:px-4 md:pb-4">
564
+ <div className="mx-auto grid h-full w-full max-w-[1760px] grid-cols-1 gap-3 md:grid-cols-[minmax(320px,1fr)_minmax(0,2fr)]">
565
+ <div
566
+ className={[
567
+ "h-full min-h-0 overflow-hidden rounded-[22px] bg-background/82 shadow-sm backdrop-blur-sm",
568
+ view === "board" ? "hidden md:block" : "block",
569
+ ].join(" ")}
570
+ >
571
+ <ChatPanel
572
+ messages={messages}
573
+ draft={draft}
574
+ setDraft={setDraft}
575
+ busy={busy}
576
+ productTitle={projectTitle}
577
+ currentActivity={currentActivity}
578
+ llmConfig={llmConfig}
579
+ setLlmConfig={setLlmConfig}
580
+ isMockMode={isMockMode}
581
+ connectionStatus={connectionStatus}
582
+ connectionMessage={connectionMessage}
583
+ onSend={handleChatSend}
584
+ />
585
+ </div>
586
+ <div
587
+ className={[
588
+ "h-full min-h-0 overflow-hidden rounded-[22px] bg-background/82 shadow-sm backdrop-blur-sm",
589
+ view === "chat" ? "hidden md:block" : "block",
590
+ ].join(" ")}
591
+ >
592
+ <div className="flex h-full min-h-0 flex-col">
593
+ <div className="min-h-0 flex-1 p-2">
594
+ <WorkPackageBoard
595
+ workPackages={workPackages}
596
+ activeWorkPackageId={activeWorkPackageId}
597
+ selectedWorkPackageId={selectedWorkPackageId}
598
+ onSelect={(id) => {
599
+ setSelectedWorkPackageId(id);
600
+ setDetailOpen(true);
601
+ const selected = workPackages.find((wp) => wp.id === id);
602
+ addLog(
603
+ "Detail opened",
604
+ `Opened ${selected?.title ?? "work package"} detail view.`,
605
+ "info",
606
+ );
607
+ }}
608
+ onPrefill={(s) => {
609
+ setView("chat");
610
+ setDraft(s);
611
+ addLog("Command prepared", `Prefilled chat input with: ${s}`, "info");
612
+ }}
613
+ detailOpen={detailOpen}
614
+ onBackToBoard={() => {
615
+ setDetailOpen(false);
616
+ addLog("Board view restored", "Returned to work package card board.", "info");
617
+ }}
618
+ />
619
+ </div>
620
+ <div className="h-[22dvh] min-h-[150px] px-2 pb-2">
621
+ <AgentLogPanel logs={agentLogs} busy={busy} />
622
+ </div>
623
+ </div>
624
+ </div>
625
+ </div>
626
+ </div>
627
+ </div>
628
+ );
629
+ }
components/ChatPanel.test.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { renderToStaticMarkup } from "react-dom/server";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { DEFAULT_LLM_BASE_URL, DEFAULT_LLM_MODEL } from "@/lib/llm-config";
4
+ import { ChatPanel } from "./ChatPanel";
5
+
6
+ function renderChatPanel() {
7
+ return renderToStaticMarkup(
8
+ <ChatPanel
9
+ messages={[]}
10
+ draft=""
11
+ setDraft={vi.fn()}
12
+ busy={false}
13
+ productTitle="Agentic PM Demo"
14
+ currentActivity="Ready"
15
+ llmConfig={{
16
+ apiKey: "",
17
+ baseUrl: DEFAULT_LLM_BASE_URL,
18
+ model: DEFAULT_LLM_MODEL,
19
+ }}
20
+ setLlmConfig={vi.fn()}
21
+ isMockMode={true}
22
+ connectionStatus="idle"
23
+ connectionMessage="Add API key, base URL, and model to enable live automation."
24
+ onSend={vi.fn()}
25
+ />,
26
+ );
27
+ }
28
+
29
+ describe("ChatPanel", () => {
30
+ it("keeps model settings folded by default in mock mode", () => {
31
+ expect(renderChatPanel()).not.toContain("Paste your API key");
32
+ });
33
+ });
components/ChatPanel.tsx ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import {
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ type Dispatch,
8
+ type SetStateAction,
9
+ } from "react";
10
+ import type { ChatMessage, ConnectionStatus } from "@/lib/work-package-types";
11
+ import type { LlmConfig } from "@/lib/llm-config";
12
+ import { Button } from "@/components/ui/button";
13
+ import { Input } from "@/components/ui/input";
14
+ import { Textarea } from "@/components/ui/textarea";
15
+ import { ScrollArea } from "@/components/ui/scroll-area";
16
+ import {
17
+ AlertCircle,
18
+ CheckCircle2,
19
+ LoaderCircle,
20
+ Send,
21
+ Settings2,
22
+ Sparkles,
23
+ X,
24
+ } from "lucide-react";
25
+
26
+ function statusTone(status: ConnectionStatus) {
27
+ if (status === "connected") return "text-emerald-700";
28
+ if (status === "checking") return "text-amber-700";
29
+ if (status === "error") return "text-rose-700";
30
+ return "text-muted-foreground";
31
+ }
32
+
33
+ function StatusIcon(props: { status: ConnectionStatus }) {
34
+ const { status } = props;
35
+ if (status === "connected") return <CheckCircle2 className="h-3.5 w-3.5" />;
36
+ if (status === "checking") {
37
+ return <LoaderCircle className="h-3.5 w-3.5 animate-spin" />;
38
+ }
39
+ if (status === "error") return <AlertCircle className="h-3.5 w-3.5" />;
40
+ return <Settings2 className="h-3.5 w-3.5" />;
41
+ }
42
+
43
+ export function ChatPanel(props: {
44
+ messages: ChatMessage[];
45
+ draft: string;
46
+ setDraft: (v: string) => void;
47
+ busy: boolean;
48
+ productTitle: string;
49
+ currentActivity: string;
50
+ llmConfig: LlmConfig;
51
+ setLlmConfig: Dispatch<SetStateAction<LlmConfig>>;
52
+ isMockMode: boolean;
53
+ connectionStatus: ConnectionStatus;
54
+ connectionMessage: string;
55
+ onSend: (text: string) => void | Promise<void>;
56
+ }) {
57
+ const {
58
+ messages,
59
+ draft,
60
+ setDraft,
61
+ busy,
62
+ productTitle,
63
+ currentActivity,
64
+ llmConfig,
65
+ setLlmConfig,
66
+ isMockMode,
67
+ connectionStatus,
68
+ connectionMessage,
69
+ onSend,
70
+ } = props;
71
+ const bottomRef = useRef<HTMLDivElement | null>(null);
72
+ const [settingsOpen, setSettingsOpen] = useState(false);
73
+ const showSettings = settingsOpen;
74
+
75
+ useEffect(() => {
76
+ bottomRef.current?.scrollIntoView({ block: "end" });
77
+ }, [messages.length]);
78
+
79
+ return (
80
+ <div className="flex h-full min-h-0 flex-col bg-muted/18">
81
+ <div className="px-4 py-3 md:px-5">
82
+ <div className="text-sm font-semibold">{productTitle}</div>
83
+ <div className="text-xs text-muted-foreground">
84
+ Use <span className="font-mono">@SRS ask</span>,{" "}
85
+ <span className="font-mono">@Design FMEA execute</span>, or paste a
86
+ product idea to auto-run the whole board.
87
+ </div>
88
+ </div>
89
+
90
+ <div className="px-4 pb-2 md:px-5">
91
+ <div className="rounded-2xl bg-background/82 px-3 py-2 shadow-sm ring-1 ring-black/5">
92
+ <div className="flex items-center gap-2 text-[11px] font-medium">
93
+ <Sparkles className="h-3.5 w-3.5 text-primary" />
94
+ <span>{busy ? currentActivity : "Ready. You can describe a product or target a package directly."}</span>
95
+ </div>
96
+ <div className="mt-1 text-[11px] leading-5 text-muted-foreground">
97
+ Tip: a full product idea will hydrate the board and run every work package in sequence. A package command will only work on that package.
98
+ </div>
99
+ </div>
100
+ </div>
101
+
102
+ <ScrollArea className="min-h-0 flex-1">
103
+ <div className="space-y-3 px-4 py-1 md:px-5">
104
+ {messages.map((message) => (
105
+ <div
106
+ key={message.id}
107
+ className={[
108
+ "max-w-[52rem] whitespace-pre-wrap rounded-xl px-3 py-2 text-sm leading-6 shadow-sm ring-1 ring-black/5",
109
+ message.role === "user"
110
+ ? "ml-auto bg-secondary/90"
111
+ : "mr-auto bg-background",
112
+ ].join(" ")}
113
+ >
114
+ <div className="mb-1 text-[11px] font-medium text-muted-foreground">
115
+ {message.role === "user" ? "You" : productTitle}
116
+ </div>
117
+ <div>{message.content}</div>
118
+ </div>
119
+ ))}
120
+ <div ref={bottomRef} />
121
+ </div>
122
+ </ScrollArea>
123
+
124
+ <div className="relative p-4 pt-3 md:p-5 md:pt-3">
125
+ {showSettings ? (
126
+ <div className="absolute inset-x-4 bottom-[calc(100%+0.75rem)] z-20 rounded-2xl bg-background px-3 py-3 shadow-lg ring-1 ring-black/10 md:inset-x-5">
127
+ <div className="flex items-start justify-between gap-3">
128
+ <div>
129
+ <div className="text-xs font-semibold">Model Settings</div>
130
+ <div className="text-[11px] text-muted-foreground">
131
+ Stored locally in this browser. Live mode turns on after the connection test succeeds.
132
+ </div>
133
+ </div>
134
+ <Button
135
+ variant="ghost"
136
+ size="icon"
137
+ className="h-7 w-7 rounded-xl"
138
+ onClick={() => setSettingsOpen(false)}
139
+ >
140
+ <X className="h-4 w-4" />
141
+ </Button>
142
+ </div>
143
+
144
+ <div
145
+ className={[
146
+ "mt-3 flex items-center gap-2 rounded-xl bg-muted/28 px-2.5 py-2 text-[11px]",
147
+ statusTone(connectionStatus),
148
+ ].join(" ")}
149
+ >
150
+ <StatusIcon status={connectionStatus} />
151
+ <span>{connectionMessage}</span>
152
+ </div>
153
+
154
+ <div className="mt-3 space-y-2.5">
155
+ <div>
156
+ <div className="mb-1 text-[11px] font-medium text-muted-foreground">
157
+ API Key
158
+ </div>
159
+ <Input
160
+ type="password"
161
+ value={llmConfig.apiKey}
162
+ placeholder="Paste your API key"
163
+ className="h-9 rounded-xl bg-background/92"
164
+ onChange={(e) =>
165
+ setLlmConfig((prev) => ({
166
+ ...prev,
167
+ apiKey: e.target.value,
168
+ }))
169
+ }
170
+ />
171
+ </div>
172
+
173
+ <div className="grid gap-2 md:grid-cols-2">
174
+ <div>
175
+ <div className="mb-1 text-[11px] font-medium text-muted-foreground">
176
+ Base URL
177
+ </div>
178
+ <Input
179
+ value={llmConfig.baseUrl}
180
+ className="h-9 rounded-xl bg-background/92"
181
+ onChange={(e) =>
182
+ setLlmConfig((prev) => ({
183
+ ...prev,
184
+ baseUrl: e.target.value,
185
+ }))
186
+ }
187
+ />
188
+ </div>
189
+ <div>
190
+ <div className="mb-1 text-[11px] font-medium text-muted-foreground">
191
+ Model
192
+ </div>
193
+ <Input
194
+ value={llmConfig.model}
195
+ className="h-9 rounded-xl bg-background/92"
196
+ onChange={(e) =>
197
+ setLlmConfig((prev) => ({
198
+ ...prev,
199
+ model: e.target.value,
200
+ }))
201
+ }
202
+ />
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ ) : null}
208
+
209
+ <form
210
+ className="flex items-end gap-2.5"
211
+ onSubmit={(event) => {
212
+ event.preventDefault();
213
+ onSend(draft);
214
+ }}
215
+ >
216
+ <Textarea
217
+ value={draft}
218
+ onChange={(event) => setDraft(event.target.value)}
219
+ placeholder="Type a product idea, or @Package ask|plan|change|execute ..."
220
+ className="min-h-[148px] resize-none rounded-[24px] bg-background/96 px-4 py-3.5 text-sm leading-6 shadow-sm ring-1 ring-black/5"
221
+ onKeyDown={(event) => {
222
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
223
+ event.preventDefault();
224
+ onSend(draft);
225
+ }
226
+ }}
227
+ />
228
+ <Button
229
+ type="submit"
230
+ disabled={busy || !draft.trim()}
231
+ size="icon"
232
+ className="h-12 w-12 rounded-2xl"
233
+ >
234
+ <Send className="h-4 w-4" />
235
+ </Button>
236
+ </form>
237
+
238
+ <div className="mt-2 flex items-center justify-between gap-3">
239
+ <div className="text-xs text-muted-foreground">
240
+ Press <span className="font-mono">Ctrl</span>+
241
+ <span className="font-mono">Enter</span> to send.
242
+ </div>
243
+ <Button
244
+ type="button"
245
+ variant="ghost"
246
+ className="h-8 rounded-xl px-2.5 text-[11px]"
247
+ onClick={() => setSettingsOpen((value) => !value)}
248
+ >
249
+ <StatusIcon status={connectionStatus} />
250
+ <span className="ml-1">
251
+ {isMockMode ? "Model Settings" : "Live Settings"}
252
+ </span>
253
+ </Button>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ );
258
+ }
components/MarkdownContent.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import ReactMarkdown from "react-markdown";
4
+ import remarkGfm from "remark-gfm";
5
+
6
+ export function MarkdownContent(props: { content: string }) {
7
+ return (
8
+ <div className="markdown-content text-sm text-foreground">
9
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
10
+ {props.content}
11
+ </ReactMarkdown>
12
+ </div>
13
+ );
14
+ }
components/WorkPackageBoard.tsx ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import type { WorkPackage, WorkPackagePhase } from "@/lib/work-package-types";
4
+ import { WorkPackageCard } from "@/components/WorkPackageCard";
5
+ import { WorkPackageDetail } from "@/components/WorkPackageDetail";
6
+ import { Badge } from "@/components/ui/badge";
7
+
8
+ const PHASES: WorkPackagePhase[] = [
9
+ "Stakeholder Needs",
10
+ "Specify Product",
11
+ "Design Product",
12
+ "Verify and Validate Product",
13
+ ];
14
+
15
+ export function WorkPackageBoard(props: {
16
+ workPackages: WorkPackage[];
17
+ activeWorkPackageId?: string;
18
+ selectedWorkPackageId?: string;
19
+ onSelect: (id: string) => void;
20
+ onPrefill: (text: string) => void;
21
+ detailOpen: boolean;
22
+ onBackToBoard: () => void;
23
+ }) {
24
+ const {
25
+ workPackages,
26
+ activeWorkPackageId,
27
+ selectedWorkPackageId,
28
+ onSelect,
29
+ onPrefill,
30
+ detailOpen,
31
+ onBackToBoard,
32
+ } = props;
33
+ const selectedWorkPackage = workPackages.find(
34
+ (workPackage) => workPackage.id === selectedWorkPackageId,
35
+ );
36
+
37
+ const phaseCount = PHASES.filter((phase) =>
38
+ workPackages.some((workPackage) => workPackage.phase === phase),
39
+ ).length;
40
+
41
+ return (
42
+ <div className="flex h-full min-h-0 flex-col rounded-2xl bg-muted/24">
43
+ <div className="px-3 py-3 md:px-4">
44
+ <div className="flex items-center justify-between gap-2">
45
+ <div>
46
+ <div className="text-sm font-semibold">Work Package Zone</div>
47
+ <div className="text-xs text-muted-foreground">
48
+ {detailOpen
49
+ ? "Selected package detail view"
50
+ : "Scrollable board of work package cards"}
51
+ </div>
52
+ </div>
53
+ <div className="flex flex-wrap items-center gap-1">
54
+ <Badge variant="outline" className="text-[11px]">
55
+ {workPackages.length} packages
56
+ </Badge>
57
+ <Badge variant="outline" className="text-[11px]">
58
+ {phaseCount} phases
59
+ </Badge>
60
+ <Badge variant="outline" className="text-[11px]">
61
+ {detailOpen ? "detail" : "board"}
62
+ </Badge>
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ {detailOpen ? (
68
+ <WorkPackageDetail
69
+ workPackage={selectedWorkPackage}
70
+ activeWorkPackageId={activeWorkPackageId}
71
+ onPrefill={onPrefill}
72
+ onBack={onBackToBoard}
73
+ />
74
+ ) : (
75
+ <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
76
+ <div className="px-3 pb-10 md:px-4">
77
+ {PHASES.map((phase) => {
78
+ const items = workPackages.filter((wp) => wp.phase === phase);
79
+ if (!items.length) return null;
80
+
81
+ return (
82
+ <div key={phase} className="mb-6">
83
+ <div className="mb-2 flex items-center justify-between gap-2">
84
+ <div className="text-xs font-semibold text-muted-foreground">
85
+ {phase}
86
+ </div>
87
+ </div>
88
+ <div className="grid grid-cols-2 gap-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
89
+ {items.map((wp) => (
90
+ <WorkPackageCard
91
+ key={wp.id}
92
+ wp={wp}
93
+ activeWorkPackageId={activeWorkPackageId}
94
+ selected={wp.id === selectedWorkPackageId}
95
+ onSelect={() => onSelect(wp.id)}
96
+ onPrefill={onPrefill}
97
+ />
98
+ ))}
99
+ </div>
100
+ </div>
101
+ );
102
+ })}
103
+ </div>
104
+ </div>
105
+ )}
106
+ </div>
107
+ );
108
+ }
components/WorkPackageCard.tsx ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import type { WorkPackage } from "@/lib/work-package-types";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuTrigger,
12
+ } from "@/components/ui/dropdown-menu";
13
+ import {
14
+ CheckCircle2,
15
+ Circle,
16
+ LoaderCircle,
17
+ MoreHorizontal,
18
+ Play,
19
+ } from "lucide-react";
20
+ import {
21
+ getWorkPackageVersion,
22
+ getWorkPackageVisualState,
23
+ } from "@/lib/work-package-ui";
24
+
25
+ function statusLabel(status: WorkPackage["status"]) {
26
+ if (status === "todo") return "To do";
27
+ if (status === "in_progress") return "In progress";
28
+ return "Done";
29
+ }
30
+
31
+ function priorityLabel(priority: WorkPackage["priority"]) {
32
+ if (priority === "high") return "High";
33
+ if (priority === "medium") return "Medium";
34
+ return "Low";
35
+ }
36
+
37
+ function displayTitle(title: string) {
38
+ return title
39
+ .replace("Customer Requirements Specification", "Customer Requirements")
40
+ .replace("System Requirements Specification", "System Requirements")
41
+ .replace("Final Engineering Concept", "Engineering Concept")
42
+ .replace("Final Concept", "Product Concept")
43
+ .replace("Product Certification", "Certification")
44
+ .replace("Safety of Products", "Product Safety")
45
+ .replace("Service Ability Review", "Service Review")
46
+ .replace("Decompose System", "System Breakdown")
47
+ .replace("Industrial Design", "Industrial Design")
48
+ .replace("Patent Check", "Patent Review")
49
+ .replace("Test Management", "Test Planning");
50
+ }
51
+
52
+ export function WorkPackageCard(props: {
53
+ wp: WorkPackage;
54
+ activeWorkPackageId?: string;
55
+ selected: boolean;
56
+ onSelect: () => void;
57
+ onPrefill: (text: string) => void;
58
+ }) {
59
+ const { wp, activeWorkPackageId, selected, onSelect, onPrefill } = props;
60
+
61
+ const refName = wp.shortName || wp.title;
62
+ const visualState = getWorkPackageVisualState({
63
+ workPackage: wp,
64
+ activeWorkPackageId,
65
+ });
66
+ const version = getWorkPackageVersion(wp);
67
+
68
+ async function copyReference() {
69
+ const text = `@${refName} `;
70
+ try {
71
+ await navigator.clipboard.writeText(text);
72
+ } catch {
73
+ // ignore
74
+ }
75
+ }
76
+
77
+ function prefill(mode: "ask" | "plan" | "change" | "execute") {
78
+ onPrefill(`@${refName} ${mode} `);
79
+ }
80
+
81
+ return (
82
+ <Card
83
+ className={[
84
+ "min-h-[112px] rounded-2xl border-0 bg-background/96 transition-colors shadow-sm ring-1 ring-black/5 hover:bg-background",
85
+ selected ? "bg-background ring-2 ring-primary/20" : "",
86
+ visualState === "running" ? "ring-2 ring-amber-300/80 shadow-[0_0_0_1px_rgba(251,191,36,0.15)]" : "",
87
+ visualState === "done" ? "ring-1 ring-emerald-300/70" : "",
88
+ ].join(" ")}
89
+ onClick={onSelect}
90
+ role="button"
91
+ tabIndex={0}
92
+ onKeyDown={(e) => {
93
+ if (e.key === "Enter" || e.key === " ") {
94
+ e.preventDefault();
95
+ onSelect();
96
+ }
97
+ }}
98
+ >
99
+ <CardHeader className="space-y-0 p-2.5">
100
+ <div className="flex items-start justify-between gap-2">
101
+ <div className="min-w-0 flex-1 pr-1">
102
+ <CardTitle className="line-clamp-2 text-[12.5px] font-semibold leading-[1.15rem]">
103
+ {displayTitle(wp.title)}
104
+ </CardTitle>
105
+ <div className="mt-1 flex flex-wrap items-center gap-1">
106
+ <Badge variant="secondary" className="h-4.5 px-1.5 text-[10px]">
107
+ {wp.shortName}
108
+ </Badge>
109
+ <Badge variant="outline" className="h-4.5 px-1.5 text-[10px]">
110
+ {statusLabel(wp.status)}
111
+ </Badge>
112
+ <Badge variant="outline" className="h-4.5 px-1.5 text-[10px]">
113
+ {priorityLabel(wp.priority)}
114
+ </Badge>
115
+ {visualState === "running" ? (
116
+ <Badge
117
+ variant="outline"
118
+ className="h-4.5 gap-1 border-amber-300 bg-amber-50 px-1.5 text-[10px] text-amber-700"
119
+ >
120
+ <LoaderCircle className="h-2.5 w-2.5 animate-spin" />
121
+ Running
122
+ </Badge>
123
+ ) : null}
124
+ {visualState === "done" && version ? (
125
+ <Badge
126
+ variant="outline"
127
+ className="h-4.5 gap-1 border-emerald-300 bg-emerald-50 px-1.5 text-[10px] text-emerald-700"
128
+ >
129
+ <CheckCircle2 className="h-2.5 w-2.5" />
130
+ {version}
131
+ </Badge>
132
+ ) : null}
133
+ </div>
134
+ </div>
135
+ <div className="flex shrink-0 items-center gap-0.5">
136
+ <Button
137
+ variant="ghost"
138
+ size="icon"
139
+ className="h-6 w-6"
140
+ onClick={(e) => {
141
+ e.stopPropagation();
142
+ prefill("execute");
143
+ }}
144
+ >
145
+ <Play className="h-3.5 w-3.5" />
146
+ </Button>
147
+ <DropdownMenu>
148
+ <DropdownMenuTrigger asChild>
149
+ <Button
150
+ variant="ghost"
151
+ size="icon"
152
+ className="h-6 w-6 rounded-lg"
153
+ onClick={(e) => e.stopPropagation()}
154
+ >
155
+ <MoreHorizontal className="h-3.5 w-3.5" />
156
+ </Button>
157
+ </DropdownMenuTrigger>
158
+ <DropdownMenuContent align="end" className="rounded-xl">
159
+ <DropdownMenuItem
160
+ onClick={(e) => {
161
+ e.stopPropagation();
162
+ prefill("ask");
163
+ }}
164
+ >
165
+ Ask
166
+ </DropdownMenuItem>
167
+ <DropdownMenuItem
168
+ onClick={(e) => {
169
+ e.stopPropagation();
170
+ prefill("plan");
171
+ }}
172
+ >
173
+ Plan
174
+ </DropdownMenuItem>
175
+ <DropdownMenuItem
176
+ onClick={(e) => {
177
+ e.stopPropagation();
178
+ prefill("change");
179
+ }}
180
+ >
181
+ Change
182
+ </DropdownMenuItem>
183
+ <DropdownMenuItem
184
+ onClick={(e) => {
185
+ e.stopPropagation();
186
+ prefill("execute");
187
+ }}
188
+ >
189
+ Execute
190
+ </DropdownMenuItem>
191
+ <DropdownMenuItem
192
+ onClick={(e) => {
193
+ e.stopPropagation();
194
+ copyReference();
195
+ onPrefill(`@${refName} `);
196
+ }}
197
+ >
198
+ Copy reference
199
+ </DropdownMenuItem>
200
+ </DropdownMenuContent>
201
+ </DropdownMenu>
202
+ </div>
203
+ </div>
204
+ </CardHeader>
205
+
206
+ <CardContent className="space-y-1.5 p-2.5 pt-0">
207
+ <div className="line-clamp-2 text-[10.5px] leading-4 text-muted-foreground">
208
+ {wp.objective}
209
+ </div>
210
+
211
+ <div className="flex flex-wrap items-center gap-1.5 text-[10px] text-muted-foreground">
212
+ <div className="inline-flex items-center gap-1">
213
+ <Circle className="h-3 w-3" />
214
+ {wp.tasks.length} tasks
215
+ </div>
216
+ <div>{wp.outputFiles.length} outputs</div>
217
+ <div>{wp.outputs.length} generated</div>
218
+ </div>
219
+
220
+ {wp.outputs.length ? (
221
+ <div className="line-clamp-1 text-[10px] text-muted-foreground">
222
+ Latest output: {wp.outputs[wp.outputs.length - 1]?.title}
223
+ </div>
224
+ ) : null}
225
+ </CardContent>
226
+ </Card>
227
+ );
228
+ }
components/WorkPackageDetail.tsx ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import type { WorkPackage } from "@/lib/work-package-types";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import { WorkPackageOutputCard } from "@/components/WorkPackageOutput";
8
+ import { MarkdownContent } from "@/components/MarkdownContent";
9
+ import {
10
+ ArrowLeft,
11
+ CheckCircle2,
12
+ ClipboardCopy,
13
+ HelpCircle,
14
+ ListTodo,
15
+ LoaderCircle,
16
+ Play,
17
+ SlidersHorizontal,
18
+ } from "lucide-react";
19
+ import {
20
+ getWorkPackageVersion,
21
+ getWorkPackageVisualState,
22
+ } from "@/lib/work-package-ui";
23
+
24
+ function statusLabel(status: WorkPackage["status"]) {
25
+ if (status === "todo") return "To do";
26
+ if (status === "in_progress") return "In progress";
27
+ return "Done";
28
+ }
29
+
30
+ function priorityLabel(priority: WorkPackage["priority"]) {
31
+ if (priority === "high") return "High";
32
+ if (priority === "medium") return "Medium";
33
+ return "Low";
34
+ }
35
+
36
+ function Section(props: {
37
+ title: string;
38
+ children: ReactNode;
39
+ }) {
40
+ return (
41
+ <section className="rounded-2xl bg-background/72 px-3 py-3 shadow-sm ring-1 ring-black/5">
42
+ <div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.04em] text-muted-foreground">
43
+ {props.title}
44
+ </div>
45
+ {props.children}
46
+ </section>
47
+ );
48
+ }
49
+
50
+ export function WorkPackageDetail(props: {
51
+ workPackage?: WorkPackage;
52
+ activeWorkPackageId?: string;
53
+ onPrefill: (text: string) => void;
54
+ onBack: () => void;
55
+ }) {
56
+ const { workPackage: wp, activeWorkPackageId, onPrefill, onBack } = props;
57
+
58
+ if (!wp) {
59
+ return (
60
+ <div className="px-3 pb-3 md:px-4">
61
+ <div className="rounded-2xl bg-background/70 px-4 py-4 text-sm text-muted-foreground shadow-sm ring-1 ring-black/5">
62
+ Select a work package to inspect its details.
63
+ </div>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ const refName = wp.shortName || wp.title;
69
+ const latestOutput = wp.outputs.at(-1);
70
+ const visualState = getWorkPackageVisualState({
71
+ workPackage: wp,
72
+ activeWorkPackageId,
73
+ });
74
+ const version = getWorkPackageVersion(wp);
75
+
76
+ async function copyReference() {
77
+ try {
78
+ await navigator.clipboard.writeText(`@${refName} `);
79
+ } catch {
80
+ // ignore
81
+ }
82
+ }
83
+
84
+ return (
85
+ <div className="flex h-full min-h-0 flex-col">
86
+ <div className="px-3 py-2 md:px-4">
87
+ <div className="rounded-2xl bg-background/72 px-3 py-2.5 shadow-sm ring-1 ring-black/5">
88
+ <div className="flex items-center justify-between gap-3">
89
+ <div className="flex min-w-0 items-center gap-2">
90
+ <Button
91
+ variant="ghost"
92
+ size="icon"
93
+ className="h-8 w-8 rounded-xl"
94
+ onClick={onBack}
95
+ >
96
+ <ArrowLeft className="h-4 w-4" />
97
+ </Button>
98
+ <div className="min-w-0">
99
+ <div className="truncate text-sm font-semibold">{wp.title}</div>
100
+ <div className="truncate text-xs text-muted-foreground">
101
+ Work package detail view
102
+ </div>
103
+ </div>
104
+ </div>
105
+ <div className="flex shrink-0 items-center gap-1">
106
+ <Button
107
+ variant="ghost"
108
+ size="icon"
109
+ className="h-7 w-7 rounded-lg"
110
+ onClick={() => onPrefill(`@${refName} ask `)}
111
+ >
112
+ <HelpCircle className="h-4 w-4" />
113
+ </Button>
114
+ <Button
115
+ variant="ghost"
116
+ size="icon"
117
+ className="h-7 w-7 rounded-lg"
118
+ onClick={() => onPrefill(`@${refName} plan `)}
119
+ >
120
+ <ListTodo className="h-4 w-4" />
121
+ </Button>
122
+ <Button
123
+ variant="ghost"
124
+ size="icon"
125
+ className="h-7 w-7 rounded-lg"
126
+ onClick={() => onPrefill(`@${refName} change `)}
127
+ >
128
+ <SlidersHorizontal className="h-4 w-4" />
129
+ </Button>
130
+ <Button
131
+ variant="ghost"
132
+ size="icon"
133
+ className="h-7 w-7 rounded-lg"
134
+ onClick={() => onPrefill(`@${refName} execute `)}
135
+ >
136
+ <Play className="h-4 w-4" />
137
+ </Button>
138
+ <Button
139
+ variant="ghost"
140
+ size="icon"
141
+ className="h-7 w-7 rounded-lg"
142
+ onClick={copyReference}
143
+ >
144
+ <ClipboardCopy className="h-4 w-4" />
145
+ </Button>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+
151
+ <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 pb-3 md:px-4">
152
+ <div className="space-y-3">
153
+ <section className="rounded-2xl bg-background/72 px-4 py-4 shadow-sm ring-1 ring-black/5">
154
+ <div className="flex flex-wrap items-start justify-between gap-3">
155
+ <div className="min-w-0">
156
+ <h2 className="text-base font-semibold">{wp.title}</h2>
157
+ <div className="mt-1 flex flex-wrap items-center gap-1">
158
+ <Badge variant="secondary" className="text-[11px]">
159
+ {wp.shortName}
160
+ </Badge>
161
+ <Badge variant="outline" className="text-[11px]">
162
+ {statusLabel(wp.status)}
163
+ </Badge>
164
+ <Badge variant="outline" className="text-[11px]">
165
+ {priorityLabel(wp.priority)}
166
+ </Badge>
167
+ <Badge variant="outline" className="text-[11px]">
168
+ {wp.phase}
169
+ </Badge>
170
+ {visualState === "running" ? (
171
+ <Badge
172
+ variant="outline"
173
+ className="gap-1 border-amber-300 bg-amber-50 text-[11px] text-amber-700"
174
+ >
175
+ <LoaderCircle className="h-3 w-3 animate-spin" />
176
+ Running
177
+ </Badge>
178
+ ) : null}
179
+ {visualState === "done" && version ? (
180
+ <Badge
181
+ variant="outline"
182
+ className="gap-1 border-emerald-300 bg-emerald-50 text-[11px] text-emerald-700"
183
+ >
184
+ <CheckCircle2 className="h-3 w-3" />
185
+ {version}
186
+ </Badge>
187
+ ) : null}
188
+ </div>
189
+ </div>
190
+ </div>
191
+
192
+ <p className="mt-3 text-sm leading-6 text-muted-foreground">
193
+ {wp.objective}
194
+ </p>
195
+ </section>
196
+
197
+ <Section title="Primary output">
198
+ {latestOutput ? (
199
+ <div className="space-y-3">
200
+ <div className="flex flex-wrap items-center gap-2">
201
+ <Badge variant="secondary" className="text-[11px]">
202
+ {latestOutput.title}
203
+ </Badge>
204
+ <Badge variant="outline" className="text-[11px]">
205
+ {latestOutput.executionMode === "real"
206
+ ? "LLM Automation"
207
+ : "Simulated Execution"}
208
+ </Badge>
209
+ <Badge variant="outline" className="text-[11px]">
210
+ {latestOutput.type}
211
+ </Badge>
212
+ </div>
213
+ <div className="rounded-xl bg-muted/20 px-4 py-4">
214
+ <MarkdownContent content={latestOutput.content} />
215
+ </div>
216
+ <div className="rounded-xl bg-muted/32 p-2.5 text-[11px] leading-5 text-muted-foreground">
217
+ {latestOutput.disclaimer}
218
+ </div>
219
+ </div>
220
+ ) : (
221
+ <div className="text-sm leading-6 text-muted-foreground">
222
+ No output yet. Run the package to generate its primary artifact.
223
+ </div>
224
+ )}
225
+ </Section>
226
+
227
+ {wp.outputs.length > 1 ? (
228
+ <details className="rounded-2xl bg-background/72 px-3 py-3 shadow-sm ring-1 ring-black/5">
229
+ <summary className="cursor-pointer text-[11px] font-semibold uppercase tracking-[0.04em] text-muted-foreground">
230
+ Output history
231
+ </summary>
232
+ <div className="mt-3 space-y-2">
233
+ {wp.outputs
234
+ .slice(0, -1)
235
+ .reverse()
236
+ .map((output) => (
237
+ <WorkPackageOutputCard
238
+ key={output.id}
239
+ output={output}
240
+ />
241
+ ))}
242
+ </div>
243
+ </details>
244
+ ) : null}
245
+
246
+ <details className="rounded-2xl bg-background/72 px-3 py-3 shadow-sm ring-1 ring-black/5">
247
+ <summary className="cursor-pointer text-[11px] font-semibold uppercase tracking-[0.04em] text-muted-foreground">
248
+ Package context
249
+ </summary>
250
+ <div className="mt-3 grid gap-3 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
251
+ <div className="space-y-3">
252
+ <Section title="Expected outputs">
253
+ <div className="flex flex-wrap gap-1.5">
254
+ {wp.outputFiles.map((output) => (
255
+ <Badge key={output} variant="outline" className="text-[11px]">
256
+ {output}
257
+ </Badge>
258
+ ))}
259
+ </div>
260
+ </Section>
261
+
262
+ {wp.inputFiles.length ? (
263
+ <Section title="Expected inputs">
264
+ <div className="text-sm leading-6 text-muted-foreground">
265
+ {wp.inputFiles.join(", ")}
266
+ </div>
267
+ </Section>
268
+ ) : null}
269
+
270
+ {wp.coreSections.length ? (
271
+ <Section title="Core sections">
272
+ <div className="text-sm leading-6 text-muted-foreground">
273
+ {wp.coreSections.join(", ")}
274
+ </div>
275
+ </Section>
276
+ ) : null}
277
+ </div>
278
+
279
+ <Section title="Tasks">
280
+ <div className="space-y-2">
281
+ {wp.tasks.map((task) => (
282
+ <div
283
+ key={task.id}
284
+ className="rounded-xl bg-muted/28 px-3 py-2.5 text-sm shadow-sm ring-1 ring-black/5"
285
+ >
286
+ <div className="font-medium">{task.title}</div>
287
+ <div className="mt-1 leading-5 text-muted-foreground">
288
+ {task.description}
289
+ </div>
290
+ </div>
291
+ ))}
292
+ </div>
293
+ </Section>
294
+ </div>
295
+ </details>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ );
300
+ }
components/WorkPackageOutput.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import type { WorkPackageOutput } from "@/lib/work-package-types";
5
+ import { SIMULATED_EXECUTION_DISCLAIMER } from "@/lib/work-package-types";
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
9
+ import { MarkdownContent } from "@/components/MarkdownContent";
10
+ import { ChevronDown, ChevronUp } from "lucide-react";
11
+
12
+ export function WorkPackageOutputCard(props: {
13
+ output: WorkPackageOutput;
14
+ defaultOpen?: boolean;
15
+ }) {
16
+ const { output, defaultOpen = false } = props;
17
+ const [open, setOpen] = useState(defaultOpen);
18
+
19
+ const disclaimer =
20
+ output.disclaimer?.trim() || SIMULATED_EXECUTION_DISCLAIMER;
21
+
22
+ return (
23
+ <Card className="rounded-2xl border-0 bg-background/88 shadow-sm ring-1 ring-black/5">
24
+ <CardHeader className="flex flex-row items-start justify-between gap-2 space-y-0 p-3">
25
+ <div className="min-w-0">
26
+ <CardTitle className="truncate text-xs font-semibold">
27
+ {output.title}
28
+ </CardTitle>
29
+ <div className="mt-1 flex flex-wrap items-center gap-1">
30
+ <Badge variant="secondary" className="text-[11px]">
31
+ {output.executionMode === "simulated"
32
+ ? "Simulated Execution"
33
+ : "LLM Automation"}
34
+ </Badge>
35
+ <Badge variant="outline" className="text-[11px]">
36
+ {output.type}
37
+ </Badge>
38
+ </div>
39
+ </div>
40
+ <Button
41
+ variant="ghost"
42
+ size="icon"
43
+ className="h-7 w-7 shrink-0"
44
+ onClick={() => setOpen((v) => !v)}
45
+ aria-label={open ? "Collapse output" : "Expand output"}
46
+ >
47
+ {open ? (
48
+ <ChevronUp className="h-4 w-4" />
49
+ ) : (
50
+ <ChevronDown className="h-4 w-4" />
51
+ )}
52
+ </Button>
53
+ </CardHeader>
54
+ <CardContent className="space-y-2 p-3 pt-0">
55
+ <div className="rounded-xl bg-muted/32 p-2.5 text-[11px] leading-5 text-muted-foreground">
56
+ {disclaimer}
57
+ </div>
58
+ {open ? (
59
+ <div className="rounded-xl bg-muted/18 p-3">
60
+ <MarkdownContent content={output.content} />
61
+ </div>
62
+ ) : null}
63
+ </CardContent>
64
+ </Card>
65
+ );
66
+ }
components/ui/badge.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Slot } from "radix-ui"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13
+ secondary:
14
+ "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
15
+ destructive:
16
+ "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
17
+ outline:
18
+ "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
19
+ ghost:
20
+ "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ variant: "default",
26
+ },
27
+ }
28
+ )
29
+
30
+ function Badge({
31
+ className,
32
+ variant = "default",
33
+ asChild = false,
34
+ ...props
35
+ }: React.ComponentProps<"span"> &
36
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
37
+ const Comp = asChild ? Slot.Root : "span"
38
+
39
+ return (
40
+ <Comp
41
+ data-slot="badge"
42
+ data-variant={variant}
43
+ className={cn(badgeVariants({ variant }), className)}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ export { Badge, badgeVariants }
components/ui/button.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Slot } from "radix-ui"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/80",
13
+ outline:
14
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
15
+ secondary:
16
+ "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
17
+ ghost:
18
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
19
+ destructive:
20
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default:
25
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
26
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
27
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
28
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
29
+ icon: "size-8",
30
+ "icon-xs":
31
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
32
+ "icon-sm":
33
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
34
+ "icon-lg": "size-9",
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: "default",
39
+ size: "default",
40
+ },
41
+ }
42
+ )
43
+
44
+ function Button({
45
+ className,
46
+ variant = "default",
47
+ size = "default",
48
+ asChild = false,
49
+ ...props
50
+ }: React.ComponentProps<"button"> &
51
+ VariantProps<typeof buttonVariants> & {
52
+ asChild?: boolean
53
+ }) {
54
+ const Comp = asChild ? Slot.Root : "button"
55
+
56
+ return (
57
+ <Comp
58
+ data-slot="button"
59
+ data-variant={variant}
60
+ data-size={size}
61
+ className={cn(buttonVariants({ variant, size, className }))}
62
+ {...props}
63
+ />
64
+ )
65
+ }
66
+
67
+ export { Button, buttonVariants }
components/ui/card.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({
6
+ className,
7
+ size = "default",
8
+ ...props
9
+ }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
10
+ return (
11
+ <div
12
+ data-slot="card"
13
+ data-size={size}
14
+ className={cn(
15
+ "group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
24
+ return (
25
+ <div
26
+ data-slot="card-header"
27
+ className={cn(
28
+ "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
29
+ className
30
+ )}
31
+ {...props}
32
+ />
33
+ )
34
+ }
35
+
36
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
37
+ return (
38
+ <div
39
+ data-slot="card-title"
40
+ className={cn(
41
+ "font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
50
+ return (
51
+ <div
52
+ data-slot="card-description"
53
+ className={cn("text-sm text-muted-foreground", className)}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
60
+ return (
61
+ <div
62
+ data-slot="card-action"
63
+ className={cn(
64
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
65
+ className
66
+ )}
67
+ {...props}
68
+ />
69
+ )
70
+ }
71
+
72
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
73
+ return (
74
+ <div
75
+ data-slot="card-content"
76
+ className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
77
+ {...props}
78
+ />
79
+ )
80
+ }
81
+
82
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
83
+ return (
84
+ <div
85
+ data-slot="card-footer"
86
+ className={cn(
87
+ "flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ export {
96
+ Card,
97
+ CardHeader,
98
+ CardFooter,
99
+ CardTitle,
100
+ CardAction,
101
+ CardDescription,
102
+ CardContent,
103
+ }
components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { CheckIcon, ChevronRightIcon } from "lucide-react"
8
+
9
+ function DropdownMenu({
10
+ ...props
11
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
12
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
13
+ }
14
+
15
+ function DropdownMenuPortal({
16
+ ...props
17
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
18
+ return (
19
+ <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
20
+ )
21
+ }
22
+
23
+ function DropdownMenuTrigger({
24
+ ...props
25
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
26
+ return (
27
+ <DropdownMenuPrimitive.Trigger
28
+ data-slot="dropdown-menu-trigger"
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ function DropdownMenuContent({
35
+ className,
36
+ align = "start",
37
+ sideOffset = 4,
38
+ ...props
39
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
40
+ return (
41
+ <DropdownMenuPrimitive.Portal>
42
+ <DropdownMenuPrimitive.Content
43
+ data-slot="dropdown-menu-content"
44
+ sideOffset={sideOffset}
45
+ align={align}
46
+ className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
47
+ {...props}
48
+ />
49
+ </DropdownMenuPrimitive.Portal>
50
+ )
51
+ }
52
+
53
+ function DropdownMenuGroup({
54
+ ...props
55
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
56
+ return (
57
+ <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
58
+ )
59
+ }
60
+
61
+ function DropdownMenuItem({
62
+ className,
63
+ inset,
64
+ variant = "default",
65
+ ...props
66
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
67
+ inset?: boolean
68
+ variant?: "default" | "destructive"
69
+ }) {
70
+ return (
71
+ <DropdownMenuPrimitive.Item
72
+ data-slot="dropdown-menu-item"
73
+ data-inset={inset}
74
+ data-variant={variant}
75
+ className={cn(
76
+ "group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ function DropdownMenuCheckboxItem({
85
+ className,
86
+ children,
87
+ checked,
88
+ inset,
89
+ ...props
90
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
91
+ inset?: boolean
92
+ }) {
93
+ return (
94
+ <DropdownMenuPrimitive.CheckboxItem
95
+ data-slot="dropdown-menu-checkbox-item"
96
+ data-inset={inset}
97
+ className={cn(
98
+ "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
99
+ className
100
+ )}
101
+ checked={checked}
102
+ {...props}
103
+ >
104
+ <span
105
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
106
+ data-slot="dropdown-menu-checkbox-item-indicator"
107
+ >
108
+ <DropdownMenuPrimitive.ItemIndicator>
109
+ <CheckIcon
110
+ />
111
+ </DropdownMenuPrimitive.ItemIndicator>
112
+ </span>
113
+ {children}
114
+ </DropdownMenuPrimitive.CheckboxItem>
115
+ )
116
+ }
117
+
118
+ function DropdownMenuRadioGroup({
119
+ ...props
120
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
121
+ return (
122
+ <DropdownMenuPrimitive.RadioGroup
123
+ data-slot="dropdown-menu-radio-group"
124
+ {...props}
125
+ />
126
+ )
127
+ }
128
+
129
+ function DropdownMenuRadioItem({
130
+ className,
131
+ children,
132
+ inset,
133
+ ...props
134
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
135
+ inset?: boolean
136
+ }) {
137
+ return (
138
+ <DropdownMenuPrimitive.RadioItem
139
+ data-slot="dropdown-menu-radio-item"
140
+ data-inset={inset}
141
+ className={cn(
142
+ "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
143
+ className
144
+ )}
145
+ {...props}
146
+ >
147
+ <span
148
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
149
+ data-slot="dropdown-menu-radio-item-indicator"
150
+ >
151
+ <DropdownMenuPrimitive.ItemIndicator>
152
+ <CheckIcon
153
+ />
154
+ </DropdownMenuPrimitive.ItemIndicator>
155
+ </span>
156
+ {children}
157
+ </DropdownMenuPrimitive.RadioItem>
158
+ )
159
+ }
160
+
161
+ function DropdownMenuLabel({
162
+ className,
163
+ inset,
164
+ ...props
165
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
166
+ inset?: boolean
167
+ }) {
168
+ return (
169
+ <DropdownMenuPrimitive.Label
170
+ data-slot="dropdown-menu-label"
171
+ data-inset={inset}
172
+ className={cn(
173
+ "px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7",
174
+ className
175
+ )}
176
+ {...props}
177
+ />
178
+ )
179
+ }
180
+
181
+ function DropdownMenuSeparator({
182
+ className,
183
+ ...props
184
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
185
+ return (
186
+ <DropdownMenuPrimitive.Separator
187
+ data-slot="dropdown-menu-separator"
188
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
189
+ {...props}
190
+ />
191
+ )
192
+ }
193
+
194
+ function DropdownMenuShortcut({
195
+ className,
196
+ ...props
197
+ }: React.ComponentProps<"span">) {
198
+ return (
199
+ <span
200
+ data-slot="dropdown-menu-shortcut"
201
+ className={cn(
202
+ "ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
203
+ className
204
+ )}
205
+ {...props}
206
+ />
207
+ )
208
+ }
209
+
210
+ function DropdownMenuSub({
211
+ ...props
212
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
213
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
214
+ }
215
+
216
+ function DropdownMenuSubTrigger({
217
+ className,
218
+ inset,
219
+ children,
220
+ ...props
221
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
222
+ inset?: boolean
223
+ }) {
224
+ return (
225
+ <DropdownMenuPrimitive.SubTrigger
226
+ data-slot="dropdown-menu-sub-trigger"
227
+ data-inset={inset}
228
+ className={cn(
229
+ "flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
230
+ className
231
+ )}
232
+ {...props}
233
+ >
234
+ {children}
235
+ <ChevronRightIcon className="ml-auto" />
236
+ </DropdownMenuPrimitive.SubTrigger>
237
+ )
238
+ }
239
+
240
+ function DropdownMenuSubContent({
241
+ className,
242
+ ...props
243
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
244
+ return (
245
+ <DropdownMenuPrimitive.SubContent
246
+ data-slot="dropdown-menu-sub-content"
247
+ className={cn("z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
248
+ {...props}
249
+ />
250
+ )
251
+ }
252
+
253
+ export {
254
+ DropdownMenu,
255
+ DropdownMenuPortal,
256
+ DropdownMenuTrigger,
257
+ DropdownMenuContent,
258
+ DropdownMenuGroup,
259
+ DropdownMenuLabel,
260
+ DropdownMenuItem,
261
+ DropdownMenuCheckboxItem,
262
+ DropdownMenuRadioGroup,
263
+ DropdownMenuRadioItem,
264
+ DropdownMenuSeparator,
265
+ DropdownMenuShortcut,
266
+ DropdownMenuSub,
267
+ DropdownMenuSubTrigger,
268
+ DropdownMenuSubContent,
269
+ }