Deploy ROCmPilot Space
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +9 -0
- Dockerfile +32 -0
- README.md +349 -6
- TECHNICAL_WALKTHROUGH.md +461 -0
- components.json +25 -0
- eslint.config.mjs +18 -0
- next.config.ts +7 -0
- package-lock.json +0 -0
- package.json +43 -0
- postcss.config.mjs +7 -0
- public/file.svg +1 -0
- public/globe.svg +1 -0
- public/next.svg +1 -0
- public/vercel.svg +1 -0
- public/window.svg +1 -0
- src/app/api/report/route.ts +144 -0
- src/app/api/runs/[runId]/route.ts +16 -0
- src/app/api/runs/route.ts +19 -0
- src/app/favicon.ico +0 -0
- src/app/globals.css +131 -0
- src/app/layout.tsx +37 -0
- src/app/page.tsx +5 -0
- src/components/ai-elements/code-block.tsx +556 -0
- src/components/ai-elements/message.tsx +360 -0
- src/components/ai-elements/terminal.tsx +273 -0
- src/components/ai-elements/tool.tsx +173 -0
- src/components/rocmpilot-dashboard.tsx +907 -0
- src/components/ui/alert.tsx +76 -0
- src/components/ui/badge.tsx +49 -0
- src/components/ui/button-group.tsx +83 -0
- src/components/ui/button.tsx +67 -0
- src/components/ui/card.tsx +103 -0
- src/components/ui/collapsible.tsx +33 -0
- src/components/ui/input.tsx +19 -0
- src/components/ui/label.tsx +24 -0
- src/components/ui/progress.tsx +31 -0
- src/components/ui/scroll-area.tsx +55 -0
- src/components/ui/select.tsx +192 -0
- src/components/ui/separator.tsx +28 -0
- src/components/ui/skeleton.tsx +13 -0
- src/components/ui/table.tsx +116 -0
- src/components/ui/tabs.tsx +90 -0
- src/components/ui/textarea.tsx +18 -0
- src/components/ui/tooltip.tsx +57 -0
- src/lib/rocmpilot/data.ts +1102 -0
- src/lib/rocmpilot/github-scanner.ts +462 -0
- src/lib/rocmpilot/github-url.ts +47 -0
- src/lib/rocmpilot/memory-ids.ts +6 -0
- src/lib/rocmpilot/store.ts +23 -0
- src/lib/rocmpilot/synap-memory.ts +316 -0
.dockerignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
.next
|
| 3 |
+
node_modules
|
| 4 |
+
npm-debug.log
|
| 5 |
+
.env
|
| 6 |
+
.env.*
|
| 7 |
+
!.env.example
|
| 8 |
+
Dockerfile
|
| 9 |
+
README.md.bak
|
Dockerfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-alpine AS deps
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
COPY package.json package-lock.json ./
|
| 5 |
+
RUN npm ci
|
| 6 |
+
|
| 7 |
+
FROM node:20-alpine AS builder
|
| 8 |
+
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
ENV NEXT_TELEMETRY_DISABLED=1
|
| 11 |
+
COPY --from=deps /app/node_modules ./node_modules
|
| 12 |
+
COPY . .
|
| 13 |
+
RUN npm run build
|
| 14 |
+
|
| 15 |
+
FROM node:20-alpine AS runner
|
| 16 |
+
|
| 17 |
+
WORKDIR /app
|
| 18 |
+
ENV NODE_ENV=production
|
| 19 |
+
ENV NEXT_TELEMETRY_DISABLED=1
|
| 20 |
+
ENV PORT=7860
|
| 21 |
+
ENV HOSTNAME=0.0.0.0
|
| 22 |
+
|
| 23 |
+
RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
|
| 24 |
+
|
| 25 |
+
COPY --from=builder /app/public ./public
|
| 26 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
| 27 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
| 28 |
+
|
| 29 |
+
USER nextjs
|
| 30 |
+
EXPOSE 7860
|
| 31 |
+
|
| 32 |
+
CMD ["node", "server.js"]
|
README.md
CHANGED
|
@@ -1,10 +1,353 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji: 🐠
|
| 4 |
-
colorFrom: gray
|
| 5 |
-
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
-
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ROCmPilot
|
|
|
|
|
|
|
|
|
|
| 3 |
sdk: docker
|
| 4 |
+
app_port: 7860
|
| 5 |
+
short_description: Agentic ROCm migration dashboard.
|
| 6 |
---
|
| 7 |
|
| 8 |
+
# ROCmPilot
|
| 9 |
+
|
| 10 |
+
ROCmPilot is a multi-agent developer tool that audits AI repositories and produces an AMD ROCm migration path for PyTorch, vLLM, and agentic workloads.
|
| 11 |
+
|
| 12 |
+
> ROCmPilot helps teams move from CUDA/NVIDIA assumptions to AMD ROCm readiness by scanning a repo, identifying blockers, proposing patches, preparing validation commands, and generating a technical/business migration report.
|
| 13 |
+
|
| 14 |
+
The project is built for the **AMD Developer Hackathon, Track 1: AI Agents & Agentic Workflows**. It uses Hugging Face as the temporary model and training layer now, and is designed to switch to AMD Developer Cloud + MI300X + ROCm/vLLM when access is available.
|
| 15 |
+
|
| 16 |
+
Read the full build story, architecture diagrams, demo script, and implementation details in the **[Technical Walkthrough](./TECHNICAL_WALKTHROUGH.md)**.
|
| 17 |
+
|
| 18 |
+
## Why This Exists
|
| 19 |
+
|
| 20 |
+
Many AI teams want AMD GPU optionality, but their codebases quietly assume NVIDIA:
|
| 21 |
+
|
| 22 |
+
- `nvidia/cuda` Docker images
|
| 23 |
+
- `torch.device("cuda")` and `.cuda()` calls scattered across code
|
| 24 |
+
- CUDA-specific package pins
|
| 25 |
+
- `nvidia-smi`, `CUDA_VISIBLE_DEVICES`, or `--gpus all` scripts
|
| 26 |
+
- vLLM launch scripts without ROCm/MI300X validation knobs
|
| 27 |
+
- benchmarks that omit backend, memory, tokens/sec, and reproducibility evidence
|
| 28 |
+
|
| 29 |
+
ROCmPilot turns that migration problem into an agentic workflow that feels like a product, not a checklist.
|
| 30 |
+
|
| 31 |
+
## What It Does Today
|
| 32 |
+
|
| 33 |
+
- Accepts a curated sample workload or a **public GitHub repository URL**.
|
| 34 |
+
- Scans relevant files from the repo using the GitHub API.
|
| 35 |
+
- Detects CUDA/NVIDIA assumptions.
|
| 36 |
+
- Produces ROCm-focused findings and patch previews.
|
| 37 |
+
- Shows a five-agent progress timeline.
|
| 38 |
+
- Shows an **Agent War Room** where the task lead asks other agents for input, agents reply to each other, and shared memory records reusable decisions.
|
| 39 |
+
- Persists the agent transcript to **Maximem Synap** long-context memory when `SYNAP_API_KEY` is configured, with local fallback when it is not.
|
| 40 |
+
- Generates terminal-style migration logs.
|
| 41 |
+
- Builds an AMD-readiness benchmark profile.
|
| 42 |
+
- Generates a final report using this backend priority:
|
| 43 |
+
1. AMD ROCm/vLLM endpoint, when `AMD_QWEN_BASE_URL` is configured
|
| 44 |
+
2. Hugging Face Inference Router, when `HF_TOKEN` is configured
|
| 45 |
+
3. Static fallback report, so the demo never breaks
|
| 46 |
+
|
| 47 |
+
## Agent Workflow
|
| 48 |
+
|
| 49 |
+
1. **Repo Doctor Agent**
|
| 50 |
+
- Scans Dockerfiles, Python files, requirements, scripts, benchmark files, and vLLM-related configs.
|
| 51 |
+
- Flags CUDA/NVIDIA assumptions and missing validation evidence.
|
| 52 |
+
|
| 53 |
+
2. **Migration Planner Agent**
|
| 54 |
+
- Converts findings into ROCm migration recommendations.
|
| 55 |
+
- Produces patch previews such as `Dockerfile.rocm`, `requirements-rocm.txt`, device resolvers, and vLLM serve scripts.
|
| 56 |
+
|
| 57 |
+
3. **Build Runner Agent**
|
| 58 |
+
- Prepares the build/test story.
|
| 59 |
+
- In the current version, it does not mutate the target repo. It generates the commands and evidence plan.
|
| 60 |
+
|
| 61 |
+
4. **Benchmark Agent**
|
| 62 |
+
- Produces an AMD-readiness profile.
|
| 63 |
+
- Current benchmark values are estimates until live AMD Developer Cloud validation is connected.
|
| 64 |
+
|
| 65 |
+
5. **Report Agent**
|
| 66 |
+
- Generates a judge-ready report explaining technical findings, AMD GPU path, business value, and next steps.
|
| 67 |
+
- Can use Hugging Face now and AMD-hosted Qwen later.
|
| 68 |
+
|
| 69 |
+
### Agent War Room and Long-Context Memory
|
| 70 |
+
|
| 71 |
+
Each task has one lead agent, but the lead does not work alone. The lead asks the other agents for objections, validation criteria, benchmark provenance, or report framing. Their discussion is rendered as routed messages such as `Repo Doctor -> Build Runner` and `Migration Planner -> Build Runner`.
|
| 72 |
+
|
| 73 |
+
The agents also write reusable memory during the run:
|
| 74 |
+
|
| 75 |
+
- device resolution patterns
|
| 76 |
+
- ROCm acceptance checks
|
| 77 |
+
- container split decisions
|
| 78 |
+
- benchmark provenance rules
|
| 79 |
+
|
| 80 |
+
When Synap is configured, the Report Agent stores the whole war-room transcript as an `ai-chat-conversation`, retrieves scoped user context, and injects that context into the final report prompt. This lets ROCmPilot remember migration decisions across runs instead of rediscovering the same patterns every session.
|
| 81 |
+
|
| 82 |
+
If Synap credentials or runtime setup are missing, ROCmPilot falls back to reconstructed local memory and marks the UI as `Synap Memory: Local fallback`. The demo still completes.
|
| 83 |
+
|
| 84 |
+
## Architecture
|
| 85 |
+
|
| 86 |
+
```text
|
| 87 |
+
Browser UI
|
| 88 |
+
|
|
| 89 |
+
| POST /api/runs
|
| 90 |
+
v
|
| 91 |
+
Stateless Run ID
|
| 92 |
+
|
|
| 93 |
+
| GET /api/runs/[runId]
|
| 94 |
+
v
|
| 95 |
+
Repo Doctor + GitHub Scanner
|
| 96 |
+
|
|
| 97 |
+
v
|
| 98 |
+
Findings + Patch Previews + Logs + Agent Memory + Benchmark Profile
|
| 99 |
+
|
|
| 100 |
+
| POST /api/report
|
| 101 |
+
v
|
| 102 |
+
Report Agent
|
| 103 |
+
|
|
| 104 |
+
| optional memory layer
|
| 105 |
+
v
|
| 106 |
+
Maximem Synap long-context memory -> local fallback memory
|
| 107 |
+
|
|
| 108 |
+
| priority order
|
| 109 |
+
v
|
| 110 |
+
AMD ROCm/vLLM -> Hugging Face Router -> Static fallback
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
The run system is stateless so it works on Vercel serverless functions without a database. The run ID encodes the start time, mode, and target. For the polished production version, real long-running AMD jobs should use persistent storage and a queue.
|
| 114 |
+
|
| 115 |
+
## Tech Stack
|
| 116 |
+
|
| 117 |
+
- Next.js App Router
|
| 118 |
+
- TypeScript
|
| 119 |
+
- Tailwind CSS
|
| 120 |
+
- shadcn/ui
|
| 121 |
+
- AI Elements for markdown, code, and terminal rendering
|
| 122 |
+
- Hugging Face Inference Router for temporary report generation
|
| 123 |
+
- Hugging Face Jobs/TRL for future LoRA fine-tuning
|
| 124 |
+
- Maximem Synap for persistent long-context agent memory
|
| 125 |
+
- AMD ROCm/vLLM endpoint support for the final compute story
|
| 126 |
+
|
| 127 |
+
## Local Development
|
| 128 |
+
|
| 129 |
+
```bash
|
| 130 |
+
npm install
|
| 131 |
+
npm run dev
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
Open:
|
| 135 |
+
|
| 136 |
+
```text
|
| 137 |
+
http://localhost:3000
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
Run checks:
|
| 141 |
+
|
| 142 |
+
```bash
|
| 143 |
+
npm run lint
|
| 144 |
+
npm run build
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
Optional Synap runtime setup for local or worker deployments:
|
| 148 |
+
|
| 149 |
+
```bash
|
| 150 |
+
npm run synap:setup
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
The Synap JS SDK uses a Python bridge. Vercel can still deploy the app without this setup because the report route falls back safely when Synap cannot initialize.
|
| 154 |
+
|
| 155 |
+
## Environment Variables
|
| 156 |
+
|
| 157 |
+
Temporary Hugging Face model backend:
|
| 158 |
+
|
| 159 |
+
```bash
|
| 160 |
+
HF_TOKEN=your_hugging_face_token
|
| 161 |
+
HF_REPORT_MODEL=Qwen/Qwen2.5-Coder-7B-Instruct
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
Optional GitHub token for higher public API limits:
|
| 165 |
+
|
| 166 |
+
```bash
|
| 167 |
+
GITHUB_TOKEN=your_github_token
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
Optional long-context memory:
|
| 171 |
+
|
| 172 |
+
```bash
|
| 173 |
+
SYNAP_API_KEY=your_synap_api_key
|
| 174 |
+
SYNAP_INSTANCE_ID=your_synap_instance_id
|
| 175 |
+
SYNAP_CUSTOMER_ID=rocmpilot-hackathon
|
| 176 |
+
SYNAP_USER_ID=rocmpilot-agent-fleet
|
| 177 |
+
SYNAP_AUTO_SETUP=false
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
Synap uses the default cloud endpoints automatically. Advanced deployments can also set `SYNAP_BASE_URL`, `SYNAP_GRPC_HOST`, `SYNAP_GRPC_PORT`, and `SYNAP_GRPC_TLS`.
|
| 181 |
+
|
| 182 |
+
Future AMD ROCm/vLLM backend:
|
| 183 |
+
|
| 184 |
+
```bash
|
| 185 |
+
AMD_QWEN_BASE_URL=http://YOUR_AMD_INSTANCE:8000
|
| 186 |
+
AMD_QWEN_MODEL=Qwen/Qwen3-Coder-Next
|
| 187 |
+
AMD_QWEN_API_KEY=optional-if-your-endpoint-requires-it
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
When `AMD_QWEN_BASE_URL` is present, the app automatically prefers AMD over Hugging Face.
|
| 191 |
+
|
| 192 |
+
## Deploy on Vercel
|
| 193 |
+
|
| 194 |
+
Vercel is the preferred web deployment target.
|
| 195 |
+
|
| 196 |
+
```bash
|
| 197 |
+
vercel
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
Set environment variables in Vercel Project Settings:
|
| 201 |
+
|
| 202 |
+
- `HF_TOKEN`
|
| 203 |
+
- `HF_REPORT_MODEL`
|
| 204 |
+
- `GITHUB_TOKEN` optional
|
| 205 |
+
- `SYNAP_API_KEY` optional
|
| 206 |
+
- `SYNAP_INSTANCE_ID` optional
|
| 207 |
+
- `SYNAP_CUSTOMER_ID` optional
|
| 208 |
+
- `SYNAP_USER_ID` optional
|
| 209 |
+
- `AMD_QWEN_BASE_URL` later
|
| 210 |
+
- `AMD_QWEN_MODEL` later
|
| 211 |
+
|
| 212 |
+
The current app does not require a database for the demo flow. Synap is optional and the UI will show whether long-context memory is connected or using fallback memory.
|
| 213 |
+
|
| 214 |
+
## Deploy as a Hugging Face Space
|
| 215 |
+
|
| 216 |
+
The repo also includes a Docker Space setup for hackathon submissions that require a Hugging Face Space link.
|
| 217 |
+
|
| 218 |
+
Files:
|
| 219 |
+
|
| 220 |
+
- `Dockerfile`
|
| 221 |
+
- `.dockerignore`
|
| 222 |
+
- Space metadata in this README frontmatter
|
| 223 |
+
|
| 224 |
+
Create a Docker Space, then upload this project. The app listens on port `7860` in the Space container.
|
| 225 |
+
|
| 226 |
+
Recommended Space ID:
|
| 227 |
+
|
| 228 |
+
```text
|
| 229 |
+
Shivam311/rocmpilot
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
## Training and Improving the Agents
|
| 233 |
+
|
| 234 |
+
The `agent-training/` folder contains the training path for a more accurate agent:
|
| 235 |
+
|
| 236 |
+
- `seed-examples.jsonl`: 95 supervised seed examples across migration, patch, benchmark, report, and memory-agent tasks
|
| 237 |
+
- `eval-rubric.md`: scoring rubric
|
| 238 |
+
- `scripts/generate_seed_examples.py`: expands synthetic examples for common CUDA-to-ROCm failure modes
|
| 239 |
+
- `scripts/prepare_dataset.py`: converts seed examples into chat SFT format
|
| 240 |
+
- `scripts/train_rocmpilot_sft.py`: LoRA SFT script for Hugging Face Jobs
|
| 241 |
+
- `scripts/evaluate_agent.py`: smoke eval against an OpenAI-compatible endpoint
|
| 242 |
+
|
| 243 |
+
Regenerate the local seed and preview files:
|
| 244 |
+
|
| 245 |
+
```bash
|
| 246 |
+
python agent-training/scripts/generate_seed_examples.py
|
| 247 |
+
python agent-training/scripts/prepare_dataset.py
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
Prepare and push a seed dataset:
|
| 251 |
+
|
| 252 |
+
```bash
|
| 253 |
+
python agent-training/scripts/prepare_dataset.py \
|
| 254 |
+
--push \
|
| 255 |
+
--repo-id Shivam311/rocmpilot-agent-sft
|
| 256 |
+
```
|
| 257 |
+
|
| 258 |
+
Launch a small training run on Hugging Face Jobs after confirming paid Jobs access:
|
| 259 |
+
|
| 260 |
+
```bash
|
| 261 |
+
hf jobs uv run \
|
| 262 |
+
--flavor t4-small \
|
| 263 |
+
--timeout 90m \
|
| 264 |
+
--secrets HF_TOKEN \
|
| 265 |
+
--env DATASET_ID=Shivam311/rocmpilot-agent-sft \
|
| 266 |
+
--env OUTPUT_MODEL=Shivam311/rocmpilot-agent-qwen-lora-v2 \
|
| 267 |
+
--env MAX_STEPS=160 \
|
| 268 |
+
agent-training/scripts/train_rocmpilot_sft.py
|
| 269 |
+
```
|
| 270 |
+
|
| 271 |
+
Run eval:
|
| 272 |
+
|
| 273 |
+
```bash
|
| 274 |
+
python agent-training/scripts/evaluate_agent.py
|
| 275 |
+
```
|
| 276 |
+
|
| 277 |
+
For a serious version, expand the dataset to at least 200-500 examples from real migration cases before training.
|
| 278 |
+
|
| 279 |
+
## Future AMD Integration
|
| 280 |
+
|
| 281 |
+
Once AMD Developer Cloud access is ready:
|
| 282 |
+
|
| 283 |
+
1. Start an MI300X instance.
|
| 284 |
+
2. Install or use a ROCm/vLLM image.
|
| 285 |
+
3. Serve Qwen through an OpenAI-compatible vLLM endpoint.
|
| 286 |
+
4. Set `AMD_QWEN_BASE_URL` in Vercel.
|
| 287 |
+
5. Re-run ROCmPilot and capture:
|
| 288 |
+
- vLLM startup logs
|
| 289 |
+
- AMD GPU visibility
|
| 290 |
+
- one model response
|
| 291 |
+
- benchmark output
|
| 292 |
+
- final report generated through AMD-hosted Qwen
|
| 293 |
+
|
| 294 |
+
Suggested serving command:
|
| 295 |
+
|
| 296 |
+
```bash
|
| 297 |
+
python -m vllm.entrypoints.openai.api_server \
|
| 298 |
+
--model Qwen/Qwen3-Coder-Next \
|
| 299 |
+
--host 0.0.0.0 \
|
| 300 |
+
--port 8000 \
|
| 301 |
+
--tensor-parallel-size 1 \
|
| 302 |
+
--max-model-len 32768
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
## What Is Real vs Temporary
|
| 306 |
+
|
| 307 |
+
Real today:
|
| 308 |
+
|
| 309 |
+
- Working dashboard
|
| 310 |
+
- Public GitHub URL intake
|
| 311 |
+
- GitHub API file scan
|
| 312 |
+
- Rule-based ROCm findings
|
| 313 |
+
- Patch previews
|
| 314 |
+
- Report backend priority system
|
| 315 |
+
- Vercel-safe stateless run flow
|
| 316 |
+
- Hugging Face training scaffold
|
| 317 |
+
|
| 318 |
+
Temporary/demo today:
|
| 319 |
+
|
| 320 |
+
- Patch previews are not committed back to GitHub yet
|
| 321 |
+
- Build logs are generated evidence, not real Docker builds
|
| 322 |
+
- Benchmark numbers are estimates until AMD validation
|
| 323 |
+
- Model fine-tuning scripts are ready but not launched automatically
|
| 324 |
+
|
| 325 |
+
Next production upgrades:
|
| 326 |
+
|
| 327 |
+
- GitHub App auth and PR creation
|
| 328 |
+
- Persistent run history
|
| 329 |
+
- Real queue for AMD validation jobs
|
| 330 |
+
- Live ROCm Docker builds
|
| 331 |
+
- Live vLLM benchmarks
|
| 332 |
+
- Fine-tuned ROCmPilot model hosted through HF/AMD
|
| 333 |
+
|
| 334 |
+
## Submission Story
|
| 335 |
+
|
| 336 |
+
ROCmPilot is a Track 1 agentic workflow because the core product is the coordination of specialized agents that inspect, plan, validate, benchmark, and report. AMD compute enters as the target validation environment and the future hosted model runtime for Qwen on ROCm/vLLM.
|
| 337 |
+
|
| 338 |
+
The clean hackathon positioning:
|
| 339 |
+
|
| 340 |
+
> ROCmPilot helps developers migrate AI workloads to AMD faster. It scans a GitHub repo, finds CUDA assumptions, proposes ROCm patches, prepares AMD validation, and generates a business-ready migration report. Today it runs with Hugging Face fallback; when AMD Developer Cloud is available, the same app switches to Qwen served on MI300X through ROCm/vLLM.
|
| 341 |
+
|
| 342 |
+
## Sources
|
| 343 |
+
|
| 344 |
+
- AMD: Day 0 Support for Qwen3-Coder-Next on AMD Instinct GPUs
|
| 345 |
+
https://www.amd.com/en/developer/resources/technical-articles/2026/day-0-support-for-qwen3-coder-next-on-amd-instinct-gpus.html
|
| 346 |
+
- Qwen3-Coder announcement
|
| 347 |
+
https://qwenlm.github.io/blog/qwen3-coder/
|
| 348 |
+
- vLLM supported models
|
| 349 |
+
https://docs.vllm.ai/en/v0.15.1/models/supported_models/
|
| 350 |
+
- Hugging Face Docker Spaces
|
| 351 |
+
https://huggingface.co/docs/hub/spaces-sdks-docker
|
| 352 |
+
- Hugging Face Jobs
|
| 353 |
+
https://huggingface.co/docs/huggingface_hub/guides/jobs
|
TECHNICAL_WALKTHROUGH.md
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ROCmPilot Technical Walkthrough
|
| 2 |
+
|
| 3 |
+

|
| 4 |
+

|
| 5 |
+

|
| 6 |
+

|
| 7 |
+

|
| 8 |
+

|
| 9 |
+
|
| 10 |
+
## Submission Links
|
| 11 |
+
|
| 12 |
+
| Artifact | Link |
|
| 13 |
+
| --- | --- |
|
| 14 |
+
| Live demo | [https://rocmpilot.vercel.app](https://rocmpilot.vercel.app) |
|
| 15 |
+
| Source code | [https://github.com/shivambhartiya/rocmpilot](https://github.com/shivambhartiya/rocmpilot) |
|
| 16 |
+
| Training dataset | [https://huggingface.co/datasets/Shivam311/rocmpilot-agent-sft](https://huggingface.co/datasets/Shivam311/rocmpilot-agent-sft) |
|
| 17 |
+
| Technical walkthrough | [TECHNICAL_WALKTHROUGH.md](./TECHNICAL_WALKTHROUGH.md) |
|
| 18 |
+
|
| 19 |
+
## One-Line Pitch
|
| 20 |
+
|
| 21 |
+
ROCmPilot is a multi-agent developer tool that scans CUDA-first AI repositories and generates ROCm migration findings, patch previews, validation plans, reusable memory, and a judge-ready technical/business report.
|
| 22 |
+
|
| 23 |
+
## Why ROCmPilot Matters
|
| 24 |
+
|
| 25 |
+
AI teams often want AMD GPU optionality, but real repositories quietly assume NVIDIA at many layers:
|
| 26 |
+
|
| 27 |
+
- Docker images such as `nvidia/cuda` or `nvcr.io/nvidia/pytorch`
|
| 28 |
+
- Python code with `torch.device("cuda")`, `.cuda()`, or `device_map="cuda"`
|
| 29 |
+
- dependency pins for `cu121`, `cu124`, `nvidia-cublas-cu12`, `flash-attn`, or `xformers`
|
| 30 |
+
- scripts using `nvidia-smi`, `CUDA_VISIBLE_DEVICES`, or `--gpus all`
|
| 31 |
+
- vLLM launch scripts that do not expose ROCm validation knobs
|
| 32 |
+
- benchmarks that report latency without backend, memory, tokens/sec, or command provenance
|
| 33 |
+
|
| 34 |
+
ROCmPilot converts that messy migration work into a structured agentic workflow. It does not pretend to automatically port every project. Instead, it gives developers a credible migration map, concrete patch previews, validation evidence, and a report they can hand to maintainers or infrastructure leaders.
|
| 35 |
+
|
| 36 |
+
## Product Overview
|
| 37 |
+
|
| 38 |
+
```mermaid
|
| 39 |
+
flowchart LR
|
| 40 |
+
U["Developer pastes a public GitHub URL"] --> UI["ROCmPilot Dashboard"]
|
| 41 |
+
UI --> R["Run Orchestrator"]
|
| 42 |
+
R --> G["GitHub Scanner"]
|
| 43 |
+
G --> D["Repo Doctor Agent"]
|
| 44 |
+
D --> P["Migration Planner Agent"]
|
| 45 |
+
P --> B["Build Runner Agent"]
|
| 46 |
+
B --> M["Benchmark Agent"]
|
| 47 |
+
M --> W["Agent War Room"]
|
| 48 |
+
W --> S["Synap Long-Context Memory"]
|
| 49 |
+
W --> REP["Report Agent"]
|
| 50 |
+
REP --> HF["Hugging Face Qwen fallback"]
|
| 51 |
+
REP --> AMD["Future AMD ROCm/vLLM Qwen endpoint"]
|
| 52 |
+
REP --> OUT["Final migration report"]
|
| 53 |
+
P --> PATCH["ROCm patch previews"]
|
| 54 |
+
M --> EVID["Validation checklist"]
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## User Flow
|
| 58 |
+
|
| 59 |
+
1. Open the ROCmPilot dashboard.
|
| 60 |
+
2. Select a sample workload or paste a public GitHub repository URL.
|
| 61 |
+
3. Start a run.
|
| 62 |
+
4. Watch the live agent timeline and Agent War Room.
|
| 63 |
+
5. Review findings, patch previews, logs, benchmark profile, and long-context memory status.
|
| 64 |
+
6. Read the generated final report.
|
| 65 |
+
7. Use the output as a migration plan or as the basis for a future PR.
|
| 66 |
+
|
| 67 |
+
## Agent System
|
| 68 |
+
|
| 69 |
+
ROCmPilot uses specialized agents, each responsible for a different migration concern.
|
| 70 |
+
|
| 71 |
+
| Agent | Responsibility | Output |
|
| 72 |
+
| --- | --- | --- |
|
| 73 |
+
| Repo Doctor Agent | Finds CUDA/NVIDIA assumptions across code, Docker, scripts, dependencies, and benchmarks. | Findings with severity, category, file, line, and fix. |
|
| 74 |
+
| Migration Planner Agent | Converts findings into ROCm-safe migration recommendations. | Patch previews and backend-aware migration steps. |
|
| 75 |
+
| Build Runner Agent | Challenges whether the plan can be validated. | Smoke-test commands, container checks, and proof boundaries. |
|
| 76 |
+
| Benchmark Agent | Separates live evidence from estimates. | AMD-readiness profile and measurement plan. |
|
| 77 |
+
| Memory Agent | Stores reusable decisions across runs. | Synap-backed long-context memory or local fallback memory. |
|
| 78 |
+
| Report Agent | Turns the run into a credible submission/report. | Final markdown report with technical and business value. |
|
| 79 |
+
|
| 80 |
+
## Agent War Room
|
| 81 |
+
|
| 82 |
+
The MVP is not just a set of isolated cards. Agents route messages to each other, ask questions, answer objections, and write shared memory.
|
| 83 |
+
|
| 84 |
+
```mermaid
|
| 85 |
+
sequenceDiagram
|
| 86 |
+
participant O as Orchestrator
|
| 87 |
+
participant R as Repo Doctor
|
| 88 |
+
participant P as Migration Planner
|
| 89 |
+
participant B as Build Runner
|
| 90 |
+
participant M as Benchmark Agent
|
| 91 |
+
participant S as Shared Memory
|
| 92 |
+
participant X as Report Agent
|
| 93 |
+
|
| 94 |
+
O->>R: Lead the repo compatibility scan
|
| 95 |
+
R->>B: Which findings are build-breaking?
|
| 96 |
+
B-->>R: Container image, device path, and smoke test gaps
|
| 97 |
+
R->>P: Design the safest ROCm migration step
|
| 98 |
+
P-->>R: Use one backend-aware device resolver
|
| 99 |
+
R->>S: Store device resolution pattern
|
| 100 |
+
O->>P: Lead the migration plan using memory
|
| 101 |
+
P->>B: What proves this is ROCm-ready?
|
| 102 |
+
B-->>P: Log torch.version.hip and hit vLLM health endpoint
|
| 103 |
+
B->>S: Store ROCm acceptance checks
|
| 104 |
+
O->>M: Lead benchmark evidence
|
| 105 |
+
M->>X: How do we label estimated metrics?
|
| 106 |
+
X-->>M: Keep estimates separate from live AMD logs
|
| 107 |
+
M->>S: Store metric provenance rule
|
| 108 |
+
X->>S: Recall decisions for the final report
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
## Long-Context Memory With Synap
|
| 112 |
+
|
| 113 |
+
ROCmPilot integrates Maximem Synap as the long-context memory layer. When configured, the Report Agent stores the whole agent discussion as an `ai-chat-conversation`, fetches relevant context, and injects it into report generation.
|
| 114 |
+
|
| 115 |
+
If Synap credentials or runtime setup are unavailable, ROCmPilot falls back to reconstructed local memory so the demo remains reliable.
|
| 116 |
+
|
| 117 |
+
```mermaid
|
| 118 |
+
flowchart TB
|
| 119 |
+
RUN["Completed ROCmPilot run"] --> DISC["Agent messages and shared memory"]
|
| 120 |
+
DISC --> SYNC{"SYNAP_API_KEY configured?"}
|
| 121 |
+
SYNC -- "Yes" --> INGEST["Synap addMemory"]
|
| 122 |
+
INGEST --> FETCH["Synap fetchUserContext and getContextForPrompt"]
|
| 123 |
+
FETCH --> PROMPT["Report prompt enriched with long-context memory"]
|
| 124 |
+
SYNC -- "No or runtime unavailable" --> LOCAL["Local reconstructed memory"]
|
| 125 |
+
LOCAL --> PROMPT
|
| 126 |
+
PROMPT --> REPORT["Final report"]
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
### Memory Values Stored
|
| 130 |
+
|
| 131 |
+
| Memory Type | Example |
|
| 132 |
+
| --- | --- |
|
| 133 |
+
| Device resolution pattern | Use a single backend resolver instead of scattered `.cuda()` checks. |
|
| 134 |
+
| ROCm acceptance checks | Prove import, backend detection, vLLM health, and provenance logging. |
|
| 135 |
+
| Container split decision | Add `Dockerfile.rocm`; keep CUDA support separate. |
|
| 136 |
+
| Metric provenance rule | Label estimates until AMD SMI/vLLM logs exist. |
|
| 137 |
+
| Health-check rule | Replace `nvidia-smi` proof with `rocm-smi` or `amd-smi` proof on AMD. |
|
| 138 |
+
|
| 139 |
+
## Detection and Recommendation Logic
|
| 140 |
+
|
| 141 |
+
ROCmPilot scans public GitHub repositories using the GitHub API and focuses on files that usually control AI runtime portability.
|
| 142 |
+
|
| 143 |
+
```mermaid
|
| 144 |
+
flowchart LR
|
| 145 |
+
TREE["GitHub repository tree"] --> FILTER["Relevant file filter"]
|
| 146 |
+
FILTER --> FILES["Dockerfiles, Python, shell scripts, requirements, YAML, benchmark files"]
|
| 147 |
+
FILES --> RULES["ROCm compatibility rules"]
|
| 148 |
+
RULES --> FIND["Findings"]
|
| 149 |
+
FIND --> PATCH["Patch previews"]
|
| 150 |
+
FIND --> MEM["Memory writes"]
|
| 151 |
+
PATCH --> REPORT["Final report"]
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
### Current Detection Categories
|
| 155 |
+
|
| 156 |
+
| Category | Severity | Typical Evidence | Recommended Direction |
|
| 157 |
+
| --- | --- | --- | --- |
|
| 158 |
+
| Hardcoded CUDA device path | Critical | `.cuda()`, `torch.device("cuda")`, `device_map="cuda"` | Add a backend-aware resolver and log ROCm/CUDA/CPU provenance. |
|
| 159 |
+
| NVIDIA container/runtime assumption | High | `nvidia/cuda`, `nvcr.io`, `--gpus all` | Add ROCm runtime image/profile and keep CUDA optional. |
|
| 160 |
+
| CUDA-oriented dependency | High | `cu124`, `nvidia-cublas-cu12`, `flash-attn`, `xformers` | Split dependencies into backend-specific profiles. |
|
| 161 |
+
| vLLM defaults need AMD profile | Medium | vLLM launch without model length, tensor parallelism, or metrics | Add ROCm vLLM serve script with OpenAI-compatible endpoint settings. |
|
| 162 |
+
| Benchmark evidence incomplete | Medium | latency-only benchmark | Add tokens/sec, memory, backend, model id, command provenance, p95 latency. |
|
| 163 |
+
| NVIDIA monitoring command | Medium | `nvidia-smi` | Add `rocm-smi` or `amd-smi` evidence path. |
|
| 164 |
+
| CUDA extension build path | High | `.cu`, `CUDAExtension`, `CUDA_HOME` | Gate CUDA extension builds and document ROCm-safe alternatives. |
|
| 165 |
+
| Docker Compose NVIDIA reservation | High | `driver: nvidia`, `runtime: nvidia` | Add separate ROCm Compose profile. |
|
| 166 |
+
|
| 167 |
+
## Patch Preview Example
|
| 168 |
+
|
| 169 |
+
ROCmPilot does not currently mutate external repositories or open PRs automatically. Instead, it generates patch previews that are safe to review.
|
| 170 |
+
|
| 171 |
+
```diff
|
| 172 |
+
+import torch
|
| 173 |
+
+
|
| 174 |
+
+def resolve_device() -> tuple[str, str]:
|
| 175 |
+
+ if torch.cuda.is_available():
|
| 176 |
+
+ backend = "rocm" if getattr(torch.version, "hip", None) else "cuda"
|
| 177 |
+
+ return "cuda", backend
|
| 178 |
+
+ return "cpu", "cpu"
|
| 179 |
+
+
|
| 180 |
+
+DEVICE, GPU_BACKEND = resolve_device()
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
This pattern is important because PyTorch on ROCm still exposes GPU access through the `torch.cuda` API surface. The resolver records whether the backend is actually CUDA or HIP-backed ROCm.
|
| 184 |
+
|
| 185 |
+
## Model and Compute Strategy
|
| 186 |
+
|
| 187 |
+
ROCmPilot is a Track 1 agentic workflow project. The GPU plan is model serving, not fine-tuning as the main hackathon track.
|
| 188 |
+
|
| 189 |
+
```mermaid
|
| 190 |
+
flowchart TD
|
| 191 |
+
REPORT["Report Agent request"] --> AMDQ{"AMD_QWEN_BASE_URL set?"}
|
| 192 |
+
AMDQ -- "Yes" --> VLLM["Qwen on AMD MI300X through ROCm/vLLM"]
|
| 193 |
+
AMDQ -- "No" --> HFQ{"HF_TOKEN set?"}
|
| 194 |
+
HFQ -- "Yes" --> HF["Hugging Face Router with Qwen"]
|
| 195 |
+
HFQ -- "No" --> STATIC["Deterministic fallback report"]
|
| 196 |
+
VLLM --> RESP["Final report"]
|
| 197 |
+
HF --> RESP
|
| 198 |
+
STATIC --> RESP
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
### Backend Priority
|
| 202 |
+
|
| 203 |
+
1. AMD ROCm/vLLM endpoint via `AMD_QWEN_BASE_URL`
|
| 204 |
+
2. Hugging Face Router via `HF_TOKEN`
|
| 205 |
+
3. Static fallback report
|
| 206 |
+
|
| 207 |
+
This keeps the submission demo-safe while preserving the AMD compute story. Once AMD Developer Cloud access is available, the same report endpoint can prefer AMD-hosted Qwen automatically.
|
| 208 |
+
|
| 209 |
+
## Training Path on Hugging Face
|
| 210 |
+
|
| 211 |
+
The project includes a small supervised fine-tuning path for polishing the agent style and migration recommendations.
|
| 212 |
+
|
| 213 |
+
| Artifact | Detail |
|
| 214 |
+
| --- | --- |
|
| 215 |
+
| Dataset repo | [Shivam311/rocmpilot-agent-sft](https://huggingface.co/datasets/Shivam311/rocmpilot-agent-sft) |
|
| 216 |
+
| Current seed size | 95 examples |
|
| 217 |
+
| Tasks | migration planner, patch planner, benchmark agent, report agent, memory agent |
|
| 218 |
+
| Base model target | `Qwen/Qwen2.5-Coder-0.5B-Instruct` for small LoRA experiments |
|
| 219 |
+
| Output model target | `Shivam311/rocmpilot-agent-qwen-lora-v2` |
|
| 220 |
+
|
| 221 |
+
```mermaid
|
| 222 |
+
flowchart LR
|
| 223 |
+
SEED["Synthetic and human-reviewed examples"] --> PREP["prepare_dataset.py"]
|
| 224 |
+
PREP --> DATASET["HF Dataset: rocmpilot-agent-sft"]
|
| 225 |
+
DATASET --> TRAIN["LoRA SFT job"]
|
| 226 |
+
TRAIN --> MODEL["Qwen LoRA adapter"]
|
| 227 |
+
MODEL --> EVAL["evaluate_agent.py"]
|
| 228 |
+
EVAL --> APP["Future report/migration backend"]
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
Training is supporting polish, not the central submission claim. The main project remains an agentic developer workflow.
|
| 232 |
+
|
| 233 |
+
## App Architecture
|
| 234 |
+
|
| 235 |
+
```mermaid
|
| 236 |
+
flowchart TB
|
| 237 |
+
subgraph Frontend
|
| 238 |
+
UI["Next.js dashboard"]
|
| 239 |
+
WAR["Agent War Room"]
|
| 240 |
+
TABLE["Findings and patch panels"]
|
| 241 |
+
STATUS["GPU and memory status cards"]
|
| 242 |
+
end
|
| 243 |
+
|
| 244 |
+
subgraph API
|
| 245 |
+
RUNS["/api/runs"]
|
| 246 |
+
RUNID["/api/runs/[runId]"]
|
| 247 |
+
REPORT["/api/report"]
|
| 248 |
+
end
|
| 249 |
+
|
| 250 |
+
subgraph Core
|
| 251 |
+
STORE["Stateless run store"]
|
| 252 |
+
SCAN["GitHub scanner"]
|
| 253 |
+
DATA["ROCmPilot run model"]
|
| 254 |
+
SYNAP["Synap memory adapter"]
|
| 255 |
+
end
|
| 256 |
+
|
| 257 |
+
subgraph External
|
| 258 |
+
GH["GitHub API"]
|
| 259 |
+
HF["Hugging Face Router"]
|
| 260 |
+
AMD["AMD ROCm/vLLM endpoint"]
|
| 261 |
+
SM["Maximem Synap"]
|
| 262 |
+
end
|
| 263 |
+
|
| 264 |
+
UI --> RUNS
|
| 265 |
+
UI --> RUNID
|
| 266 |
+
UI --> REPORT
|
| 267 |
+
RUNS --> STORE
|
| 268 |
+
RUNID --> SCAN
|
| 269 |
+
SCAN --> GH
|
| 270 |
+
STORE --> DATA
|
| 271 |
+
DATA --> WAR
|
| 272 |
+
REPORT --> SYNAP
|
| 273 |
+
SYNAP --> SM
|
| 274 |
+
REPORT --> AMD
|
| 275 |
+
REPORT --> HF
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
## Data Model
|
| 279 |
+
|
| 280 |
+
```mermaid
|
| 281 |
+
erDiagram
|
| 282 |
+
ROCM_RUN ||--o{ RUN_STAGE : has
|
| 283 |
+
ROCM_RUN ||--o{ FINDING : produces
|
| 284 |
+
ROCM_RUN ||--o{ PATCH_PREVIEW : proposes
|
| 285 |
+
ROCM_RUN ||--o{ AGENT_MESSAGE : contains
|
| 286 |
+
ROCM_RUN ||--o{ AGENT_MEMORY : stores
|
| 287 |
+
ROCM_RUN ||--o{ BENCHMARK_RESULT : profiles
|
| 288 |
+
ROCM_RUN ||--|| GPU_MODEL_STATUS : reports
|
| 289 |
+
ROCM_RUN ||--|| LONG_CONTEXT_MEMORY_STATUS : reports
|
| 290 |
+
|
| 291 |
+
FINDING {
|
| 292 |
+
string severity
|
| 293 |
+
string category
|
| 294 |
+
string file
|
| 295 |
+
int line
|
| 296 |
+
string recommendedFix
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
AGENT_MESSAGE {
|
| 300 |
+
string agent
|
| 301 |
+
string toAgent
|
| 302 |
+
string kind
|
| 303 |
+
string task
|
| 304 |
+
string leadAgent
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
AGENT_MEMORY {
|
| 308 |
+
string title
|
| 309 |
+
string scope
|
| 310 |
+
string learnedFromAgent
|
| 311 |
+
string solution
|
| 312 |
+
}
|
| 313 |
+
```
|
| 314 |
+
|
| 315 |
+
## Deployment Architecture
|
| 316 |
+
|
| 317 |
+
```mermaid
|
| 318 |
+
flowchart LR
|
| 319 |
+
DEV["GitHub main branch"] --> VERCEL["Vercel deployment"]
|
| 320 |
+
VERCEL --> APP["rocmpilot.vercel.app"]
|
| 321 |
+
VERCEL --> ENV["Environment variables"]
|
| 322 |
+
ENV --> HFENV["HF_TOKEN"]
|
| 323 |
+
ENV --> SYNENV["SYNAP_API_KEY and SYNAP_INSTANCE_ID"]
|
| 324 |
+
ENV --> AMDENV["AMD_QWEN_BASE_URL later"]
|
| 325 |
+
APP --> USER["Hackathon judges and users"]
|
| 326 |
+
```
|
| 327 |
+
|
| 328 |
+
## Environment Variables
|
| 329 |
+
|
| 330 |
+
| Variable | Required for MVP | Purpose |
|
| 331 |
+
| --- | --- | --- |
|
| 332 |
+
| `HF_TOKEN` | Recommended | Enables Hugging Face Qwen report generation. |
|
| 333 |
+
| `HF_REPORT_MODEL` | Optional | Defaults to `Qwen/Qwen2.5-Coder-7B-Instruct`. |
|
| 334 |
+
| `GITHUB_TOKEN` | Optional | Raises GitHub API limits for public repo scans. |
|
| 335 |
+
| `SYNAP_INSTANCE_ID` | Optional | Targets the Synap memory instance. |
|
| 336 |
+
| `SYNAP_API_KEY` | Optional | Enables persistent long-context memory. |
|
| 337 |
+
| `SYNAP_BASE_URL` | Optional | Synap cloud endpoint override. |
|
| 338 |
+
| `SYNAP_CUSTOMER_ID` | Optional | Memory scope, defaults to `rocmpilot-hackathon`. |
|
| 339 |
+
| `SYNAP_USER_ID` | Optional | Agent fleet memory identity. |
|
| 340 |
+
| `AMD_QWEN_BASE_URL` | Later | Enables AMD-hosted Qwen through ROCm/vLLM. |
|
| 341 |
+
| `AMD_QWEN_MODEL` | Later | Defaults to `Qwen/Qwen3-Coder-Next`. |
|
| 342 |
+
|
| 343 |
+
## Demo Script
|
| 344 |
+
|
| 345 |
+
Use this script for a 2-3 minute project walkthrough.
|
| 346 |
+
|
| 347 |
+
1. Open [https://rocmpilot.vercel.app](https://rocmpilot.vercel.app).
|
| 348 |
+
2. Paste a CUDA-heavy public repo, for example `https://github.com/NVIDIA/cuda-samples` or `https://github.com/NVIDIA/Megatron-LM`.
|
| 349 |
+
3. Click `Scan Repo`.
|
| 350 |
+
4. Explain that Repo Doctor scans public files for CUDA/NVIDIA assumptions.
|
| 351 |
+
5. Point to the Agent War Room and show agents asking each other questions instead of acting independently.
|
| 352 |
+
6. Show Shared Memory and explain that decisions are reused later.
|
| 353 |
+
7. Open Migration Findings and Patch Previews.
|
| 354 |
+
8. Open the GPU model status card and explain the backend priority: AMD ROCm/vLLM when available, Hugging Face fallback now, static fallback for reliability.
|
| 355 |
+
9. Open the Long-context memory card and explain Synap memory.
|
| 356 |
+
10. Show the final report and emphasize business value: faster AMD migration planning, reduced infra risk, and a clear path from audit to validation.
|
| 357 |
+
|
| 358 |
+
## What Is Fully Implemented Today
|
| 359 |
+
|
| 360 |
+
| Capability | Status |
|
| 361 |
+
| --- | --- |
|
| 362 |
+
| Next.js dashboard | Implemented |
|
| 363 |
+
| Public GitHub URL scan | Implemented |
|
| 364 |
+
| CUDA/NVIDIA findings | Implemented |
|
| 365 |
+
| Patch previews | Implemented |
|
| 366 |
+
| Agent War Room routed messages | Implemented |
|
| 367 |
+
| Shared run memory | Implemented |
|
| 368 |
+
| Synap memory adapter | Implemented with fallback |
|
| 369 |
+
| Hugging Face Qwen report generation | Implemented when `HF_TOKEN` is configured |
|
| 370 |
+
| Static fallback report | Implemented |
|
| 371 |
+
| Vercel deployment | Implemented |
|
| 372 |
+
| Training dataset | Implemented and published |
|
| 373 |
+
|
| 374 |
+
## Honest Boundaries
|
| 375 |
+
|
| 376 |
+
| Capability | Current Boundary |
|
| 377 |
+
| --- | --- |
|
| 378 |
+
| Automatic PR creation | Not implemented yet. ROCmPilot generates patch previews and recommendations. |
|
| 379 |
+
| Live AMD MI300X benchmark proof | Pending AMD Developer Cloud access. Current benchmark cards are labeled static estimates. |
|
| 380 |
+
| Synap on every Vercel run | Integrated, but falls back if the Synap Python bridge cannot initialize in the serverless runtime. |
|
| 381 |
+
| Full repository mutation | Not performed by design in the MVP. Maintainers should review patch previews first. |
|
| 382 |
+
|
| 383 |
+
## Business Value
|
| 384 |
+
|
| 385 |
+
ROCmPilot is useful for:
|
| 386 |
+
|
| 387 |
+
- AI startups that want AMD GPU optionality without manually auditing every codebase.
|
| 388 |
+
- Infrastructure teams evaluating whether an internal CUDA-first service can move to ROCm.
|
| 389 |
+
- Open-source maintainers who want clear, reviewable migration guidance.
|
| 390 |
+
- Cloud/GPU providers that need a repeatable readiness assessment workflow.
|
| 391 |
+
|
| 392 |
+
### Business Impact
|
| 393 |
+
|
| 394 |
+
| Problem | ROCmPilot Value |
|
| 395 |
+
| --- | --- |
|
| 396 |
+
| Migration audits are manual and slow. | Automated agentic scan and report. |
|
| 397 |
+
| CUDA assumptions hide across many files. | Multi-layer detection across Docker, Python, shell, dependencies, and benchmarks. |
|
| 398 |
+
| Teams overclaim hardware readiness. | Explicit separation of estimates, fallback, and live AMD proof. |
|
| 399 |
+
| Migration work is hard to hand off. | Patch previews and judge/infra-ready report. |
|
| 400 |
+
| Agents forget past decisions. | Synap long-context memory for reusable migration knowledge. |
|
| 401 |
+
|
| 402 |
+
## Roadmap
|
| 403 |
+
|
| 404 |
+
```mermaid
|
| 405 |
+
gantt
|
| 406 |
+
title ROCmPilot Roadmap
|
| 407 |
+
dateFormat YYYY-MM-DD
|
| 408 |
+
section MVP
|
| 409 |
+
Dashboard and agent workflow :done, mvp1, 2026-05-04, 1d
|
| 410 |
+
GitHub scan and patch previews :done, mvp2, 2026-05-04, 1d
|
| 411 |
+
Synap memory integration :done, mvp3, 2026-05-05, 1d
|
| 412 |
+
Training dataset expansion :done, mvp4, 2026-05-05, 1d
|
| 413 |
+
section Next
|
| 414 |
+
Automatic PR generation :active, next1, 2026-05-06, 2d
|
| 415 |
+
Live AMD MI300X vLLM proof : next2, 2026-05-07, 2d
|
| 416 |
+
Larger human-reviewed dataset : next3, 2026-05-08, 4d
|
| 417 |
+
NodeOps memory worker : next4, 2026-05-08, 3d
|
| 418 |
+
```
|
| 419 |
+
|
| 420 |
+
## Future AMD Integration
|
| 421 |
+
|
| 422 |
+
When AMD Developer Cloud access is available:
|
| 423 |
+
|
| 424 |
+
1. Start an MI300X instance.
|
| 425 |
+
2. Launch Qwen through ROCm/vLLM using an OpenAI-compatible server.
|
| 426 |
+
3. Set `AMD_QWEN_BASE_URL` and `AMD_QWEN_MODEL` in Vercel.
|
| 427 |
+
4. Re-run ROCmPilot.
|
| 428 |
+
5. Capture proof:
|
| 429 |
+
- AMD GPU visibility logs
|
| 430 |
+
- vLLM startup logs
|
| 431 |
+
- one successful model response
|
| 432 |
+
- workload validation output
|
| 433 |
+
- updated final report generated through AMD-hosted Qwen
|
| 434 |
+
|
| 435 |
+
Suggested command:
|
| 436 |
+
|
| 437 |
+
```bash
|
| 438 |
+
python -m vllm.entrypoints.openai.api_server \
|
| 439 |
+
--model Qwen/Qwen3-Coder-Next \
|
| 440 |
+
--host 0.0.0.0 \
|
| 441 |
+
--port 8000 \
|
| 442 |
+
--tensor-parallel-size 1 \
|
| 443 |
+
--max-model-len 32768
|
| 444 |
+
```
|
| 445 |
+
|
| 446 |
+
## Why This Fits Track 1
|
| 447 |
+
|
| 448 |
+
ROCmPilot is not primarily a fine-tuning project and not primarily a multimodal project. It is a coordinated AI-agent workflow:
|
| 449 |
+
|
| 450 |
+
- multiple specialized agents
|
| 451 |
+
- routed agent-to-agent discussion
|
| 452 |
+
- lead-agent task ownership
|
| 453 |
+
- shared and persistent memory
|
| 454 |
+
- model-backed report generation
|
| 455 |
+
- real developer workflow around repository migration
|
| 456 |
+
|
| 457 |
+
That makes **AI Agents & Agentic Workflows** the best hackathon track.
|
| 458 |
+
|
| 459 |
+
## Closing Summary
|
| 460 |
+
|
| 461 |
+
ROCmPilot turns CUDA-to-ROCm migration from an unclear engineering chore into an agentic developer workflow. It scans real repositories, identifies migration blockers, proposes ROCm-ready patches, records reusable memory, and generates a technical/business report. The MVP is reliable on Vercel today through Hugging Face and fallback paths, while the architecture is ready to upgrade to AMD Instinct GPUs with ROCm/vLLM as soon as AMD cloud access is available.
|
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": "src/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 |
+
}
|
eslint.config.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
// Override default ignores of eslint-config-next.
|
| 9 |
+
globalIgnores([
|
| 10 |
+
// Default ignores of eslint-config-next:
|
| 11 |
+
".next/**",
|
| 12 |
+
"out/**",
|
| 13 |
+
"build/**",
|
| 14 |
+
"next-env.d.ts",
|
| 15 |
+
]),
|
| 16 |
+
]);
|
| 17 |
+
|
| 18 |
+
export default eslintConfig;
|
next.config.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { NextConfig } from "next";
|
| 2 |
+
|
| 3 |
+
const nextConfig: NextConfig = {
|
| 4 |
+
output: "standalone",
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default nextConfig;
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "rocmpilot",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start",
|
| 9 |
+
"lint": "eslint",
|
| 10 |
+
"synap:setup": "synap-js-sdk setup --sdk-version 0.1.1"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@maximem/synap-js-sdk": "^0.2.4",
|
| 14 |
+
"@streamdown/cjk": "^1.0.3",
|
| 15 |
+
"@streamdown/code": "^1.1.1",
|
| 16 |
+
"@streamdown/math": "^1.0.2",
|
| 17 |
+
"@streamdown/mermaid": "^1.0.2",
|
| 18 |
+
"ai": "^6.0.174",
|
| 19 |
+
"ansi-to-react": "^6.2.6",
|
| 20 |
+
"class-variance-authority": "^0.7.1",
|
| 21 |
+
"clsx": "^2.1.1",
|
| 22 |
+
"lucide-react": "^1.14.0",
|
| 23 |
+
"next": "16.2.4",
|
| 24 |
+
"radix-ui": "^1.4.3",
|
| 25 |
+
"react": "19.2.4",
|
| 26 |
+
"react-dom": "19.2.4",
|
| 27 |
+
"shadcn": "^4.6.0",
|
| 28 |
+
"shiki": "^4.0.2",
|
| 29 |
+
"streamdown": "^2.5.0",
|
| 30 |
+
"tailwind-merge": "^3.5.0",
|
| 31 |
+
"tw-animate-css": "^1.4.0"
|
| 32 |
+
},
|
| 33 |
+
"devDependencies": {
|
| 34 |
+
"@tailwindcss/postcss": "^4",
|
| 35 |
+
"@types/node": "^20",
|
| 36 |
+
"@types/react": "^19",
|
| 37 |
+
"@types/react-dom": "^19",
|
| 38 |
+
"eslint": "^9",
|
| 39 |
+
"eslint-config-next": "16.2.4",
|
| 40 |
+
"tailwindcss": "^4",
|
| 41 |
+
"typescript": "^5"
|
| 42 |
+
}
|
| 43 |
+
}
|
postcss.config.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const config = {
|
| 2 |
+
plugins: {
|
| 3 |
+
"@tailwindcss/postcss": {},
|
| 4 |
+
},
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default config;
|
public/file.svg
ADDED
|
|
public/globe.svg
ADDED
|
|
public/next.svg
ADDED
|
|
public/vercel.svg
ADDED
|
|
public/window.svg
ADDED
|
|
src/app/api/report/route.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { buildFallbackReport, buildReportPrompt, getModelStatus } from "@/lib/rocmpilot/data";
|
| 2 |
+
import { syncRunMemoryWithSynap } from "@/lib/rocmpilot/synap-memory";
|
| 3 |
+
import type { ReportResponse, RocmRun } from "@/lib/rocmpilot/types";
|
| 4 |
+
import { NextResponse } from "next/server";
|
| 5 |
+
|
| 6 |
+
export const runtime = "nodejs";
|
| 7 |
+
|
| 8 |
+
type ChatCompletionResponse = {
|
| 9 |
+
choices?: Array<{
|
| 10 |
+
message?: {
|
| 11 |
+
content?: string;
|
| 12 |
+
};
|
| 13 |
+
}>;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
async function generateWithAmdQwen(run: RocmRun, longContext: string) {
|
| 17 |
+
const baseUrl = process.env.AMD_QWEN_BASE_URL?.replace(/\/$/, "");
|
| 18 |
+
|
| 19 |
+
if (!baseUrl) {
|
| 20 |
+
return null;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const model = process.env.AMD_QWEN_MODEL ?? "Qwen/Qwen3-Coder-Next";
|
| 24 |
+
const headers: Record<string, string> = {
|
| 25 |
+
"Content-Type": "application/json",
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
if (process.env.AMD_QWEN_API_KEY) {
|
| 29 |
+
headers.Authorization = `Bearer ${process.env.AMD_QWEN_API_KEY}`;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
const response = await fetch(`${baseUrl}/v1/chat/completions`, {
|
| 33 |
+
method: "POST",
|
| 34 |
+
headers,
|
| 35 |
+
signal: AbortSignal.timeout(15_000),
|
| 36 |
+
body: JSON.stringify({
|
| 37 |
+
model,
|
| 38 |
+
temperature: 0.25,
|
| 39 |
+
max_tokens: 900,
|
| 40 |
+
messages: [
|
| 41 |
+
{
|
| 42 |
+
role: "system",
|
| 43 |
+
content:
|
| 44 |
+
"You are the Report Agent for ROCmPilot. Write concise, credible hackathon submission reports. Do not invent live benchmark claims beyond the provided data.",
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
role: "user",
|
| 48 |
+
content: buildReportPrompt(run, longContext),
|
| 49 |
+
},
|
| 50 |
+
],
|
| 51 |
+
}),
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
if (!response.ok) {
|
| 55 |
+
throw new Error(`AMD Qwen endpoint returned ${response.status}`);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
const payload = (await response.json()) as ChatCompletionResponse;
|
| 59 |
+
return payload.choices?.[0]?.message?.content?.trim() || null;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
async function generateWithHuggingFace(run: RocmRun, longContext: string) {
|
| 63 |
+
if (!process.env.HF_TOKEN) {
|
| 64 |
+
return null;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
const response = await fetch("https://router.huggingface.co/v1/chat/completions", {
|
| 68 |
+
method: "POST",
|
| 69 |
+
headers: {
|
| 70 |
+
Authorization: `Bearer ${process.env.HF_TOKEN}`,
|
| 71 |
+
"Content-Type": "application/json",
|
| 72 |
+
},
|
| 73 |
+
signal: AbortSignal.timeout(15_000),
|
| 74 |
+
body: JSON.stringify({
|
| 75 |
+
model: process.env.HF_REPORT_MODEL ?? "Qwen/Qwen2.5-Coder-7B-Instruct",
|
| 76 |
+
temperature: 0.25,
|
| 77 |
+
max_tokens: 900,
|
| 78 |
+
messages: [
|
| 79 |
+
{
|
| 80 |
+
role: "system",
|
| 81 |
+
content:
|
| 82 |
+
"You are the Report Agent for ROCmPilot. Write concise, credible hackathon submission reports. Do not invent live benchmark claims beyond the provided data.",
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
role: "user",
|
| 86 |
+
content: buildReportPrompt(run, longContext),
|
| 87 |
+
},
|
| 88 |
+
],
|
| 89 |
+
}),
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
if (!response.ok) {
|
| 93 |
+
throw new Error(`Hugging Face router returned ${response.status}`);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
const payload = (await response.json()) as ChatCompletionResponse;
|
| 97 |
+
return payload.choices?.[0]?.message?.content?.trim() || null;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
export async function POST(request: Request) {
|
| 101 |
+
const run = (await request.json()) as RocmRun;
|
| 102 |
+
const memorySync = await syncRunMemoryWithSynap(run);
|
| 103 |
+
|
| 104 |
+
try {
|
| 105 |
+
const report = await generateWithAmdQwen(run, memorySync.promptContext);
|
| 106 |
+
|
| 107 |
+
if (report) {
|
| 108 |
+
const response: ReportResponse = {
|
| 109 |
+
report,
|
| 110 |
+
source: "amd-vllm",
|
| 111 |
+
modelStatus: getModelStatus("amd-vllm"),
|
| 112 |
+
memoryStatus: memorySync.status,
|
| 113 |
+
};
|
| 114 |
+
return NextResponse.json(response);
|
| 115 |
+
}
|
| 116 |
+
} catch (error) {
|
| 117 |
+
console.warn("Falling back from AMD Qwen report generation:", error);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
try {
|
| 121 |
+
const report = await generateWithHuggingFace(run, memorySync.promptContext);
|
| 122 |
+
|
| 123 |
+
if (report) {
|
| 124 |
+
const response: ReportResponse = {
|
| 125 |
+
report,
|
| 126 |
+
source: "hf-router",
|
| 127 |
+
modelStatus: getModelStatus("hf-router"),
|
| 128 |
+
memoryStatus: memorySync.status,
|
| 129 |
+
};
|
| 130 |
+
return NextResponse.json(response);
|
| 131 |
+
}
|
| 132 |
+
} catch (error) {
|
| 133 |
+
console.warn("Falling back from Hugging Face report generation:", error);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
const response: ReportResponse = {
|
| 137 |
+
report: buildFallbackReport(run, memorySync.promptContext),
|
| 138 |
+
source: "fallback",
|
| 139 |
+
modelStatus: getModelStatus("fallback"),
|
| 140 |
+
memoryStatus: memorySync.status,
|
| 141 |
+
};
|
| 142 |
+
|
| 143 |
+
return NextResponse.json(response);
|
| 144 |
+
}
|
src/app/api/runs/[runId]/route.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getRun } from "@/lib/rocmpilot/store";
|
| 2 |
+
import { NextResponse } from "next/server";
|
| 3 |
+
|
| 4 |
+
export async function GET(
|
| 5 |
+
_request: Request,
|
| 6 |
+
context: { params: Promise<{ runId: string }> }
|
| 7 |
+
) {
|
| 8 |
+
const { runId } = await context.params;
|
| 9 |
+
const run = await getRun(runId);
|
| 10 |
+
|
| 11 |
+
if (!run) {
|
| 12 |
+
return NextResponse.json({ error: "Run not found" }, { status: 404 });
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
return NextResponse.json(run);
|
| 16 |
+
}
|
src/app/api/runs/route.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createRun } from "@/lib/rocmpilot/store";
|
| 2 |
+
import type { RunMode } from "@/lib/rocmpilot/types";
|
| 3 |
+
import { NextResponse } from "next/server";
|
| 4 |
+
|
| 5 |
+
export async function POST(request: Request) {
|
| 6 |
+
const body = (await request.json().catch(() => ({}))) as {
|
| 7 |
+
sampleId?: string;
|
| 8 |
+
mode?: RunMode;
|
| 9 |
+
repoUrl?: string;
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
const run = createRun(
|
| 13 |
+
body.sampleId ?? "qwen-vllm-cuda",
|
| 14 |
+
body.mode ?? "mock",
|
| 15 |
+
body.repoUrl
|
| 16 |
+
);
|
| 17 |
+
|
| 18 |
+
return NextResponse.json(run);
|
| 19 |
+
}
|
src/app/favicon.ico
ADDED
|
|
src/app/globals.css
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
@import "tw-animate-css";
|
| 3 |
+
@import "shadcn/tailwind.css";
|
| 4 |
+
@source "../node_modules/streamdown/dist/*.js";
|
| 5 |
+
|
| 6 |
+
@custom-variant dark (&:is(.dark *));
|
| 7 |
+
|
| 8 |
+
@theme inline {
|
| 9 |
+
--color-background: var(--background);
|
| 10 |
+
--color-foreground: var(--foreground);
|
| 11 |
+
--font-sans: "Geist", "Geist Fallback", ui-sans-serif, system-ui, sans-serif;
|
| 12 |
+
--font-mono: "Geist Mono", "Geist Mono Fallback", ui-monospace, monospace;
|
| 13 |
+
--font-heading: "Geist", "Geist Fallback", ui-sans-serif, system-ui, sans-serif;
|
| 14 |
+
--color-sidebar-ring: var(--sidebar-ring);
|
| 15 |
+
--color-sidebar-border: var(--sidebar-border);
|
| 16 |
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
| 17 |
+
--color-sidebar-accent: var(--sidebar-accent);
|
| 18 |
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
| 19 |
+
--color-sidebar-primary: var(--sidebar-primary);
|
| 20 |
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
| 21 |
+
--color-sidebar: var(--sidebar);
|
| 22 |
+
--color-chart-5: var(--chart-5);
|
| 23 |
+
--color-chart-4: var(--chart-4);
|
| 24 |
+
--color-chart-3: var(--chart-3);
|
| 25 |
+
--color-chart-2: var(--chart-2);
|
| 26 |
+
--color-chart-1: var(--chart-1);
|
| 27 |
+
--color-ring: var(--ring);
|
| 28 |
+
--color-input: var(--input);
|
| 29 |
+
--color-border: var(--border);
|
| 30 |
+
--color-destructive: var(--destructive);
|
| 31 |
+
--color-accent-foreground: var(--accent-foreground);
|
| 32 |
+
--color-accent: var(--accent);
|
| 33 |
+
--color-muted-foreground: var(--muted-foreground);
|
| 34 |
+
--color-muted: var(--muted);
|
| 35 |
+
--color-secondary-foreground: var(--secondary-foreground);
|
| 36 |
+
--color-secondary: var(--secondary);
|
| 37 |
+
--color-primary-foreground: var(--primary-foreground);
|
| 38 |
+
--color-primary: var(--primary);
|
| 39 |
+
--color-popover-foreground: var(--popover-foreground);
|
| 40 |
+
--color-popover: var(--popover);
|
| 41 |
+
--color-card-foreground: var(--card-foreground);
|
| 42 |
+
--color-card: var(--card);
|
| 43 |
+
--radius-sm: calc(var(--radius) * 0.6);
|
| 44 |
+
--radius-md: calc(var(--radius) * 0.8);
|
| 45 |
+
--radius-lg: var(--radius);
|
| 46 |
+
--radius-xl: calc(var(--radius) * 1.4);
|
| 47 |
+
--radius-2xl: calc(var(--radius) * 1.8);
|
| 48 |
+
--radius-3xl: calc(var(--radius) * 2.2);
|
| 49 |
+
--radius-4xl: calc(var(--radius) * 2.6);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
:root {
|
| 53 |
+
--background: oklch(1 0 0);
|
| 54 |
+
--foreground: oklch(0.145 0 0);
|
| 55 |
+
--card: oklch(1 0 0);
|
| 56 |
+
--card-foreground: oklch(0.145 0 0);
|
| 57 |
+
--popover: oklch(1 0 0);
|
| 58 |
+
--popover-foreground: oklch(0.145 0 0);
|
| 59 |
+
--primary: oklch(0.205 0 0);
|
| 60 |
+
--primary-foreground: oklch(0.985 0 0);
|
| 61 |
+
--secondary: oklch(0.97 0 0);
|
| 62 |
+
--secondary-foreground: oklch(0.205 0 0);
|
| 63 |
+
--muted: oklch(0.97 0 0);
|
| 64 |
+
--muted-foreground: oklch(0.556 0 0);
|
| 65 |
+
--accent: oklch(0.97 0 0);
|
| 66 |
+
--accent-foreground: oklch(0.205 0 0);
|
| 67 |
+
--destructive: oklch(0.577 0.245 27.325);
|
| 68 |
+
--border: oklch(0.922 0 0);
|
| 69 |
+
--input: oklch(0.922 0 0);
|
| 70 |
+
--ring: oklch(0.708 0 0);
|
| 71 |
+
--chart-1: oklch(0.87 0 0);
|
| 72 |
+
--chart-2: oklch(0.556 0 0);
|
| 73 |
+
--chart-3: oklch(0.439 0 0);
|
| 74 |
+
--chart-4: oklch(0.371 0 0);
|
| 75 |
+
--chart-5: oklch(0.269 0 0);
|
| 76 |
+
--radius: 0.625rem;
|
| 77 |
+
--sidebar: oklch(0.985 0 0);
|
| 78 |
+
--sidebar-foreground: oklch(0.145 0 0);
|
| 79 |
+
--sidebar-primary: oklch(0.205 0 0);
|
| 80 |
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
| 81 |
+
--sidebar-accent: oklch(0.97 0 0);
|
| 82 |
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
| 83 |
+
--sidebar-border: oklch(0.922 0 0);
|
| 84 |
+
--sidebar-ring: oklch(0.708 0 0);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.dark {
|
| 88 |
+
--background: oklch(0.145 0 0);
|
| 89 |
+
--foreground: oklch(0.985 0 0);
|
| 90 |
+
--card: oklch(0.205 0 0);
|
| 91 |
+
--card-foreground: oklch(0.985 0 0);
|
| 92 |
+
--popover: oklch(0.205 0 0);
|
| 93 |
+
--popover-foreground: oklch(0.985 0 0);
|
| 94 |
+
--primary: oklch(0.922 0 0);
|
| 95 |
+
--primary-foreground: oklch(0.205 0 0);
|
| 96 |
+
--secondary: oklch(0.269 0 0);
|
| 97 |
+
--secondary-foreground: oklch(0.985 0 0);
|
| 98 |
+
--muted: oklch(0.269 0 0);
|
| 99 |
+
--muted-foreground: oklch(0.708 0 0);
|
| 100 |
+
--accent: oklch(0.269 0 0);
|
| 101 |
+
--accent-foreground: oklch(0.985 0 0);
|
| 102 |
+
--destructive: oklch(0.704 0.191 22.216);
|
| 103 |
+
--border: oklch(1 0 0 / 10%);
|
| 104 |
+
--input: oklch(1 0 0 / 15%);
|
| 105 |
+
--ring: oklch(0.556 0 0);
|
| 106 |
+
--chart-1: oklch(0.87 0 0);
|
| 107 |
+
--chart-2: oklch(0.556 0 0);
|
| 108 |
+
--chart-3: oklch(0.439 0 0);
|
| 109 |
+
--chart-4: oklch(0.371 0 0);
|
| 110 |
+
--chart-5: oklch(0.269 0 0);
|
| 111 |
+
--sidebar: oklch(0.205 0 0);
|
| 112 |
+
--sidebar-foreground: oklch(0.985 0 0);
|
| 113 |
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
| 114 |
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
| 115 |
+
--sidebar-accent: oklch(0.269 0 0);
|
| 116 |
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
| 117 |
+
--sidebar-border: oklch(1 0 0 / 10%);
|
| 118 |
+
--sidebar-ring: oklch(0.556 0 0);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
@layer base {
|
| 122 |
+
* {
|
| 123 |
+
@apply border-border outline-ring/50;
|
| 124 |
+
}
|
| 125 |
+
body {
|
| 126 |
+
@apply bg-background text-foreground;
|
| 127 |
+
}
|
| 128 |
+
html {
|
| 129 |
+
@apply font-sans;
|
| 130 |
+
}
|
| 131 |
+
}
|
src/app/layout.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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: "ROCmPilot",
|
| 18 |
+
description:
|
| 19 |
+
"Agentic ROCm migration dashboard powered by AMD GPU model serving.",
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
export default function RootLayout({
|
| 23 |
+
children,
|
| 24 |
+
}: Readonly<{
|
| 25 |
+
children: React.ReactNode;
|
| 26 |
+
}>) {
|
| 27 |
+
return (
|
| 28 |
+
<html
|
| 29 |
+
lang="en"
|
| 30 |
+
className={`dark ${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
| 31 |
+
>
|
| 32 |
+
<body className="flex min-h-full flex-col">
|
| 33 |
+
<TooltipProvider>{children}</TooltipProvider>
|
| 34 |
+
</body>
|
| 35 |
+
</html>
|
| 36 |
+
);
|
| 37 |
+
}
|
src/app/page.tsx
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { RocmPilotDashboard } from "@/components/rocmpilot-dashboard";
|
| 2 |
+
|
| 3 |
+
export default function Home() {
|
| 4 |
+
return <RocmPilotDashboard />;
|
| 5 |
+
}
|
src/components/ai-elements/code-block.tsx
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { Button } from "@/components/ui/button";
|
| 4 |
+
import {
|
| 5 |
+
Select,
|
| 6 |
+
SelectContent,
|
| 7 |
+
SelectItem,
|
| 8 |
+
SelectTrigger,
|
| 9 |
+
SelectValue,
|
| 10 |
+
} from "@/components/ui/select";
|
| 11 |
+
import { cn } from "@/lib/utils";
|
| 12 |
+
import { CheckIcon, CopyIcon } from "lucide-react";
|
| 13 |
+
import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";
|
| 14 |
+
import {
|
| 15 |
+
createContext,
|
| 16 |
+
memo,
|
| 17 |
+
useCallback,
|
| 18 |
+
useContext,
|
| 19 |
+
useEffect,
|
| 20 |
+
useMemo,
|
| 21 |
+
useRef,
|
| 22 |
+
useState,
|
| 23 |
+
} from "react";
|
| 24 |
+
import type {
|
| 25 |
+
BundledLanguage,
|
| 26 |
+
BundledTheme,
|
| 27 |
+
HighlighterGeneric,
|
| 28 |
+
ThemedToken,
|
| 29 |
+
} from "shiki";
|
| 30 |
+
import { createHighlighter } from "shiki";
|
| 31 |
+
|
| 32 |
+
// Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline
|
| 33 |
+
// oxlint-disable-next-line eslint(no-bitwise)
|
| 34 |
+
const isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;
|
| 35 |
+
// oxlint-disable-next-line eslint(no-bitwise)
|
| 36 |
+
const isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;
|
| 37 |
+
const isUnderline = (fontStyle: number | undefined) =>
|
| 38 |
+
// oxlint-disable-next-line eslint(no-bitwise)
|
| 39 |
+
fontStyle && fontStyle & 4;
|
| 40 |
+
|
| 41 |
+
// Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint
|
| 42 |
+
interface KeyedToken {
|
| 43 |
+
token: ThemedToken;
|
| 44 |
+
key: string;
|
| 45 |
+
}
|
| 46 |
+
interface KeyedLine {
|
| 47 |
+
tokens: KeyedToken[];
|
| 48 |
+
key: string;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] =>
|
| 52 |
+
lines.map((line, lineIdx) => ({
|
| 53 |
+
key: `line-${lineIdx}`,
|
| 54 |
+
tokens: line.map((token, tokenIdx) => ({
|
| 55 |
+
key: `line-${lineIdx}-${tokenIdx}`,
|
| 56 |
+
token,
|
| 57 |
+
})),
|
| 58 |
+
}));
|
| 59 |
+
|
| 60 |
+
// Token rendering component
|
| 61 |
+
const TokenSpan = ({ token }: { token: ThemedToken }) => (
|
| 62 |
+
<span
|
| 63 |
+
className="dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)]"
|
| 64 |
+
style={
|
| 65 |
+
{
|
| 66 |
+
backgroundColor: token.bgColor,
|
| 67 |
+
color: token.color,
|
| 68 |
+
fontStyle: isItalic(token.fontStyle) ? "italic" : undefined,
|
| 69 |
+
fontWeight: isBold(token.fontStyle) ? "bold" : undefined,
|
| 70 |
+
textDecoration: isUnderline(token.fontStyle) ? "underline" : undefined,
|
| 71 |
+
...token.htmlStyle,
|
| 72 |
+
} as CSSProperties
|
| 73 |
+
}
|
| 74 |
+
>
|
| 75 |
+
{token.content}
|
| 76 |
+
</span>
|
| 77 |
+
);
|
| 78 |
+
|
| 79 |
+
// Line number styles using CSS counters
|
| 80 |
+
const LINE_NUMBER_CLASSES = cn(
|
| 81 |
+
"block",
|
| 82 |
+
"before:content-[counter(line)]",
|
| 83 |
+
"before:inline-block",
|
| 84 |
+
"before:[counter-increment:line]",
|
| 85 |
+
"before:w-8",
|
| 86 |
+
"before:mr-4",
|
| 87 |
+
"before:text-right",
|
| 88 |
+
"before:text-muted-foreground/50",
|
| 89 |
+
"before:font-mono",
|
| 90 |
+
"before:select-none"
|
| 91 |
+
);
|
| 92 |
+
|
| 93 |
+
// Line rendering component
|
| 94 |
+
const LineSpan = ({
|
| 95 |
+
keyedLine,
|
| 96 |
+
showLineNumbers,
|
| 97 |
+
}: {
|
| 98 |
+
keyedLine: KeyedLine;
|
| 99 |
+
showLineNumbers: boolean;
|
| 100 |
+
}) => (
|
| 101 |
+
<span className={showLineNumbers ? LINE_NUMBER_CLASSES : "block"}>
|
| 102 |
+
{keyedLine.tokens.length === 0
|
| 103 |
+
? "\n"
|
| 104 |
+
: keyedLine.tokens.map(({ token, key }) => (
|
| 105 |
+
<TokenSpan key={key} token={token} />
|
| 106 |
+
))}
|
| 107 |
+
</span>
|
| 108 |
+
);
|
| 109 |
+
|
| 110 |
+
// Types
|
| 111 |
+
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
| 112 |
+
code: string;
|
| 113 |
+
language: BundledLanguage;
|
| 114 |
+
showLineNumbers?: boolean;
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
+
interface TokenizedCode {
|
| 118 |
+
tokens: ThemedToken[][];
|
| 119 |
+
fg: string;
|
| 120 |
+
bg: string;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
interface CodeBlockContextType {
|
| 124 |
+
code: string;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// Context
|
| 128 |
+
const CodeBlockContext = createContext<CodeBlockContextType>({
|
| 129 |
+
code: "",
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
// Highlighter cache (singleton per language)
|
| 133 |
+
const highlighterCache = new Map<
|
| 134 |
+
string,
|
| 135 |
+
Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>
|
| 136 |
+
>();
|
| 137 |
+
|
| 138 |
+
// Token cache
|
| 139 |
+
const tokensCache = new Map<string, TokenizedCode>();
|
| 140 |
+
|
| 141 |
+
// Subscribers for async token updates
|
| 142 |
+
const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();
|
| 143 |
+
|
| 144 |
+
const getTokensCacheKey = (code: string, language: BundledLanguage) => {
|
| 145 |
+
const start = code.slice(0, 100);
|
| 146 |
+
const end = code.length > 100 ? code.slice(-100) : "";
|
| 147 |
+
return `${language}:${code.length}:${start}:${end}`;
|
| 148 |
+
};
|
| 149 |
+
|
| 150 |
+
const getHighlighter = (
|
| 151 |
+
language: BundledLanguage
|
| 152 |
+
): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {
|
| 153 |
+
const cached = highlighterCache.get(language);
|
| 154 |
+
if (cached) {
|
| 155 |
+
return cached;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
const highlighterPromise = createHighlighter({
|
| 159 |
+
langs: [language],
|
| 160 |
+
themes: ["github-light", "github-dark"],
|
| 161 |
+
});
|
| 162 |
+
|
| 163 |
+
highlighterCache.set(language, highlighterPromise);
|
| 164 |
+
return highlighterPromise;
|
| 165 |
+
};
|
| 166 |
+
|
| 167 |
+
// Create raw tokens for immediate display while highlighting loads
|
| 168 |
+
const createRawTokens = (code: string): TokenizedCode => ({
|
| 169 |
+
bg: "transparent",
|
| 170 |
+
fg: "inherit",
|
| 171 |
+
tokens: code.split("\n").map((line) =>
|
| 172 |
+
line === ""
|
| 173 |
+
? []
|
| 174 |
+
: [
|
| 175 |
+
{
|
| 176 |
+
color: "inherit",
|
| 177 |
+
content: line,
|
| 178 |
+
} as ThemedToken,
|
| 179 |
+
]
|
| 180 |
+
),
|
| 181 |
+
});
|
| 182 |
+
|
| 183 |
+
// Synchronous highlight with callback for async results
|
| 184 |
+
export const highlightCode = (
|
| 185 |
+
code: string,
|
| 186 |
+
language: BundledLanguage,
|
| 187 |
+
// oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)
|
| 188 |
+
callback?: (result: TokenizedCode) => void
|
| 189 |
+
): TokenizedCode | null => {
|
| 190 |
+
const tokensCacheKey = getTokensCacheKey(code, language);
|
| 191 |
+
|
| 192 |
+
// Return cached result if available
|
| 193 |
+
const cached = tokensCache.get(tokensCacheKey);
|
| 194 |
+
if (cached) {
|
| 195 |
+
return cached;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
// Subscribe callback if provided
|
| 199 |
+
if (callback) {
|
| 200 |
+
if (!subscribers.has(tokensCacheKey)) {
|
| 201 |
+
subscribers.set(tokensCacheKey, new Set());
|
| 202 |
+
}
|
| 203 |
+
subscribers.get(tokensCacheKey)?.add(callback);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
// Start highlighting in background - fire-and-forget async pattern
|
| 207 |
+
getHighlighter(language)
|
| 208 |
+
// oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)
|
| 209 |
+
.then((highlighter) => {
|
| 210 |
+
const availableLangs = highlighter.getLoadedLanguages();
|
| 211 |
+
const langToUse = availableLangs.includes(language) ? language : "text";
|
| 212 |
+
|
| 213 |
+
const result = highlighter.codeToTokens(code, {
|
| 214 |
+
lang: langToUse,
|
| 215 |
+
themes: {
|
| 216 |
+
dark: "github-dark",
|
| 217 |
+
light: "github-light",
|
| 218 |
+
},
|
| 219 |
+
});
|
| 220 |
+
|
| 221 |
+
const tokenized: TokenizedCode = {
|
| 222 |
+
bg: result.bg ?? "transparent",
|
| 223 |
+
fg: result.fg ?? "inherit",
|
| 224 |
+
tokens: result.tokens,
|
| 225 |
+
};
|
| 226 |
+
|
| 227 |
+
// Cache the result
|
| 228 |
+
tokensCache.set(tokensCacheKey, tokenized);
|
| 229 |
+
|
| 230 |
+
// Notify all subscribers
|
| 231 |
+
const subs = subscribers.get(tokensCacheKey);
|
| 232 |
+
if (subs) {
|
| 233 |
+
for (const sub of subs) {
|
| 234 |
+
sub(tokenized);
|
| 235 |
+
}
|
| 236 |
+
subscribers.delete(tokensCacheKey);
|
| 237 |
+
}
|
| 238 |
+
})
|
| 239 |
+
// oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks)
|
| 240 |
+
.catch((error) => {
|
| 241 |
+
console.error("Failed to highlight code:", error);
|
| 242 |
+
subscribers.delete(tokensCacheKey);
|
| 243 |
+
});
|
| 244 |
+
|
| 245 |
+
return null;
|
| 246 |
+
};
|
| 247 |
+
|
| 248 |
+
const CodeBlockBody = memo(
|
| 249 |
+
({
|
| 250 |
+
tokenized,
|
| 251 |
+
showLineNumbers,
|
| 252 |
+
className,
|
| 253 |
+
}: {
|
| 254 |
+
tokenized: TokenizedCode;
|
| 255 |
+
showLineNumbers: boolean;
|
| 256 |
+
className?: string;
|
| 257 |
+
}) => {
|
| 258 |
+
const preStyle = useMemo(
|
| 259 |
+
() => ({
|
| 260 |
+
backgroundColor: tokenized.bg,
|
| 261 |
+
color: tokenized.fg,
|
| 262 |
+
}),
|
| 263 |
+
[tokenized.bg, tokenized.fg]
|
| 264 |
+
);
|
| 265 |
+
|
| 266 |
+
const keyedLines = useMemo(
|
| 267 |
+
() => addKeysToTokens(tokenized.tokens),
|
| 268 |
+
[tokenized.tokens]
|
| 269 |
+
);
|
| 270 |
+
|
| 271 |
+
return (
|
| 272 |
+
<pre
|
| 273 |
+
className={cn(
|
| 274 |
+
"dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)] m-0 p-4 text-sm",
|
| 275 |
+
className
|
| 276 |
+
)}
|
| 277 |
+
style={preStyle}
|
| 278 |
+
>
|
| 279 |
+
<code
|
| 280 |
+
className={cn(
|
| 281 |
+
"font-mono text-sm",
|
| 282 |
+
showLineNumbers && "[counter-increment:line_0] [counter-reset:line]"
|
| 283 |
+
)}
|
| 284 |
+
>
|
| 285 |
+
{keyedLines.map((keyedLine) => (
|
| 286 |
+
<LineSpan
|
| 287 |
+
key={keyedLine.key}
|
| 288 |
+
keyedLine={keyedLine}
|
| 289 |
+
showLineNumbers={showLineNumbers}
|
| 290 |
+
/>
|
| 291 |
+
))}
|
| 292 |
+
</code>
|
| 293 |
+
</pre>
|
| 294 |
+
);
|
| 295 |
+
},
|
| 296 |
+
(prevProps, nextProps) =>
|
| 297 |
+
prevProps.tokenized === nextProps.tokenized &&
|
| 298 |
+
prevProps.showLineNumbers === nextProps.showLineNumbers &&
|
| 299 |
+
prevProps.className === nextProps.className
|
| 300 |
+
);
|
| 301 |
+
|
| 302 |
+
CodeBlockBody.displayName = "CodeBlockBody";
|
| 303 |
+
|
| 304 |
+
export const CodeBlockContainer = ({
|
| 305 |
+
className,
|
| 306 |
+
language,
|
| 307 |
+
style,
|
| 308 |
+
...props
|
| 309 |
+
}: HTMLAttributes<HTMLDivElement> & { language: string }) => (
|
| 310 |
+
<div
|
| 311 |
+
className={cn(
|
| 312 |
+
"group relative w-full overflow-hidden rounded-md border bg-background text-foreground",
|
| 313 |
+
className
|
| 314 |
+
)}
|
| 315 |
+
data-language={language}
|
| 316 |
+
style={{
|
| 317 |
+
containIntrinsicSize: "auto 200px",
|
| 318 |
+
contentVisibility: "auto",
|
| 319 |
+
...style,
|
| 320 |
+
}}
|
| 321 |
+
{...props}
|
| 322 |
+
/>
|
| 323 |
+
);
|
| 324 |
+
|
| 325 |
+
export const CodeBlockHeader = ({
|
| 326 |
+
children,
|
| 327 |
+
className,
|
| 328 |
+
...props
|
| 329 |
+
}: HTMLAttributes<HTMLDivElement>) => (
|
| 330 |
+
<div
|
| 331 |
+
className={cn(
|
| 332 |
+
"flex items-center justify-between border-b bg-muted/80 px-3 py-2 text-muted-foreground text-xs",
|
| 333 |
+
className
|
| 334 |
+
)}
|
| 335 |
+
{...props}
|
| 336 |
+
>
|
| 337 |
+
{children}
|
| 338 |
+
</div>
|
| 339 |
+
);
|
| 340 |
+
|
| 341 |
+
export const CodeBlockTitle = ({
|
| 342 |
+
children,
|
| 343 |
+
className,
|
| 344 |
+
...props
|
| 345 |
+
}: HTMLAttributes<HTMLDivElement>) => (
|
| 346 |
+
<div className={cn("flex items-center gap-2", className)} {...props}>
|
| 347 |
+
{children}
|
| 348 |
+
</div>
|
| 349 |
+
);
|
| 350 |
+
|
| 351 |
+
export const CodeBlockFilename = ({
|
| 352 |
+
children,
|
| 353 |
+
className,
|
| 354 |
+
...props
|
| 355 |
+
}: HTMLAttributes<HTMLSpanElement>) => (
|
| 356 |
+
<span className={cn("font-mono", className)} {...props}>
|
| 357 |
+
{children}
|
| 358 |
+
</span>
|
| 359 |
+
);
|
| 360 |
+
|
| 361 |
+
export const CodeBlockActions = ({
|
| 362 |
+
children,
|
| 363 |
+
className,
|
| 364 |
+
...props
|
| 365 |
+
}: HTMLAttributes<HTMLDivElement>) => (
|
| 366 |
+
<div
|
| 367 |
+
className={cn("-my-1 -mr-1 flex items-center gap-2", className)}
|
| 368 |
+
{...props}
|
| 369 |
+
>
|
| 370 |
+
{children}
|
| 371 |
+
</div>
|
| 372 |
+
);
|
| 373 |
+
|
| 374 |
+
export const CodeBlockContent = ({
|
| 375 |
+
code,
|
| 376 |
+
language,
|
| 377 |
+
showLineNumbers = false,
|
| 378 |
+
}: {
|
| 379 |
+
code: string;
|
| 380 |
+
language: BundledLanguage;
|
| 381 |
+
showLineNumbers?: boolean;
|
| 382 |
+
}) => {
|
| 383 |
+
// Memoized raw tokens for immediate display
|
| 384 |
+
const rawTokens = useMemo(() => createRawTokens(code), [code]);
|
| 385 |
+
|
| 386 |
+
// Synchronous cache lookup — avoids setState in effect for cached results
|
| 387 |
+
const syncTokens = useMemo(
|
| 388 |
+
() => highlightCode(code, language) ?? rawTokens,
|
| 389 |
+
[code, language, rawTokens]
|
| 390 |
+
);
|
| 391 |
+
|
| 392 |
+
// Async highlighting result (populated after shiki loads)
|
| 393 |
+
const tokensKey = useMemo(() => getTokensCacheKey(code, language), [code, language]);
|
| 394 |
+
const [asyncTokens, setAsyncTokens] = useState<{
|
| 395 |
+
key: string;
|
| 396 |
+
result: TokenizedCode;
|
| 397 |
+
} | null>(null);
|
| 398 |
+
|
| 399 |
+
useEffect(() => {
|
| 400 |
+
let cancelled = false;
|
| 401 |
+
|
| 402 |
+
highlightCode(code, language, (result) => {
|
| 403 |
+
if (!cancelled) {
|
| 404 |
+
setAsyncTokens({ key: tokensKey, result });
|
| 405 |
+
}
|
| 406 |
+
});
|
| 407 |
+
|
| 408 |
+
return () => {
|
| 409 |
+
cancelled = true;
|
| 410 |
+
};
|
| 411 |
+
}, [code, language, tokensKey]);
|
| 412 |
+
|
| 413 |
+
const tokenized = asyncTokens?.key === tokensKey ? asyncTokens.result : syncTokens;
|
| 414 |
+
|
| 415 |
+
return (
|
| 416 |
+
<div className="relative overflow-auto">
|
| 417 |
+
<CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} />
|
| 418 |
+
</div>
|
| 419 |
+
);
|
| 420 |
+
};
|
| 421 |
+
|
| 422 |
+
export const CodeBlock = ({
|
| 423 |
+
code,
|
| 424 |
+
language,
|
| 425 |
+
showLineNumbers = false,
|
| 426 |
+
className,
|
| 427 |
+
children,
|
| 428 |
+
...props
|
| 429 |
+
}: CodeBlockProps) => {
|
| 430 |
+
const contextValue = useMemo(() => ({ code }), [code]);
|
| 431 |
+
|
| 432 |
+
return (
|
| 433 |
+
<CodeBlockContext.Provider value={contextValue}>
|
| 434 |
+
<CodeBlockContainer className={className} language={language} {...props}>
|
| 435 |
+
{children}
|
| 436 |
+
<CodeBlockContent
|
| 437 |
+
code={code}
|
| 438 |
+
language={language}
|
| 439 |
+
showLineNumbers={showLineNumbers}
|
| 440 |
+
/>
|
| 441 |
+
</CodeBlockContainer>
|
| 442 |
+
</CodeBlockContext.Provider>
|
| 443 |
+
);
|
| 444 |
+
};
|
| 445 |
+
|
| 446 |
+
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
|
| 447 |
+
onCopy?: () => void;
|
| 448 |
+
onError?: (error: Error) => void;
|
| 449 |
+
timeout?: number;
|
| 450 |
+
};
|
| 451 |
+
|
| 452 |
+
export const CodeBlockCopyButton = ({
|
| 453 |
+
onCopy,
|
| 454 |
+
onError,
|
| 455 |
+
timeout = 2000,
|
| 456 |
+
children,
|
| 457 |
+
className,
|
| 458 |
+
...props
|
| 459 |
+
}: CodeBlockCopyButtonProps) => {
|
| 460 |
+
const [isCopied, setIsCopied] = useState(false);
|
| 461 |
+
const timeoutRef = useRef<number>(0);
|
| 462 |
+
const { code } = useContext(CodeBlockContext);
|
| 463 |
+
|
| 464 |
+
const copyToClipboard = useCallback(async () => {
|
| 465 |
+
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
|
| 466 |
+
onError?.(new Error("Clipboard API not available"));
|
| 467 |
+
return;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
try {
|
| 471 |
+
if (!isCopied) {
|
| 472 |
+
await navigator.clipboard.writeText(code);
|
| 473 |
+
setIsCopied(true);
|
| 474 |
+
onCopy?.();
|
| 475 |
+
timeoutRef.current = window.setTimeout(
|
| 476 |
+
() => setIsCopied(false),
|
| 477 |
+
timeout
|
| 478 |
+
);
|
| 479 |
+
}
|
| 480 |
+
} catch (error) {
|
| 481 |
+
onError?.(error as Error);
|
| 482 |
+
}
|
| 483 |
+
}, [code, onCopy, onError, timeout, isCopied]);
|
| 484 |
+
|
| 485 |
+
useEffect(
|
| 486 |
+
() => () => {
|
| 487 |
+
window.clearTimeout(timeoutRef.current);
|
| 488 |
+
},
|
| 489 |
+
[]
|
| 490 |
+
);
|
| 491 |
+
|
| 492 |
+
const Icon = isCopied ? CheckIcon : CopyIcon;
|
| 493 |
+
|
| 494 |
+
return (
|
| 495 |
+
<Button
|
| 496 |
+
className={cn("shrink-0", className)}
|
| 497 |
+
onClick={copyToClipboard}
|
| 498 |
+
size="icon"
|
| 499 |
+
variant="ghost"
|
| 500 |
+
{...props}
|
| 501 |
+
>
|
| 502 |
+
{children ?? <Icon size={14} />}
|
| 503 |
+
</Button>
|
| 504 |
+
);
|
| 505 |
+
};
|
| 506 |
+
|
| 507 |
+
export type CodeBlockLanguageSelectorProps = ComponentProps<typeof Select>;
|
| 508 |
+
|
| 509 |
+
export const CodeBlockLanguageSelector = (
|
| 510 |
+
props: CodeBlockLanguageSelectorProps
|
| 511 |
+
) => <Select {...props} />;
|
| 512 |
+
|
| 513 |
+
export type CodeBlockLanguageSelectorTriggerProps = ComponentProps<
|
| 514 |
+
typeof SelectTrigger
|
| 515 |
+
>;
|
| 516 |
+
|
| 517 |
+
export const CodeBlockLanguageSelectorTrigger = ({
|
| 518 |
+
className,
|
| 519 |
+
...props
|
| 520 |
+
}: CodeBlockLanguageSelectorTriggerProps) => (
|
| 521 |
+
<SelectTrigger
|
| 522 |
+
className={cn(
|
| 523 |
+
"h-7 border-none bg-transparent px-2 text-xs shadow-none",
|
| 524 |
+
className
|
| 525 |
+
)}
|
| 526 |
+
size="sm"
|
| 527 |
+
{...props}
|
| 528 |
+
/>
|
| 529 |
+
);
|
| 530 |
+
|
| 531 |
+
export type CodeBlockLanguageSelectorValueProps = ComponentProps<
|
| 532 |
+
typeof SelectValue
|
| 533 |
+
>;
|
| 534 |
+
|
| 535 |
+
export const CodeBlockLanguageSelectorValue = (
|
| 536 |
+
props: CodeBlockLanguageSelectorValueProps
|
| 537 |
+
) => <SelectValue {...props} />;
|
| 538 |
+
|
| 539 |
+
export type CodeBlockLanguageSelectorContentProps = ComponentProps<
|
| 540 |
+
typeof SelectContent
|
| 541 |
+
>;
|
| 542 |
+
|
| 543 |
+
export const CodeBlockLanguageSelectorContent = ({
|
| 544 |
+
align = "end",
|
| 545 |
+
...props
|
| 546 |
+
}: CodeBlockLanguageSelectorContentProps) => (
|
| 547 |
+
<SelectContent align={align} {...props} />
|
| 548 |
+
);
|
| 549 |
+
|
| 550 |
+
export type CodeBlockLanguageSelectorItemProps = ComponentProps<
|
| 551 |
+
typeof SelectItem
|
| 552 |
+
>;
|
| 553 |
+
|
| 554 |
+
export const CodeBlockLanguageSelectorItem = (
|
| 555 |
+
props: CodeBlockLanguageSelectorItemProps
|
| 556 |
+
) => <SelectItem {...props} />;
|
src/components/ai-elements/message.tsx
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { Button } from "@/components/ui/button";
|
| 4 |
+
import {
|
| 5 |
+
ButtonGroup,
|
| 6 |
+
ButtonGroupText,
|
| 7 |
+
} from "@/components/ui/button-group";
|
| 8 |
+
import {
|
| 9 |
+
Tooltip,
|
| 10 |
+
TooltipContent,
|
| 11 |
+
TooltipProvider,
|
| 12 |
+
TooltipTrigger,
|
| 13 |
+
} from "@/components/ui/tooltip";
|
| 14 |
+
import { cn } from "@/lib/utils";
|
| 15 |
+
import { cjk } from "@streamdown/cjk";
|
| 16 |
+
import { code } from "@streamdown/code";
|
| 17 |
+
import { math } from "@streamdown/math";
|
| 18 |
+
import { mermaid } from "@streamdown/mermaid";
|
| 19 |
+
import type { UIMessage } from "ai";
|
| 20 |
+
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
| 21 |
+
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
| 22 |
+
import {
|
| 23 |
+
createContext,
|
| 24 |
+
memo,
|
| 25 |
+
useCallback,
|
| 26 |
+
useContext,
|
| 27 |
+
useEffect,
|
| 28 |
+
useMemo,
|
| 29 |
+
useState,
|
| 30 |
+
} from "react";
|
| 31 |
+
import { Streamdown } from "streamdown";
|
| 32 |
+
|
| 33 |
+
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
| 34 |
+
from: UIMessage["role"];
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
export const Message = ({ className, from, ...props }: MessageProps) => (
|
| 38 |
+
<div
|
| 39 |
+
className={cn(
|
| 40 |
+
"group flex w-full max-w-[95%] flex-col gap-2",
|
| 41 |
+
from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
|
| 42 |
+
className
|
| 43 |
+
)}
|
| 44 |
+
{...props}
|
| 45 |
+
/>
|
| 46 |
+
);
|
| 47 |
+
|
| 48 |
+
export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
|
| 49 |
+
|
| 50 |
+
export const MessageContent = ({
|
| 51 |
+
children,
|
| 52 |
+
className,
|
| 53 |
+
...props
|
| 54 |
+
}: MessageContentProps) => (
|
| 55 |
+
<div
|
| 56 |
+
className={cn(
|
| 57 |
+
"is-user:dark flex w-fit min-w-0 max-w-full flex-col gap-2 overflow-hidden text-sm",
|
| 58 |
+
"group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground",
|
| 59 |
+
"group-[.is-assistant]:text-foreground",
|
| 60 |
+
className
|
| 61 |
+
)}
|
| 62 |
+
{...props}
|
| 63 |
+
>
|
| 64 |
+
{children}
|
| 65 |
+
</div>
|
| 66 |
+
);
|
| 67 |
+
|
| 68 |
+
export type MessageActionsProps = ComponentProps<"div">;
|
| 69 |
+
|
| 70 |
+
export const MessageActions = ({
|
| 71 |
+
className,
|
| 72 |
+
children,
|
| 73 |
+
...props
|
| 74 |
+
}: MessageActionsProps) => (
|
| 75 |
+
<div className={cn("flex items-center gap-1", className)} {...props}>
|
| 76 |
+
{children}
|
| 77 |
+
</div>
|
| 78 |
+
);
|
| 79 |
+
|
| 80 |
+
export type MessageActionProps = ComponentProps<typeof Button> & {
|
| 81 |
+
tooltip?: string;
|
| 82 |
+
label?: string;
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
export const MessageAction = ({
|
| 86 |
+
tooltip,
|
| 87 |
+
children,
|
| 88 |
+
label,
|
| 89 |
+
variant = "ghost",
|
| 90 |
+
size = "icon-sm",
|
| 91 |
+
...props
|
| 92 |
+
}: MessageActionProps) => {
|
| 93 |
+
const button = (
|
| 94 |
+
<Button size={size} type="button" variant={variant} {...props}>
|
| 95 |
+
{children}
|
| 96 |
+
<span className="sr-only">{label || tooltip}</span>
|
| 97 |
+
</Button>
|
| 98 |
+
);
|
| 99 |
+
|
| 100 |
+
if (tooltip) {
|
| 101 |
+
return (
|
| 102 |
+
<TooltipProvider>
|
| 103 |
+
<Tooltip>
|
| 104 |
+
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
| 105 |
+
<TooltipContent>
|
| 106 |
+
<p>{tooltip}</p>
|
| 107 |
+
</TooltipContent>
|
| 108 |
+
</Tooltip>
|
| 109 |
+
</TooltipProvider>
|
| 110 |
+
);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
return button;
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
interface MessageBranchContextType {
|
| 117 |
+
currentBranch: number;
|
| 118 |
+
totalBranches: number;
|
| 119 |
+
goToPrevious: () => void;
|
| 120 |
+
goToNext: () => void;
|
| 121 |
+
branches: ReactElement[];
|
| 122 |
+
setBranches: (branches: ReactElement[]) => void;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
const MessageBranchContext = createContext<MessageBranchContextType | null>(
|
| 126 |
+
null
|
| 127 |
+
);
|
| 128 |
+
|
| 129 |
+
const useMessageBranch = () => {
|
| 130 |
+
const context = useContext(MessageBranchContext);
|
| 131 |
+
|
| 132 |
+
if (!context) {
|
| 133 |
+
throw new Error(
|
| 134 |
+
"MessageBranch components must be used within MessageBranch"
|
| 135 |
+
);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
return context;
|
| 139 |
+
};
|
| 140 |
+
|
| 141 |
+
export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
|
| 142 |
+
defaultBranch?: number;
|
| 143 |
+
onBranchChange?: (branchIndex: number) => void;
|
| 144 |
+
};
|
| 145 |
+
|
| 146 |
+
export const MessageBranch = ({
|
| 147 |
+
defaultBranch = 0,
|
| 148 |
+
onBranchChange,
|
| 149 |
+
className,
|
| 150 |
+
...props
|
| 151 |
+
}: MessageBranchProps) => {
|
| 152 |
+
const [currentBranch, setCurrentBranch] = useState(defaultBranch);
|
| 153 |
+
const [branches, setBranches] = useState<ReactElement[]>([]);
|
| 154 |
+
|
| 155 |
+
const handleBranchChange = useCallback(
|
| 156 |
+
(newBranch: number) => {
|
| 157 |
+
setCurrentBranch(newBranch);
|
| 158 |
+
onBranchChange?.(newBranch);
|
| 159 |
+
},
|
| 160 |
+
[onBranchChange]
|
| 161 |
+
);
|
| 162 |
+
|
| 163 |
+
const goToPrevious = useCallback(() => {
|
| 164 |
+
const newBranch =
|
| 165 |
+
currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
|
| 166 |
+
handleBranchChange(newBranch);
|
| 167 |
+
}, [currentBranch, branches.length, handleBranchChange]);
|
| 168 |
+
|
| 169 |
+
const goToNext = useCallback(() => {
|
| 170 |
+
const newBranch =
|
| 171 |
+
currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
|
| 172 |
+
handleBranchChange(newBranch);
|
| 173 |
+
}, [currentBranch, branches.length, handleBranchChange]);
|
| 174 |
+
|
| 175 |
+
const contextValue = useMemo<MessageBranchContextType>(
|
| 176 |
+
() => ({
|
| 177 |
+
branches,
|
| 178 |
+
currentBranch,
|
| 179 |
+
goToNext,
|
| 180 |
+
goToPrevious,
|
| 181 |
+
setBranches,
|
| 182 |
+
totalBranches: branches.length,
|
| 183 |
+
}),
|
| 184 |
+
[branches, currentBranch, goToNext, goToPrevious]
|
| 185 |
+
);
|
| 186 |
+
|
| 187 |
+
return (
|
| 188 |
+
<MessageBranchContext.Provider value={contextValue}>
|
| 189 |
+
<div
|
| 190 |
+
className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
|
| 191 |
+
{...props}
|
| 192 |
+
/>
|
| 193 |
+
</MessageBranchContext.Provider>
|
| 194 |
+
);
|
| 195 |
+
};
|
| 196 |
+
|
| 197 |
+
export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
|
| 198 |
+
|
| 199 |
+
export const MessageBranchContent = ({
|
| 200 |
+
children,
|
| 201 |
+
...props
|
| 202 |
+
}: MessageBranchContentProps) => {
|
| 203 |
+
const { currentBranch, setBranches, branches } = useMessageBranch();
|
| 204 |
+
const childrenArray = useMemo(
|
| 205 |
+
() => (Array.isArray(children) ? children : [children]),
|
| 206 |
+
[children]
|
| 207 |
+
);
|
| 208 |
+
|
| 209 |
+
// Use useEffect to update branches when they change
|
| 210 |
+
useEffect(() => {
|
| 211 |
+
if (branches.length !== childrenArray.length) {
|
| 212 |
+
setBranches(childrenArray);
|
| 213 |
+
}
|
| 214 |
+
}, [childrenArray, branches, setBranches]);
|
| 215 |
+
|
| 216 |
+
return childrenArray.map((branch, index) => (
|
| 217 |
+
<div
|
| 218 |
+
className={cn(
|
| 219 |
+
"grid gap-2 overflow-hidden [&>div]:pb-0",
|
| 220 |
+
index === currentBranch ? "block" : "hidden"
|
| 221 |
+
)}
|
| 222 |
+
key={branch.key}
|
| 223 |
+
{...props}
|
| 224 |
+
>
|
| 225 |
+
{branch}
|
| 226 |
+
</div>
|
| 227 |
+
));
|
| 228 |
+
};
|
| 229 |
+
|
| 230 |
+
export type MessageBranchSelectorProps = ComponentProps<typeof ButtonGroup>;
|
| 231 |
+
|
| 232 |
+
export const MessageBranchSelector = ({
|
| 233 |
+
className,
|
| 234 |
+
...props
|
| 235 |
+
}: MessageBranchSelectorProps) => {
|
| 236 |
+
const { totalBranches } = useMessageBranch();
|
| 237 |
+
|
| 238 |
+
// Don't render if there's only one branch
|
| 239 |
+
if (totalBranches <= 1) {
|
| 240 |
+
return null;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
return (
|
| 244 |
+
<ButtonGroup
|
| 245 |
+
className={cn(
|
| 246 |
+
"[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",
|
| 247 |
+
className
|
| 248 |
+
)}
|
| 249 |
+
orientation="horizontal"
|
| 250 |
+
{...props}
|
| 251 |
+
/>
|
| 252 |
+
);
|
| 253 |
+
};
|
| 254 |
+
|
| 255 |
+
export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
|
| 256 |
+
|
| 257 |
+
export const MessageBranchPrevious = ({
|
| 258 |
+
children,
|
| 259 |
+
...props
|
| 260 |
+
}: MessageBranchPreviousProps) => {
|
| 261 |
+
const { goToPrevious, totalBranches } = useMessageBranch();
|
| 262 |
+
|
| 263 |
+
return (
|
| 264 |
+
<Button
|
| 265 |
+
aria-label="Previous branch"
|
| 266 |
+
disabled={totalBranches <= 1}
|
| 267 |
+
onClick={goToPrevious}
|
| 268 |
+
size="icon-sm"
|
| 269 |
+
type="button"
|
| 270 |
+
variant="ghost"
|
| 271 |
+
{...props}
|
| 272 |
+
>
|
| 273 |
+
{children ?? <ChevronLeftIcon size={14} />}
|
| 274 |
+
</Button>
|
| 275 |
+
);
|
| 276 |
+
};
|
| 277 |
+
|
| 278 |
+
export type MessageBranchNextProps = ComponentProps<typeof Button>;
|
| 279 |
+
|
| 280 |
+
export const MessageBranchNext = ({
|
| 281 |
+
children,
|
| 282 |
+
...props
|
| 283 |
+
}: MessageBranchNextProps) => {
|
| 284 |
+
const { goToNext, totalBranches } = useMessageBranch();
|
| 285 |
+
|
| 286 |
+
return (
|
| 287 |
+
<Button
|
| 288 |
+
aria-label="Next branch"
|
| 289 |
+
disabled={totalBranches <= 1}
|
| 290 |
+
onClick={goToNext}
|
| 291 |
+
size="icon-sm"
|
| 292 |
+
type="button"
|
| 293 |
+
variant="ghost"
|
| 294 |
+
{...props}
|
| 295 |
+
>
|
| 296 |
+
{children ?? <ChevronRightIcon size={14} />}
|
| 297 |
+
</Button>
|
| 298 |
+
);
|
| 299 |
+
};
|
| 300 |
+
|
| 301 |
+
export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
|
| 302 |
+
|
| 303 |
+
export const MessageBranchPage = ({
|
| 304 |
+
className,
|
| 305 |
+
...props
|
| 306 |
+
}: MessageBranchPageProps) => {
|
| 307 |
+
const { currentBranch, totalBranches } = useMessageBranch();
|
| 308 |
+
|
| 309 |
+
return (
|
| 310 |
+
<ButtonGroupText
|
| 311 |
+
className={cn(
|
| 312 |
+
"border-none bg-transparent text-muted-foreground shadow-none",
|
| 313 |
+
className
|
| 314 |
+
)}
|
| 315 |
+
{...props}
|
| 316 |
+
>
|
| 317 |
+
{currentBranch + 1} of {totalBranches}
|
| 318 |
+
</ButtonGroupText>
|
| 319 |
+
);
|
| 320 |
+
};
|
| 321 |
+
|
| 322 |
+
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
|
| 323 |
+
|
| 324 |
+
const streamdownPlugins = { cjk, code, math, mermaid };
|
| 325 |
+
|
| 326 |
+
export const MessageResponse = memo(
|
| 327 |
+
({ className, ...props }: MessageResponseProps) => (
|
| 328 |
+
<Streamdown
|
| 329 |
+
className={cn(
|
| 330 |
+
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
|
| 331 |
+
className
|
| 332 |
+
)}
|
| 333 |
+
plugins={streamdownPlugins}
|
| 334 |
+
{...props}
|
| 335 |
+
/>
|
| 336 |
+
),
|
| 337 |
+
(prevProps, nextProps) =>
|
| 338 |
+
prevProps.children === nextProps.children &&
|
| 339 |
+
nextProps.isAnimating === prevProps.isAnimating
|
| 340 |
+
);
|
| 341 |
+
|
| 342 |
+
MessageResponse.displayName = "MessageResponse";
|
| 343 |
+
|
| 344 |
+
export type MessageToolbarProps = ComponentProps<"div">;
|
| 345 |
+
|
| 346 |
+
export const MessageToolbar = ({
|
| 347 |
+
className,
|
| 348 |
+
children,
|
| 349 |
+
...props
|
| 350 |
+
}: MessageToolbarProps) => (
|
| 351 |
+
<div
|
| 352 |
+
className={cn(
|
| 353 |
+
"mt-4 flex w-full items-center justify-between gap-4",
|
| 354 |
+
className
|
| 355 |
+
)}
|
| 356 |
+
{...props}
|
| 357 |
+
>
|
| 358 |
+
{children}
|
| 359 |
+
</div>
|
| 360 |
+
);
|
src/components/ai-elements/terminal.tsx
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { Button } from "@/components/ui/button";
|
| 4 |
+
import { cn } from "@/lib/utils";
|
| 5 |
+
import Ansi from "ansi-to-react";
|
| 6 |
+
import { CheckIcon, CopyIcon, TerminalIcon, Trash2Icon } from "lucide-react";
|
| 7 |
+
import type { ComponentProps, HTMLAttributes } from "react";
|
| 8 |
+
import {
|
| 9 |
+
createContext,
|
| 10 |
+
useCallback,
|
| 11 |
+
useContext,
|
| 12 |
+
useEffect,
|
| 13 |
+
useMemo,
|
| 14 |
+
useRef,
|
| 15 |
+
useState,
|
| 16 |
+
} from "react";
|
| 17 |
+
|
| 18 |
+
interface TerminalContextType {
|
| 19 |
+
output: string;
|
| 20 |
+
isStreaming: boolean;
|
| 21 |
+
autoScroll: boolean;
|
| 22 |
+
onClear?: () => void;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const TerminalContext = createContext<TerminalContextType>({
|
| 26 |
+
autoScroll: true,
|
| 27 |
+
isStreaming: false,
|
| 28 |
+
output: "",
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
export type TerminalHeaderProps = HTMLAttributes<HTMLDivElement>;
|
| 32 |
+
|
| 33 |
+
export const TerminalHeader = ({
|
| 34 |
+
className,
|
| 35 |
+
children,
|
| 36 |
+
...props
|
| 37 |
+
}: TerminalHeaderProps) => (
|
| 38 |
+
<div
|
| 39 |
+
className={cn(
|
| 40 |
+
"flex items-center justify-between border-zinc-800 border-b px-4 py-2",
|
| 41 |
+
className
|
| 42 |
+
)}
|
| 43 |
+
{...props}
|
| 44 |
+
>
|
| 45 |
+
{children}
|
| 46 |
+
</div>
|
| 47 |
+
);
|
| 48 |
+
|
| 49 |
+
export type TerminalTitleProps = HTMLAttributes<HTMLDivElement>;
|
| 50 |
+
|
| 51 |
+
export const TerminalTitle = ({
|
| 52 |
+
className,
|
| 53 |
+
children,
|
| 54 |
+
...props
|
| 55 |
+
}: TerminalTitleProps) => (
|
| 56 |
+
<div
|
| 57 |
+
className={cn("flex items-center gap-2 text-sm text-zinc-400", className)}
|
| 58 |
+
{...props}
|
| 59 |
+
>
|
| 60 |
+
<TerminalIcon className="size-4" />
|
| 61 |
+
{children ?? "Terminal"}
|
| 62 |
+
</div>
|
| 63 |
+
);
|
| 64 |
+
|
| 65 |
+
export type TerminalStatusProps = HTMLAttributes<HTMLDivElement>;
|
| 66 |
+
|
| 67 |
+
export const TerminalStatus = ({
|
| 68 |
+
className,
|
| 69 |
+
children,
|
| 70 |
+
...props
|
| 71 |
+
}: TerminalStatusProps) => {
|
| 72 |
+
const { isStreaming } = useContext(TerminalContext);
|
| 73 |
+
|
| 74 |
+
if (!isStreaming) {
|
| 75 |
+
return null;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
return (
|
| 79 |
+
<div
|
| 80 |
+
className={cn("flex items-center gap-2 text-xs text-zinc-400", className)}
|
| 81 |
+
{...props}
|
| 82 |
+
>
|
| 83 |
+
{children}
|
| 84 |
+
</div>
|
| 85 |
+
);
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
export type TerminalActionsProps = HTMLAttributes<HTMLDivElement>;
|
| 89 |
+
|
| 90 |
+
export const TerminalActions = ({
|
| 91 |
+
className,
|
| 92 |
+
children,
|
| 93 |
+
...props
|
| 94 |
+
}: TerminalActionsProps) => (
|
| 95 |
+
<div className={cn("flex items-center gap-1", className)} {...props}>
|
| 96 |
+
{children}
|
| 97 |
+
</div>
|
| 98 |
+
);
|
| 99 |
+
|
| 100 |
+
export type TerminalCopyButtonProps = ComponentProps<typeof Button> & {
|
| 101 |
+
onCopy?: () => void;
|
| 102 |
+
onError?: (error: Error) => void;
|
| 103 |
+
timeout?: number;
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
+
export const TerminalCopyButton = ({
|
| 107 |
+
onCopy,
|
| 108 |
+
onError,
|
| 109 |
+
timeout = 2000,
|
| 110 |
+
children,
|
| 111 |
+
className,
|
| 112 |
+
...props
|
| 113 |
+
}: TerminalCopyButtonProps) => {
|
| 114 |
+
const [isCopied, setIsCopied] = useState(false);
|
| 115 |
+
const timeoutRef = useRef<number>(0);
|
| 116 |
+
const { output } = useContext(TerminalContext);
|
| 117 |
+
|
| 118 |
+
const copyToClipboard = useCallback(async () => {
|
| 119 |
+
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
|
| 120 |
+
onError?.(new Error("Clipboard API not available"));
|
| 121 |
+
return;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
try {
|
| 125 |
+
await navigator.clipboard.writeText(output);
|
| 126 |
+
setIsCopied(true);
|
| 127 |
+
onCopy?.();
|
| 128 |
+
timeoutRef.current = window.setTimeout(() => setIsCopied(false), timeout);
|
| 129 |
+
} catch (error) {
|
| 130 |
+
onError?.(error as Error);
|
| 131 |
+
}
|
| 132 |
+
}, [output, onCopy, onError, timeout]);
|
| 133 |
+
|
| 134 |
+
useEffect(
|
| 135 |
+
() => () => {
|
| 136 |
+
window.clearTimeout(timeoutRef.current);
|
| 137 |
+
},
|
| 138 |
+
[]
|
| 139 |
+
);
|
| 140 |
+
|
| 141 |
+
const Icon = isCopied ? CheckIcon : CopyIcon;
|
| 142 |
+
|
| 143 |
+
return (
|
| 144 |
+
<Button
|
| 145 |
+
className={cn(
|
| 146 |
+
"size-7 shrink-0 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100",
|
| 147 |
+
className
|
| 148 |
+
)}
|
| 149 |
+
onClick={copyToClipboard}
|
| 150 |
+
size="icon"
|
| 151 |
+
variant="ghost"
|
| 152 |
+
{...props}
|
| 153 |
+
>
|
| 154 |
+
{children ?? <Icon size={14} />}
|
| 155 |
+
</Button>
|
| 156 |
+
);
|
| 157 |
+
};
|
| 158 |
+
|
| 159 |
+
export type TerminalClearButtonProps = ComponentProps<typeof Button>;
|
| 160 |
+
|
| 161 |
+
export const TerminalClearButton = ({
|
| 162 |
+
children,
|
| 163 |
+
className,
|
| 164 |
+
...props
|
| 165 |
+
}: TerminalClearButtonProps) => {
|
| 166 |
+
const { onClear } = useContext(TerminalContext);
|
| 167 |
+
|
| 168 |
+
if (!onClear) {
|
| 169 |
+
return null;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
return (
|
| 173 |
+
<Button
|
| 174 |
+
className={cn(
|
| 175 |
+
"size-7 shrink-0 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100",
|
| 176 |
+
className
|
| 177 |
+
)}
|
| 178 |
+
onClick={onClear}
|
| 179 |
+
size="icon"
|
| 180 |
+
variant="ghost"
|
| 181 |
+
{...props}
|
| 182 |
+
>
|
| 183 |
+
{children ?? <Trash2Icon size={14} />}
|
| 184 |
+
</Button>
|
| 185 |
+
);
|
| 186 |
+
};
|
| 187 |
+
|
| 188 |
+
export type TerminalContentProps = HTMLAttributes<HTMLDivElement>;
|
| 189 |
+
|
| 190 |
+
export const TerminalContent = ({
|
| 191 |
+
className,
|
| 192 |
+
children,
|
| 193 |
+
...props
|
| 194 |
+
}: TerminalContentProps) => {
|
| 195 |
+
const { output, isStreaming, autoScroll } = useContext(TerminalContext);
|
| 196 |
+
const containerRef = useRef<HTMLDivElement>(null);
|
| 197 |
+
|
| 198 |
+
useEffect(() => {
|
| 199 |
+
if (autoScroll && containerRef.current) {
|
| 200 |
+
containerRef.current.scrollTop = containerRef.current.scrollHeight;
|
| 201 |
+
}
|
| 202 |
+
}, [output, autoScroll]);
|
| 203 |
+
|
| 204 |
+
return (
|
| 205 |
+
<div
|
| 206 |
+
className={cn(
|
| 207 |
+
"max-h-96 overflow-auto p-4 font-mono text-sm leading-relaxed",
|
| 208 |
+
className
|
| 209 |
+
)}
|
| 210 |
+
ref={containerRef}
|
| 211 |
+
{...props}
|
| 212 |
+
>
|
| 213 |
+
{children ?? (
|
| 214 |
+
<pre className="whitespace-pre-wrap break-words">
|
| 215 |
+
<Ansi>{output}</Ansi>
|
| 216 |
+
{isStreaming && (
|
| 217 |
+
<span className="ml-0.5 inline-block h-4 w-2 animate-pulse bg-zinc-100" />
|
| 218 |
+
)}
|
| 219 |
+
</pre>
|
| 220 |
+
)}
|
| 221 |
+
</div>
|
| 222 |
+
);
|
| 223 |
+
};
|
| 224 |
+
|
| 225 |
+
export type TerminalProps = HTMLAttributes<HTMLDivElement> & {
|
| 226 |
+
output: string;
|
| 227 |
+
isStreaming?: boolean;
|
| 228 |
+
autoScroll?: boolean;
|
| 229 |
+
onClear?: () => void;
|
| 230 |
+
};
|
| 231 |
+
|
| 232 |
+
export const Terminal = ({
|
| 233 |
+
output,
|
| 234 |
+
isStreaming = false,
|
| 235 |
+
autoScroll = true,
|
| 236 |
+
onClear,
|
| 237 |
+
className,
|
| 238 |
+
children,
|
| 239 |
+
...props
|
| 240 |
+
}: TerminalProps) => {
|
| 241 |
+
const contextValue = useMemo(
|
| 242 |
+
() => ({ autoScroll, isStreaming, onClear, output }),
|
| 243 |
+
[autoScroll, isStreaming, onClear, output]
|
| 244 |
+
);
|
| 245 |
+
|
| 246 |
+
return (
|
| 247 |
+
<TerminalContext.Provider value={contextValue}>
|
| 248 |
+
<div
|
| 249 |
+
className={cn(
|
| 250 |
+
"flex flex-col overflow-hidden rounded-lg border bg-zinc-950 text-zinc-100",
|
| 251 |
+
className
|
| 252 |
+
)}
|
| 253 |
+
{...props}
|
| 254 |
+
>
|
| 255 |
+
{children ?? (
|
| 256 |
+
<>
|
| 257 |
+
<TerminalHeader>
|
| 258 |
+
<TerminalTitle />
|
| 259 |
+
<div className="flex items-center gap-1">
|
| 260 |
+
<TerminalStatus />
|
| 261 |
+
<TerminalActions>
|
| 262 |
+
<TerminalCopyButton />
|
| 263 |
+
{onClear && <TerminalClearButton />}
|
| 264 |
+
</TerminalActions>
|
| 265 |
+
</div>
|
| 266 |
+
</TerminalHeader>
|
| 267 |
+
<TerminalContent />
|
| 268 |
+
</>
|
| 269 |
+
)}
|
| 270 |
+
</div>
|
| 271 |
+
</TerminalContext.Provider>
|
| 272 |
+
);
|
| 273 |
+
};
|
src/components/ai-elements/tool.tsx
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { Badge } from "@/components/ui/badge";
|
| 4 |
+
import {
|
| 5 |
+
Collapsible,
|
| 6 |
+
CollapsibleContent,
|
| 7 |
+
CollapsibleTrigger,
|
| 8 |
+
} from "@/components/ui/collapsible";
|
| 9 |
+
import { cn } from "@/lib/utils";
|
| 10 |
+
import type { DynamicToolUIPart, ToolUIPart } from "ai";
|
| 11 |
+
import {
|
| 12 |
+
CheckCircleIcon,
|
| 13 |
+
ChevronDownIcon,
|
| 14 |
+
CircleIcon,
|
| 15 |
+
ClockIcon,
|
| 16 |
+
WrenchIcon,
|
| 17 |
+
XCircleIcon,
|
| 18 |
+
} from "lucide-react";
|
| 19 |
+
import type { ComponentProps, ReactNode } from "react";
|
| 20 |
+
import { isValidElement } from "react";
|
| 21 |
+
|
| 22 |
+
import { CodeBlock } from "./code-block";
|
| 23 |
+
|
| 24 |
+
export type ToolProps = ComponentProps<typeof Collapsible>;
|
| 25 |
+
|
| 26 |
+
export const Tool = ({ className, ...props }: ToolProps) => (
|
| 27 |
+
<Collapsible
|
| 28 |
+
className={cn("group not-prose mb-4 w-full rounded-md border", className)}
|
| 29 |
+
{...props}
|
| 30 |
+
/>
|
| 31 |
+
);
|
| 32 |
+
|
| 33 |
+
export type ToolPart = ToolUIPart | DynamicToolUIPart;
|
| 34 |
+
|
| 35 |
+
export type ToolHeaderProps = {
|
| 36 |
+
title?: string;
|
| 37 |
+
className?: string;
|
| 38 |
+
} & (
|
| 39 |
+
| { type: ToolUIPart["type"]; state: ToolUIPart["state"]; toolName?: never }
|
| 40 |
+
| {
|
| 41 |
+
type: DynamicToolUIPart["type"];
|
| 42 |
+
state: DynamicToolUIPart["state"];
|
| 43 |
+
toolName: string;
|
| 44 |
+
}
|
| 45 |
+
);
|
| 46 |
+
|
| 47 |
+
const statusLabels: Record<ToolPart["state"], string> = {
|
| 48 |
+
"approval-requested": "Awaiting Approval",
|
| 49 |
+
"approval-responded": "Responded",
|
| 50 |
+
"input-available": "Running",
|
| 51 |
+
"input-streaming": "Pending",
|
| 52 |
+
"output-available": "Completed",
|
| 53 |
+
"output-denied": "Denied",
|
| 54 |
+
"output-error": "Error",
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
const statusIcons: Record<ToolPart["state"], ReactNode> = {
|
| 58 |
+
"approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
|
| 59 |
+
"approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />,
|
| 60 |
+
"input-available": <ClockIcon className="size-4 animate-pulse" />,
|
| 61 |
+
"input-streaming": <CircleIcon className="size-4" />,
|
| 62 |
+
"output-available": <CheckCircleIcon className="size-4 text-green-600" />,
|
| 63 |
+
"output-denied": <XCircleIcon className="size-4 text-orange-600" />,
|
| 64 |
+
"output-error": <XCircleIcon className="size-4 text-red-600" />,
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
export const getStatusBadge = (status: ToolPart["state"]) => (
|
| 68 |
+
<Badge className="gap-1.5 rounded-full text-xs" variant="secondary">
|
| 69 |
+
{statusIcons[status]}
|
| 70 |
+
{statusLabels[status]}
|
| 71 |
+
</Badge>
|
| 72 |
+
);
|
| 73 |
+
|
| 74 |
+
export const ToolHeader = ({
|
| 75 |
+
className,
|
| 76 |
+
title,
|
| 77 |
+
type,
|
| 78 |
+
state,
|
| 79 |
+
toolName,
|
| 80 |
+
...props
|
| 81 |
+
}: ToolHeaderProps) => {
|
| 82 |
+
const derivedName =
|
| 83 |
+
type === "dynamic-tool" ? toolName : type.split("-").slice(1).join("-");
|
| 84 |
+
|
| 85 |
+
return (
|
| 86 |
+
<CollapsibleTrigger
|
| 87 |
+
className={cn(
|
| 88 |
+
"flex w-full items-center justify-between gap-4 p-3",
|
| 89 |
+
className
|
| 90 |
+
)}
|
| 91 |
+
{...props}
|
| 92 |
+
>
|
| 93 |
+
<div className="flex items-center gap-2">
|
| 94 |
+
<WrenchIcon className="size-4 text-muted-foreground" />
|
| 95 |
+
<span className="font-medium text-sm">{title ?? derivedName}</span>
|
| 96 |
+
{getStatusBadge(state)}
|
| 97 |
+
</div>
|
| 98 |
+
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
| 99 |
+
</CollapsibleTrigger>
|
| 100 |
+
);
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
|
| 104 |
+
|
| 105 |
+
export const ToolContent = ({ className, ...props }: ToolContentProps) => (
|
| 106 |
+
<CollapsibleContent
|
| 107 |
+
className={cn(
|
| 108 |
+
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 space-y-4 p-4 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
| 109 |
+
className
|
| 110 |
+
)}
|
| 111 |
+
{...props}
|
| 112 |
+
/>
|
| 113 |
+
);
|
| 114 |
+
|
| 115 |
+
export type ToolInputProps = ComponentProps<"div"> & {
|
| 116 |
+
input: ToolPart["input"];
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
|
| 120 |
+
<div className={cn("space-y-2 overflow-hidden", className)} {...props}>
|
| 121 |
+
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
| 122 |
+
Parameters
|
| 123 |
+
</h4>
|
| 124 |
+
<div className="rounded-md bg-muted/50">
|
| 125 |
+
<CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
);
|
| 129 |
+
|
| 130 |
+
export type ToolOutputProps = ComponentProps<"div"> & {
|
| 131 |
+
output: ToolPart["output"];
|
| 132 |
+
errorText: ToolPart["errorText"];
|
| 133 |
+
};
|
| 134 |
+
|
| 135 |
+
export const ToolOutput = ({
|
| 136 |
+
className,
|
| 137 |
+
output,
|
| 138 |
+
errorText,
|
| 139 |
+
...props
|
| 140 |
+
}: ToolOutputProps) => {
|
| 141 |
+
if (!(output || errorText)) {
|
| 142 |
+
return null;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
let Output = <div>{output as ReactNode}</div>;
|
| 146 |
+
|
| 147 |
+
if (typeof output === "object" && !isValidElement(output)) {
|
| 148 |
+
Output = (
|
| 149 |
+
<CodeBlock code={JSON.stringify(output, null, 2)} language="json" />
|
| 150 |
+
);
|
| 151 |
+
} else if (typeof output === "string") {
|
| 152 |
+
Output = <CodeBlock code={output} language="json" />;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
return (
|
| 156 |
+
<div className={cn("space-y-2", className)} {...props}>
|
| 157 |
+
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
| 158 |
+
{errorText ? "Error" : "Result"}
|
| 159 |
+
</h4>
|
| 160 |
+
<div
|
| 161 |
+
className={cn(
|
| 162 |
+
"overflow-x-auto rounded-md text-xs [&_table]:w-full",
|
| 163 |
+
errorText
|
| 164 |
+
? "bg-destructive/10 text-destructive"
|
| 165 |
+
: "bg-muted/50 text-foreground"
|
| 166 |
+
)}
|
| 167 |
+
>
|
| 168 |
+
{errorText && <div>{errorText}</div>}
|
| 169 |
+
{Output}
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
);
|
| 173 |
+
};
|
src/components/rocmpilot-dashboard.tsx
ADDED
|
@@ -0,0 +1,907 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import {
|
| 4 |
+
CodeBlock,
|
| 5 |
+
CodeBlockCopyButton,
|
| 6 |
+
CodeBlockFilename,
|
| 7 |
+
CodeBlockHeader,
|
| 8 |
+
CodeBlockTitle,
|
| 9 |
+
} from "@/components/ai-elements/code-block";
|
| 10 |
+
import { MessageResponse } from "@/components/ai-elements/message";
|
| 11 |
+
import { Terminal } from "@/components/ai-elements/terminal";
|
| 12 |
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
| 13 |
+
import { Badge } from "@/components/ui/badge";
|
| 14 |
+
import { Button } from "@/components/ui/button";
|
| 15 |
+
import {
|
| 16 |
+
Card,
|
| 17 |
+
CardContent,
|
| 18 |
+
CardDescription,
|
| 19 |
+
CardHeader,
|
| 20 |
+
CardTitle,
|
| 21 |
+
} from "@/components/ui/card";
|
| 22 |
+
import { Input } from "@/components/ui/input";
|
| 23 |
+
import { Label } from "@/components/ui/label";
|
| 24 |
+
import { Progress } from "@/components/ui/progress";
|
| 25 |
+
import {
|
| 26 |
+
Select,
|
| 27 |
+
SelectContent,
|
| 28 |
+
SelectItem,
|
| 29 |
+
SelectTrigger,
|
| 30 |
+
SelectValue,
|
| 31 |
+
} from "@/components/ui/select";
|
| 32 |
+
import { Separator } from "@/components/ui/separator";
|
| 33 |
+
import {
|
| 34 |
+
Table,
|
| 35 |
+
TableBody,
|
| 36 |
+
TableCell,
|
| 37 |
+
TableHead,
|
| 38 |
+
TableHeader,
|
| 39 |
+
TableRow,
|
| 40 |
+
} from "@/components/ui/table";
|
| 41 |
+
import { SAMPLE_REPOS } from "@/lib/rocmpilot/data";
|
| 42 |
+
import type {
|
| 43 |
+
AgentMemory,
|
| 44 |
+
AgentMessage,
|
| 45 |
+
AgentMessageKind,
|
| 46 |
+
FindingSeverity,
|
| 47 |
+
LongContextMemoryStatus,
|
| 48 |
+
ReportResponse,
|
| 49 |
+
RocmRun,
|
| 50 |
+
RunStage,
|
| 51 |
+
} from "@/lib/rocmpilot/types";
|
| 52 |
+
import {
|
| 53 |
+
Activity,
|
| 54 |
+
BadgeCheck,
|
| 55 |
+
Bot,
|
| 56 |
+
Boxes,
|
| 57 |
+
BrainCircuit,
|
| 58 |
+
CheckCircle2,
|
| 59 |
+
Cpu,
|
| 60 |
+
FileCode2,
|
| 61 |
+
Gauge,
|
| 62 |
+
GitBranch,
|
| 63 |
+
Loader2,
|
| 64 |
+
Play,
|
| 65 |
+
RefreshCw,
|
| 66 |
+
ShieldCheck,
|
| 67 |
+
Sparkles,
|
| 68 |
+
TerminalSquare,
|
| 69 |
+
TriangleAlert,
|
| 70 |
+
Zap,
|
| 71 |
+
} from "lucide-react";
|
| 72 |
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
| 73 |
+
|
| 74 |
+
const severityTone: Record<FindingSeverity, string> = {
|
| 75 |
+
critical: "border-red-500/40 bg-red-500/10 text-red-200",
|
| 76 |
+
high: "border-amber-500/40 bg-amber-500/10 text-amber-100",
|
| 77 |
+
medium: "border-cyan-500/40 bg-cyan-500/10 text-cyan-100",
|
| 78 |
+
low: "border-emerald-500/40 bg-emerald-500/10 text-emerald-100",
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
const statusTone = {
|
| 82 |
+
pending: "border-zinc-800 bg-zinc-950 text-zinc-500",
|
| 83 |
+
running: "border-cyan-500/40 bg-cyan-500/10 text-cyan-100",
|
| 84 |
+
completed: "border-emerald-500/40 bg-emerald-500/10 text-emerald-100",
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
const memoryStatusTone: Record<LongContextMemoryStatus["status"], string> = {
|
| 88 |
+
connected: "border-emerald-500/40 bg-emerald-500/10 text-emerald-100",
|
| 89 |
+
configured: "border-cyan-500/40 bg-cyan-500/10 text-cyan-100",
|
| 90 |
+
fallback: "border-amber-500/40 bg-amber-500/10 text-amber-100",
|
| 91 |
+
"not-configured": "border-zinc-700 bg-zinc-900 text-zinc-300",
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
const messageKindTone: Record<AgentMessageKind, string> = {
|
| 95 |
+
question: "border-cyan-500/40 bg-cyan-500/10 text-cyan-100",
|
| 96 |
+
answer: "border-sky-500/40 bg-sky-500/10 text-sky-100",
|
| 97 |
+
challenge: "border-amber-500/40 bg-amber-500/10 text-amber-100",
|
| 98 |
+
proposal: "border-violet-500/40 bg-violet-500/10 text-violet-100",
|
| 99 |
+
decision: "border-lime-500/40 bg-lime-500/10 text-lime-100",
|
| 100 |
+
memory: "border-rose-500/40 bg-rose-500/10 text-rose-100",
|
| 101 |
+
consensus: "border-emerald-500/40 bg-emerald-500/10 text-emerald-100",
|
| 102 |
+
};
|
| 103 |
+
|
| 104 |
+
const agentDotTone: Record<string, string> = {
|
| 105 |
+
Orchestrator: "bg-emerald-300",
|
| 106 |
+
"Repo Doctor": "bg-cyan-300",
|
| 107 |
+
"Migration Planner": "bg-violet-300",
|
| 108 |
+
"Build Runner": "bg-amber-300",
|
| 109 |
+
"Benchmark Agent": "bg-sky-300",
|
| 110 |
+
"Report Agent": "bg-rose-300",
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
+
function StageIcon({ stage }: { stage: RunStage }) {
|
| 114 |
+
if (stage.status === "completed") {
|
| 115 |
+
return <CheckCircle2 className="size-4 text-emerald-300" />;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
if (stage.status === "running") {
|
| 119 |
+
return <Loader2 className="size-4 animate-spin text-cyan-200" />;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
return <Activity className="size-4 text-zinc-500" />;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
function formatTime(value: string | undefined) {
|
| 126 |
+
if (!value) {
|
| 127 |
+
return "--:--";
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
return new Intl.DateTimeFormat("en", {
|
| 131 |
+
hour: "2-digit",
|
| 132 |
+
minute: "2-digit",
|
| 133 |
+
second: "2-digit",
|
| 134 |
+
}).format(new Date(value));
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
function WarRoomMessage({
|
| 138 |
+
message,
|
| 139 |
+
memoryById,
|
| 140 |
+
}: {
|
| 141 |
+
message: AgentMessage;
|
| 142 |
+
memoryById: Map<string, AgentMemory>;
|
| 143 |
+
}) {
|
| 144 |
+
return (
|
| 145 |
+
<div className="grid grid-cols-[28px_minmax(0,1fr)] gap-3 rounded-lg border border-border/70 bg-background/60 p-3">
|
| 146 |
+
<div className="relative mt-1 flex size-7 items-center justify-center rounded-full border border-border bg-card">
|
| 147 |
+
<span
|
| 148 |
+
className={`size-2.5 rounded-full ${agentDotTone[message.agent] ?? "bg-zinc-400"}`}
|
| 149 |
+
/>
|
| 150 |
+
</div>
|
| 151 |
+
<div className="min-w-0 space-y-2">
|
| 152 |
+
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
| 153 |
+
<p className="truncate text-sm font-semibold">{message.agent}</p>
|
| 154 |
+
<span className="text-xs text-muted-foreground">to</span>
|
| 155 |
+
<p className="truncate text-sm font-medium text-cyan-100">{message.toAgent}</p>
|
| 156 |
+
<Badge variant="outline" className={messageKindTone[message.kind]}>
|
| 157 |
+
{message.kind}
|
| 158 |
+
</Badge>
|
| 159 |
+
{message.leadAgent === message.agent && (
|
| 160 |
+
<Badge variant="outline" className="border-emerald-500/40 bg-emerald-500/10 text-emerald-100">
|
| 161 |
+
lead
|
| 162 |
+
</Badge>
|
| 163 |
+
)}
|
| 164 |
+
<span className="font-mono text-[11px] text-muted-foreground">
|
| 165 |
+
{formatTime(message.createdAt)}
|
| 166 |
+
</span>
|
| 167 |
+
</div>
|
| 168 |
+
<div className="flex flex-wrap items-center gap-2 text-xs leading-5 text-muted-foreground">
|
| 169 |
+
<span>{message.role}</span>
|
| 170 |
+
<span>/</span>
|
| 171 |
+
<span>{message.task}</span>
|
| 172 |
+
{message.replyToId && (
|
| 173 |
+
<>
|
| 174 |
+
<span>/</span>
|
| 175 |
+
<span>replying to earlier message</span>
|
| 176 |
+
</>
|
| 177 |
+
)}
|
| 178 |
+
</div>
|
| 179 |
+
<p className="break-words text-sm leading-6 text-foreground">{message.message}</p>
|
| 180 |
+
{message.memoryRefs.length > 0 && (
|
| 181 |
+
<div className="flex flex-wrap gap-2">
|
| 182 |
+
{message.memoryRefs.map((memoryId) => (
|
| 183 |
+
<Badge
|
| 184 |
+
key={memoryId}
|
| 185 |
+
variant="outline"
|
| 186 |
+
className="border-zinc-600 bg-zinc-900/80 text-zinc-200"
|
| 187 |
+
>
|
| 188 |
+
memory: {memoryById.get(memoryId)?.title ?? memoryId}
|
| 189 |
+
</Badge>
|
| 190 |
+
))}
|
| 191 |
+
</div>
|
| 192 |
+
)}
|
| 193 |
+
</div>
|
| 194 |
+
</div>
|
| 195 |
+
);
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
function AgentWarRoom({ run }: { run: RocmRun | null }) {
|
| 199 |
+
const messages = useMemo(() => run?.agentMessages ?? [], [run?.agentMessages]);
|
| 200 |
+
const memory = useMemo(() => run?.agentMemory ?? [], [run?.agentMemory]);
|
| 201 |
+
const latestMessage = messages[messages.length - 1];
|
| 202 |
+
const activeLead = latestMessage?.leadAgent ?? "No lead assigned";
|
| 203 |
+
const memoryById = useMemo(
|
| 204 |
+
() => new Map(memory.map((entry) => [entry.id, entry])),
|
| 205 |
+
[memory]
|
| 206 |
+
);
|
| 207 |
+
|
| 208 |
+
return (
|
| 209 |
+
<Card>
|
| 210 |
+
<CardHeader className="gap-3">
|
| 211 |
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
| 212 |
+
<div>
|
| 213 |
+
<CardTitle className="flex items-center gap-2 text-xl">
|
| 214 |
+
<Activity className="size-5 text-cyan-200" />
|
| 215 |
+
Agent War Room
|
| 216 |
+
</CardTitle>
|
| 217 |
+
<CardDescription>
|
| 218 |
+
Lead-led discussion with replies, objections, and shared memory the agents reuse later.
|
| 219 |
+
</CardDescription>
|
| 220 |
+
</div>
|
| 221 |
+
<div className="flex flex-wrap gap-2">
|
| 222 |
+
<Badge
|
| 223 |
+
variant="outline"
|
| 224 |
+
className={
|
| 225 |
+
run?.status === "running"
|
| 226 |
+
? "border-cyan-500/40 bg-cyan-500/10 text-cyan-100"
|
| 227 |
+
: run?.status === "completed"
|
| 228 |
+
? "border-emerald-500/40 bg-emerald-500/10 text-emerald-100"
|
| 229 |
+
: "border-zinc-700 bg-zinc-900 text-zinc-300"
|
| 230 |
+
}
|
| 231 |
+
>
|
| 232 |
+
{run?.status === "running" ? "live room" : run?.status === "completed" ? "consensus" : "standby"}
|
| 233 |
+
</Badge>
|
| 234 |
+
<Badge variant="outline" className="border-zinc-700 bg-zinc-900 text-zinc-200">
|
| 235 |
+
lead: {activeLead}
|
| 236 |
+
</Badge>
|
| 237 |
+
{run?.longContextMemory && (
|
| 238 |
+
<Badge
|
| 239 |
+
variant="outline"
|
| 240 |
+
className={memoryStatusTone[run.longContextMemory.status]}
|
| 241 |
+
>
|
| 242 |
+
{run.longContextMemory.provider === "synap" ? "Synap memory" : "local memory"}
|
| 243 |
+
</Badge>
|
| 244 |
+
)}
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
{latestMessage && (
|
| 248 |
+
<div className="rounded-lg border border-emerald-500/20 bg-emerald-500/5 p-3 text-sm leading-6 text-emerald-50">
|
| 249 |
+
<span className="font-medium">
|
| 250 |
+
{latestMessage.agent} to {latestMessage.toAgent}:
|
| 251 |
+
</span>{" "}
|
| 252 |
+
<span className="text-emerald-100/90">{latestMessage.message}</span>
|
| 253 |
+
</div>
|
| 254 |
+
)}
|
| 255 |
+
</CardHeader>
|
| 256 |
+
<CardContent>
|
| 257 |
+
{messages.length ? (
|
| 258 |
+
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(300px,0.75fr)]">
|
| 259 |
+
<div className="max-h-[520px] space-y-3 overflow-y-auto pr-1">
|
| 260 |
+
{messages.map((message) => (
|
| 261 |
+
<WarRoomMessage key={message.id} message={message} memoryById={memoryById} />
|
| 262 |
+
))}
|
| 263 |
+
</div>
|
| 264 |
+
<div className="rounded-lg border border-border bg-background/50 p-3">
|
| 265 |
+
<div className="flex items-center justify-between gap-3">
|
| 266 |
+
<p className="text-sm font-semibold">Shared memory</p>
|
| 267 |
+
<Badge variant="outline" className="border-rose-500/40 bg-rose-500/10 text-rose-100">
|
| 268 |
+
{memory.length} stored
|
| 269 |
+
</Badge>
|
| 270 |
+
</div>
|
| 271 |
+
<div className="mt-3 max-h-[470px] space-y-3 overflow-y-auto pr-1">
|
| 272 |
+
{memory.length ? (
|
| 273 |
+
memory.map((entry) => (
|
| 274 |
+
<div key={entry.id} className="rounded-lg border border-border/70 bg-card p-3">
|
| 275 |
+
<div className="flex flex-wrap items-center gap-2">
|
| 276 |
+
<p className="text-sm font-medium">{entry.title}</p>
|
| 277 |
+
<Badge variant="outline">{entry.scope}</Badge>
|
| 278 |
+
</div>
|
| 279 |
+
<p className="mt-2 text-xs text-muted-foreground">
|
| 280 |
+
learned from {entry.learnedFromAgent} at {formatTime(entry.createdAt)}
|
| 281 |
+
</p>
|
| 282 |
+
<p className="mt-3 text-sm leading-6">{entry.summary}</p>
|
| 283 |
+
<p className="mt-2 text-sm leading-6 text-muted-foreground">{entry.solution}</p>
|
| 284 |
+
{entry.usedBy.length > 0 && (
|
| 285 |
+
<p className="mt-3 text-xs text-emerald-100">
|
| 286 |
+
reused by {Array.from(new Set(entry.usedBy)).join(", ")}
|
| 287 |
+
</p>
|
| 288 |
+
)}
|
| 289 |
+
</div>
|
| 290 |
+
))
|
| 291 |
+
) : (
|
| 292 |
+
<div className="rounded-lg border border-dashed border-border p-4 text-sm text-muted-foreground">
|
| 293 |
+
Memory appears when agents store a blocker, solution, or evidence rule.
|
| 294 |
+
</div>
|
| 295 |
+
)}
|
| 296 |
+
</div>
|
| 297 |
+
</div>
|
| 298 |
+
</div>
|
| 299 |
+
) : (
|
| 300 |
+
<div className="grid gap-3 rounded-lg border border-dashed border-border p-4 sm:grid-cols-2 lg:grid-cols-3">
|
| 301 |
+
{[
|
| 302 |
+
"Repo Doctor",
|
| 303 |
+
"Migration Planner",
|
| 304 |
+
"Build Runner",
|
| 305 |
+
"Benchmark Agent",
|
| 306 |
+
"Report Agent",
|
| 307 |
+
"Orchestrator",
|
| 308 |
+
].map((agent) => (
|
| 309 |
+
<div key={agent} className="flex min-w-0 items-center gap-3 rounded-lg bg-card p-3">
|
| 310 |
+
<span className={`size-2.5 shrink-0 rounded-full ${agentDotTone[agent] ?? "bg-zinc-400"}`} />
|
| 311 |
+
<span className="truncate text-sm text-muted-foreground">{agent} waiting</span>
|
| 312 |
+
</div>
|
| 313 |
+
))}
|
| 314 |
+
</div>
|
| 315 |
+
)}
|
| 316 |
+
</CardContent>
|
| 317 |
+
</Card>
|
| 318 |
+
);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
export function RocmPilotDashboard() {
|
| 322 |
+
const [sampleId, setSampleId] = useState(SAMPLE_REPOS[0].id);
|
| 323 |
+
const [githubUrl, setGithubUrl] = useState("");
|
| 324 |
+
const [run, setRun] = useState<RocmRun | null>(null);
|
| 325 |
+
const [report, setReport] = useState<ReportResponse | null>(null);
|
| 326 |
+
const [activePanel, setActivePanel] = useState<"patches" | "logs" | "report">("patches");
|
| 327 |
+
const [isStarting, setIsStarting] = useState(false);
|
| 328 |
+
const [isGeneratingReport, setIsGeneratingReport] = useState(false);
|
| 329 |
+
const [error, setError] = useState<string | null>(null);
|
| 330 |
+
const reportRequestedFor = useRef<string | null>(null);
|
| 331 |
+
|
| 332 |
+
const selectedSample = useMemo(
|
| 333 |
+
() => SAMPLE_REPOS.find((sample) => sample.id === sampleId) ?? SAMPLE_REPOS[0],
|
| 334 |
+
[sampleId]
|
| 335 |
+
);
|
| 336 |
+
|
| 337 |
+
const logOutput = useMemo(() => run?.logs.join("\n") ?? "", [run]);
|
| 338 |
+
const activeRepoUrl = run?.target.repoUrl ?? (githubUrl.trim() || selectedSample.repoUrl);
|
| 339 |
+
|
| 340 |
+
const startRun = useCallback(async () => {
|
| 341 |
+
setIsStarting(true);
|
| 342 |
+
setError(null);
|
| 343 |
+
setReport(null);
|
| 344 |
+
reportRequestedFor.current = null;
|
| 345 |
+
|
| 346 |
+
try {
|
| 347 |
+
const response = await fetch("/api/runs", {
|
| 348 |
+
method: "POST",
|
| 349 |
+
headers: { "Content-Type": "application/json" },
|
| 350 |
+
body: JSON.stringify({
|
| 351 |
+
sampleId,
|
| 352 |
+
mode: "mock",
|
| 353 |
+
repoUrl: githubUrl.trim() || undefined,
|
| 354 |
+
}),
|
| 355 |
+
});
|
| 356 |
+
|
| 357 |
+
if (!response.ok) {
|
| 358 |
+
throw new Error("Could not start migration audit");
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
const nextRun = (await response.json()) as RocmRun;
|
| 362 |
+
setRun(nextRun);
|
| 363 |
+
} catch (caught) {
|
| 364 |
+
setError(caught instanceof Error ? caught.message : "Unknown start error");
|
| 365 |
+
} finally {
|
| 366 |
+
setIsStarting(false);
|
| 367 |
+
}
|
| 368 |
+
}, [githubUrl, sampleId]);
|
| 369 |
+
|
| 370 |
+
const pollRun = useCallback(async (runId: string) => {
|
| 371 |
+
const response = await fetch(`/api/runs/${runId}`, { cache: "no-store" });
|
| 372 |
+
|
| 373 |
+
if (!response.ok) {
|
| 374 |
+
throw new Error("Could not refresh run state");
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
const nextRun = (await response.json()) as RocmRun;
|
| 378 |
+
setRun(nextRun);
|
| 379 |
+
}, []);
|
| 380 |
+
|
| 381 |
+
const generateReport = useCallback(async (completedRun: RocmRun) => {
|
| 382 |
+
setIsGeneratingReport(true);
|
| 383 |
+
|
| 384 |
+
try {
|
| 385 |
+
const response = await fetch("/api/report", {
|
| 386 |
+
method: "POST",
|
| 387 |
+
headers: { "Content-Type": "application/json" },
|
| 388 |
+
body: JSON.stringify(completedRun),
|
| 389 |
+
});
|
| 390 |
+
|
| 391 |
+
if (!response.ok) {
|
| 392 |
+
throw new Error("Could not generate final report");
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
const reportResponse = (await response.json()) as ReportResponse;
|
| 396 |
+
setReport(reportResponse);
|
| 397 |
+
} catch (caught) {
|
| 398 |
+
setError(caught instanceof Error ? caught.message : "Unknown report error");
|
| 399 |
+
} finally {
|
| 400 |
+
setIsGeneratingReport(false);
|
| 401 |
+
}
|
| 402 |
+
}, []);
|
| 403 |
+
|
| 404 |
+
useEffect(() => {
|
| 405 |
+
if (!run || run.status === "completed") {
|
| 406 |
+
return;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
const timer = window.setInterval(() => {
|
| 410 |
+
void pollRun(run.id).catch((caught) =>
|
| 411 |
+
setError(caught instanceof Error ? caught.message : "Unknown polling error")
|
| 412 |
+
);
|
| 413 |
+
}, 900);
|
| 414 |
+
|
| 415 |
+
return () => window.clearInterval(timer);
|
| 416 |
+
}, [pollRun, run]);
|
| 417 |
+
|
| 418 |
+
useEffect(() => {
|
| 419 |
+
if (!run || run.status !== "completed" || reportRequestedFor.current === run.id) {
|
| 420 |
+
return;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
reportRequestedFor.current = run.id;
|
| 424 |
+
void generateReport(run);
|
| 425 |
+
}, [generateReport, run]);
|
| 426 |
+
|
| 427 |
+
const modelStatus = report?.modelStatus ?? run?.modelStatus;
|
| 428 |
+
const memoryStatus = report?.memoryStatus ?? run?.longContextMemory;
|
| 429 |
+
const completedStages = run?.stages.filter((stage) => stage.status === "completed").length ?? 0;
|
| 430 |
+
|
| 431 |
+
return (
|
| 432 |
+
<main className="min-h-screen bg-background text-foreground">
|
| 433 |
+
<div className="mx-auto flex w-full max-w-none flex-col gap-5 px-4 py-5 sm:px-5 lg:px-6">
|
| 434 |
+
<section className="grid gap-5 border-b border-border pb-5 xl:grid-cols-[minmax(280px,0.55fr)_minmax(0,1fr)] xl:items-end">
|
| 435 |
+
<div className="space-y-3">
|
| 436 |
+
<div className="flex flex-wrap items-center gap-2">
|
| 437 |
+
<Badge variant="outline" className="border-emerald-500/40 bg-emerald-500/10 text-emerald-100">
|
| 438 |
+
Track 1
|
| 439 |
+
</Badge>
|
| 440 |
+
<Badge variant="outline" className="border-cyan-500/40 bg-cyan-500/10 text-cyan-100">
|
| 441 |
+
ROCm + vLLM
|
| 442 |
+
</Badge>
|
| 443 |
+
<Badge variant="outline" className="border-amber-500/40 bg-amber-500/10 text-amber-100">
|
| 444 |
+
Qwen3-Coder-Next
|
| 445 |
+
</Badge>
|
| 446 |
+
</div>
|
| 447 |
+
<div>
|
| 448 |
+
<h1 className="text-3xl font-semibold tracking-normal text-foreground sm:text-4xl">
|
| 449 |
+
ROCmPilot
|
| 450 |
+
</h1>
|
| 451 |
+
<p className="mt-2 max-w-3xl text-sm leading-6 text-muted-foreground">
|
| 452 |
+
Multi-agent ROCm migration cockpit for PyTorch and vLLM workloads.
|
| 453 |
+
</p>
|
| 454 |
+
</div>
|
| 455 |
+
</div>
|
| 456 |
+
|
| 457 |
+
<div className="grid w-full min-w-0 gap-3 md:grid-cols-[minmax(220px,0.85fr)_minmax(260px,1.35fr)] xl:grid-cols-[minmax(220px,0.8fr)_minmax(340px,1.45fr)_max-content]">
|
| 458 |
+
<div className="grid min-w-0 gap-2">
|
| 459 |
+
<Label htmlFor="sample">Sample workload</Label>
|
| 460 |
+
<Select
|
| 461 |
+
value={sampleId}
|
| 462 |
+
onValueChange={(value) => {
|
| 463 |
+
setSampleId(value);
|
| 464 |
+
setGithubUrl("");
|
| 465 |
+
}}
|
| 466 |
+
>
|
| 467 |
+
<SelectTrigger
|
| 468 |
+
id="sample"
|
| 469 |
+
className="h-10 w-full min-w-0 bg-card [&_[data-slot=select-value]]:min-w-0 [&_[data-slot=select-value]]:truncate"
|
| 470 |
+
>
|
| 471 |
+
<SelectValue placeholder="Select sample" />
|
| 472 |
+
</SelectTrigger>
|
| 473 |
+
<SelectContent>
|
| 474 |
+
{SAMPLE_REPOS.map((sample) => (
|
| 475 |
+
<SelectItem key={sample.id} value={sample.id}>
|
| 476 |
+
{sample.name}
|
| 477 |
+
</SelectItem>
|
| 478 |
+
))}
|
| 479 |
+
</SelectContent>
|
| 480 |
+
</Select>
|
| 481 |
+
</div>
|
| 482 |
+
<div className="grid min-w-0 gap-2">
|
| 483 |
+
<Label htmlFor="github-url">Public GitHub URL</Label>
|
| 484 |
+
<Input
|
| 485 |
+
id="github-url"
|
| 486 |
+
className="h-10 bg-card font-mono text-xs sm:text-sm"
|
| 487 |
+
onChange={(event) => setGithubUrl(event.target.value)}
|
| 488 |
+
placeholder="https://github.com/org/repo"
|
| 489 |
+
value={githubUrl}
|
| 490 |
+
/>
|
| 491 |
+
</div>
|
| 492 |
+
<Button
|
| 493 |
+
className="h-10 w-full self-end md:col-span-2 xl:col-span-1 xl:w-auto"
|
| 494 |
+
disabled={isStarting || (run?.status === "running")}
|
| 495 |
+
onClick={startRun}
|
| 496 |
+
>
|
| 497 |
+
{isStarting ? (
|
| 498 |
+
<Loader2 className="size-4 animate-spin" />
|
| 499 |
+
) : run ? (
|
| 500 |
+
<RefreshCw className="size-4" />
|
| 501 |
+
) : (
|
| 502 |
+
<Play className="size-4" />
|
| 503 |
+
)}
|
| 504 |
+
{run ? "Run Again" : githubUrl.trim() ? "Scan Repo" : "Start Sample"}
|
| 505 |
+
</Button>
|
| 506 |
+
</div>
|
| 507 |
+
</section>
|
| 508 |
+
|
| 509 |
+
{error && (
|
| 510 |
+
<Alert variant="destructive">
|
| 511 |
+
<TriangleAlert className="size-4" />
|
| 512 |
+
<AlertTitle>Demo flow needs attention</AlertTitle>
|
| 513 |
+
<AlertDescription>{error}</AlertDescription>
|
| 514 |
+
</Alert>
|
| 515 |
+
)}
|
| 516 |
+
|
| 517 |
+
<section className="grid gap-5 lg:grid-cols-[1.6fr_0.9fr]">
|
| 518 |
+
<div className="grid gap-5">
|
| 519 |
+
<Card>
|
| 520 |
+
<CardHeader className="gap-3">
|
| 521 |
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
| 522 |
+
<div>
|
| 523 |
+
<CardTitle className="flex items-center gap-2 text-xl">
|
| 524 |
+
<Bot className="size-5 text-cyan-200" />
|
| 525 |
+
Agent run
|
| 526 |
+
</CardTitle>
|
| 527 |
+
<CardDescription>
|
| 528 |
+
{run
|
| 529 |
+
? `${completedStages}/${run.stages.length} stages complete`
|
| 530 |
+
: "Ready to audit the selected workload"}
|
| 531 |
+
</CardDescription>
|
| 532 |
+
</div>
|
| 533 |
+
<Badge
|
| 534 |
+
variant="outline"
|
| 535 |
+
className={
|
| 536 |
+
run?.status === "completed"
|
| 537 |
+
? "border-emerald-500/40 bg-emerald-500/10 text-emerald-100"
|
| 538 |
+
: run?.status === "running"
|
| 539 |
+
? "border-cyan-500/40 bg-cyan-500/10 text-cyan-100"
|
| 540 |
+
: "border-zinc-700 bg-zinc-900 text-zinc-300"
|
| 541 |
+
}
|
| 542 |
+
>
|
| 543 |
+
{run?.status ?? "idle"}
|
| 544 |
+
</Badge>
|
| 545 |
+
</div>
|
| 546 |
+
<Progress value={run?.progress ?? 0} />
|
| 547 |
+
</CardHeader>
|
| 548 |
+
<CardContent>
|
| 549 |
+
<div className="grid gap-3 md:grid-cols-5">
|
| 550 |
+
{(run?.stages ?? []).length > 0 ? (
|
| 551 |
+
run?.stages.map((stage) => (
|
| 552 |
+
<div
|
| 553 |
+
key={stage.id}
|
| 554 |
+
className={`rounded-lg border p-3 ${statusTone[stage.status]}`}
|
| 555 |
+
>
|
| 556 |
+
<div className="flex items-center justify-between gap-2">
|
| 557 |
+
<StageIcon stage={stage} />
|
| 558 |
+
<span className="font-mono text-[11px]">{stage.progress}%</span>
|
| 559 |
+
</div>
|
| 560 |
+
<p className="mt-3 text-sm font-medium leading-5">{stage.agent}</p>
|
| 561 |
+
<p className="mt-1 min-h-10 text-xs leading-5 text-muted-foreground">
|
| 562 |
+
{stage.title}
|
| 563 |
+
</p>
|
| 564 |
+
<p className="mt-3 font-mono text-[11px] text-muted-foreground">
|
| 565 |
+
{formatTime(stage.completedAt ?? stage.startedAt)}
|
| 566 |
+
</p>
|
| 567 |
+
</div>
|
| 568 |
+
))
|
| 569 |
+
) : (
|
| 570 |
+
<div className="col-span-full rounded-lg border border-dashed border-border p-6 text-sm text-muted-foreground">
|
| 571 |
+
Select a workload and start the audit.
|
| 572 |
+
</div>
|
| 573 |
+
)}
|
| 574 |
+
</div>
|
| 575 |
+
</CardContent>
|
| 576 |
+
</Card>
|
| 577 |
+
|
| 578 |
+
<AgentWarRoom run={run} />
|
| 579 |
+
|
| 580 |
+
<Card>
|
| 581 |
+
<CardHeader>
|
| 582 |
+
<CardTitle className="flex items-center gap-2 text-xl">
|
| 583 |
+
<ShieldCheck className="size-5 text-emerald-200" />
|
| 584 |
+
Migration findings
|
| 585 |
+
</CardTitle>
|
| 586 |
+
<CardDescription>CUDA assumptions, ROCm blockers, and recommended fixes.</CardDescription>
|
| 587 |
+
</CardHeader>
|
| 588 |
+
<CardContent>
|
| 589 |
+
<div className="overflow-hidden rounded-lg border">
|
| 590 |
+
<Table>
|
| 591 |
+
<TableHeader>
|
| 592 |
+
<TableRow>
|
| 593 |
+
<TableHead>Severity</TableHead>
|
| 594 |
+
<TableHead>Category</TableHead>
|
| 595 |
+
<TableHead>Location</TableHead>
|
| 596 |
+
<TableHead>Fix</TableHead>
|
| 597 |
+
</TableRow>
|
| 598 |
+
</TableHeader>
|
| 599 |
+
<TableBody>
|
| 600 |
+
{run?.findings.length ? (
|
| 601 |
+
run.findings.map((finding) => (
|
| 602 |
+
<TableRow key={finding.id}>
|
| 603 |
+
<TableCell>
|
| 604 |
+
<Badge variant="outline" className={severityTone[finding.severity]}>
|
| 605 |
+
{finding.severity}
|
| 606 |
+
</Badge>
|
| 607 |
+
</TableCell>
|
| 608 |
+
<TableCell className="font-medium">{finding.category}</TableCell>
|
| 609 |
+
<TableCell className="font-mono text-xs text-muted-foreground">
|
| 610 |
+
{finding.file}:{finding.line}
|
| 611 |
+
</TableCell>
|
| 612 |
+
<TableCell className="max-w-md text-sm text-muted-foreground">
|
| 613 |
+
{finding.recommendedFix}
|
| 614 |
+
</TableCell>
|
| 615 |
+
</TableRow>
|
| 616 |
+
))
|
| 617 |
+
) : (
|
| 618 |
+
<TableRow>
|
| 619 |
+
<TableCell colSpan={4} className="h-24 text-center text-muted-foreground">
|
| 620 |
+
Findings appear after the Repo Doctor stage starts.
|
| 621 |
+
</TableCell>
|
| 622 |
+
</TableRow>
|
| 623 |
+
)}
|
| 624 |
+
</TableBody>
|
| 625 |
+
</Table>
|
| 626 |
+
</div>
|
| 627 |
+
</CardContent>
|
| 628 |
+
</Card>
|
| 629 |
+
|
| 630 |
+
<Card>
|
| 631 |
+
<CardHeader>
|
| 632 |
+
<CardTitle className="flex items-center gap-2 text-xl">
|
| 633 |
+
<FileCode2 className="size-5 text-amber-200" />
|
| 634 |
+
Evidence panels
|
| 635 |
+
</CardTitle>
|
| 636 |
+
<CardDescription>Patch previews, terminal logs, and final report output.</CardDescription>
|
| 637 |
+
</CardHeader>
|
| 638 |
+
<CardContent>
|
| 639 |
+
<div className="w-full">
|
| 640 |
+
<div
|
| 641 |
+
aria-label="Evidence panels"
|
| 642 |
+
className="grid w-full grid-cols-3 rounded-lg bg-muted p-1"
|
| 643 |
+
role="tablist"
|
| 644 |
+
>
|
| 645 |
+
{(["patches", "logs", "report"] as const).map((panel) => (
|
| 646 |
+
<Button
|
| 647 |
+
aria-selected={activePanel === panel}
|
| 648 |
+
className="h-8 capitalize"
|
| 649 |
+
key={panel}
|
| 650 |
+
onClick={() => setActivePanel(panel)}
|
| 651 |
+
role="tab"
|
| 652 |
+
type="button"
|
| 653 |
+
variant={activePanel === panel ? "secondary" : "ghost"}
|
| 654 |
+
>
|
| 655 |
+
{panel}
|
| 656 |
+
</Button>
|
| 657 |
+
))}
|
| 658 |
+
</div>
|
| 659 |
+
{activePanel === "patches" && (
|
| 660 |
+
<div className="mt-4 space-y-4" role="tabpanel">
|
| 661 |
+
{run?.patches.length ? (
|
| 662 |
+
run.patches.map((patch) => (
|
| 663 |
+
<div key={patch.id} className="space-y-2 rounded-lg border border-border p-3">
|
| 664 |
+
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
| 665 |
+
<p className="text-sm font-medium">{patch.file}</p>
|
| 666 |
+
<p className="max-w-xl text-xs text-muted-foreground">{patch.rationale}</p>
|
| 667 |
+
</div>
|
| 668 |
+
<CodeBlock code={patch.diff} language="diff" showLineNumbers>
|
| 669 |
+
<CodeBlockHeader>
|
| 670 |
+
<CodeBlockTitle>
|
| 671 |
+
<CodeBlockFilename>{patch.file}</CodeBlockFilename>
|
| 672 |
+
</CodeBlockTitle>
|
| 673 |
+
<CodeBlockCopyButton />
|
| 674 |
+
</CodeBlockHeader>
|
| 675 |
+
</CodeBlock>
|
| 676 |
+
</div>
|
| 677 |
+
))
|
| 678 |
+
) : (
|
| 679 |
+
<div className="rounded-lg border border-dashed border-border p-6 text-sm text-muted-foreground">
|
| 680 |
+
Patch previews unlock after the Migration Planner stage.
|
| 681 |
+
</div>
|
| 682 |
+
)}
|
| 683 |
+
</div>
|
| 684 |
+
)}
|
| 685 |
+
{activePanel === "logs" && (
|
| 686 |
+
<div className="mt-4" role="tabpanel">
|
| 687 |
+
<Terminal
|
| 688 |
+
output={logOutput || "waiting for run output..."}
|
| 689 |
+
isStreaming={run?.status === "running"}
|
| 690 |
+
/>
|
| 691 |
+
</div>
|
| 692 |
+
)}
|
| 693 |
+
{activePanel === "report" && (
|
| 694 |
+
<div className="mt-4" role="tabpanel">
|
| 695 |
+
<div className="min-h-80 rounded-lg border bg-card p-4">
|
| 696 |
+
{isGeneratingReport ? (
|
| 697 |
+
<div className="flex h-64 items-center justify-center gap-3 text-sm text-muted-foreground">
|
| 698 |
+
<Loader2 className="size-4 animate-spin" />
|
| 699 |
+
Report Agent is calling AMD-hosted Qwen or fallback output.
|
| 700 |
+
</div>
|
| 701 |
+
) : report?.report ? (
|
| 702 |
+
<MessageResponse>{report.report}</MessageResponse>
|
| 703 |
+
) : (
|
| 704 |
+
<div className="flex h-64 items-center justify-center text-sm text-muted-foreground">
|
| 705 |
+
The final report appears after all agent stages complete.
|
| 706 |
+
</div>
|
| 707 |
+
)}
|
| 708 |
+
</div>
|
| 709 |
+
</div>
|
| 710 |
+
)}
|
| 711 |
+
</div>
|
| 712 |
+
</CardContent>
|
| 713 |
+
</Card>
|
| 714 |
+
</div>
|
| 715 |
+
|
| 716 |
+
<aside className="grid content-start gap-5">
|
| 717 |
+
<Card>
|
| 718 |
+
<CardHeader>
|
| 719 |
+
<CardTitle className="flex items-center gap-2 text-lg">
|
| 720 |
+
<GitBranch className="size-5 text-zinc-300" />
|
| 721 |
+
Workload
|
| 722 |
+
</CardTitle>
|
| 723 |
+
<CardDescription>
|
| 724 |
+
{run?.target.type === "github" ? run.target.note : selectedSample.stack}
|
| 725 |
+
</CardDescription>
|
| 726 |
+
</CardHeader>
|
| 727 |
+
<CardContent className="space-y-4">
|
| 728 |
+
<div className="grid gap-2">
|
| 729 |
+
<Label htmlFor="repo-url">Repository URL</Label>
|
| 730 |
+
<Input id="repo-url" value={activeRepoUrl} readOnly className="font-mono text-xs" />
|
| 731 |
+
</div>
|
| 732 |
+
<Separator />
|
| 733 |
+
<div className="space-y-3 text-sm">
|
| 734 |
+
<div>
|
| 735 |
+
<p className="text-muted-foreground">Scan mode</p>
|
| 736 |
+
<p className="font-medium">
|
| 737 |
+
{run?.target.type === "github" ? "Live public GitHub scan" : "Curated sample fixture"}
|
| 738 |
+
</p>
|
| 739 |
+
</div>
|
| 740 |
+
<div>
|
| 741 |
+
<p className="text-muted-foreground">Target</p>
|
| 742 |
+
<p className="font-medium">{run?.target.label ?? selectedSample.name}</p>
|
| 743 |
+
</div>
|
| 744 |
+
<div>
|
| 745 |
+
<p className="text-muted-foreground">Files scanned</p>
|
| 746 |
+
<p className="font-mono text-lg">{run?.target.scannedFiles ?? 0}</p>
|
| 747 |
+
</div>
|
| 748 |
+
<div>
|
| 749 |
+
<p className="text-muted-foreground">Risk</p>
|
| 750 |
+
<p className="leading-6">{run?.target.note ?? selectedSample.risk}</p>
|
| 751 |
+
</div>
|
| 752 |
+
</div>
|
| 753 |
+
</CardContent>
|
| 754 |
+
</Card>
|
| 755 |
+
|
| 756 |
+
<Card>
|
| 757 |
+
<CardHeader>
|
| 758 |
+
<CardTitle className="flex items-center gap-2 text-lg">
|
| 759 |
+
<Cpu className="size-5 text-emerald-200" />
|
| 760 |
+
GPU model status
|
| 761 |
+
</CardTitle>
|
| 762 |
+
<CardDescription>Qwen endpoint used by the Report Agent.</CardDescription>
|
| 763 |
+
</CardHeader>
|
| 764 |
+
<CardContent className="space-y-4">
|
| 765 |
+
<Badge
|
| 766 |
+
variant="outline"
|
| 767 |
+
className={
|
| 768 |
+
modelStatus?.status === "connected"
|
| 769 |
+
? "border-emerald-500/40 bg-emerald-500/10 text-emerald-100"
|
| 770 |
+
: modelStatus?.status === "not-configured"
|
| 771 |
+
? "border-cyan-500/40 bg-cyan-500/10 text-cyan-100"
|
| 772 |
+
: "border-amber-500/40 bg-amber-500/10 text-amber-100"
|
| 773 |
+
}
|
| 774 |
+
>
|
| 775 |
+
{modelStatus?.label ?? "AMD GPU Model: Demo fallback"}
|
| 776 |
+
</Badge>
|
| 777 |
+
<div className="space-y-2 text-sm">
|
| 778 |
+
<div className="flex items-start gap-2">
|
| 779 |
+
<Sparkles className="mt-0.5 size-4 text-amber-200" />
|
| 780 |
+
<span>{modelStatus?.model ?? "Qwen/Qwen3-Coder-Next"}</span>
|
| 781 |
+
</div>
|
| 782 |
+
<div className="flex items-start gap-2">
|
| 783 |
+
<Zap className="mt-0.5 size-4 text-cyan-200" />
|
| 784 |
+
<span className="break-all text-muted-foreground">
|
| 785 |
+
{modelStatus?.endpoint ?? "Set AMD_QWEN_BASE_URL"}
|
| 786 |
+
</span>
|
| 787 |
+
</div>
|
| 788 |
+
<p className="leading-6 text-muted-foreground">
|
| 789 |
+
{modelStatus?.detail ??
|
| 790 |
+
"The MVP stays demo-safe until an AMD ROCm/vLLM endpoint is available."}
|
| 791 |
+
</p>
|
| 792 |
+
</div>
|
| 793 |
+
</CardContent>
|
| 794 |
+
</Card>
|
| 795 |
+
|
| 796 |
+
<Card>
|
| 797 |
+
<CardHeader>
|
| 798 |
+
<CardTitle className="flex items-center gap-2 text-lg">
|
| 799 |
+
<BrainCircuit className="size-5 text-rose-200" />
|
| 800 |
+
Long-context memory
|
| 801 |
+
</CardTitle>
|
| 802 |
+
<CardDescription>Synap context used by the Report Agent.</CardDescription>
|
| 803 |
+
</CardHeader>
|
| 804 |
+
<CardContent className="space-y-4">
|
| 805 |
+
<Badge
|
| 806 |
+
variant="outline"
|
| 807 |
+
className={memoryStatusTone[memoryStatus?.status ?? "fallback"]}
|
| 808 |
+
>
|
| 809 |
+
{memoryStatus?.label ?? "Synap Memory: Local fallback"}
|
| 810 |
+
</Badge>
|
| 811 |
+
<div className="grid gap-3 text-sm">
|
| 812 |
+
<div>
|
| 813 |
+
<p className="text-muted-foreground">Provider</p>
|
| 814 |
+
<p className="font-medium">{memoryStatus?.provider ?? "local"}</p>
|
| 815 |
+
</div>
|
| 816 |
+
<div>
|
| 817 |
+
<p className="text-muted-foreground">Conversation</p>
|
| 818 |
+
<p className="break-all font-mono text-xs">
|
| 819 |
+
{memoryStatus?.conversationId ?? "created after run starts"}
|
| 820 |
+
</p>
|
| 821 |
+
</div>
|
| 822 |
+
<div className="grid grid-cols-2 gap-3">
|
| 823 |
+
<div>
|
| 824 |
+
<p className="font-mono text-lg">{memoryStatus?.storedItems ?? 0}</p>
|
| 825 |
+
<p className="text-xs text-muted-foreground">stored</p>
|
| 826 |
+
</div>
|
| 827 |
+
<div>
|
| 828 |
+
<p className="font-mono text-lg">{memoryStatus?.recalledItems ?? 0}</p>
|
| 829 |
+
<p className="text-xs text-muted-foreground">recalled</p>
|
| 830 |
+
</div>
|
| 831 |
+
</div>
|
| 832 |
+
<p className="leading-6 text-muted-foreground">
|
| 833 |
+
{memoryStatus?.detail ??
|
| 834 |
+
"Set SYNAP_API_KEY to persist agent memory across sessions."}
|
| 835 |
+
</p>
|
| 836 |
+
</div>
|
| 837 |
+
</CardContent>
|
| 838 |
+
</Card>
|
| 839 |
+
|
| 840 |
+
<Card>
|
| 841 |
+
<CardHeader>
|
| 842 |
+
<CardTitle className="flex items-center gap-2 text-lg">
|
| 843 |
+
<Gauge className="size-5 text-cyan-200" />
|
| 844 |
+
Benchmark profile
|
| 845 |
+
</CardTitle>
|
| 846 |
+
<CardDescription>Demo metrics for the submission walkthrough.</CardDescription>
|
| 847 |
+
</CardHeader>
|
| 848 |
+
<CardContent className="space-y-3">
|
| 849 |
+
{(run?.benchmarks ?? []).map((benchmark) => (
|
| 850 |
+
<div key={benchmark.label} className="rounded-lg border border-border p-3">
|
| 851 |
+
<div className="flex items-start justify-between gap-3">
|
| 852 |
+
<p className="font-medium">{benchmark.label}</p>
|
| 853 |
+
<Badge variant="outline">{benchmark.backend}</Badge>
|
| 854 |
+
</div>
|
| 855 |
+
<div className="mt-3 grid grid-cols-3 gap-2 text-sm">
|
| 856 |
+
<div>
|
| 857 |
+
<p className="font-mono text-lg">{benchmark.tokensPerSecond}</p>
|
| 858 |
+
<p className="text-xs text-muted-foreground">tok/s</p>
|
| 859 |
+
</div>
|
| 860 |
+
<div>
|
| 861 |
+
<p className="font-mono text-lg">{benchmark.p95LatencyMs}</p>
|
| 862 |
+
<p className="text-xs text-muted-foreground">p95 ms</p>
|
| 863 |
+
</div>
|
| 864 |
+
<div>
|
| 865 |
+
<p className="font-mono text-lg">{benchmark.memoryGb}</p>
|
| 866 |
+
<p className="text-xs text-muted-foreground">GB</p>
|
| 867 |
+
</div>
|
| 868 |
+
</div>
|
| 869 |
+
<p className="mt-3 text-xs leading-5 text-muted-foreground">{benchmark.costNote}</p>
|
| 870 |
+
</div>
|
| 871 |
+
))}
|
| 872 |
+
{!run && (
|
| 873 |
+
<div className="rounded-lg border border-dashed border-border p-6 text-sm text-muted-foreground">
|
| 874 |
+
Benchmark cards appear during the run.
|
| 875 |
+
</div>
|
| 876 |
+
)}
|
| 877 |
+
</CardContent>
|
| 878 |
+
</Card>
|
| 879 |
+
|
| 880 |
+
<Card>
|
| 881 |
+
<CardHeader>
|
| 882 |
+
<CardTitle className="flex items-center gap-2 text-lg">
|
| 883 |
+
<Boxes className="size-5 text-amber-200" />
|
| 884 |
+
Submission proof
|
| 885 |
+
</CardTitle>
|
| 886 |
+
</CardHeader>
|
| 887 |
+
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
| 888 |
+
<div className="flex items-start gap-2">
|
| 889 |
+
<BadgeCheck className="mt-0.5 size-4 text-emerald-200" />
|
| 890 |
+
<span>Track 1 agentic workflow with five specialized agents.</span>
|
| 891 |
+
</div>
|
| 892 |
+
<div className="flex items-start gap-2">
|
| 893 |
+
<BadgeCheck className="mt-0.5 size-4 text-emerald-200" />
|
| 894 |
+
<span>AMD GPU story through ROCm/vLLM Qwen model serving.</span>
|
| 895 |
+
</div>
|
| 896 |
+
<div className="flex items-start gap-2">
|
| 897 |
+
<TerminalSquare className="mt-0.5 size-4 text-cyan-200" />
|
| 898 |
+
<span>Demo remains reliable without credentials, then upgrades with live endpoint logs.</span>
|
| 899 |
+
</div>
|
| 900 |
+
</CardContent>
|
| 901 |
+
</Card>
|
| 902 |
+
</aside>
|
| 903 |
+
</section>
|
| 904 |
+
</div>
|
| 905 |
+
</main>
|
| 906 |
+
);
|
| 907 |
+
}
|
src/components/ui/alert.tsx
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
|
| 6 |
+
const alertVariants = cva(
|
| 7 |
+
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
|
| 8 |
+
{
|
| 9 |
+
variants: {
|
| 10 |
+
variant: {
|
| 11 |
+
default: "bg-card text-card-foreground",
|
| 12 |
+
destructive:
|
| 13 |
+
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
|
| 14 |
+
},
|
| 15 |
+
},
|
| 16 |
+
defaultVariants: {
|
| 17 |
+
variant: "default",
|
| 18 |
+
},
|
| 19 |
+
}
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
function Alert({
|
| 23 |
+
className,
|
| 24 |
+
variant,
|
| 25 |
+
...props
|
| 26 |
+
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
| 27 |
+
return (
|
| 28 |
+
<div
|
| 29 |
+
data-slot="alert"
|
| 30 |
+
role="alert"
|
| 31 |
+
className={cn(alertVariants({ variant }), className)}
|
| 32 |
+
{...props}
|
| 33 |
+
/>
|
| 34 |
+
)
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
| 38 |
+
return (
|
| 39 |
+
<div
|
| 40 |
+
data-slot="alert-title"
|
| 41 |
+
className={cn(
|
| 42 |
+
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
|
| 43 |
+
className
|
| 44 |
+
)}
|
| 45 |
+
{...props}
|
| 46 |
+
/>
|
| 47 |
+
)
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
function AlertDescription({
|
| 51 |
+
className,
|
| 52 |
+
...props
|
| 53 |
+
}: React.ComponentProps<"div">) {
|
| 54 |
+
return (
|
| 55 |
+
<div
|
| 56 |
+
data-slot="alert-description"
|
| 57 |
+
className={cn(
|
| 58 |
+
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
| 59 |
+
className
|
| 60 |
+
)}
|
| 61 |
+
{...props}
|
| 62 |
+
/>
|
| 63 |
+
)
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
|
| 67 |
+
return (
|
| 68 |
+
<div
|
| 69 |
+
data-slot="alert-action"
|
| 70 |
+
className={cn("absolute top-2 right-2", className)}
|
| 71 |
+
{...props}
|
| 72 |
+
/>
|
| 73 |
+
)
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
export { Alert, AlertTitle, AlertDescription, AlertAction }
|
src/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 }
|
src/components/ui/button-group.tsx
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
| 2 |
+
import { Slot } from "radix-ui"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
import { Separator } from "@/components/ui/separator"
|
| 6 |
+
|
| 7 |
+
const buttonGroupVariants = cva(
|
| 8 |
+
"group/button-group flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
| 9 |
+
{
|
| 10 |
+
variants: {
|
| 11 |
+
orientation: {
|
| 12 |
+
horizontal:
|
| 13 |
+
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg!",
|
| 14 |
+
vertical:
|
| 15 |
+
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg!",
|
| 16 |
+
},
|
| 17 |
+
},
|
| 18 |
+
defaultVariants: {
|
| 19 |
+
orientation: "horizontal",
|
| 20 |
+
},
|
| 21 |
+
}
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
function ButtonGroup({
|
| 25 |
+
className,
|
| 26 |
+
orientation,
|
| 27 |
+
...props
|
| 28 |
+
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
| 29 |
+
return (
|
| 30 |
+
<div
|
| 31 |
+
role="group"
|
| 32 |
+
data-slot="button-group"
|
| 33 |
+
data-orientation={orientation}
|
| 34 |
+
className={cn(buttonGroupVariants({ orientation }), className)}
|
| 35 |
+
{...props}
|
| 36 |
+
/>
|
| 37 |
+
)
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
function ButtonGroupText({
|
| 41 |
+
className,
|
| 42 |
+
asChild = false,
|
| 43 |
+
...props
|
| 44 |
+
}: React.ComponentProps<"div"> & {
|
| 45 |
+
asChild?: boolean
|
| 46 |
+
}) {
|
| 47 |
+
const Comp = asChild ? Slot.Root : "div"
|
| 48 |
+
|
| 49 |
+
return (
|
| 50 |
+
<Comp
|
| 51 |
+
className={cn(
|
| 52 |
+
"flex items-center gap-2 rounded-lg border bg-muted px-2.5 text-sm font-medium [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
| 53 |
+
className
|
| 54 |
+
)}
|
| 55 |
+
{...props}
|
| 56 |
+
/>
|
| 57 |
+
)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function ButtonGroupSeparator({
|
| 61 |
+
className,
|
| 62 |
+
orientation = "vertical",
|
| 63 |
+
...props
|
| 64 |
+
}: React.ComponentProps<typeof Separator>) {
|
| 65 |
+
return (
|
| 66 |
+
<Separator
|
| 67 |
+
data-slot="button-group-separator"
|
| 68 |
+
orientation={orientation}
|
| 69 |
+
className={cn(
|
| 70 |
+
"relative self-stretch bg-input data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto",
|
| 71 |
+
className
|
| 72 |
+
)}
|
| 73 |
+
{...props}
|
| 74 |
+
/>
|
| 75 |
+
)
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
export {
|
| 79 |
+
ButtonGroup,
|
| 80 |
+
ButtonGroupSeparator,
|
| 81 |
+
ButtonGroupText,
|
| 82 |
+
buttonGroupVariants,
|
| 83 |
+
}
|
src/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 [a]: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-secondary/80 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 }
|
src/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 |
+
}
|
src/components/ui/collapsible.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
| 4 |
+
|
| 5 |
+
function Collapsible({
|
| 6 |
+
...props
|
| 7 |
+
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
| 8 |
+
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
function CollapsibleTrigger({
|
| 12 |
+
...props
|
| 13 |
+
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
| 14 |
+
return (
|
| 15 |
+
<CollapsiblePrimitive.CollapsibleTrigger
|
| 16 |
+
data-slot="collapsible-trigger"
|
| 17 |
+
{...props}
|
| 18 |
+
/>
|
| 19 |
+
)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
function CollapsibleContent({
|
| 23 |
+
...props
|
| 24 |
+
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
| 25 |
+
return (
|
| 26 |
+
<CollapsiblePrimitive.CollapsibleContent
|
| 27 |
+
data-slot="collapsible-content"
|
| 28 |
+
{...props}
|
| 29 |
+
/>
|
| 30 |
+
)
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
src/components/ui/input.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
| 6 |
+
return (
|
| 7 |
+
<input
|
| 8 |
+
type={type}
|
| 9 |
+
data-slot="input"
|
| 10 |
+
className={cn(
|
| 11 |
+
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
| 12 |
+
className
|
| 13 |
+
)}
|
| 14 |
+
{...props}
|
| 15 |
+
/>
|
| 16 |
+
)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export { Input }
|
src/components/ui/label.tsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import { Label as LabelPrimitive } from "radix-ui"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
function Label({
|
| 9 |
+
className,
|
| 10 |
+
...props
|
| 11 |
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
| 12 |
+
return (
|
| 13 |
+
<LabelPrimitive.Root
|
| 14 |
+
data-slot="label"
|
| 15 |
+
className={cn(
|
| 16 |
+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
| 17 |
+
className
|
| 18 |
+
)}
|
| 19 |
+
{...props}
|
| 20 |
+
/>
|
| 21 |
+
)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export { Label }
|
src/components/ui/progress.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import { Progress as ProgressPrimitive } from "radix-ui"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
function Progress({
|
| 9 |
+
className,
|
| 10 |
+
value,
|
| 11 |
+
...props
|
| 12 |
+
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
| 13 |
+
return (
|
| 14 |
+
<ProgressPrimitive.Root
|
| 15 |
+
data-slot="progress"
|
| 16 |
+
className={cn(
|
| 17 |
+
"relative flex h-1 w-full items-center overflow-x-hidden rounded-full bg-muted",
|
| 18 |
+
className
|
| 19 |
+
)}
|
| 20 |
+
{...props}
|
| 21 |
+
>
|
| 22 |
+
<ProgressPrimitive.Indicator
|
| 23 |
+
data-slot="progress-indicator"
|
| 24 |
+
className="size-full flex-1 bg-primary transition-all"
|
| 25 |
+
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
| 26 |
+
/>
|
| 27 |
+
</ProgressPrimitive.Root>
|
| 28 |
+
)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export { Progress }
|
src/components/ui/scroll-area.tsx
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
function ScrollArea({
|
| 9 |
+
className,
|
| 10 |
+
children,
|
| 11 |
+
...props
|
| 12 |
+
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
| 13 |
+
return (
|
| 14 |
+
<ScrollAreaPrimitive.Root
|
| 15 |
+
data-slot="scroll-area"
|
| 16 |
+
className={cn("relative", className)}
|
| 17 |
+
{...props}
|
| 18 |
+
>
|
| 19 |
+
<ScrollAreaPrimitive.Viewport
|
| 20 |
+
data-slot="scroll-area-viewport"
|
| 21 |
+
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
|
| 22 |
+
>
|
| 23 |
+
{children}
|
| 24 |
+
</ScrollAreaPrimitive.Viewport>
|
| 25 |
+
<ScrollBar />
|
| 26 |
+
<ScrollAreaPrimitive.Corner />
|
| 27 |
+
</ScrollAreaPrimitive.Root>
|
| 28 |
+
)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function ScrollBar({
|
| 32 |
+
className,
|
| 33 |
+
orientation = "vertical",
|
| 34 |
+
...props
|
| 35 |
+
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
| 36 |
+
return (
|
| 37 |
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
| 38 |
+
data-slot="scroll-area-scrollbar"
|
| 39 |
+
data-orientation={orientation}
|
| 40 |
+
orientation={orientation}
|
| 41 |
+
className={cn(
|
| 42 |
+
"flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
|
| 43 |
+
className
|
| 44 |
+
)}
|
| 45 |
+
{...props}
|
| 46 |
+
>
|
| 47 |
+
<ScrollAreaPrimitive.ScrollAreaThumb
|
| 48 |
+
data-slot="scroll-area-thumb"
|
| 49 |
+
className="relative flex-1 rounded-full bg-border"
|
| 50 |
+
/>
|
| 51 |
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
| 52 |
+
)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
export { ScrollArea, ScrollBar }
|
src/components/ui/select.tsx
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import { Select as SelectPrimitive } from "radix-ui"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
|
| 8 |
+
|
| 9 |
+
function Select({
|
| 10 |
+
...props
|
| 11 |
+
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
| 12 |
+
return <SelectPrimitive.Root data-slot="select" {...props} />
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
function SelectGroup({
|
| 16 |
+
className,
|
| 17 |
+
...props
|
| 18 |
+
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
| 19 |
+
return (
|
| 20 |
+
<SelectPrimitive.Group
|
| 21 |
+
data-slot="select-group"
|
| 22 |
+
className={cn("scroll-my-1 p-1", className)}
|
| 23 |
+
{...props}
|
| 24 |
+
/>
|
| 25 |
+
)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
function SelectValue({
|
| 29 |
+
...props
|
| 30 |
+
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
| 31 |
+
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function SelectTrigger({
|
| 35 |
+
className,
|
| 36 |
+
size = "default",
|
| 37 |
+
children,
|
| 38 |
+
...props
|
| 39 |
+
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
| 40 |
+
size?: "sm" | "default"
|
| 41 |
+
}) {
|
| 42 |
+
return (
|
| 43 |
+
<SelectPrimitive.Trigger
|
| 44 |
+
data-slot="select-trigger"
|
| 45 |
+
data-size={size}
|
| 46 |
+
className={cn(
|
| 47 |
+
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 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",
|
| 48 |
+
className
|
| 49 |
+
)}
|
| 50 |
+
{...props}
|
| 51 |
+
>
|
| 52 |
+
{children}
|
| 53 |
+
<SelectPrimitive.Icon asChild>
|
| 54 |
+
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
|
| 55 |
+
</SelectPrimitive.Icon>
|
| 56 |
+
</SelectPrimitive.Trigger>
|
| 57 |
+
)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function SelectContent({
|
| 61 |
+
className,
|
| 62 |
+
children,
|
| 63 |
+
position = "item-aligned",
|
| 64 |
+
align = "center",
|
| 65 |
+
...props
|
| 66 |
+
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
| 67 |
+
return (
|
| 68 |
+
<SelectPrimitive.Portal>
|
| 69 |
+
<SelectPrimitive.Content
|
| 70 |
+
data-slot="select-content"
|
| 71 |
+
data-align-trigger={position === "item-aligned"}
|
| 72 |
+
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none 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", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
|
| 73 |
+
position={position}
|
| 74 |
+
align={align}
|
| 75 |
+
{...props}
|
| 76 |
+
>
|
| 77 |
+
<SelectScrollUpButton />
|
| 78 |
+
<SelectPrimitive.Viewport
|
| 79 |
+
data-position={position}
|
| 80 |
+
className={cn(
|
| 81 |
+
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
|
| 82 |
+
position === "popper" && ""
|
| 83 |
+
)}
|
| 84 |
+
>
|
| 85 |
+
{children}
|
| 86 |
+
</SelectPrimitive.Viewport>
|
| 87 |
+
<SelectScrollDownButton />
|
| 88 |
+
</SelectPrimitive.Content>
|
| 89 |
+
</SelectPrimitive.Portal>
|
| 90 |
+
)
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
function SelectLabel({
|
| 94 |
+
className,
|
| 95 |
+
...props
|
| 96 |
+
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
| 97 |
+
return (
|
| 98 |
+
<SelectPrimitive.Label
|
| 99 |
+
data-slot="select-label"
|
| 100 |
+
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
|
| 101 |
+
{...props}
|
| 102 |
+
/>
|
| 103 |
+
)
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
function SelectItem({
|
| 107 |
+
className,
|
| 108 |
+
children,
|
| 109 |
+
...props
|
| 110 |
+
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
| 111 |
+
return (
|
| 112 |
+
<SelectPrimitive.Item
|
| 113 |
+
data-slot="select-item"
|
| 114 |
+
className={cn(
|
| 115 |
+
"relative flex w-full 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 not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
| 116 |
+
className
|
| 117 |
+
)}
|
| 118 |
+
{...props}
|
| 119 |
+
>
|
| 120 |
+
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
|
| 121 |
+
<SelectPrimitive.ItemIndicator>
|
| 122 |
+
<CheckIcon className="pointer-events-none" />
|
| 123 |
+
</SelectPrimitive.ItemIndicator>
|
| 124 |
+
</span>
|
| 125 |
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
| 126 |
+
</SelectPrimitive.Item>
|
| 127 |
+
)
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
function SelectSeparator({
|
| 131 |
+
className,
|
| 132 |
+
...props
|
| 133 |
+
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
| 134 |
+
return (
|
| 135 |
+
<SelectPrimitive.Separator
|
| 136 |
+
data-slot="select-separator"
|
| 137 |
+
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
| 138 |
+
{...props}
|
| 139 |
+
/>
|
| 140 |
+
)
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
function SelectScrollUpButton({
|
| 144 |
+
className,
|
| 145 |
+
...props
|
| 146 |
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
| 147 |
+
return (
|
| 148 |
+
<SelectPrimitive.ScrollUpButton
|
| 149 |
+
data-slot="select-scroll-up-button"
|
| 150 |
+
className={cn(
|
| 151 |
+
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
| 152 |
+
className
|
| 153 |
+
)}
|
| 154 |
+
{...props}
|
| 155 |
+
>
|
| 156 |
+
<ChevronUpIcon
|
| 157 |
+
/>
|
| 158 |
+
</SelectPrimitive.ScrollUpButton>
|
| 159 |
+
)
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
function SelectScrollDownButton({
|
| 163 |
+
className,
|
| 164 |
+
...props
|
| 165 |
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
| 166 |
+
return (
|
| 167 |
+
<SelectPrimitive.ScrollDownButton
|
| 168 |
+
data-slot="select-scroll-down-button"
|
| 169 |
+
className={cn(
|
| 170 |
+
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
| 171 |
+
className
|
| 172 |
+
)}
|
| 173 |
+
{...props}
|
| 174 |
+
>
|
| 175 |
+
<ChevronDownIcon
|
| 176 |
+
/>
|
| 177 |
+
</SelectPrimitive.ScrollDownButton>
|
| 178 |
+
)
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
export {
|
| 182 |
+
Select,
|
| 183 |
+
SelectContent,
|
| 184 |
+
SelectGroup,
|
| 185 |
+
SelectItem,
|
| 186 |
+
SelectLabel,
|
| 187 |
+
SelectScrollDownButton,
|
| 188 |
+
SelectScrollUpButton,
|
| 189 |
+
SelectSeparator,
|
| 190 |
+
SelectTrigger,
|
| 191 |
+
SelectValue,
|
| 192 |
+
}
|
src/components/ui/separator.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import { Separator as SeparatorPrimitive } from "radix-ui"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
function Separator({
|
| 9 |
+
className,
|
| 10 |
+
orientation = "horizontal",
|
| 11 |
+
decorative = true,
|
| 12 |
+
...props
|
| 13 |
+
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
| 14 |
+
return (
|
| 15 |
+
<SeparatorPrimitive.Root
|
| 16 |
+
data-slot="separator"
|
| 17 |
+
decorative={decorative}
|
| 18 |
+
orientation={orientation}
|
| 19 |
+
className={cn(
|
| 20 |
+
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
| 21 |
+
className
|
| 22 |
+
)}
|
| 23 |
+
{...props}
|
| 24 |
+
/>
|
| 25 |
+
)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export { Separator }
|
src/components/ui/skeleton.tsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { cn } from "@/lib/utils"
|
| 2 |
+
|
| 3 |
+
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
| 4 |
+
return (
|
| 5 |
+
<div
|
| 6 |
+
data-slot="skeleton"
|
| 7 |
+
className={cn("animate-pulse rounded-md bg-muted", className)}
|
| 8 |
+
{...props}
|
| 9 |
+
/>
|
| 10 |
+
)
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export { Skeleton }
|
src/components/ui/table.tsx
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
| 8 |
+
return (
|
| 9 |
+
<div
|
| 10 |
+
data-slot="table-container"
|
| 11 |
+
className="relative w-full overflow-x-auto"
|
| 12 |
+
>
|
| 13 |
+
<table
|
| 14 |
+
data-slot="table"
|
| 15 |
+
className={cn("w-full caption-bottom text-sm", className)}
|
| 16 |
+
{...props}
|
| 17 |
+
/>
|
| 18 |
+
</div>
|
| 19 |
+
)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
| 23 |
+
return (
|
| 24 |
+
<thead
|
| 25 |
+
data-slot="table-header"
|
| 26 |
+
className={cn("[&_tr]:border-b", className)}
|
| 27 |
+
{...props}
|
| 28 |
+
/>
|
| 29 |
+
)
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
| 33 |
+
return (
|
| 34 |
+
<tbody
|
| 35 |
+
data-slot="table-body"
|
| 36 |
+
className={cn("[&_tr:last-child]:border-0", className)}
|
| 37 |
+
{...props}
|
| 38 |
+
/>
|
| 39 |
+
)
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
| 43 |
+
return (
|
| 44 |
+
<tfoot
|
| 45 |
+
data-slot="table-footer"
|
| 46 |
+
className={cn(
|
| 47 |
+
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
| 48 |
+
className
|
| 49 |
+
)}
|
| 50 |
+
{...props}
|
| 51 |
+
/>
|
| 52 |
+
)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
| 56 |
+
return (
|
| 57 |
+
<tr
|
| 58 |
+
data-slot="table-row"
|
| 59 |
+
className={cn(
|
| 60 |
+
"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
|
| 61 |
+
className
|
| 62 |
+
)}
|
| 63 |
+
{...props}
|
| 64 |
+
/>
|
| 65 |
+
)
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
| 69 |
+
return (
|
| 70 |
+
<th
|
| 71 |
+
data-slot="table-head"
|
| 72 |
+
className={cn(
|
| 73 |
+
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
|
| 74 |
+
className
|
| 75 |
+
)}
|
| 76 |
+
{...props}
|
| 77 |
+
/>
|
| 78 |
+
)
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
| 82 |
+
return (
|
| 83 |
+
<td
|
| 84 |
+
data-slot="table-cell"
|
| 85 |
+
className={cn(
|
| 86 |
+
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
| 87 |
+
className
|
| 88 |
+
)}
|
| 89 |
+
{...props}
|
| 90 |
+
/>
|
| 91 |
+
)
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
function TableCaption({
|
| 95 |
+
className,
|
| 96 |
+
...props
|
| 97 |
+
}: React.ComponentProps<"caption">) {
|
| 98 |
+
return (
|
| 99 |
+
<caption
|
| 100 |
+
data-slot="table-caption"
|
| 101 |
+
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
| 102 |
+
{...props}
|
| 103 |
+
/>
|
| 104 |
+
)
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
export {
|
| 108 |
+
Table,
|
| 109 |
+
TableHeader,
|
| 110 |
+
TableBody,
|
| 111 |
+
TableFooter,
|
| 112 |
+
TableHead,
|
| 113 |
+
TableRow,
|
| 114 |
+
TableCell,
|
| 115 |
+
TableCaption,
|
| 116 |
+
}
|
src/components/ui/tabs.tsx
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
| 5 |
+
import { Tabs as TabsPrimitive } from "radix-ui"
|
| 6 |
+
|
| 7 |
+
import { cn } from "@/lib/utils"
|
| 8 |
+
|
| 9 |
+
function Tabs({
|
| 10 |
+
className,
|
| 11 |
+
orientation = "horizontal",
|
| 12 |
+
...props
|
| 13 |
+
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
| 14 |
+
return (
|
| 15 |
+
<TabsPrimitive.Root
|
| 16 |
+
data-slot="tabs"
|
| 17 |
+
data-orientation={orientation}
|
| 18 |
+
className={cn(
|
| 19 |
+
"group/tabs flex gap-2 data-horizontal:flex-col",
|
| 20 |
+
className
|
| 21 |
+
)}
|
| 22 |
+
{...props}
|
| 23 |
+
/>
|
| 24 |
+
)
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
const tabsListVariants = cva(
|
| 28 |
+
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
|
| 29 |
+
{
|
| 30 |
+
variants: {
|
| 31 |
+
variant: {
|
| 32 |
+
default: "bg-muted",
|
| 33 |
+
line: "gap-1 bg-transparent",
|
| 34 |
+
},
|
| 35 |
+
},
|
| 36 |
+
defaultVariants: {
|
| 37 |
+
variant: "default",
|
| 38 |
+
},
|
| 39 |
+
}
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
function TabsList({
|
| 43 |
+
className,
|
| 44 |
+
variant = "default",
|
| 45 |
+
...props
|
| 46 |
+
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
| 47 |
+
VariantProps<typeof tabsListVariants>) {
|
| 48 |
+
return (
|
| 49 |
+
<TabsPrimitive.List
|
| 50 |
+
data-slot="tabs-list"
|
| 51 |
+
data-variant={variant}
|
| 52 |
+
className={cn(tabsListVariants({ variant }), className)}
|
| 53 |
+
{...props}
|
| 54 |
+
/>
|
| 55 |
+
)
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
function TabsTrigger({
|
| 59 |
+
className,
|
| 60 |
+
...props
|
| 61 |
+
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
| 62 |
+
return (
|
| 63 |
+
<TabsPrimitive.Trigger
|
| 64 |
+
data-slot="tabs-trigger"
|
| 65 |
+
className={cn(
|
| 66 |
+
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
| 67 |
+
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
| 68 |
+
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
| 69 |
+
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
| 70 |
+
className
|
| 71 |
+
)}
|
| 72 |
+
{...props}
|
| 73 |
+
/>
|
| 74 |
+
)
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
function TabsContent({
|
| 78 |
+
className,
|
| 79 |
+
...props
|
| 80 |
+
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
| 81 |
+
return (
|
| 82 |
+
<TabsPrimitive.Content
|
| 83 |
+
data-slot="tabs-content"
|
| 84 |
+
className={cn("flex-1 text-sm outline-none", className)}
|
| 85 |
+
{...props}
|
| 86 |
+
/>
|
| 87 |
+
)
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
src/components/ui/textarea.tsx
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
| 6 |
+
return (
|
| 7 |
+
<textarea
|
| 8 |
+
data-slot="textarea"
|
| 9 |
+
className={cn(
|
| 10 |
+
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
| 11 |
+
className
|
| 12 |
+
)}
|
| 13 |
+
{...props}
|
| 14 |
+
/>
|
| 15 |
+
)
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export { Textarea }
|
src/components/ui/tooltip.tsx
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
function TooltipProvider({
|
| 9 |
+
delayDuration = 0,
|
| 10 |
+
...props
|
| 11 |
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
| 12 |
+
return (
|
| 13 |
+
<TooltipPrimitive.Provider
|
| 14 |
+
data-slot="tooltip-provider"
|
| 15 |
+
delayDuration={delayDuration}
|
| 16 |
+
{...props}
|
| 17 |
+
/>
|
| 18 |
+
)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
function Tooltip({
|
| 22 |
+
...props
|
| 23 |
+
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
| 24 |
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
function TooltipTrigger({
|
| 28 |
+
...props
|
| 29 |
+
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
| 30 |
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function TooltipContent({
|
| 34 |
+
className,
|
| 35 |
+
sideOffset = 0,
|
| 36 |
+
children,
|
| 37 |
+
...props
|
| 38 |
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
| 39 |
+
return (
|
| 40 |
+
<TooltipPrimitive.Portal>
|
| 41 |
+
<TooltipPrimitive.Content
|
| 42 |
+
data-slot="tooltip-content"
|
| 43 |
+
sideOffset={sideOffset}
|
| 44 |
+
className={cn(
|
| 45 |
+
"z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 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-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 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",
|
| 46 |
+
className
|
| 47 |
+
)}
|
| 48 |
+
{...props}
|
| 49 |
+
>
|
| 50 |
+
{children}
|
| 51 |
+
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
| 52 |
+
</TooltipPrimitive.Content>
|
| 53 |
+
</TooltipPrimitive.Portal>
|
| 54 |
+
)
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
src/lib/rocmpilot/data.ts
ADDED
|
@@ -0,0 +1,1102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type {
|
| 2 |
+
AgentMessage,
|
| 3 |
+
AgentMessageKind,
|
| 4 |
+
AgentMemory,
|
| 5 |
+
BenchmarkResult,
|
| 6 |
+
Finding,
|
| 7 |
+
GpuModelStatus,
|
| 8 |
+
LongContextMemoryStatus,
|
| 9 |
+
PatchPreview,
|
| 10 |
+
RocmRun,
|
| 11 |
+
RunMode,
|
| 12 |
+
RunStatus,
|
| 13 |
+
RunStage,
|
| 14 |
+
StageStatus,
|
| 15 |
+
SampleRepo,
|
| 16 |
+
RunTarget,
|
| 17 |
+
} from "./types";
|
| 18 |
+
import { isRealGitHubRepoUrl, parseGitHubRepoUrl } from "./github-url";
|
| 19 |
+
import type { RepoAnalysis } from "./github-scanner";
|
| 20 |
+
import {
|
| 21 |
+
buildMemoryConversationId,
|
| 22 |
+
DEFAULT_MEMORY_CUSTOMER_ID,
|
| 23 |
+
DEFAULT_MEMORY_USER_ID,
|
| 24 |
+
} from "./memory-ids";
|
| 25 |
+
|
| 26 |
+
const TOTAL_DURATION_MS = 30_000;
|
| 27 |
+
|
| 28 |
+
const STAGES: Array<Omit<RunStage, "status" | "progress" | "startedAt" | "completedAt"> & {
|
| 29 |
+
durationMs: number;
|
| 30 |
+
}> = [
|
| 31 |
+
{
|
| 32 |
+
id: "repo-doctor",
|
| 33 |
+
agent: "Repo Doctor Agent",
|
| 34 |
+
title: "Repo compatibility scan",
|
| 35 |
+
description: "Dependency graph, Docker image, device paths, and runtime flags",
|
| 36 |
+
durationMs: 5_000,
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
id: "migration-planner",
|
| 40 |
+
agent: "Migration Planner Agent",
|
| 41 |
+
title: "ROCm migration plan",
|
| 42 |
+
description: "PyTorch ROCm wheels, vLLM runtime, and device abstraction changes",
|
| 43 |
+
durationMs: 6_000,
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
id: "build-runner",
|
| 47 |
+
agent: "Build Runner Agent",
|
| 48 |
+
title: "Build and smoke tests",
|
| 49 |
+
description: "Container validation, import checks, and inference dry run",
|
| 50 |
+
durationMs: 5_500,
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
id: "benchmark-agent",
|
| 54 |
+
agent: "Benchmark Agent",
|
| 55 |
+
title: "MI300X readiness benchmark",
|
| 56 |
+
description: "Throughput, latency, memory, and fallback path comparison",
|
| 57 |
+
durationMs: 5_000,
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
id: "report-agent",
|
| 61 |
+
agent: "Report Agent",
|
| 62 |
+
title: "Judge-ready report",
|
| 63 |
+
description: "Technical summary, business value, and AMD proof points",
|
| 64 |
+
durationMs: 6_000,
|
| 65 |
+
},
|
| 66 |
+
];
|
| 67 |
+
|
| 68 |
+
export const SAMPLE_REPOS: SampleRepo[] = [
|
| 69 |
+
{
|
| 70 |
+
id: "qwen-vllm-cuda",
|
| 71 |
+
name: "Qwen vLLM CUDA Starter",
|
| 72 |
+
repoUrl: "https://github.com/example/qwen-vllm-cuda-starter",
|
| 73 |
+
stack: "FastAPI, PyTorch, vLLM, Docker",
|
| 74 |
+
model: "Qwen/Qwen2.5-Coder-7B-Instruct",
|
| 75 |
+
description: "A common NVIDIA-first inference service with CUDA-only install steps.",
|
| 76 |
+
risk: "Hardcoded CUDA device checks block AMD Developer Cloud deployment.",
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
id: "torch-agent-worker",
|
| 80 |
+
name: "Torch Agent Worker",
|
| 81 |
+
repoUrl: "https://github.com/example/torch-agent-worker",
|
| 82 |
+
stack: "Python workers, PyTorch, Redis queue",
|
| 83 |
+
model: "Qwen/Qwen3-Coder-Next",
|
| 84 |
+
description: "Background coding-agent worker that assumes NVIDIA runtime images.",
|
| 85 |
+
risk: "Docker and benchmark scripts hide GPU vendor assumptions.",
|
| 86 |
+
},
|
| 87 |
+
];
|
| 88 |
+
|
| 89 |
+
export const FINDINGS: Finding[] = [
|
| 90 |
+
{
|
| 91 |
+
id: "cuda-device",
|
| 92 |
+
severity: "critical",
|
| 93 |
+
category: "Runtime device lock",
|
| 94 |
+
file: "src/inference/server.py",
|
| 95 |
+
line: 42,
|
| 96 |
+
explanation:
|
| 97 |
+
"`torch.device('cuda')` is used directly, so the app never checks ROCm-compatible PyTorch device availability or CPU fallback.",
|
| 98 |
+
recommendedFix:
|
| 99 |
+
"Introduce a device resolver that accepts HIP-backed PyTorch as CUDA-compatible and records the detected backend.",
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
id: "docker-image",
|
| 103 |
+
severity: "high",
|
| 104 |
+
category: "Container image",
|
| 105 |
+
file: "Dockerfile",
|
| 106 |
+
line: 1,
|
| 107 |
+
explanation:
|
| 108 |
+
"The base image is `nvidia/cuda`, which prevents a clean ROCm/vLLM deployment on AMD Developer Cloud.",
|
| 109 |
+
recommendedFix:
|
| 110 |
+
"Use the ROCm vLLM image for AMD runs and keep CUDA images only as an optional backend.",
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
id: "vllm-flags",
|
| 114 |
+
severity: "high",
|
| 115 |
+
category: "Serving configuration",
|
| 116 |
+
file: "scripts/serve.sh",
|
| 117 |
+
line: 9,
|
| 118 |
+
explanation:
|
| 119 |
+
"The vLLM launch script omits ROCm-oriented environment flags and does not expose tensor-parallel settings.",
|
| 120 |
+
recommendedFix:
|
| 121 |
+
"Add backend-aware vLLM launch arguments and document MI300X model-serving defaults.",
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
id: "metrics",
|
| 125 |
+
severity: "medium",
|
| 126 |
+
category: "Benchmark visibility",
|
| 127 |
+
file: "benchmarks/run_latency.py",
|
| 128 |
+
line: 18,
|
| 129 |
+
explanation:
|
| 130 |
+
"The benchmark reports request latency only and misses GPU memory, tokens/sec, and backend provenance.",
|
| 131 |
+
recommendedFix:
|
| 132 |
+
"Add AMD SMI/vLLM metrics capture so submission evidence includes GPU model, memory, and throughput.",
|
| 133 |
+
},
|
| 134 |
+
];
|
| 135 |
+
|
| 136 |
+
export const PATCHES: PatchPreview[] = [
|
| 137 |
+
{
|
| 138 |
+
id: "device-resolver",
|
| 139 |
+
file: "src/inference/device.py",
|
| 140 |
+
rationale:
|
| 141 |
+
"Centralizes device selection so ROCm-backed PyTorch can run without scattering vendor checks across the service.",
|
| 142 |
+
diff: `+import torch
|
| 143 |
+
+
|
| 144 |
+
+def resolve_device() -> tuple[str, str]:
|
| 145 |
+
+ if torch.cuda.is_available():
|
| 146 |
+
+ backend = "rocm" if getattr(torch.version, "hip", None) else "cuda"
|
| 147 |
+
+ return "cuda", backend
|
| 148 |
+
+ return "cpu", "cpu"
|
| 149 |
+
+
|
| 150 |
+
+DEVICE, GPU_BACKEND = resolve_device()
|
| 151 |
+
`,
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
id: "rocm-docker",
|
| 155 |
+
file: "Dockerfile.rocm",
|
| 156 |
+
rationale:
|
| 157 |
+
"Adds an AMD-specific runtime image while preserving the original CUDA path for teams that need dual-vendor support.",
|
| 158 |
+
diff: `+FROM rocm/vllm:latest
|
| 159 |
+
+
|
| 160 |
+
+WORKDIR /workspace
|
| 161 |
+
+COPY requirements-rocm.txt .
|
| 162 |
+
+RUN pip install --no-cache-dir -r requirements-rocm.txt
|
| 163 |
+
+COPY . .
|
| 164 |
+
+
|
| 165 |
+
+ENV HIP_VISIBLE_DEVICES=0
|
| 166 |
+
+ENV VLLM_USE_ROCM=1
|
| 167 |
+
+CMD ["bash", "scripts/serve-rocm.sh"]
|
| 168 |
+
`,
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
id: "serve-rocm",
|
| 172 |
+
file: "scripts/serve-rocm.sh",
|
| 173 |
+
rationale:
|
| 174 |
+
"Launches an OpenAI-compatible vLLM endpoint for the migration/report agents on AMD Instinct GPUs.",
|
| 175 |
+
diff: `+#!/usr/bin/env bash
|
| 176 |
+
+set -euo pipefail
|
| 177 |
+
+
|
| 178 |
+
+MODEL="\${MODEL:-Qwen/Qwen3-Coder-Next}"
|
| 179 |
+
+PORT="\${PORT:-8000}"
|
| 180 |
+
+
|
| 181 |
+
+python -m vllm.entrypoints.openai.api_server \\
|
| 182 |
+
+ --model "$MODEL" \\
|
| 183 |
+
+ --host 0.0.0.0 \\
|
| 184 |
+
+ --port "$PORT" \\
|
| 185 |
+
+ --tensor-parallel-size "\${TENSOR_PARALLEL_SIZE:-1}" \\
|
| 186 |
+
+ --max-model-len "\${MAX_MODEL_LEN:-32768}"
|
| 187 |
+
`,
|
| 188 |
+
},
|
| 189 |
+
];
|
| 190 |
+
|
| 191 |
+
export const BENCHMARKS: BenchmarkResult[] = [
|
| 192 |
+
{
|
| 193 |
+
label: "Before migration",
|
| 194 |
+
backend: "CUDA-only config",
|
| 195 |
+
tokensPerSecond: 0,
|
| 196 |
+
p95LatencyMs: 0,
|
| 197 |
+
memoryGb: 0,
|
| 198 |
+
costNote: "Does not boot on AMD ROCm image.",
|
| 199 |
+
},
|
| 200 |
+
{
|
| 201 |
+
label: "ROCm-ready target",
|
| 202 |
+
backend: "ROCm + vLLM on MI300X",
|
| 203 |
+
tokensPerSecond: 182,
|
| 204 |
+
p95LatencyMs: 730,
|
| 205 |
+
memoryGb: 92,
|
| 206 |
+
costNote: "Estimated from demo profile; replace with live AMD run evidence.",
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
label: "Agent report model",
|
| 210 |
+
backend: "Qwen3-Coder-Next via OpenAI-compatible endpoint",
|
| 211 |
+
tokensPerSecond: 64,
|
| 212 |
+
p95LatencyMs: 1180,
|
| 213 |
+
memoryGb: 46,
|
| 214 |
+
costNote: "Runs as the Report Agent when AMD_QWEN_BASE_URL is configured.",
|
| 215 |
+
},
|
| 216 |
+
];
|
| 217 |
+
|
| 218 |
+
const LOGS = [
|
| 219 |
+
"queued run qwen-vllm-cuda-starter in mock-safe mode",
|
| 220 |
+
"repo-doctor: scanning pyproject.toml, Dockerfile, scripts, and src/inference",
|
| 221 |
+
"repo-doctor: found nvidia/cuda base image in Dockerfile",
|
| 222 |
+
"repo-doctor: found direct torch.device('cuda') usage in src/inference/server.py:42",
|
| 223 |
+
"migration-planner: generated ROCm runtime image proposal",
|
| 224 |
+
"migration-planner: created backend-aware device resolver",
|
| 225 |
+
"build-runner: docker build -f Dockerfile.rocm .",
|
| 226 |
+
"build-runner: import torch; torch.version.hip detected when ROCm wheel is present",
|
| 227 |
+
"build-runner: vLLM OpenAI endpoint smoke test passed in demo mode",
|
| 228 |
+
"benchmark-agent: captured target profile for MI300X/vLLM serving",
|
| 229 |
+
"report-agent: preparing technical and business summary",
|
| 230 |
+
"completed run with fallback-safe report path",
|
| 231 |
+
];
|
| 232 |
+
|
| 233 |
+
type MessageBlueprint = {
|
| 234 |
+
offsetMs: number;
|
| 235 |
+
agent: string;
|
| 236 |
+
toAgent: string;
|
| 237 |
+
role: string;
|
| 238 |
+
task: string;
|
| 239 |
+
leadAgent: string;
|
| 240 |
+
kind: AgentMessageKind;
|
| 241 |
+
replyToOffsetMs?: number;
|
| 242 |
+
memoryRefs?: string[];
|
| 243 |
+
message: (context: {
|
| 244 |
+
target: RunTarget;
|
| 245 |
+
sample: SampleRepo;
|
| 246 |
+
findings: Finding[];
|
| 247 |
+
patches: PatchPreview[];
|
| 248 |
+
}) => string;
|
| 249 |
+
};
|
| 250 |
+
|
| 251 |
+
const WAR_ROOM_MESSAGES: MessageBlueprint[] = [
|
| 252 |
+
{
|
| 253 |
+
offsetMs: 1_200,
|
| 254 |
+
agent: "Orchestrator",
|
| 255 |
+
toAgent: "Repo Doctor",
|
| 256 |
+
role: "Run coordinator",
|
| 257 |
+
task: "Repo compatibility scan",
|
| 258 |
+
leadAgent: "Repo Doctor",
|
| 259 |
+
kind: "question",
|
| 260 |
+
message: ({ target }) =>
|
| 261 |
+
`You are lead for the first task on ${target.label}. Ask the other agents what evidence they need before you mark any blocker as real.`,
|
| 262 |
+
},
|
| 263 |
+
{
|
| 264 |
+
offsetMs: 2_600,
|
| 265 |
+
agent: "Repo Doctor",
|
| 266 |
+
toAgent: "Build Runner",
|
| 267 |
+
role: "Compatibility scout",
|
| 268 |
+
task: "Repo compatibility scan",
|
| 269 |
+
leadAgent: "Repo Doctor",
|
| 270 |
+
kind: "question",
|
| 271 |
+
replyToOffsetMs: 1_200,
|
| 272 |
+
message: ({ target }) =>
|
| 273 |
+
target.type === "github"
|
| 274 |
+
? `I am scanning ${target.scannedFiles || "the selected"} files. Which findings should I flag as build-breaking instead of just advisory?`
|
| 275 |
+
: "I am scanning Docker, vLLM scripts, PyTorch device paths, and benchmarks. Which findings should I flag as build-breaking instead of advisory?",
|
| 276 |
+
},
|
| 277 |
+
{
|
| 278 |
+
offsetMs: 3_700,
|
| 279 |
+
agent: "Build Runner",
|
| 280 |
+
toAgent: "Repo Doctor",
|
| 281 |
+
role: "Skeptical validator",
|
| 282 |
+
task: "Repo compatibility scan",
|
| 283 |
+
leadAgent: "Repo Doctor",
|
| 284 |
+
kind: "answer",
|
| 285 |
+
replyToOffsetMs: 2_600,
|
| 286 |
+
message:
|
| 287 |
+
() =>
|
| 288 |
+
"Treat container base images, hardcoded device selection, and missing smoke commands as build-breaking. Those decide whether the workload even starts on ROCm.",
|
| 289 |
+
},
|
| 290 |
+
{
|
| 291 |
+
offsetMs: 5_200,
|
| 292 |
+
agent: "Repo Doctor",
|
| 293 |
+
toAgent: "Migration Planner",
|
| 294 |
+
role: "Compatibility scout",
|
| 295 |
+
task: "Repo compatibility scan",
|
| 296 |
+
leadAgent: "Repo Doctor",
|
| 297 |
+
kind: "question",
|
| 298 |
+
replyToOffsetMs: 3_700,
|
| 299 |
+
message: ({ findings }) =>
|
| 300 |
+
findings[0]
|
| 301 |
+
? `I found ${findings[0].category} in ${findings[0].file}:${findings[0].line}. Can you design the safest migration step for this first?`
|
| 302 |
+
: "I have no hard blocker yet. Can you prepare a safe migration pattern for hidden GPU vendor assumptions?",
|
| 303 |
+
},
|
| 304 |
+
{
|
| 305 |
+
offsetMs: 6_700,
|
| 306 |
+
agent: "Migration Planner",
|
| 307 |
+
toAgent: "Repo Doctor",
|
| 308 |
+
role: "Patch strategist",
|
| 309 |
+
task: "Repo compatibility scan",
|
| 310 |
+
leadAgent: "Repo Doctor",
|
| 311 |
+
kind: "proposal",
|
| 312 |
+
replyToOffsetMs: 5_200,
|
| 313 |
+
message: ({ findings }) =>
|
| 314 |
+
findings[0]
|
| 315 |
+
? `Yes. First migration step: ${findings[0].recommendedFix} I will store that as the device-resolution pattern for later stages.`
|
| 316 |
+
: "Yes. I will store a pattern that keeps CUDA and ROCm paths explicit instead of hidden in environment assumptions.",
|
| 317 |
+
},
|
| 318 |
+
{
|
| 319 |
+
offsetMs: 8_200,
|
| 320 |
+
agent: "Repo Doctor",
|
| 321 |
+
toAgent: "Shared Memory",
|
| 322 |
+
role: "Compatibility scout",
|
| 323 |
+
task: "Repo compatibility scan",
|
| 324 |
+
leadAgent: "Repo Doctor",
|
| 325 |
+
kind: "memory",
|
| 326 |
+
replyToOffsetMs: 6_700,
|
| 327 |
+
memoryRefs: ["mem-device-resolution"],
|
| 328 |
+
message:
|
| 329 |
+
() =>
|
| 330 |
+
"Memory write: hardcoded device selection must be solved with one backend resolver, not scattered if/else checks.",
|
| 331 |
+
},
|
| 332 |
+
{
|
| 333 |
+
offsetMs: 9_600,
|
| 334 |
+
agent: "Orchestrator",
|
| 335 |
+
toAgent: "Migration Planner",
|
| 336 |
+
role: "Run coordinator",
|
| 337 |
+
task: "ROCm migration plan",
|
| 338 |
+
leadAgent: "Migration Planner",
|
| 339 |
+
kind: "question",
|
| 340 |
+
memoryRefs: ["mem-device-resolution"],
|
| 341 |
+
message:
|
| 342 |
+
() =>
|
| 343 |
+
"You are lead now. Use the device-resolution memory and ask Build Runner what would make the patch testable.",
|
| 344 |
+
},
|
| 345 |
+
{
|
| 346 |
+
offsetMs: 10_800,
|
| 347 |
+
agent: "Migration Planner",
|
| 348 |
+
toAgent: "Build Runner",
|
| 349 |
+
role: "Patch strategist",
|
| 350 |
+
task: "ROCm migration plan",
|
| 351 |
+
leadAgent: "Migration Planner",
|
| 352 |
+
kind: "question",
|
| 353 |
+
replyToOffsetMs: 9_600,
|
| 354 |
+
memoryRefs: ["mem-device-resolution"],
|
| 355 |
+
message:
|
| 356 |
+
() =>
|
| 357 |
+
"I can patch the resolver, but what acceptance check should prove this is ROCm-ready and not just cleaner code?",
|
| 358 |
+
},
|
| 359 |
+
{
|
| 360 |
+
offsetMs: 12_100,
|
| 361 |
+
agent: "Build Runner",
|
| 362 |
+
toAgent: "Migration Planner",
|
| 363 |
+
role: "Skeptical validator",
|
| 364 |
+
task: "ROCm migration plan",
|
| 365 |
+
leadAgent: "Migration Planner",
|
| 366 |
+
kind: "answer",
|
| 367 |
+
replyToOffsetMs: 10_800,
|
| 368 |
+
memoryRefs: ["mem-device-resolution"],
|
| 369 |
+
message:
|
| 370 |
+
() =>
|
| 371 |
+
"Acceptance check: import torch, report torch.version.hip when present, start vLLM with an OpenAI-compatible health request, then log backend provenance.",
|
| 372 |
+
},
|
| 373 |
+
{
|
| 374 |
+
offsetMs: 13_500,
|
| 375 |
+
agent: "Migration Planner",
|
| 376 |
+
toAgent: "Shared Memory",
|
| 377 |
+
role: "Patch strategist",
|
| 378 |
+
task: "ROCm migration plan",
|
| 379 |
+
leadAgent: "Migration Planner",
|
| 380 |
+
kind: "proposal",
|
| 381 |
+
replyToOffsetMs: 12_100,
|
| 382 |
+
memoryRefs: ["mem-device-resolution", "mem-rocm-acceptance"],
|
| 383 |
+
message: ({ patches }) =>
|
| 384 |
+
patches[0]
|
| 385 |
+
? `Patch candidate for ${patches[0].file} is ready, and I am storing Build Runner's acceptance check with it.`
|
| 386 |
+
: "Patch candidate is ready, and I am storing Build Runner's acceptance check with it.",
|
| 387 |
+
},
|
| 388 |
+
{
|
| 389 |
+
offsetMs: 15_200,
|
| 390 |
+
agent: "Orchestrator",
|
| 391 |
+
toAgent: "Build Runner",
|
| 392 |
+
role: "Run coordinator",
|
| 393 |
+
task: "Build and smoke tests",
|
| 394 |
+
leadAgent: "Build Runner",
|
| 395 |
+
kind: "question",
|
| 396 |
+
memoryRefs: ["mem-device-resolution", "mem-rocm-acceptance"],
|
| 397 |
+
message:
|
| 398 |
+
() =>
|
| 399 |
+
"You are lead for validation. Look back at memory before challenging the plan: what can still fail later?",
|
| 400 |
+
},
|
| 401 |
+
{
|
| 402 |
+
offsetMs: 16_500,
|
| 403 |
+
agent: "Build Runner",
|
| 404 |
+
toAgent: "Migration Planner",
|
| 405 |
+
role: "Skeptical validator",
|
| 406 |
+
task: "Build and smoke tests",
|
| 407 |
+
leadAgent: "Build Runner",
|
| 408 |
+
kind: "challenge",
|
| 409 |
+
replyToOffsetMs: 15_200,
|
| 410 |
+
memoryRefs: ["mem-rocm-acceptance"],
|
| 411 |
+
message:
|
| 412 |
+
() =>
|
| 413 |
+
"I checked the memory. Device resolution is covered, but the Docker path can still fail. A CUDA image with ROCm notes is still a deployment trap.",
|
| 414 |
+
},
|
| 415 |
+
{
|
| 416 |
+
offsetMs: 17_800,
|
| 417 |
+
agent: "Migration Planner",
|
| 418 |
+
toAgent: "Build Runner",
|
| 419 |
+
role: "Patch strategist",
|
| 420 |
+
task: "Build and smoke tests",
|
| 421 |
+
leadAgent: "Build Runner",
|
| 422 |
+
kind: "answer",
|
| 423 |
+
replyToOffsetMs: 16_500,
|
| 424 |
+
message:
|
| 425 |
+
() =>
|
| 426 |
+
"Agreed. I will keep Dockerfile.rocm separate and avoid pretending the existing CUDA container is portable.",
|
| 427 |
+
},
|
| 428 |
+
{
|
| 429 |
+
offsetMs: 19_000,
|
| 430 |
+
agent: "Build Runner",
|
| 431 |
+
toAgent: "Shared Memory",
|
| 432 |
+
role: "Skeptical validator",
|
| 433 |
+
task: "Build and smoke tests",
|
| 434 |
+
leadAgent: "Build Runner",
|
| 435 |
+
kind: "memory",
|
| 436 |
+
replyToOffsetMs: 17_800,
|
| 437 |
+
memoryRefs: ["mem-container-split"],
|
| 438 |
+
message:
|
| 439 |
+
() =>
|
| 440 |
+
"Memory write: ROCm validation needs a separate container path plus a smoke command, not just migration notes in README.",
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
offsetMs: 20_300,
|
| 444 |
+
agent: "Orchestrator",
|
| 445 |
+
toAgent: "Benchmark Agent",
|
| 446 |
+
role: "Run coordinator",
|
| 447 |
+
task: "MI300X readiness benchmark",
|
| 448 |
+
leadAgent: "Benchmark Agent",
|
| 449 |
+
kind: "question",
|
| 450 |
+
memoryRefs: ["mem-rocm-acceptance", "mem-container-split"],
|
| 451 |
+
message:
|
| 452 |
+
() =>
|
| 453 |
+
"You are lead for measurement. Use the earlier memories and ask what numbers are safe to show before AMD access is connected.",
|
| 454 |
+
},
|
| 455 |
+
{
|
| 456 |
+
offsetMs: 21_500,
|
| 457 |
+
agent: "Benchmark Agent",
|
| 458 |
+
toAgent: "Report Agent",
|
| 459 |
+
role: "Evidence analyst",
|
| 460 |
+
task: "MI300X readiness benchmark",
|
| 461 |
+
leadAgent: "Benchmark Agent",
|
| 462 |
+
kind: "question",
|
| 463 |
+
replyToOffsetMs: 20_300,
|
| 464 |
+
memoryRefs: ["mem-rocm-acceptance", "mem-container-split"],
|
| 465 |
+
message:
|
| 466 |
+
() =>
|
| 467 |
+
"I can show estimated tokens/sec, p95 latency, memory, and bootability. How should I label estimates so the story stays credible?",
|
| 468 |
+
},
|
| 469 |
+
{
|
| 470 |
+
offsetMs: 22_700,
|
| 471 |
+
agent: "Report Agent",
|
| 472 |
+
toAgent: "Benchmark Agent",
|
| 473 |
+
role: "Submission narrator",
|
| 474 |
+
task: "MI300X readiness benchmark",
|
| 475 |
+
leadAgent: "Benchmark Agent",
|
| 476 |
+
kind: "answer",
|
| 477 |
+
replyToOffsetMs: 21_500,
|
| 478 |
+
message:
|
| 479 |
+
() =>
|
| 480 |
+
"Label estimates as a static ROCmPilot profile and reserve final proof for AMD Developer Cloud logs. Judges trust explicit provenance.",
|
| 481 |
+
},
|
| 482 |
+
{
|
| 483 |
+
offsetMs: 24_000,
|
| 484 |
+
agent: "Benchmark Agent",
|
| 485 |
+
toAgent: "Shared Memory",
|
| 486 |
+
role: "Evidence analyst",
|
| 487 |
+
task: "MI300X readiness benchmark",
|
| 488 |
+
leadAgent: "Benchmark Agent",
|
| 489 |
+
kind: "memory",
|
| 490 |
+
replyToOffsetMs: 22_700,
|
| 491 |
+
memoryRefs: ["mem-metric-provenance"],
|
| 492 |
+
message:
|
| 493 |
+
() =>
|
| 494 |
+
"Memory write: estimated metrics are acceptable only when labeled with provenance and paired with the exact AMD proof still needed.",
|
| 495 |
+
},
|
| 496 |
+
{
|
| 497 |
+
offsetMs: 25_200,
|
| 498 |
+
agent: "Orchestrator",
|
| 499 |
+
toAgent: "Report Agent",
|
| 500 |
+
role: "Run coordinator",
|
| 501 |
+
task: "Judge-ready report",
|
| 502 |
+
leadAgent: "Report Agent",
|
| 503 |
+
kind: "question",
|
| 504 |
+
memoryRefs: ["mem-device-resolution", "mem-container-split", "mem-metric-provenance"],
|
| 505 |
+
message:
|
| 506 |
+
() =>
|
| 507 |
+
"You are lead for the final report. Read the shared memory first, then ask if anyone disagrees with the submission story.",
|
| 508 |
+
},
|
| 509 |
+
{
|
| 510 |
+
offsetMs: 26_000,
|
| 511 |
+
agent: "Report Agent",
|
| 512 |
+
toAgent: "All agents",
|
| 513 |
+
role: "Submission narrator",
|
| 514 |
+
task: "Judge-ready report",
|
| 515 |
+
leadAgent: "Report Agent",
|
| 516 |
+
kind: "question",
|
| 517 |
+
replyToOffsetMs: 25_200,
|
| 518 |
+
memoryRefs: ["mem-device-resolution", "mem-container-split", "mem-metric-provenance"],
|
| 519 |
+
message:
|
| 520 |
+
() =>
|
| 521 |
+
"I read the memories. Final story: find CUDA locks, create ROCm patch path, validate container/smoke tests, show metric provenance. Any objections?",
|
| 522 |
+
},
|
| 523 |
+
{
|
| 524 |
+
offsetMs: 26_800,
|
| 525 |
+
agent: "Build Runner",
|
| 526 |
+
toAgent: "Report Agent",
|
| 527 |
+
role: "Skeptical validator",
|
| 528 |
+
task: "Judge-ready report",
|
| 529 |
+
leadAgent: "Report Agent",
|
| 530 |
+
kind: "answer",
|
| 531 |
+
replyToOffsetMs: 26_000,
|
| 532 |
+
memoryRefs: ["mem-container-split"],
|
| 533 |
+
message:
|
| 534 |
+
() =>
|
| 535 |
+
"No objection if the report says validation is complete for the demo path and pending for live MI300X logs.",
|
| 536 |
+
},
|
| 537 |
+
{
|
| 538 |
+
offsetMs: 27_400,
|
| 539 |
+
agent: "Benchmark Agent",
|
| 540 |
+
toAgent: "Report Agent",
|
| 541 |
+
role: "Evidence analyst",
|
| 542 |
+
task: "Judge-ready report",
|
| 543 |
+
leadAgent: "Report Agent",
|
| 544 |
+
kind: "answer",
|
| 545 |
+
replyToOffsetMs: 26_000,
|
| 546 |
+
memoryRefs: ["mem-metric-provenance"],
|
| 547 |
+
message:
|
| 548 |
+
() =>
|
| 549 |
+
"No objection if every metric names its source. I want the next live run to overwrite estimates with AMD SMI and vLLM logs.",
|
| 550 |
+
},
|
| 551 |
+
{
|
| 552 |
+
offsetMs: 28_200,
|
| 553 |
+
agent: "Orchestrator",
|
| 554 |
+
toAgent: "All agents",
|
| 555 |
+
role: "Run coordinator",
|
| 556 |
+
task: "Judge-ready report",
|
| 557 |
+
leadAgent: "Report Agent",
|
| 558 |
+
kind: "consensus",
|
| 559 |
+
replyToOffsetMs: 27_400,
|
| 560 |
+
memoryRefs: ["mem-device-resolution", "mem-container-split", "mem-metric-provenance"],
|
| 561 |
+
message:
|
| 562 |
+
() =>
|
| 563 |
+
"Consensus stored: ship a ROCm readiness report, patch previews, shared discussion memory, and an AMD/vLLM validation checklist. Next run should replace estimates with MI300X logs.",
|
| 564 |
+
},
|
| 565 |
+
];
|
| 566 |
+
|
| 567 |
+
type MemoryBlueprint = {
|
| 568 |
+
offsetMs: number;
|
| 569 |
+
id: string;
|
| 570 |
+
title: string;
|
| 571 |
+
scope: string;
|
| 572 |
+
learnedFromAgent: string;
|
| 573 |
+
summary: (context: { findings: Finding[]; patches: PatchPreview[] }) => string;
|
| 574 |
+
solution: string;
|
| 575 |
+
};
|
| 576 |
+
|
| 577 |
+
const WAR_ROOM_MEMORY: MemoryBlueprint[] = [
|
| 578 |
+
{
|
| 579 |
+
offsetMs: 8_200,
|
| 580 |
+
id: "mem-device-resolution",
|
| 581 |
+
title: "Device resolution pattern",
|
| 582 |
+
scope: "Runtime compatibility",
|
| 583 |
+
learnedFromAgent: "Repo Doctor",
|
| 584 |
+
summary: ({ findings }) =>
|
| 585 |
+
findings[0]
|
| 586 |
+
? `${findings[0].category} should be handled once in a resolver instead of repeated across inference code.`
|
| 587 |
+
: "GPU backend detection should be handled once in a resolver instead of repeated across inference code.",
|
| 588 |
+
solution: "Create a backend-aware resolver that accepts HIP-backed PyTorch as CUDA-compatible and records backend provenance.",
|
| 589 |
+
},
|
| 590 |
+
{
|
| 591 |
+
offsetMs: 13_500,
|
| 592 |
+
id: "mem-rocm-acceptance",
|
| 593 |
+
title: "ROCm acceptance checks",
|
| 594 |
+
scope: "Build validation",
|
| 595 |
+
learnedFromAgent: "Build Runner",
|
| 596 |
+
summary: () =>
|
| 597 |
+
"A patch is not enough unless the run proves import, backend detection, vLLM health, and provenance logging.",
|
| 598 |
+
solution: "Use a smoke command that imports torch, reports torch.version.hip, starts vLLM, and hits the OpenAI-compatible health path.",
|
| 599 |
+
},
|
| 600 |
+
{
|
| 601 |
+
offsetMs: 19_000,
|
| 602 |
+
id: "mem-container-split",
|
| 603 |
+
title: "Separate ROCm container path",
|
| 604 |
+
scope: "Deployment safety",
|
| 605 |
+
learnedFromAgent: "Build Runner",
|
| 606 |
+
summary: () =>
|
| 607 |
+
"A CUDA image with ROCm comments still leaves teams with a deployment trap.",
|
| 608 |
+
solution: "Keep Dockerfile.rocm and ROCm launch scripts separate, with CUDA preserved only as an optional backend.",
|
| 609 |
+
},
|
| 610 |
+
{
|
| 611 |
+
offsetMs: 24_000,
|
| 612 |
+
id: "mem-metric-provenance",
|
| 613 |
+
title: "Metric provenance rule",
|
| 614 |
+
scope: "Benchmark evidence",
|
| 615 |
+
learnedFromAgent: "Benchmark Agent",
|
| 616 |
+
summary: () =>
|
| 617 |
+
"Estimated benchmark cards are useful for the MVP only when their source is explicit.",
|
| 618 |
+
solution: "Label estimates as static ROCmPilot profiles and replace them with AMD SMI plus vLLM logs when MI300X access is available.",
|
| 619 |
+
},
|
| 620 |
+
];
|
| 621 |
+
|
| 622 |
+
export type RunRecord = {
|
| 623 |
+
id: string;
|
| 624 |
+
sampleId: string;
|
| 625 |
+
mode: RunMode;
|
| 626 |
+
startedAt: number;
|
| 627 |
+
targetType: "sample" | "github";
|
| 628 |
+
repoUrl?: string;
|
| 629 |
+
};
|
| 630 |
+
|
| 631 |
+
export function getSample(sampleId: string | undefined) {
|
| 632 |
+
return SAMPLE_REPOS.find((sample) => sample.id === sampleId) ?? SAMPLE_REPOS[0];
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
function toBase64Url(value: string) {
|
| 636 |
+
return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
function fromBase64Url(value: string) {
|
| 640 |
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
| 641 |
+
return atob(padded);
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
function buildGitHubSample(repoUrl: string): SampleRepo {
|
| 645 |
+
const parsed = parseGitHubRepoUrl(repoUrl);
|
| 646 |
+
|
| 647 |
+
return {
|
| 648 |
+
id: "github-repo",
|
| 649 |
+
name: parsed?.label ?? "Public GitHub Repository",
|
| 650 |
+
repoUrl,
|
| 651 |
+
stack: "Detected from public GitHub files",
|
| 652 |
+
model: "Detected workload",
|
| 653 |
+
description: "A public repository scanned live by ROCmPilot.",
|
| 654 |
+
risk: "ROCm compatibility depends on detected CUDA/NVIDIA assumptions and live AMD validation.",
|
| 655 |
+
};
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
export function createRunRecord(sampleId: string, mode: RunMode, repoUrl?: string): RunRecord {
|
| 659 |
+
const startedAt = Date.now();
|
| 660 |
+
const safeSampleId = getSample(sampleId).id;
|
| 661 |
+
const nonce = Math.random().toString(36).slice(2, 8);
|
| 662 |
+
const parsedRepo = isRealGitHubRepoUrl(repoUrl) ? parseGitHubRepoUrl(repoUrl) : null;
|
| 663 |
+
const targetType = parsedRepo ? "github" : "sample";
|
| 664 |
+
const payload = parsedRepo ? toBase64Url(parsedRepo.repoUrl) : safeSampleId;
|
| 665 |
+
|
| 666 |
+
return {
|
| 667 |
+
id: `run.${startedAt.toString(36)}.${mode}.${targetType}.${payload}.${nonce}`,
|
| 668 |
+
sampleId: safeSampleId,
|
| 669 |
+
mode,
|
| 670 |
+
startedAt,
|
| 671 |
+
targetType,
|
| 672 |
+
repoUrl: parsedRepo?.repoUrl,
|
| 673 |
+
};
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
export function parseRunRecord(runId: string): RunRecord | null {
|
| 677 |
+
const parts = runId.split(".");
|
| 678 |
+
|
| 679 |
+
if (parts.length !== 6 || parts[0] !== "run") {
|
| 680 |
+
return null;
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
const [, startedAtBase36, mode, targetType, payload] = parts;
|
| 684 |
+
const startedAt = Number.parseInt(startedAtBase36, 36);
|
| 685 |
+
|
| 686 |
+
if (
|
| 687 |
+
!Number.isFinite(startedAt) ||
|
| 688 |
+
(mode !== "mock" && mode !== "amd") ||
|
| 689 |
+
(targetType !== "sample" && targetType !== "github")
|
| 690 |
+
) {
|
| 691 |
+
return null;
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
if (targetType === "github") {
|
| 695 |
+
const repoUrl = fromBase64Url(payload);
|
| 696 |
+
|
| 697 |
+
if (!isRealGitHubRepoUrl(repoUrl)) {
|
| 698 |
+
return null;
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
return {
|
| 702 |
+
id: runId,
|
| 703 |
+
sampleId: "qwen-vllm-cuda",
|
| 704 |
+
mode,
|
| 705 |
+
startedAt,
|
| 706 |
+
targetType,
|
| 707 |
+
repoUrl,
|
| 708 |
+
};
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
const sample = SAMPLE_REPOS.find((candidate) => candidate.id === payload);
|
| 712 |
+
|
| 713 |
+
if (!sample) {
|
| 714 |
+
return null;
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
return {
|
| 718 |
+
id: runId,
|
| 719 |
+
sampleId: sample.id,
|
| 720 |
+
mode,
|
| 721 |
+
startedAt,
|
| 722 |
+
targetType,
|
| 723 |
+
};
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
export function getModelStatus(source: GpuModelStatus["source"] = "fallback"): GpuModelStatus {
|
| 727 |
+
const endpoint = process.env.AMD_QWEN_BASE_URL?.replace(/\/$/, "");
|
| 728 |
+
const model = process.env.AMD_QWEN_MODEL ?? "Qwen/Qwen3-Coder-Next";
|
| 729 |
+
const hfModel = process.env.HF_REPORT_MODEL ?? "Qwen/Qwen2.5-Coder-7B-Instruct";
|
| 730 |
+
|
| 731 |
+
if (endpoint && source === "amd-vllm") {
|
| 732 |
+
return {
|
| 733 |
+
status: "connected",
|
| 734 |
+
label: "AMD GPU Model: Connected",
|
| 735 |
+
model,
|
| 736 |
+
endpoint,
|
| 737 |
+
detail: "Report generation used the configured ROCm/vLLM OpenAI-compatible endpoint.",
|
| 738 |
+
source,
|
| 739 |
+
};
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
if (source === "hf-router") {
|
| 743 |
+
return {
|
| 744 |
+
status: "connected",
|
| 745 |
+
label: "HF Router: Connected",
|
| 746 |
+
model: hfModel,
|
| 747 |
+
endpoint: "https://router.huggingface.co/v1",
|
| 748 |
+
detail: "Report generation used Hugging Face Inference Providers as the temporary model backend.",
|
| 749 |
+
source,
|
| 750 |
+
};
|
| 751 |
+
}
|
| 752 |
+
|
| 753 |
+
if (endpoint) {
|
| 754 |
+
return {
|
| 755 |
+
status: "not-configured",
|
| 756 |
+
label: "AMD GPU Model: Endpoint configured",
|
| 757 |
+
model,
|
| 758 |
+
endpoint,
|
| 759 |
+
detail: "Endpoint is configured; report generation will attempt AMD-hosted Qwen first.",
|
| 760 |
+
source,
|
| 761 |
+
};
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
if (process.env.HF_TOKEN) {
|
| 765 |
+
return {
|
| 766 |
+
status: "not-configured",
|
| 767 |
+
label: "HF Router: Available",
|
| 768 |
+
model: hfModel,
|
| 769 |
+
endpoint: "https://router.huggingface.co/v1",
|
| 770 |
+
detail: "Hugging Face token is configured; final report generation will use HF unless AMD is configured.",
|
| 771 |
+
source: "hf-router",
|
| 772 |
+
};
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
return {
|
| 776 |
+
status: "fallback",
|
| 777 |
+
label: "AMD GPU Model: Demo fallback",
|
| 778 |
+
model,
|
| 779 |
+
endpoint: "Set AMD_QWEN_BASE_URL to enable live ROCm/vLLM inference",
|
| 780 |
+
detail: "The dashboard is using deterministic fallback output so the MVP demo remains reliable.",
|
| 781 |
+
source: "fallback",
|
| 782 |
+
};
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
export function getLongContextMemoryStatus(
|
| 786 |
+
runId: string,
|
| 787 |
+
storedItems = 0,
|
| 788 |
+
recalledItems = 0,
|
| 789 |
+
source: LongContextMemoryStatus["status"] = process.env.SYNAP_API_KEY ? "configured" : "fallback"
|
| 790 |
+
): LongContextMemoryStatus {
|
| 791 |
+
const conversationId = buildMemoryConversationId(runId);
|
| 792 |
+
const customerId = process.env.SYNAP_CUSTOMER_ID ?? DEFAULT_MEMORY_CUSTOMER_ID;
|
| 793 |
+
const userId = process.env.SYNAP_USER_ID ?? DEFAULT_MEMORY_USER_ID;
|
| 794 |
+
|
| 795 |
+
if (source === "connected") {
|
| 796 |
+
return {
|
| 797 |
+
status: "connected",
|
| 798 |
+
label: "Synap Memory: Connected",
|
| 799 |
+
provider: "synap",
|
| 800 |
+
conversationId,
|
| 801 |
+
scope: `${customerId}/${userId}`,
|
| 802 |
+
detail: "Report Agent stored this run and retrieved scoped long-context memory from Synap.",
|
| 803 |
+
storedItems,
|
| 804 |
+
recalledItems,
|
| 805 |
+
};
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
if (source === "configured") {
|
| 809 |
+
return {
|
| 810 |
+
status: "configured",
|
| 811 |
+
label: "Synap Memory: Ready",
|
| 812 |
+
provider: "synap",
|
| 813 |
+
conversationId,
|
| 814 |
+
scope: `${customerId}/${userId}`,
|
| 815 |
+
detail: "Synap is configured; the Report Agent will ingest the war-room transcript during report generation.",
|
| 816 |
+
storedItems,
|
| 817 |
+
recalledItems,
|
| 818 |
+
};
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
if (source === "not-configured") {
|
| 822 |
+
return {
|
| 823 |
+
status: "not-configured",
|
| 824 |
+
label: "Synap Memory: Setup needed",
|
| 825 |
+
provider: "synap",
|
| 826 |
+
conversationId,
|
| 827 |
+
scope: `${customerId}/${userId}`,
|
| 828 |
+
detail: "Set SYNAP_API_KEY and run the Synap JS runtime setup to enable persistent memory.",
|
| 829 |
+
storedItems,
|
| 830 |
+
recalledItems,
|
| 831 |
+
};
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
+
return {
|
| 835 |
+
status: "fallback",
|
| 836 |
+
label: "Synap Memory: Local fallback",
|
| 837 |
+
provider: "local",
|
| 838 |
+
conversationId,
|
| 839 |
+
scope: "current stateless run",
|
| 840 |
+
detail: "Using reconstructed run memory now; Synap can persist it across sessions once credentials are configured.",
|
| 841 |
+
storedItems,
|
| 842 |
+
recalledItems,
|
| 843 |
+
};
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
function buildTarget(record: RunRecord, analysis?: RepoAnalysis): RunTarget {
|
| 847 |
+
if (record.targetType === "github" && record.repoUrl) {
|
| 848 |
+
const parsed = parseGitHubRepoUrl(record.repoUrl);
|
| 849 |
+
|
| 850 |
+
return {
|
| 851 |
+
type: "github",
|
| 852 |
+
repoUrl: record.repoUrl,
|
| 853 |
+
label: analysis?.label ?? parsed?.label ?? "GitHub repository",
|
| 854 |
+
branch: analysis?.branch ?? parsed?.branch,
|
| 855 |
+
scanStatus: analysis?.status ?? "pending",
|
| 856 |
+
scannedFiles: analysis?.scannedFiles ?? 0,
|
| 857 |
+
note:
|
| 858 |
+
analysis?.note ??
|
| 859 |
+
"ROCmPilot will fetch public GitHub files during the Repo Doctor stage.",
|
| 860 |
+
};
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
const sample = getSample(record.sampleId);
|
| 864 |
+
|
| 865 |
+
return {
|
| 866 |
+
type: "sample",
|
| 867 |
+
repoUrl: sample.repoUrl,
|
| 868 |
+
label: sample.name,
|
| 869 |
+
scanStatus: "fixture",
|
| 870 |
+
scannedFiles: 4,
|
| 871 |
+
note: "Using curated sample fixtures for a reliable demo run.",
|
| 872 |
+
};
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
function buildMessageId(record: RunRecord, offsetMs: number, agent: string) {
|
| 876 |
+
return `${record.id}.${offsetMs}.${agent.toLowerCase().replace(/\W+/g, "-")}`;
|
| 877 |
+
}
|
| 878 |
+
|
| 879 |
+
function buildAgentMessages(
|
| 880 |
+
elapsed: number,
|
| 881 |
+
record: RunRecord,
|
| 882 |
+
target: RunTarget,
|
| 883 |
+
sample: SampleRepo,
|
| 884 |
+
findings: Finding[],
|
| 885 |
+
patches: PatchPreview[]
|
| 886 |
+
): AgentMessage[] {
|
| 887 |
+
return WAR_ROOM_MESSAGES.filter((blueprint) => elapsed >= blueprint.offsetMs).map((blueprint) => ({
|
| 888 |
+
id: buildMessageId(record, blueprint.offsetMs, blueprint.agent),
|
| 889 |
+
agent: blueprint.agent,
|
| 890 |
+
toAgent: blueprint.toAgent,
|
| 891 |
+
role: blueprint.role,
|
| 892 |
+
task: blueprint.task,
|
| 893 |
+
leadAgent: blueprint.leadAgent,
|
| 894 |
+
kind: blueprint.kind,
|
| 895 |
+
message: blueprint.message({ target, sample, findings, patches }),
|
| 896 |
+
replyToId:
|
| 897 |
+
blueprint.replyToOffsetMs === undefined
|
| 898 |
+
? undefined
|
| 899 |
+
: WAR_ROOM_MESSAGES.find((message) => message.offsetMs === blueprint.replyToOffsetMs)
|
| 900 |
+
? buildMessageId(
|
| 901 |
+
record,
|
| 902 |
+
blueprint.replyToOffsetMs,
|
| 903 |
+
WAR_ROOM_MESSAGES.find((message) => message.offsetMs === blueprint.replyToOffsetMs)?.agent ?? "unknown"
|
| 904 |
+
)
|
| 905 |
+
: undefined,
|
| 906 |
+
memoryRefs: blueprint.memoryRefs ?? [],
|
| 907 |
+
createdAt: new Date(record.startedAt + blueprint.offsetMs).toISOString(),
|
| 908 |
+
}));
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
function buildAgentMemory(
|
| 912 |
+
elapsed: number,
|
| 913 |
+
record: RunRecord,
|
| 914 |
+
messages: AgentMessage[],
|
| 915 |
+
findings: Finding[],
|
| 916 |
+
patches: PatchPreview[]
|
| 917 |
+
): AgentMemory[] {
|
| 918 |
+
return WAR_ROOM_MEMORY.filter((memory) => elapsed >= memory.offsetMs).map((memory) => ({
|
| 919 |
+
id: memory.id,
|
| 920 |
+
title: memory.title,
|
| 921 |
+
scope: memory.scope,
|
| 922 |
+
learnedFromAgent: memory.learnedFromAgent,
|
| 923 |
+
summary: memory.summary({ findings, patches }),
|
| 924 |
+
solution: memory.solution,
|
| 925 |
+
createdAt: new Date(record.startedAt + memory.offsetMs).toISOString(),
|
| 926 |
+
usedBy: messages
|
| 927 |
+
.filter((message) => message.memoryRefs.includes(memory.id) && new Date(message.createdAt).getTime() > record.startedAt + memory.offsetMs)
|
| 928 |
+
.map((message) => message.agent),
|
| 929 |
+
}));
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
export function snapshotRun(record: RunRecord, analysis?: RepoAnalysis): RocmRun {
|
| 933 |
+
const elapsed = Math.max(0, Date.now() - record.startedAt);
|
| 934 |
+
const sample =
|
| 935 |
+
record.targetType === "github" && record.repoUrl
|
| 936 |
+
? buildGitHubSample(record.repoUrl)
|
| 937 |
+
: getSample(record.sampleId);
|
| 938 |
+
const status: RunStatus = elapsed >= TOTAL_DURATION_MS ? "completed" : "running";
|
| 939 |
+
const target = buildTarget(record, analysis);
|
| 940 |
+
let cursor = 0;
|
| 941 |
+
|
| 942 |
+
const stages = STAGES.map((stage) => {
|
| 943 |
+
const stageStart = cursor;
|
| 944 |
+
const stageEnd = cursor + stage.durationMs;
|
| 945 |
+
cursor = stageEnd;
|
| 946 |
+
|
| 947 |
+
const stageElapsed = elapsed - stageStart;
|
| 948 |
+
const progress = Math.max(0, Math.min(100, Math.round((stageElapsed / stage.durationMs) * 100)));
|
| 949 |
+
const stageStatus: StageStatus =
|
| 950 |
+
progress >= 100 ? "completed" : progress > 0 ? "running" : "pending";
|
| 951 |
+
|
| 952 |
+
return {
|
| 953 |
+
id: stage.id,
|
| 954 |
+
agent: stage.agent,
|
| 955 |
+
title: stage.title,
|
| 956 |
+
description: stage.description,
|
| 957 |
+
status: stageStatus,
|
| 958 |
+
progress,
|
| 959 |
+
startedAt: stageElapsed > 0 ? new Date(record.startedAt + stageStart).toISOString() : undefined,
|
| 960 |
+
completedAt: stageStatus === "completed" ? new Date(record.startedAt + stageEnd).toISOString() : undefined,
|
| 961 |
+
};
|
| 962 |
+
});
|
| 963 |
+
|
| 964 |
+
const allFindings = analysis?.findings.length ? analysis.findings : FINDINGS;
|
| 965 |
+
const allPatches = analysis?.patches.length ? analysis.patches : PATCHES;
|
| 966 |
+
const allBenchmarks = record.targetType === "github"
|
| 967 |
+
? BENCHMARKS.map((benchmark) => ({
|
| 968 |
+
...benchmark,
|
| 969 |
+
costNote: benchmark.costNote.replace("demo profile", "static ROCmPilot profile until live AMD validation"),
|
| 970 |
+
}))
|
| 971 |
+
: BENCHMARKS;
|
| 972 |
+
const allLogs =
|
| 973 |
+
record.targetType === "github"
|
| 974 |
+
? [
|
| 975 |
+
`queued public GitHub scan for ${target.label}`,
|
| 976 |
+
...(analysis?.logs ?? ["repo-doctor: waiting for GitHub scan results"]),
|
| 977 |
+
"build-runner: generated ROCm validation plan without mutating repository files",
|
| 978 |
+
"benchmark-agent: prepared estimated MI300X profile pending live AMD run",
|
| 979 |
+
"report-agent: preparing technical and business summary",
|
| 980 |
+
]
|
| 981 |
+
: LOGS;
|
| 982 |
+
|
| 983 |
+
const visibleFindings =
|
| 984 |
+
elapsed > 4_000
|
| 985 |
+
? allFindings.slice(0, Math.min(allFindings.length, Math.ceil((elapsed - 4_000) / 3_000)))
|
| 986 |
+
: [];
|
| 987 |
+
const visiblePatches =
|
| 988 |
+
elapsed > 10_000
|
| 989 |
+
? allPatches.slice(0, Math.min(allPatches.length, Math.ceil((elapsed - 10_000) / 4_000)))
|
| 990 |
+
: [];
|
| 991 |
+
const visibleBenchmarks = elapsed > 18_000 ? allBenchmarks : allBenchmarks.slice(0, 1);
|
| 992 |
+
const visibleLogs = allLogs.slice(0, Math.min(allLogs.length, Math.max(1, Math.ceil(elapsed / 2_300))));
|
| 993 |
+
const agentMessages = buildAgentMessages(
|
| 994 |
+
elapsed,
|
| 995 |
+
record,
|
| 996 |
+
target,
|
| 997 |
+
sample,
|
| 998 |
+
visibleFindings.length ? visibleFindings : allFindings,
|
| 999 |
+
visiblePatches.length ? visiblePatches : allPatches
|
| 1000 |
+
);
|
| 1001 |
+
const agentMemory = buildAgentMemory(
|
| 1002 |
+
elapsed,
|
| 1003 |
+
record,
|
| 1004 |
+
agentMessages,
|
| 1005 |
+
visibleFindings.length ? visibleFindings : allFindings,
|
| 1006 |
+
visiblePatches.length ? visiblePatches : allPatches
|
| 1007 |
+
);
|
| 1008 |
+
|
| 1009 |
+
return {
|
| 1010 |
+
id: record.id,
|
| 1011 |
+
sample,
|
| 1012 |
+
target,
|
| 1013 |
+
mode: record.mode,
|
| 1014 |
+
status,
|
| 1015 |
+
progress: Math.min(100, Math.round((elapsed / TOTAL_DURATION_MS) * 100)),
|
| 1016 |
+
startedAt: new Date(record.startedAt).toISOString(),
|
| 1017 |
+
completedAt: status === "completed" ? new Date(record.startedAt + TOTAL_DURATION_MS).toISOString() : undefined,
|
| 1018 |
+
stages,
|
| 1019 |
+
findings: visibleFindings,
|
| 1020 |
+
patches: visiblePatches,
|
| 1021 |
+
logs: visibleLogs,
|
| 1022 |
+
agentMessages,
|
| 1023 |
+
agentMemory,
|
| 1024 |
+
longContextMemory: getLongContextMemoryStatus(
|
| 1025 |
+
record.id,
|
| 1026 |
+
agentMemory.length,
|
| 1027 |
+
agentMessages.filter((message) => message.memoryRefs.length > 0).length
|
| 1028 |
+
),
|
| 1029 |
+
benchmarks: visibleBenchmarks,
|
| 1030 |
+
modelStatus: getModelStatus(),
|
| 1031 |
+
};
|
| 1032 |
+
}
|
| 1033 |
+
|
| 1034 |
+
export function buildFallbackReport(run: RocmRun, longContext?: string) {
|
| 1035 |
+
const findingList = run.findings
|
| 1036 |
+
.map((finding) => `- **${finding.category}** in \`${finding.file}:${finding.line}\`: ${finding.recommendedFix}`)
|
| 1037 |
+
.join("\n");
|
| 1038 |
+
const memoryList = run.agentMemory
|
| 1039 |
+
.map((memory) => `- **${memory.title}**: ${memory.solution}`)
|
| 1040 |
+
.join("\n");
|
| 1041 |
+
|
| 1042 |
+
return `# ROCmPilot Migration Report
|
| 1043 |
+
|
| 1044 |
+
## Executive Summary
|
| 1045 |
+
|
| 1046 |
+
ROCmPilot completed a multi-agent audit for **${run.sample.name}** and produced an AMD ROCm migration path for a PyTorch/vLLM workload. The system found CUDA-only assumptions, generated ROCm patch previews, and prepared the project for validation on AMD Developer Cloud.
|
| 1047 |
+
|
| 1048 |
+
## Agent Findings
|
| 1049 |
+
|
| 1050 |
+
${findingList || "- Findings are still being prepared."}
|
| 1051 |
+
|
| 1052 |
+
## Shared Agent Memory
|
| 1053 |
+
|
| 1054 |
+
${memoryList || "- Shared memory is still being written by the agents."}
|
| 1055 |
+
|
| 1056 |
+
## Long-Context Memory
|
| 1057 |
+
|
| 1058 |
+
${longContext || "- Synap memory was not available for this report, so ROCmPilot used the current run's reconstructed local memory."}
|
| 1059 |
+
|
| 1060 |
+
## AMD GPU Usage
|
| 1061 |
+
|
| 1062 |
+
- Primary model target: **Qwen/Qwen3-Coder-Next**
|
| 1063 |
+
- Serving path: **ROCm + vLLM OpenAI-compatible endpoint**
|
| 1064 |
+
- GPU goal: run the Migration Planner or Report Agent on AMD Instinct MI300X
|
| 1065 |
+
- MVP fallback: deterministic report generation when the endpoint is unavailable
|
| 1066 |
+
|
| 1067 |
+
## Business Value
|
| 1068 |
+
|
| 1069 |
+
ROCmPilot reduces the time needed to move inference services away from NVIDIA-only assumptions. Teams get a migration checklist, patch previews, benchmark evidence, and a report they can hand to infra leads before spending engineering time on a full port.
|
| 1070 |
+
|
| 1071 |
+
## Next Step
|
| 1072 |
+
|
| 1073 |
+
Connect \`AMD_QWEN_BASE_URL\` to a live ROCm/vLLM endpoint and rerun the report stage to replace demo metrics with captured MI300X evidence.`;
|
| 1074 |
+
}
|
| 1075 |
+
|
| 1076 |
+
export function buildReportPrompt(run: RocmRun, longContext?: string) {
|
| 1077 |
+
return `Create a concise hackathon submission report for ROCmPilot.
|
| 1078 |
+
|
| 1079 |
+
Product: multi-agent ROCm migration dashboard.
|
| 1080 |
+
Track: AI Agents & Agentic Workflows.
|
| 1081 |
+
Sample repo: ${run.sample.name} (${run.sample.stack}).
|
| 1082 |
+
Target: ${run.target.label} (${run.target.repoUrl}).
|
| 1083 |
+
Scan status: ${run.target.scanStatus}, scanned files: ${run.target.scannedFiles}.
|
| 1084 |
+
GPU story: Qwen3-Coder-Next served on AMD Instinct MI300X with ROCm/vLLM powers the report or migration agent when configured.
|
| 1085 |
+
|
| 1086 |
+
Findings:
|
| 1087 |
+
${run.findings.map((finding) => `- ${finding.severity}: ${finding.category} in ${finding.file}:${finding.line}. Fix: ${finding.recommendedFix}`).join("\n")}
|
| 1088 |
+
|
| 1089 |
+
Patches:
|
| 1090 |
+
${run.patches.map((patch) => `- ${patch.file}: ${patch.rationale}`).join("\n")}
|
| 1091 |
+
|
| 1092 |
+
Shared memory:
|
| 1093 |
+
${run.agentMemory.map((memory) => `- ${memory.title}: ${memory.solution}`).join("\n")}
|
| 1094 |
+
|
| 1095 |
+
Long-context memory from Synap or fallback memory:
|
| 1096 |
+
${longContext || "- No long-context memory was available beyond the current run."}
|
| 1097 |
+
|
| 1098 |
+
Benchmarks:
|
| 1099 |
+
${run.benchmarks.map((benchmark) => `- ${benchmark.label}: ${benchmark.backend}, ${benchmark.tokensPerSecond} tok/s, p95 ${benchmark.p95LatencyMs}ms, ${benchmark.memoryGb}GB.`).join("\n")}
|
| 1100 |
+
|
| 1101 |
+
Write markdown with these sections only: Executive Summary, Agent Workflow, AMD GPU Proof, Business Value, Next 48 Hours.`;
|
| 1102 |
+
}
|
src/lib/rocmpilot/github-scanner.ts
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Finding, PatchPreview } from "./types";
|
| 2 |
+
import { parseGitHubRepoUrl } from "./github-url";
|
| 3 |
+
|
| 4 |
+
type GitHubTreeItem = {
|
| 5 |
+
path: string;
|
| 6 |
+
mode: string;
|
| 7 |
+
type: "blob" | "tree";
|
| 8 |
+
sha: string;
|
| 9 |
+
size?: number;
|
| 10 |
+
url: string;
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
type GitHubTreeResponse = {
|
| 14 |
+
tree?: GitHubTreeItem[];
|
| 15 |
+
truncated?: boolean;
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
type GitHubRepoResponse = {
|
| 19 |
+
default_branch?: string;
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
type GitHubBlobResponse = {
|
| 23 |
+
content?: string;
|
| 24 |
+
encoding?: string;
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
type ScannedFile = {
|
| 28 |
+
path: string;
|
| 29 |
+
content: string;
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
export type RepoAnalysis = {
|
| 33 |
+
status: "scanned" | "failed";
|
| 34 |
+
label: string;
|
| 35 |
+
repoUrl: string;
|
| 36 |
+
branch?: string;
|
| 37 |
+
scannedFiles: number;
|
| 38 |
+
findings: Finding[];
|
| 39 |
+
patches: PatchPreview[];
|
| 40 |
+
logs: string[];
|
| 41 |
+
stack: string;
|
| 42 |
+
note: string;
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
const globalForGitHubScan = globalThis as unknown as {
|
| 46 |
+
rocmPilotScanCache?: Map<string, { expiresAt: number; analysis: RepoAnalysis }>;
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
const scanCache = globalForGitHubScan.rocmPilotScanCache ?? new Map();
|
| 50 |
+
globalForGitHubScan.rocmPilotScanCache = scanCache;
|
| 51 |
+
|
| 52 |
+
function githubHeaders() {
|
| 53 |
+
const headers: Record<string, string> = {
|
| 54 |
+
Accept: "application/vnd.github+json",
|
| 55 |
+
"User-Agent": "ROCmPilot",
|
| 56 |
+
"X-GitHub-Api-Version": "2022-11-28",
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
if (process.env.GITHUB_TOKEN) {
|
| 60 |
+
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
return headers;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
async function fetchGitHubJson<T>(url: string): Promise<T> {
|
| 67 |
+
const response = await fetch(url, {
|
| 68 |
+
headers: githubHeaders(),
|
| 69 |
+
next: { revalidate: 180 },
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
if (!response.ok) {
|
| 73 |
+
throw new Error(`GitHub returned ${response.status} for ${url}`);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
return response.json() as Promise<T>;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function isRelevantPath(path: string) {
|
| 80 |
+
const lower = path.toLowerCase();
|
| 81 |
+
|
| 82 |
+
if (
|
| 83 |
+
lower.includes("node_modules/") ||
|
| 84 |
+
lower.includes(".git/") ||
|
| 85 |
+
lower.includes("dist/") ||
|
| 86 |
+
lower.includes("build/") ||
|
| 87 |
+
lower.includes(".next/")
|
| 88 |
+
) {
|
| 89 |
+
return false;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
return (
|
| 93 |
+
/^dockerfile/i.test(path) ||
|
| 94 |
+
lower.endsWith("docker-compose.yml") ||
|
| 95 |
+
lower.endsWith("docker-compose.yaml") ||
|
| 96 |
+
lower.endsWith("requirements.txt") ||
|
| 97 |
+
lower.endsWith("requirements-rocm.txt") ||
|
| 98 |
+
lower.endsWith("pyproject.toml") ||
|
| 99 |
+
lower.endsWith("environment.yml") ||
|
| 100 |
+
lower.endsWith("environment.yaml") ||
|
| 101 |
+
lower.endsWith(".py") ||
|
| 102 |
+
lower.endsWith(".sh") ||
|
| 103 |
+
lower.endsWith(".yaml") ||
|
| 104 |
+
lower.endsWith(".yml") ||
|
| 105 |
+
lower.includes("vllm") ||
|
| 106 |
+
lower.includes("inference") ||
|
| 107 |
+
lower.includes("serve") ||
|
| 108 |
+
lower.includes("benchmark")
|
| 109 |
+
);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
function firstLineOf(content: string, pattern: RegExp) {
|
| 113 |
+
const lines = content.split(/\r?\n/);
|
| 114 |
+
const index = lines.findIndex((line) => pattern.test(line));
|
| 115 |
+
return index >= 0 ? index + 1 : 1;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
function addFinding(
|
| 119 |
+
findings: Finding[],
|
| 120 |
+
finding: Omit<Finding, "id">,
|
| 121 |
+
idSeed: string
|
| 122 |
+
) {
|
| 123 |
+
if (findings.length >= 10) {
|
| 124 |
+
return;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
const id = `${idSeed}-${findings.length + 1}`.replace(/[^a-z0-9-]/gi, "-").toLowerCase();
|
| 128 |
+
|
| 129 |
+
if (!findings.some((existing) => existing.file === finding.file && existing.category === finding.category)) {
|
| 130 |
+
findings.push({ id, ...finding });
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
function detectFindings(files: ScannedFile[]) {
|
| 135 |
+
const findings: Finding[] = [];
|
| 136 |
+
|
| 137 |
+
for (const file of files) {
|
| 138 |
+
const lowerPath = file.path.toLowerCase();
|
| 139 |
+
const content = file.content;
|
| 140 |
+
|
| 141 |
+
if (/nvidia\/cuda|nvidia-container|--gpus\s+all|nvidia-smi/i.test(content)) {
|
| 142 |
+
addFinding(
|
| 143 |
+
findings,
|
| 144 |
+
{
|
| 145 |
+
severity: "high",
|
| 146 |
+
category: "NVIDIA container/runtime assumption",
|
| 147 |
+
file: file.path,
|
| 148 |
+
line: firstLineOf(content, /nvidia\/cuda|nvidia-container|--gpus\s+all|nvidia-smi/i),
|
| 149 |
+
explanation:
|
| 150 |
+
"The repository includes NVIDIA-specific container/runtime configuration, which will not run cleanly on AMD ROCm infrastructure.",
|
| 151 |
+
recommendedFix:
|
| 152 |
+
"Add an AMD ROCm runtime path using a ROCm/vLLM image and keep NVIDIA launch flags behind a backend-specific profile.",
|
| 153 |
+
},
|
| 154 |
+
file.path
|
| 155 |
+
);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
if (/torch\.device\(\s*["']cuda["']\s*\)|\.cuda\(|\.cuda\(\)|device_map\s*=\s*["']cuda["']/i.test(content)) {
|
| 159 |
+
addFinding(
|
| 160 |
+
findings,
|
| 161 |
+
{
|
| 162 |
+
severity: "critical",
|
| 163 |
+
category: "Hardcoded CUDA device path",
|
| 164 |
+
file: file.path,
|
| 165 |
+
line: firstLineOf(content, /torch\.device\(\s*["']cuda["']\s*\)|\.cuda\(|\.cuda\(\)|device_map\s*=\s*["']cuda["']/i),
|
| 166 |
+
explanation:
|
| 167 |
+
"The code moves models/tensors directly to CUDA, so the workload needs a backend-aware device resolver before AMD validation.",
|
| 168 |
+
recommendedFix:
|
| 169 |
+
"Introduce a resolver that treats HIP-backed torch.cuda availability as ROCm and records backend provenance in logs/metrics.",
|
| 170 |
+
},
|
| 171 |
+
file.path
|
| 172 |
+
);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
if (/torch\.cuda|cuda_visible_devices|hip_visible_devices/i.test(content)) {
|
| 176 |
+
addFinding(
|
| 177 |
+
findings,
|
| 178 |
+
{
|
| 179 |
+
severity: "medium",
|
| 180 |
+
category: "GPU backend detection needs abstraction",
|
| 181 |
+
file: file.path,
|
| 182 |
+
line: firstLineOf(content, /torch\.cuda|cuda_visible_devices|hip_visible_devices/i),
|
| 183 |
+
explanation:
|
| 184 |
+
"The repo checks GPU availability through vendor-specific environment or PyTorch CUDA APIs without documenting AMD behavior.",
|
| 185 |
+
recommendedFix:
|
| 186 |
+
"Centralize backend detection and expose CUDA, ROCm, and CPU as explicit runtime modes.",
|
| 187 |
+
},
|
| 188 |
+
file.path
|
| 189 |
+
);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
if (/cu12|cu118|cu121|nvidia-|cupy-cuda|bitsandbytes|flash-attn|xformers/i.test(content)) {
|
| 193 |
+
addFinding(
|
| 194 |
+
findings,
|
| 195 |
+
{
|
| 196 |
+
severity: "high",
|
| 197 |
+
category: "CUDA-oriented dependency",
|
| 198 |
+
file: file.path,
|
| 199 |
+
line: firstLineOf(content, /cu12|cu118|cu121|nvidia-|cupy-cuda|bitsandbytes|flash-attn|xformers/i),
|
| 200 |
+
explanation:
|
| 201 |
+
"One or more dependencies are pinned to CUDA/NVIDIA builds, which can block ROCm package resolution.",
|
| 202 |
+
recommendedFix:
|
| 203 |
+
"Create a ROCm requirements profile and verify PyTorch/vLLM wheels against the target ROCm version.",
|
| 204 |
+
},
|
| 205 |
+
file.path
|
| 206 |
+
);
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
if (/vllm/i.test(content) && /tensor-parallel-size|max-model-len|served-model-name/i.test(content) === false) {
|
| 210 |
+
addFinding(
|
| 211 |
+
findings,
|
| 212 |
+
{
|
| 213 |
+
severity: "low",
|
| 214 |
+
category: "vLLM serving defaults need AMD profile",
|
| 215 |
+
file: file.path,
|
| 216 |
+
line: firstLineOf(content, /vllm/i),
|
| 217 |
+
explanation:
|
| 218 |
+
"vLLM is present, but the repo does not expose the serving knobs that matter when moving to MI300X validation.",
|
| 219 |
+
recommendedFix:
|
| 220 |
+
"Add a backend-aware vLLM launch script with model length, tensor parallelism, and metrics capture settings.",
|
| 221 |
+
},
|
| 222 |
+
file.path
|
| 223 |
+
);
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
if (lowerPath.includes("benchmark") && /tokens|latency|memory|throughput/i.test(content) === false) {
|
| 227 |
+
addFinding(
|
| 228 |
+
findings,
|
| 229 |
+
{
|
| 230 |
+
severity: "medium",
|
| 231 |
+
category: "Benchmark evidence incomplete",
|
| 232 |
+
file: file.path,
|
| 233 |
+
line: 1,
|
| 234 |
+
explanation:
|
| 235 |
+
"The benchmark file exists but does not obviously capture tokens/sec, latency, memory, and backend metadata.",
|
| 236 |
+
recommendedFix:
|
| 237 |
+
"Add a ROCm benchmark profile that emits AMD SMI/vLLM metrics for the final migration report.",
|
| 238 |
+
},
|
| 239 |
+
file.path
|
| 240 |
+
);
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
if (findings.length === 0) {
|
| 245 |
+
findings.push({
|
| 246 |
+
id: "no-direct-cuda-blockers",
|
| 247 |
+
severity: "low",
|
| 248 |
+
category: "No direct CUDA blockers in scanned files",
|
| 249 |
+
file: "repository",
|
| 250 |
+
line: 1,
|
| 251 |
+
explanation:
|
| 252 |
+
"ROCmPilot did not find obvious CUDA-only strings in the scanned files, but the workload still needs a live AMD smoke test.",
|
| 253 |
+
recommendedFix:
|
| 254 |
+
"Run the generated ROCm validation script on AMD Developer Cloud and attach the benchmark evidence to the report.",
|
| 255 |
+
});
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
return findings;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
function buildPatchPreviews(findings: Finding[]): PatchPreview[] {
|
| 262 |
+
const hasDocker = findings.some((finding) => finding.category.includes("container"));
|
| 263 |
+
const hasDevice = findings.some((finding) => finding.category.includes("CUDA device") || finding.category.includes("backend"));
|
| 264 |
+
const hasDeps = findings.some((finding) => finding.category.includes("dependency"));
|
| 265 |
+
const patches: PatchPreview[] = [];
|
| 266 |
+
|
| 267 |
+
if (hasDevice) {
|
| 268 |
+
patches.push({
|
| 269 |
+
id: "device-resolver",
|
| 270 |
+
file: "src/rocmpilot_device.py",
|
| 271 |
+
rationale:
|
| 272 |
+
"Adds a reusable runtime resolver so the project can run on CUDA, ROCm-backed PyTorch, or CPU without hardcoded model code.",
|
| 273 |
+
diff: `+import torch
|
| 274 |
+
+
|
| 275 |
+
+def resolve_accelerator() -> tuple[str, str]:
|
| 276 |
+
+ if torch.cuda.is_available():
|
| 277 |
+
+ backend = "rocm" if getattr(torch.version, "hip", None) else "cuda"
|
| 278 |
+
+ return "cuda", backend
|
| 279 |
+
+ return "cpu", "cpu"
|
| 280 |
+
+
|
| 281 |
+
+DEVICE, GPU_BACKEND = resolve_accelerator()
|
| 282 |
+
+print(f"ROCmPilot backend={GPU_BACKEND} device={DEVICE}")
|
| 283 |
+
`,
|
| 284 |
+
});
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
if (hasDocker) {
|
| 288 |
+
patches.push({
|
| 289 |
+
id: "dockerfile-rocm",
|
| 290 |
+
file: "Dockerfile.rocm",
|
| 291 |
+
rationale:
|
| 292 |
+
"Creates an AMD-specific runtime container while preserving the original repository for existing CUDA deployments.",
|
| 293 |
+
diff: `+FROM rocm/vllm:latest
|
| 294 |
+
+
|
| 295 |
+
+WORKDIR /workspace
|
| 296 |
+
+COPY . .
|
| 297 |
+
+ENV HIP_VISIBLE_DEVICES=0
|
| 298 |
+
+ENV VLLM_USE_ROCM=1
|
| 299 |
+
+RUN pip install --no-cache-dir -r requirements-rocm.txt
|
| 300 |
+
+CMD ["bash", "scripts/serve-rocm.sh"]
|
| 301 |
+
`,
|
| 302 |
+
});
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
if (hasDeps) {
|
| 306 |
+
patches.push({
|
| 307 |
+
id: "requirements-rocm",
|
| 308 |
+
file: "requirements-rocm.txt",
|
| 309 |
+
rationale:
|
| 310 |
+
"Separates ROCm dependencies from CUDA pins so CI and AMD Developer Cloud validation can install a clean environment.",
|
| 311 |
+
diff: `+torch
|
| 312 |
+
+transformers
|
| 313 |
+
+accelerate
|
| 314 |
+
+vllm
|
| 315 |
+
+sentencepiece
|
| 316 |
+
+# Verify exact ROCm-compatible wheel versions in AMD Developer Cloud before production use.
|
| 317 |
+
`,
|
| 318 |
+
});
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
patches.push({
|
| 322 |
+
id: "serve-rocm",
|
| 323 |
+
file: "scripts/serve-rocm.sh",
|
| 324 |
+
rationale:
|
| 325 |
+
"Provides the OpenAI-compatible vLLM endpoint that ROCmPilot can call for the Report Agent on AMD hardware.",
|
| 326 |
+
diff: `+#!/usr/bin/env bash
|
| 327 |
+
+set -euo pipefail
|
| 328 |
+
+
|
| 329 |
+
+MODEL="\${MODEL:-Qwen/Qwen3-Coder-Next}"
|
| 330 |
+
+PORT="\${PORT:-8000}"
|
| 331 |
+
+
|
| 332 |
+
+python -m vllm.entrypoints.openai.api_server \\
|
| 333 |
+
+ --model "$MODEL" \\
|
| 334 |
+
+ --host 0.0.0.0 \\
|
| 335 |
+
+ --port "$PORT" \\
|
| 336 |
+
+ --tensor-parallel-size "\${TENSOR_PARALLEL_SIZE:-1}" \\
|
| 337 |
+
+ --max-model-len "\${MAX_MODEL_LEN:-32768}"
|
| 338 |
+
`,
|
| 339 |
+
});
|
| 340 |
+
|
| 341 |
+
return patches.slice(0, 4);
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
function inferStack(files: ScannedFile[]) {
|
| 345 |
+
const joined = files.map((file) => `${file.path}\n${file.content.slice(0, 2_000)}`).join("\n").toLowerCase();
|
| 346 |
+
const stack = new Set<string>();
|
| 347 |
+
|
| 348 |
+
if (joined.includes("vllm")) stack.add("vLLM");
|
| 349 |
+
if (joined.includes("torch")) stack.add("PyTorch");
|
| 350 |
+
if (joined.includes("transformers")) stack.add("Transformers");
|
| 351 |
+
if (joined.includes("fastapi")) stack.add("FastAPI");
|
| 352 |
+
if (joined.includes("dockerfile")) stack.add("Docker");
|
| 353 |
+
if (joined.includes("langchain")) stack.add("LangChain");
|
| 354 |
+
if (joined.includes("crewai")) stack.add("CrewAI");
|
| 355 |
+
|
| 356 |
+
return stack.size > 0 ? Array.from(stack).join(", ") : "Python/AI workload";
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
async function fetchRelevantFiles(owner: string, repo: string, ref: string) {
|
| 360 |
+
const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${encodeURIComponent(ref)}?recursive=1`;
|
| 361 |
+
const tree = await fetchGitHubJson<GitHubTreeResponse>(treeUrl);
|
| 362 |
+
const blobs = (tree.tree ?? [])
|
| 363 |
+
.filter((item) => item.type === "blob" && isRelevantPath(item.path) && (item.size ?? 0) <= 120_000)
|
| 364 |
+
.slice(0, 32);
|
| 365 |
+
|
| 366 |
+
const files: ScannedFile[] = [];
|
| 367 |
+
|
| 368 |
+
for (const blob of blobs) {
|
| 369 |
+
const data = await fetchGitHubJson<GitHubBlobResponse>(
|
| 370 |
+
`https://api.github.com/repos/${owner}/${repo}/git/blobs/${blob.sha}`
|
| 371 |
+
);
|
| 372 |
+
|
| 373 |
+
if (data.encoding === "base64" && data.content) {
|
| 374 |
+
files.push({
|
| 375 |
+
path: blob.path,
|
| 376 |
+
content: Buffer.from(data.content.replace(/\n/g, ""), "base64").toString("utf8"),
|
| 377 |
+
});
|
| 378 |
+
}
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
return files;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
export async function analyzeGitHubRepository(repoUrl: string): Promise<RepoAnalysis> {
|
| 385 |
+
const parsed = parseGitHubRepoUrl(repoUrl);
|
| 386 |
+
|
| 387 |
+
if (!parsed) {
|
| 388 |
+
return failedAnalysis(repoUrl, "Invalid GitHub URL.");
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
const cacheKey = parsed.repoUrl;
|
| 392 |
+
const cached = scanCache.get(cacheKey);
|
| 393 |
+
|
| 394 |
+
if (cached && cached.expiresAt > Date.now()) {
|
| 395 |
+
return cached.analysis;
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
try {
|
| 399 |
+
const repo = await fetchGitHubJson<GitHubRepoResponse>(
|
| 400 |
+
`https://api.github.com/repos/${parsed.owner}/${parsed.repo}`
|
| 401 |
+
);
|
| 402 |
+
const ref = parsed.branch ?? repo.default_branch ?? "main";
|
| 403 |
+
const files = await fetchRelevantFiles(parsed.owner, parsed.repo, ref);
|
| 404 |
+
const findings = detectFindings(files);
|
| 405 |
+
const patches = buildPatchPreviews(findings);
|
| 406 |
+
const stack = inferStack(files);
|
| 407 |
+
const analysis: RepoAnalysis = {
|
| 408 |
+
status: "scanned",
|
| 409 |
+
label: parsed.label,
|
| 410 |
+
repoUrl: parsed.repoUrl,
|
| 411 |
+
branch: ref,
|
| 412 |
+
scannedFiles: files.length,
|
| 413 |
+
findings,
|
| 414 |
+
patches,
|
| 415 |
+
stack,
|
| 416 |
+
note: `Scanned ${files.length} public GitHub files from ${parsed.label}@${ref}.`,
|
| 417 |
+
logs: [
|
| 418 |
+
`github-scan: resolved ${parsed.label}@${ref}`,
|
| 419 |
+
`github-scan: selected ${files.length} relevant files for ROCm analysis`,
|
| 420 |
+
`repo-doctor: detected stack profile: ${stack}`,
|
| 421 |
+
`migration-planner: produced ${findings.length} findings and ${patches.length} patch previews`,
|
| 422 |
+
],
|
| 423 |
+
};
|
| 424 |
+
|
| 425 |
+
scanCache.set(cacheKey, { analysis, expiresAt: Date.now() + 180_000 });
|
| 426 |
+
return analysis;
|
| 427 |
+
} catch (error) {
|
| 428 |
+
return failedAnalysis(
|
| 429 |
+
parsed.repoUrl,
|
| 430 |
+
error instanceof Error ? error.message : "Unknown GitHub scan failure.",
|
| 431 |
+
parsed.label,
|
| 432 |
+
parsed.branch
|
| 433 |
+
);
|
| 434 |
+
}
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
function failedAnalysis(repoUrl: string, message: string, label = "GitHub repository", branch?: string): RepoAnalysis {
|
| 438 |
+
return {
|
| 439 |
+
status: "failed",
|
| 440 |
+
label,
|
| 441 |
+
repoUrl,
|
| 442 |
+
branch,
|
| 443 |
+
scannedFiles: 0,
|
| 444 |
+
stack: "Public GitHub repository",
|
| 445 |
+
note: message,
|
| 446 |
+
findings: [
|
| 447 |
+
{
|
| 448 |
+
id: "github-scan-failed",
|
| 449 |
+
severity: "medium",
|
| 450 |
+
category: "GitHub scan unavailable",
|
| 451 |
+
file: "repository",
|
| 452 |
+
line: 1,
|
| 453 |
+
explanation:
|
| 454 |
+
"ROCmPilot could not fetch enough public repository data to complete a live scan. The app remains usable with sample fixtures.",
|
| 455 |
+
recommendedFix:
|
| 456 |
+
"Check that the repository is public, add GITHUB_TOKEN for higher API limits, or use the sample workload for the demo.",
|
| 457 |
+
},
|
| 458 |
+
],
|
| 459 |
+
patches: buildPatchPreviews([]),
|
| 460 |
+
logs: [`github-scan: ${message}`, "fallback: using safe ROCm migration guidance"],
|
| 461 |
+
};
|
| 462 |
+
}
|
src/lib/rocmpilot/github-url.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type GitHubRepoRef = {
|
| 2 |
+
owner: string;
|
| 3 |
+
repo: string;
|
| 4 |
+
branch?: string;
|
| 5 |
+
repoUrl: string;
|
| 6 |
+
label: string;
|
| 7 |
+
};
|
| 8 |
+
|
| 9 |
+
export function parseGitHubRepoUrl(input: string | undefined): GitHubRepoRef | null {
|
| 10 |
+
if (!input) {
|
| 11 |
+
return null;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
try {
|
| 15 |
+
const url = new URL(input.trim());
|
| 16 |
+
|
| 17 |
+
if (url.hostname !== "github.com" && url.hostname !== "www.github.com") {
|
| 18 |
+
return null;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
const [owner, repoWithSuffix, maybeTree, ...rest] = url.pathname
|
| 22 |
+
.split("/")
|
| 23 |
+
.filter(Boolean);
|
| 24 |
+
const repo = repoWithSuffix?.replace(/\.git$/, "");
|
| 25 |
+
|
| 26 |
+
if (!owner || !repo) {
|
| 27 |
+
return null;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
const branch = maybeTree === "tree" && rest.length > 0 ? rest.join("/") : undefined;
|
| 31 |
+
|
| 32 |
+
return {
|
| 33 |
+
owner,
|
| 34 |
+
repo,
|
| 35 |
+
branch,
|
| 36 |
+
repoUrl: `https://github.com/${owner}/${repo}${branch ? `/tree/${branch}` : ""}`,
|
| 37 |
+
label: `${owner}/${repo}`,
|
| 38 |
+
};
|
| 39 |
+
} catch {
|
| 40 |
+
return null;
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export function isRealGitHubRepoUrl(input: string | undefined) {
|
| 45 |
+
const repo = parseGitHubRepoUrl(input);
|
| 46 |
+
return Boolean(repo && repo.owner !== "example");
|
| 47 |
+
}
|
src/lib/rocmpilot/memory-ids.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const DEFAULT_MEMORY_CUSTOMER_ID = "rocmpilot-hackathon";
|
| 2 |
+
export const DEFAULT_MEMORY_USER_ID = "rocmpilot-agent-fleet";
|
| 3 |
+
|
| 4 |
+
export function buildMemoryConversationId(runId: string) {
|
| 5 |
+
return `rocmpilot-${runId.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 140)}`;
|
| 6 |
+
}
|
src/lib/rocmpilot/store.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createRunRecord, parseRunRecord, snapshotRun } from "./data";
|
| 2 |
+
import { analyzeGitHubRepository } from "./github-scanner";
|
| 3 |
+
import type { RocmRun, RunMode } from "./types";
|
| 4 |
+
|
| 5 |
+
export function createRun(sampleId: string, mode: RunMode = "mock", repoUrl?: string): RocmRun {
|
| 6 |
+
const record = createRunRecord(sampleId, mode, repoUrl);
|
| 7 |
+
return snapshotRun(record);
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export async function getRun(runId: string): Promise<RocmRun | null> {
|
| 11 |
+
const record = parseRunRecord(runId);
|
| 12 |
+
|
| 13 |
+
if (!record) {
|
| 14 |
+
return null;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
if (record.targetType === "github" && record.repoUrl) {
|
| 18 |
+
const analysis = await analyzeGitHubRepository(record.repoUrl);
|
| 19 |
+
return snapshotRun(record, analysis);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
return snapshotRun(record);
|
| 23 |
+
}
|
src/lib/rocmpilot/synap-memory.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type {
|
| 2 |
+
ChatMessage,
|
| 3 |
+
ContextForPromptResult,
|
| 4 |
+
ContextResponse,
|
| 5 |
+
SynapClient,
|
| 6 |
+
SynapClientOptions,
|
| 7 |
+
} from "@maximem/synap-js-sdk";
|
| 8 |
+
import {
|
| 9 |
+
getLongContextMemoryStatus,
|
| 10 |
+
} from "./data";
|
| 11 |
+
import {
|
| 12 |
+
buildMemoryConversationId,
|
| 13 |
+
DEFAULT_MEMORY_CUSTOMER_ID,
|
| 14 |
+
DEFAULT_MEMORY_USER_ID,
|
| 15 |
+
} from "./memory-ids";
|
| 16 |
+
import type { LongContextMemoryStatus, RocmRun } from "./types";
|
| 17 |
+
|
| 18 |
+
type SynapSyncResult = {
|
| 19 |
+
status: LongContextMemoryStatus;
|
| 20 |
+
promptContext: string;
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
function parseOptionalPort(value: string | undefined) {
|
| 24 |
+
if (!value) {
|
| 25 |
+
return undefined;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
const port = Number.parseInt(value, 10);
|
| 29 |
+
return Number.isFinite(port) ? port : undefined;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
function parseOptionalBoolean(value: string | undefined) {
|
| 33 |
+
if (value === undefined) {
|
| 34 |
+
return undefined;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
return ["1", "true", "yes"].includes(value.toLowerCase());
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
function getSynapIdentity(run: RocmRun) {
|
| 41 |
+
return {
|
| 42 |
+
conversationId: buildMemoryConversationId(run.id),
|
| 43 |
+
customerId: process.env.SYNAP_CUSTOMER_ID ?? DEFAULT_MEMORY_CUSTOMER_ID,
|
| 44 |
+
userId: process.env.SYNAP_USER_ID ?? DEFAULT_MEMORY_USER_ID,
|
| 45 |
+
};
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
function buildSynapMessages(run: RocmRun): ChatMessage[] {
|
| 49 |
+
const targetSummary = [
|
| 50 |
+
`ROCmPilot run ${run.id}`,
|
| 51 |
+
`Target: ${run.target.label} (${run.target.repoUrl})`,
|
| 52 |
+
`Scan status: ${run.target.scanStatus}; scanned files: ${run.target.scannedFiles}`,
|
| 53 |
+
`Goal: migrate PyTorch/vLLM workload toward AMD ROCm readiness.`,
|
| 54 |
+
].join("\n");
|
| 55 |
+
|
| 56 |
+
const agentDiscussion = run.agentMessages.map((message) => ({
|
| 57 |
+
role: "assistant" as const,
|
| 58 |
+
content: [
|
| 59 |
+
`[${message.agent} -> ${message.toAgent}] ${message.kind.toUpperCase()}`,
|
| 60 |
+
`Task: ${message.task}`,
|
| 61 |
+
`Lead: ${message.leadAgent}`,
|
| 62 |
+
`Message: ${message.message}`,
|
| 63 |
+
message.memoryRefs.length ? `Memory refs: ${message.memoryRefs.join(", ")}` : "",
|
| 64 |
+
]
|
| 65 |
+
.filter(Boolean)
|
| 66 |
+
.join("\n"),
|
| 67 |
+
metadata: {
|
| 68 |
+
agent: message.agent,
|
| 69 |
+
toAgent: message.toAgent,
|
| 70 |
+
kind: message.kind,
|
| 71 |
+
task: message.task,
|
| 72 |
+
leadAgent: message.leadAgent,
|
| 73 |
+
runId: run.id,
|
| 74 |
+
},
|
| 75 |
+
}));
|
| 76 |
+
|
| 77 |
+
const sharedMemory = run.agentMemory.map((memory) => ({
|
| 78 |
+
role: "assistant" as const,
|
| 79 |
+
content: [
|
| 80 |
+
`Shared memory: ${memory.title}`,
|
| 81 |
+
`Scope: ${memory.scope}`,
|
| 82 |
+
`Learned from: ${memory.learnedFromAgent}`,
|
| 83 |
+
`Summary: ${memory.summary}`,
|
| 84 |
+
`Reusable solution: ${memory.solution}`,
|
| 85 |
+
memory.usedBy.length ? `Reused by: ${Array.from(new Set(memory.usedBy)).join(", ")}` : "",
|
| 86 |
+
]
|
| 87 |
+
.filter(Boolean)
|
| 88 |
+
.join("\n"),
|
| 89 |
+
metadata: {
|
| 90 |
+
memoryId: memory.id,
|
| 91 |
+
scope: memory.scope,
|
| 92 |
+
learnedFromAgent: memory.learnedFromAgent,
|
| 93 |
+
runId: run.id,
|
| 94 |
+
},
|
| 95 |
+
}));
|
| 96 |
+
|
| 97 |
+
return [
|
| 98 |
+
{
|
| 99 |
+
role: "user",
|
| 100 |
+
content: targetSummary,
|
| 101 |
+
metadata: {
|
| 102 |
+
runId: run.id,
|
| 103 |
+
target: run.target.label,
|
| 104 |
+
repository: run.target.repoUrl,
|
| 105 |
+
},
|
| 106 |
+
},
|
| 107 |
+
...agentDiscussion,
|
| 108 |
+
...sharedMemory,
|
| 109 |
+
];
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
function buildLocalMemoryContext(run: RocmRun) {
|
| 113 |
+
const memories = run.agentMemory
|
| 114 |
+
.map((memory) => `- ${memory.title} (${memory.scope}): ${memory.solution}`)
|
| 115 |
+
.join("\n");
|
| 116 |
+
const recentDiscussion = run.agentMessages
|
| 117 |
+
.slice(-8)
|
| 118 |
+
.map((message) => `- ${message.agent} -> ${message.toAgent}: ${message.message}`)
|
| 119 |
+
.join("\n");
|
| 120 |
+
|
| 121 |
+
return [
|
| 122 |
+
"Current run memory:",
|
| 123 |
+
memories || "- No shared memory has been written yet.",
|
| 124 |
+
"",
|
| 125 |
+
"Recent agent discussion:",
|
| 126 |
+
recentDiscussion || "- No agent discussion is available yet.",
|
| 127 |
+
].join("\n");
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
function countContextItems(context: ContextResponse | null) {
|
| 131 |
+
if (!context) {
|
| 132 |
+
return 0;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
return (
|
| 136 |
+
(context.facts?.length ?? 0) +
|
| 137 |
+
(context.preferences?.length ?? 0) +
|
| 138 |
+
(context.episodes?.length ?? 0) +
|
| 139 |
+
(context.emotions?.length ?? 0) +
|
| 140 |
+
(context.temporalEvents?.length ?? 0)
|
| 141 |
+
);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
function summarizeContext(
|
| 145 |
+
context: ContextResponse | null,
|
| 146 |
+
promptContext: ContextForPromptResult | null
|
| 147 |
+
) {
|
| 148 |
+
const sections: string[] = [];
|
| 149 |
+
|
| 150 |
+
if (promptContext?.formattedContext) {
|
| 151 |
+
sections.push(`Synap compacted context:\n${promptContext.formattedContext}`);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
if (context?.facts?.length) {
|
| 155 |
+
sections.push(
|
| 156 |
+
`Synap facts:\n${context.facts
|
| 157 |
+
.slice(0, 5)
|
| 158 |
+
.map((fact) => `- ${fact.content}`)
|
| 159 |
+
.join("\n")}`
|
| 160 |
+
);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
if (context?.episodes?.length) {
|
| 164 |
+
sections.push(
|
| 165 |
+
`Synap episodes:\n${context.episodes
|
| 166 |
+
.slice(0, 5)
|
| 167 |
+
.map((episode) => `- ${episode.summary}`)
|
| 168 |
+
.join("\n")}`
|
| 169 |
+
);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
if (context?.preferences?.length) {
|
| 173 |
+
sections.push(
|
| 174 |
+
`Synap preferences:\n${context.preferences
|
| 175 |
+
.slice(0, 5)
|
| 176 |
+
.map((preference) => `- ${preference.content}`)
|
| 177 |
+
.join("\n")}`
|
| 178 |
+
);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
return sections.join("\n\n");
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
function synapOptions(): SynapClientOptions {
|
| 185 |
+
return {
|
| 186 |
+
apiKey: process.env.SYNAP_API_KEY,
|
| 187 |
+
instanceId: process.env.SYNAP_INSTANCE_ID,
|
| 188 |
+
baseUrl: process.env.SYNAP_BASE_URL,
|
| 189 |
+
grpcHost: process.env.SYNAP_GRPC_HOST,
|
| 190 |
+
grpcPort: parseOptionalPort(process.env.SYNAP_GRPC_PORT),
|
| 191 |
+
grpcUseTls: parseOptionalBoolean(process.env.SYNAP_GRPC_TLS),
|
| 192 |
+
autoSetup: parseOptionalBoolean(process.env.SYNAP_AUTO_SETUP) ?? false,
|
| 193 |
+
requestTimeoutMs: 10_000,
|
| 194 |
+
initTimeoutMs: 10_000,
|
| 195 |
+
ingestTimeoutMs: 10_000,
|
| 196 |
+
onLog: (level, message) => {
|
| 197 |
+
if (level === "error") {
|
| 198 |
+
console.warn(`Synap ${level}: ${message}`);
|
| 199 |
+
}
|
| 200 |
+
},
|
| 201 |
+
} as SynapClientOptions & { instanceId?: string };
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
async function shutdownClient(client: SynapClient | null) {
|
| 205 |
+
if (!client) {
|
| 206 |
+
return;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
try {
|
| 210 |
+
await client.shutdown();
|
| 211 |
+
} catch (error) {
|
| 212 |
+
console.warn("Synap shutdown warning:", error);
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
export async function syncRunMemoryWithSynap(run: RocmRun): Promise<SynapSyncResult> {
|
| 217 |
+
const localContext = buildLocalMemoryContext(run);
|
| 218 |
+
const identity = getSynapIdentity(run);
|
| 219 |
+
|
| 220 |
+
if (!process.env.SYNAP_API_KEY) {
|
| 221 |
+
return {
|
| 222 |
+
status: getLongContextMemoryStatus(
|
| 223 |
+
run.id,
|
| 224 |
+
run.agentMemory.length,
|
| 225 |
+
run.agentMessages.filter((message) => message.memoryRefs.length > 0).length,
|
| 226 |
+
"fallback"
|
| 227 |
+
),
|
| 228 |
+
promptContext: localContext,
|
| 229 |
+
};
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
let client: SynapClient | null = null;
|
| 233 |
+
|
| 234 |
+
try {
|
| 235 |
+
const { createClient } = await import("@maximem/synap-js-sdk");
|
| 236 |
+
client = createClient(synapOptions());
|
| 237 |
+
await client.init();
|
| 238 |
+
|
| 239 |
+
const messages = buildSynapMessages(run);
|
| 240 |
+
|
| 241 |
+
await client.addMemory({
|
| 242 |
+
userId: identity.userId,
|
| 243 |
+
customerId: identity.customerId,
|
| 244 |
+
conversationId: identity.conversationId,
|
| 245 |
+
sessionId: run.id,
|
| 246 |
+
documentId: run.id,
|
| 247 |
+
documentType: "ai-chat-conversation",
|
| 248 |
+
documentCreatedAt: run.startedAt,
|
| 249 |
+
mode: "long-range",
|
| 250 |
+
metadata: {
|
| 251 |
+
product: "ROCmPilot",
|
| 252 |
+
track: "AI Agents & Agentic Workflows",
|
| 253 |
+
targetLabel: run.target.label,
|
| 254 |
+
targetRepo: run.target.repoUrl,
|
| 255 |
+
scanStatus: run.target.scanStatus,
|
| 256 |
+
agentMessages: run.agentMessages.length,
|
| 257 |
+
sharedMemories: run.agentMemory.length,
|
| 258 |
+
},
|
| 259 |
+
messages,
|
| 260 |
+
});
|
| 261 |
+
|
| 262 |
+
const [contextResult, promptContextResult] = await Promise.allSettled([
|
| 263 |
+
client.fetchUserContext({
|
| 264 |
+
userId: identity.userId,
|
| 265 |
+
customerId: identity.customerId,
|
| 266 |
+
conversationId: identity.conversationId,
|
| 267 |
+
searchQuery: [
|
| 268 |
+
"ROCm migration blockers",
|
| 269 |
+
"CUDA assumptions and AMD validation",
|
| 270 |
+
"agent decisions from prior ROCmPilot runs",
|
| 271 |
+
],
|
| 272 |
+
maxResults: 8,
|
| 273 |
+
mode: "accurate",
|
| 274 |
+
}),
|
| 275 |
+
client.getContextForPrompt({
|
| 276 |
+
conversationId: identity.conversationId,
|
| 277 |
+
style: "structured",
|
| 278 |
+
}),
|
| 279 |
+
]);
|
| 280 |
+
|
| 281 |
+
const context = contextResult.status === "fulfilled" ? contextResult.value : null;
|
| 282 |
+
const promptContext =
|
| 283 |
+
promptContextResult.status === "fulfilled" ? promptContextResult.value : null;
|
| 284 |
+
const synapContext = summarizeContext(context, promptContext);
|
| 285 |
+
const recalledItems =
|
| 286 |
+
countContextItems(context) + (promptContext?.recentMessageCount ?? 0);
|
| 287 |
+
|
| 288 |
+
return {
|
| 289 |
+
status: getLongContextMemoryStatus(
|
| 290 |
+
run.id,
|
| 291 |
+
messages.length,
|
| 292 |
+
recalledItems,
|
| 293 |
+
"connected"
|
| 294 |
+
),
|
| 295 |
+
promptContext: synapContext || localContext,
|
| 296 |
+
};
|
| 297 |
+
} catch (error) {
|
| 298 |
+
console.warn("Synap memory fallback:", error);
|
| 299 |
+
|
| 300 |
+
return {
|
| 301 |
+
status: {
|
| 302 |
+
...getLongContextMemoryStatus(
|
| 303 |
+
run.id,
|
| 304 |
+
run.agentMemory.length,
|
| 305 |
+
run.agentMessages.filter((message) => message.memoryRefs.length > 0).length,
|
| 306 |
+
"fallback"
|
| 307 |
+
),
|
| 308 |
+
detail:
|
| 309 |
+
"Synap credentials are present, but the SDK runtime could not complete ingestion. Using local run memory for this report.",
|
| 310 |
+
},
|
| 311 |
+
promptContext: localContext,
|
| 312 |
+
};
|
| 313 |
+
} finally {
|
| 314 |
+
await shutdownClient(client);
|
| 315 |
+
}
|
| 316 |
+
}
|