-
Notifications
You must be signed in to change notification settings - Fork 3
Feat/web dashboard #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5892313
802de7f
3ab1edf
fc4f921
8dbd467
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -107,12 +107,21 @@ packages/ | |
| Set these variables in `apps/api/.env`: | ||
|
|
||
| ```bash | ||
| STACKFORGE_PROVIDER=openrouter | ||
| OPENROUTER_API_KEY=your_key_here | ||
| OPENROUTER_ENDPOINT=https://openrouter.ai/api/v1/chat/completions | ||
| OPENROUTER_APP_NAME=stackforge-api | ||
| OPENROUTER_APP_URL=http://localhost:3001 | ||
| ``` | ||
|
|
||
| `STACKFORGE_PROVIDER` supports: | ||
| - `openrouter`: forces real LLM execution (requires `OPENROUTER_API_KEY`) | ||
| - `mock`: forces deterministic offline agent responses | ||
| - omitted/`auto`: uses `openrouter` when API key is present, otherwise `mock` | ||
|
|
||
| Runtime/provider health is exposed at: | ||
| - `GET /api/runtime` | ||
|
|
||
|
Comment on lines
+122
to
+124
|
||
| --- | ||
|
|
||
| ## 📉 Token Tuning Guide | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,18 @@ const PORT = process.env["PORT"] ?? "3001"; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.use(express.json()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // CORS for Vite dev server | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.use((_req, res, next) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.setHeader("Access-Control-Allow-Origin", "*"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (_req.method === "OPTIONS") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.sendStatus(204); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
8
to
+19
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app.use(express.json()); | |
| // CORS for Vite dev server | |
| app.use((_req, res, next) => { | |
| res.setHeader("Access-Control-Allow-Origin", "*"); | |
| res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); | |
| res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); | |
| if (_req.method === "OPTIONS") { | |
| res.sendStatus(204); | |
| return; | |
| } | |
| const allowedOrigins = | |
| (process.env["CORS_ORIGINS"] ?? "") | |
| .split(",") | |
| .map((origin) => origin.trim()) | |
| .filter((origin) => origin.length > 0); | |
| app.use(express.json()); | |
| // CORS for Vite dev server (permissive in development, restricted by allowlist otherwise) | |
| app.use((_req, res, next) => { | |
| const origin = _req.headers.origin as string | undefined; | |
| if (process.env["NODE_ENV"] === "development") { | |
| res.setHeader("Access-Control-Allow-Origin", "*"); | |
| } else if (origin && allowedOrigins.includes(origin)) { | |
| res.setHeader("Access-Control-Allow-Origin", origin); | |
| } | |
| res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); | |
| res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); | |
| if (_req.method === "OPTIONS") { | |
| res.sendStatus(204); | |
| return; | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,10 +1,16 @@ | ||||||||||||||||||
| import { Router, type IRouter } from "express"; | ||||||||||||||||||
| import { generateController } from "../controllers/generate.controller.js"; | ||||||||||||||||||
| import { listJobsController, getJobController, streamController } from "../controllers/jobs.controller.js"; | ||||||||||||||||||
| import { | ||||||||||||||||||
| runtimeController, | ||||||||||||||||||
| listJobsController, | ||||||||||||||||||
| getJobController, | ||||||||||||||||||
| streamController, | ||||||||||||||||||
|
Comment on lines
+4
to
+7
|
||||||||||||||||||
| runtimeController, | |
| listJobsController, | |
| getJobController, | |
| streamController, | |
| runtimeController, | |
| listJobsController, | |
| getJobController, | |
| streamController, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <meta name="description" content="StackForge — AI-powered full-stack project scaffolding. Describe your idea, pick your stack, and watch agents build your blueprint in real time." /> | ||
| <title>StackForge — AI Project Scaffolding</title> | ||
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | ||
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet" /> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.tsx"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import React from "react"; | ||
| import { BrowserRouter, Routes, Route } from "react-router-dom"; | ||
| import { ToastProvider } from "@stackforge/ui"; | ||
| import { Layout } from "./components/Layout"; | ||
| import { Home } from "./pages/Home"; | ||
| import { JobPage } from "./pages/JobPage"; | ||
|
|
||
| export function App() { | ||
| return ( | ||
| <BrowserRouter> | ||
| <ToastProvider> | ||
| <Layout> | ||
| <Routes> | ||
| <Route path="/" element={<Home />} /> | ||
| <Route path="/jobs/:jobId" element={<JobPage />} /> | ||
| </Routes> | ||
| </Layout> | ||
| </ToastProvider> | ||
| </BrowserRouter> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import React from "react"; | ||
| import { Badge, Card, Spinner } from "@stackforge/ui"; | ||
| import type { AgentState } from "../hooks/useJobStream"; | ||
|
|
||
| const AGENT_META: Record<string, { icon: string; label: string; description: string }> = { | ||
| planner: { icon: "📋", label: "Planner", description: "Defines project structure and tech stack" }, | ||
| schema: { icon: "🗄️", label: "Schema", description: "Designs database entities and relationships" }, | ||
| api: { icon: "⚡", label: "API", description: "Plans API routes and endpoints" }, | ||
| frontend: { icon: "🎨", label: "Frontend", description: "Creates frontend pages and components" }, | ||
| devops: { icon: "🚀", label: "DevOps", description: "Sets up CI/CD, Docker, and deployment" }, | ||
| reviewer: { icon: "🔍", label: "Reviewer", description: "Reviews the entire blueprint for quality" }, | ||
| }; | ||
|
|
||
| export function AgentCard({ agent }: { agent: AgentState }) { | ||
| const meta = AGENT_META[agent.name] ?? { icon: "🤖", label: agent.name, description: "" }; | ||
|
|
||
| const borderColor = | ||
| agent.status === "running" | ||
| ? "rgba(56, 189, 248, 0.3)" | ||
| : agent.status === "completed" | ||
| ? "rgba(52, 211, 153, 0.2)" | ||
| : agent.status === "failed" | ||
| ? "rgba(244, 63, 94, 0.2)" | ||
| : "#23232f"; | ||
|
|
||
| return ( | ||
| <Card | ||
| hover={false} | ||
| style={{ | ||
| padding: "18px 20px", | ||
| borderColor, | ||
| borderRadius: "14px", | ||
| animation: agent.status === "running" ? "pulse-glow 2s ease-in-out infinite" : undefined, | ||
| transition: "all 350ms cubic-bezier(0.4, 0, 0.2, 1)", | ||
| }} | ||
| > | ||
| <div style={{ display: "flex", alignItems: "center", gap: "14px" }}> | ||
| {/* Icon */} | ||
| <div | ||
| style={{ | ||
| width: "42px", | ||
| height: "42px", | ||
| borderRadius: "12px", | ||
| background: | ||
| agent.status === "running" | ||
| ? "rgba(56, 189, 248, 0.1)" | ||
| : agent.status === "completed" | ||
| ? "rgba(52, 211, 153, 0.1)" | ||
| : "rgba(92, 92, 111, 0.1)", | ||
| display: "flex", | ||
| alignItems: "center", | ||
| justifyContent: "center", | ||
| fontSize: "20px", | ||
| flexShrink: 0, | ||
| }} | ||
| > | ||
| {agent.status === "running" ? <Spinner size={18} color="#38bdf8" /> : meta.icon} | ||
| </div> | ||
|
|
||
| {/* Info */} | ||
| <div style={{ flex: 1, minWidth: 0 }}> | ||
| <div style={{ display: "flex", alignItems: "center", gap: "10px", marginBottom: "4px" }}> | ||
| <span style={{ fontSize: "15px", fontWeight: 600 }}>{meta.label}</span> | ||
| <Badge status={agent.status} /> | ||
| </div> | ||
| <p style={{ fontSize: "13px", color: "#9898a8", margin: 0 }}>{meta.description}</p> | ||
| </div> | ||
|
|
||
| {/* Duration / details */} | ||
| <div style={{ textAlign: "right", flexShrink: 0 }}> | ||
| {agent.durationMs != null && ( | ||
| <span style={{ fontSize: "13px", color: "#9898a8", fontVariantNumeric: "tabular-nums" }}> | ||
| {(agent.durationMs / 1000).toFixed(1)}s | ||
| </span> | ||
| )} | ||
| {agent.totalTokens != null && ( | ||
| <div style={{ fontSize: "11px", color: "#5c5c6f", marginTop: "2px" }}> | ||
| {agent.totalTokens.toLocaleString()} tokens | ||
| </div> | ||
| )} | ||
| {agent.error && ( | ||
| <span style={{ fontSize: "12px", color: "#f43f5e" }}>{agent.error}</span> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </Card> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
STACKFORGE_PROVIDERis set toopenrouterin the README example, but the text below says the default/auto mode will fall back tomockwhen no API key is present. Settingopenrouterhere will force 503 errors for anyone who hasn’t setOPENROUTER_API_KEY. Consider changing the example toSTACKFORGE_PROVIDER=auto(or omitting it) and mentioningopenrouteras an optional override.