Spaces:
Running
Running
Tiger's Macbook Air commited on
Commit ·
3f76ff4
1
Parent(s): 4015524
Build agentic PM demo app
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env.example +4 -0
- .gitignore +46 -0
- AGENTS.md +29 -0
- Dockerfile +16 -0
- README.md +31 -2
- agentic_pm_demo_codex_plans/.gitignore +6 -0
- agentic_pm_demo_codex_plans/AGENTS.md +57 -0
- agentic_pm_demo_codex_plans/README.md +61 -0
- agentic_pm_demo_codex_plans/app/globals.css +1007 -0
- agentic_pm_demo_codex_plans/app/layout.tsx +19 -0
- agentic_pm_demo_codex_plans/app/page.tsx +5 -0
- agentic_pm_demo_codex_plans/components/AgenticPmWorkbench.tsx +826 -0
- agentic_pm_demo_codex_plans/data/work-packages.seed.json +338 -0
- agentic_pm_demo_codex_plans/eslint.config.mjs +11 -0
- agentic_pm_demo_codex_plans/lib/command-parser.ts +86 -0
- agentic_pm_demo_codex_plans/lib/mock-agent.ts +177 -0
- agentic_pm_demo_codex_plans/lib/work-package-types.ts +105 -0
- agentic_pm_demo_codex_plans/next.config.ts +10 -0
- agentic_pm_demo_codex_plans/package-lock.json +0 -0
- agentic_pm_demo_codex_plans/package.json +24 -0
- agentic_pm_demo_codex_plans/plans/01-product-vision-and-scope.md +59 -0
- agentic_pm_demo_codex_plans/plans/02-ux-layout-and-interaction.md +110 -0
- agentic_pm_demo_codex_plans/plans/03-work-package-data-model.md +124 -0
- agentic_pm_demo_codex_plans/plans/04-work-package-catalog.md +298 -0
- agentic_pm_demo_codex_plans/plans/05-reference-command-and-execution.md +108 -0
- agentic_pm_demo_codex_plans/plans/06-llm-api-and-prompt-contract.md +87 -0
- agentic_pm_demo_codex_plans/plans/07-technical-architecture-and-file-structure.md +100 -0
- agentic_pm_demo_codex_plans/plans/08-local-run-and-huggingface-deployment.md +83 -0
- agentic_pm_demo_codex_plans/prompts/work-package-system-prompt.md +103 -0
- agentic_pm_demo_codex_plans/tsconfig.json +33 -0
- app/api/chat/route.ts +361 -0
- app/api/test-connection/route.ts +61 -0
- app/favicon.ico +0 -0
- app/globals.css +204 -0
- app/layout.tsx +36 -0
- app/page.tsx +5 -0
- components.json +25 -0
- components/AgentLogPanel.tsx +73 -0
- components/AppShell.tsx +629 -0
- components/ChatPanel.test.tsx +33 -0
- components/ChatPanel.tsx +258 -0
- components/MarkdownContent.tsx +14 -0
- components/WorkPackageBoard.tsx +108 -0
- components/WorkPackageCard.tsx +228 -0
- components/WorkPackageDetail.tsx +300 -0
- components/WorkPackageOutput.tsx +66 -0
- components/ui/badge.tsx +49 -0
- components/ui/button.tsx +67 -0
- components/ui/card.tsx +103 -0
- 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:
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|