emergent-agent-e1 commited on
Commit ·
2f00f43
1
Parent(s): 7a7199f
Auto-generated changes
Browse files- .emergent/emergent.yml +3 -1
- .gitconfig +3 -0
- .gitignore +9 -1
- backend/agents.py +198 -0
- backend/server.py +310 -56
- frontend/public/index.html +1 -1
- frontend/src/App.css +185 -25
- frontend/src/App.js +15 -40
- frontend/src/components/AgentTranscript.jsx +111 -0
- frontend/src/components/Nav.jsx +54 -0
- frontend/src/components/TelemetryWidget.jsx +67 -0
- frontend/src/index.css +25 -76
- frontend/src/lib/api.js +18 -0
- frontend/src/pages/Blueprint.jsx +128 -0
- frontend/src/pages/Console.jsx +227 -0
- frontend/src/pages/Feed.jsx +174 -0
- frontend/src/pages/Journal.jsx +187 -0
- frontend/src/pages/Landing.jsx +164 -0
- frontend/yarn.lock +0 -0
- yarn.lock +4 -0
.emergent/emergent.yml
CHANGED
|
@@ -1,3 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"env_image_name": "fastapi_react_mongo_shadcn_base_image_cloud_arm:release-17042026-1"
|
|
|
|
|
|
|
| 3 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"env_image_name": "fastapi_react_mongo_shadcn_base_image_cloud_arm:release-17042026-1",
|
| 3 |
+
"job_id": "d5829a2e-bc03-4880-adcd-73acc809a3bd",
|
| 4 |
+
"created_at": "2026-05-01T17:02:20.223681+00:00Z"
|
| 5 |
}
|
.gitconfig
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[user]
|
| 2 |
+
email = github@emergent.sh
|
| 3 |
+
name = emergent-agent-e1
|
.gitignore
CHANGED
|
@@ -79,4 +79,12 @@ agenthub/agents/youtube/db
|
|
| 79 |
memory/test_credentials.md
|
| 80 |
|
| 81 |
# Mobile development
|
| 82 |
-
android-sdk/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
memory/test_credentials.md
|
| 80 |
|
| 81 |
# Mobile development
|
| 82 |
+
android-sdk/-e
|
| 83 |
+
# Environment and credential files
|
| 84 |
+
.env
|
| 85 |
+
.env.*
|
| 86 |
+
*.env
|
| 87 |
+
credentials.json
|
| 88 |
+
*.pem
|
| 89 |
+
*.key
|
| 90 |
+
.credentials
|
backend/agents.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ForgeSight multi-agent quality-control pipeline.
|
| 3 |
+
Uses emergentintegrations.LlmChat with the Emergent Universal LLM key.
|
| 4 |
+
Each agent gets a fresh LlmChat session (per the playbook guidance).
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import uuid
|
| 9 |
+
import re
|
| 10 |
+
from typing import Optional, List, Dict, Any
|
| 11 |
+
from emergentintegrations.llm.chat import LlmChat, UserMessage, ImageContent
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
EMERGENT_LLM_KEY = os.environ.get("EMERGENT_LLM_KEY", "")
|
| 15 |
+
|
| 16 |
+
# Model choices — Claude Sonnet 4.5 is vision-capable and strong for reasoning.
|
| 17 |
+
VISION_MODEL = ("anthropic", "claude-sonnet-4-5-20250929")
|
| 18 |
+
TEXT_MODEL = ("anthropic", "claude-sonnet-4-5-20250929")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
INSPECTOR_SYSTEM = """You are the INSPECTOR agent of ForgeSight — a multimodal quality-control copilot
|
| 22 |
+
running on AMD Instinct MI300X + ROCm. Your job: analyze the submitted product/assembly-line
|
| 23 |
+
image and surface visible defects, anomalies, or violations.
|
| 24 |
+
|
| 25 |
+
Return ONLY compact JSON with this exact shape (no prose, no code fences):
|
| 26 |
+
{
|
| 27 |
+
"verdict": "pass" | "warn" | "fail",
|
| 28 |
+
"confidence": 0.0-1.0,
|
| 29 |
+
"defects": [
|
| 30 |
+
{"type": "short category e.g. surface-scratch", "severity": "low|medium|high", "location": "short spatial description", "description": "one sentence"}
|
| 31 |
+
],
|
| 32 |
+
"observation": "2-3 sentence plain-english summary of what you see"
|
| 33 |
+
}
|
| 34 |
+
Be precise. If the image shows no manufacturing artifact at all, still describe what is visible
|
| 35 |
+
and mark verdict "warn" with a defect explaining the mismatch."""
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
DIAGNOSTICIAN_SYSTEM = """You are the DIAGNOSTICIAN agent of ForgeSight. Given the INSPECTOR's
|
| 39 |
+
JSON report and user notes, produce a probable root-cause analysis.
|
| 40 |
+
|
| 41 |
+
Return ONLY compact JSON:
|
| 42 |
+
{
|
| 43 |
+
"probable_cause": "one-sentence most likely cause",
|
| 44 |
+
"contributing_factors": ["factor 1", "factor 2", "factor 3"],
|
| 45 |
+
"affected_process_step": "e.g. CNC milling, injection cooling, weld pass 2"
|
| 46 |
+
}
|
| 47 |
+
Be concrete and industry-literate."""
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
ACTION_SYSTEM = """You are the ACTION agent of ForgeSight. Given the INSPECTOR and DIAGNOSTICIAN
|
| 51 |
+
outputs, draft an actionable work order.
|
| 52 |
+
|
| 53 |
+
Return ONLY compact JSON:
|
| 54 |
+
{
|
| 55 |
+
"priority": "P0|P1|P2|P3",
|
| 56 |
+
"assignee_role": "e.g. line-lead, maintenance-tech, quality-engineer",
|
| 57 |
+
"steps": ["step 1", "step 2", "step 3"],
|
| 58 |
+
"estimated_minutes": integer,
|
| 59 |
+
"parts_or_tools": ["item 1", "item 2"]
|
| 60 |
+
}"""
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
REPORTER_SYSTEM = """You are the REPORTER agent of ForgeSight. Compile a final human-readable
|
| 64 |
+
summary of the full inspection in <=70 words. Return ONLY JSON:
|
| 65 |
+
{
|
| 66 |
+
"headline": "<=10 word title",
|
| 67 |
+
"summary": "<=70 word paragraph",
|
| 68 |
+
"tags": ["tag1", "tag2", "tag3"]
|
| 69 |
+
}"""
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _extract_json(raw: str) -> Dict[str, Any]:
|
| 73 |
+
"""Best-effort JSON extraction from an LLM response."""
|
| 74 |
+
if not raw:
|
| 75 |
+
return {}
|
| 76 |
+
# Strip code fences
|
| 77 |
+
cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw.strip(), flags=re.MULTILINE)
|
| 78 |
+
# Try direct
|
| 79 |
+
try:
|
| 80 |
+
return json.loads(cleaned)
|
| 81 |
+
except Exception:
|
| 82 |
+
pass
|
| 83 |
+
# Find first {...} block
|
| 84 |
+
match = re.search(r"\{[\s\S]*\}", cleaned)
|
| 85 |
+
if match:
|
| 86 |
+
try:
|
| 87 |
+
return json.loads(match.group(0))
|
| 88 |
+
except Exception:
|
| 89 |
+
pass
|
| 90 |
+
return {"_raw": raw}
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
async def _run_agent(
|
| 94 |
+
name: str,
|
| 95 |
+
system_message: str,
|
| 96 |
+
user_text: str,
|
| 97 |
+
image_base64: Optional[str] = None,
|
| 98 |
+
provider_model: tuple = TEXT_MODEL,
|
| 99 |
+
) -> Dict[str, Any]:
|
| 100 |
+
session_id = f"forgesight-{name}-{uuid.uuid4().hex[:8]}"
|
| 101 |
+
chat = LlmChat(
|
| 102 |
+
api_key=EMERGENT_LLM_KEY,
|
| 103 |
+
session_id=session_id,
|
| 104 |
+
system_message=system_message,
|
| 105 |
+
).with_model(provider_model[0], provider_model[1])
|
| 106 |
+
|
| 107 |
+
if image_base64:
|
| 108 |
+
msg = UserMessage(
|
| 109 |
+
text=user_text,
|
| 110 |
+
file_contents=[ImageContent(image_base64=image_base64)],
|
| 111 |
+
)
|
| 112 |
+
else:
|
| 113 |
+
msg = UserMessage(text=user_text)
|
| 114 |
+
|
| 115 |
+
raw = await chat.send_message(msg)
|
| 116 |
+
raw_str = raw if isinstance(raw, str) else str(raw)
|
| 117 |
+
parsed = _extract_json(raw_str)
|
| 118 |
+
return {"raw": raw_str, "parsed": parsed}
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
async def run_pipeline(
|
| 122 |
+
image_base64: str,
|
| 123 |
+
notes: str = "",
|
| 124 |
+
product_spec: str = "",
|
| 125 |
+
) -> Dict[str, Any]:
|
| 126 |
+
"""
|
| 127 |
+
Run the 4-agent pipeline sequentially and return the full transcript.
|
| 128 |
+
"""
|
| 129 |
+
context = f"Operator notes: {notes or '(none)'}\nProduct spec: {product_spec or '(generic)'}"
|
| 130 |
+
|
| 131 |
+
# 1) Inspector (vision)
|
| 132 |
+
inspector = await _run_agent(
|
| 133 |
+
"inspector",
|
| 134 |
+
INSPECTOR_SYSTEM,
|
| 135 |
+
f"Inspect this image for manufacturing defects.\n{context}",
|
| 136 |
+
image_base64=image_base64,
|
| 137 |
+
provider_model=VISION_MODEL,
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
# 2) Diagnostician
|
| 141 |
+
diagnostician = await _run_agent(
|
| 142 |
+
"diagnostician",
|
| 143 |
+
DIAGNOSTICIAN_SYSTEM,
|
| 144 |
+
f"INSPECTOR_REPORT:\n{json.dumps(inspector['parsed'])}\n\n{context}",
|
| 145 |
+
provider_model=TEXT_MODEL,
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
# 3) Action
|
| 149 |
+
action = await _run_agent(
|
| 150 |
+
"action",
|
| 151 |
+
ACTION_SYSTEM,
|
| 152 |
+
(
|
| 153 |
+
f"INSPECTOR_REPORT:\n{json.dumps(inspector['parsed'])}\n\n"
|
| 154 |
+
f"DIAGNOSTICIAN_REPORT:\n{json.dumps(diagnostician['parsed'])}"
|
| 155 |
+
),
|
| 156 |
+
provider_model=TEXT_MODEL,
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
# 4) Reporter
|
| 160 |
+
reporter = await _run_agent(
|
| 161 |
+
"reporter",
|
| 162 |
+
REPORTER_SYSTEM,
|
| 163 |
+
(
|
| 164 |
+
f"INSPECTOR_REPORT:\n{json.dumps(inspector['parsed'])}\n\n"
|
| 165 |
+
f"DIAGNOSTICIAN_REPORT:\n{json.dumps(diagnostician['parsed'])}\n\n"
|
| 166 |
+
f"ACTION_REPORT:\n{json.dumps(action['parsed'])}"
|
| 167 |
+
),
|
| 168 |
+
provider_model=TEXT_MODEL,
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
return {
|
| 172 |
+
"agents": [
|
| 173 |
+
{"role": "inspector", "label": "Inspector Agent", "model": "Claude Sonnet 4.5 (Vision)", "output": inspector},
|
| 174 |
+
{"role": "diagnostician", "label": "Diagnostician Agent", "model": "Claude Sonnet 4.5", "output": diagnostician},
|
| 175 |
+
{"role": "action", "label": "Action Agent", "model": "Claude Sonnet 4.5", "output": action},
|
| 176 |
+
{"role": "reporter", "label": "Reporter Agent", "model": "Claude Sonnet 4.5", "output": reporter},
|
| 177 |
+
],
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
async def generate_social_post(milestone_title: str, milestone_body: str) -> Dict[str, str]:
|
| 182 |
+
"""Generate X + LinkedIn social post drafts for a build-in-public milestone."""
|
| 183 |
+
system = """You craft punchy Build-in-Public social posts for a hackathon project named
|
| 184 |
+
"ForgeSight" — a multimodal agentic quality-control copilot running on AMD Instinct MI300X + ROCm.
|
| 185 |
+
Always include hashtags: #AMDHackathon #ROCm #AIatAMD #lablab and mention @AIatAMD and @lablab.
|
| 186 |
+
Return ONLY JSON:
|
| 187 |
+
{"x_post": "<=260 chars, punchy, 1-2 emojis ok", "linkedin_post": "<=600 chars, narrative, 3 short paragraphs"}"""
|
| 188 |
+
result = await _run_agent(
|
| 189 |
+
"social",
|
| 190 |
+
system,
|
| 191 |
+
f"Milestone: {milestone_title}\n\nDetails: {milestone_body}",
|
| 192 |
+
provider_model=TEXT_MODEL,
|
| 193 |
+
)
|
| 194 |
+
parsed = result["parsed"]
|
| 195 |
+
return {
|
| 196 |
+
"x_post": parsed.get("x_post", result["raw"][:260]),
|
| 197 |
+
"linkedin_post": parsed.get("linkedin_post", result["raw"][:600]),
|
| 198 |
+
}
|
backend/server.py
CHANGED
|
@@ -1,89 +1,343 @@
|
|
| 1 |
-
from fastapi import FastAPI, APIRouter
|
| 2 |
from dotenv import load_dotenv
|
| 3 |
from starlette.middleware.cors import CORSMiddleware
|
| 4 |
from motor.motor_asyncio import AsyncIOMotorClient
|
| 5 |
import os
|
| 6 |
import logging
|
|
|
|
|
|
|
|
|
|
| 7 |
from pathlib import Path
|
| 8 |
from pydantic import BaseModel, Field, ConfigDict
|
| 9 |
-
from typing import List
|
| 10 |
-
import uuid
|
| 11 |
from datetime import datetime, timezone
|
| 12 |
|
|
|
|
|
|
|
| 13 |
|
| 14 |
ROOT_DIR = Path(__file__).parent
|
| 15 |
-
load_dotenv(ROOT_DIR /
|
| 16 |
|
| 17 |
-
|
| 18 |
-
mongo_url = os.environ['MONGO_URL']
|
| 19 |
client = AsyncIOMotorClient(mongo_url)
|
| 20 |
-
db = client[os.environ[
|
| 21 |
|
| 22 |
-
|
| 23 |
-
app = FastAPI()
|
| 24 |
-
|
| 25 |
-
# Create a router with the /api prefix
|
| 26 |
api_router = APIRouter(prefix="/api")
|
| 27 |
|
| 28 |
|
| 29 |
-
#
|
| 30 |
-
class
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
class StatusCheckCreate(BaseModel):
|
| 38 |
-
client_name: str
|
| 39 |
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
@api_router.get("/")
|
| 42 |
async def root():
|
| 43 |
-
return {"
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
app.include_router(api_router)
|
| 71 |
|
| 72 |
app.add_middleware(
|
| 73 |
CORSMiddleware,
|
| 74 |
allow_credentials=True,
|
| 75 |
-
allow_origins=os.environ.get(
|
| 76 |
allow_methods=["*"],
|
| 77 |
allow_headers=["*"],
|
| 78 |
)
|
| 79 |
|
| 80 |
-
|
| 81 |
-
logging.
|
| 82 |
-
|
| 83 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 84 |
-
)
|
| 85 |
-
logger = logging.getLogger(__name__)
|
| 86 |
|
| 87 |
@app.on_event("shutdown")
|
| 88 |
async def shutdown_db_client():
|
| 89 |
-
client.close()
|
|
|
|
| 1 |
+
from fastapi import FastAPI, APIRouter, HTTPException
|
| 2 |
from dotenv import load_dotenv
|
| 3 |
from starlette.middleware.cors import CORSMiddleware
|
| 4 |
from motor.motor_asyncio import AsyncIOMotorClient
|
| 5 |
import os
|
| 6 |
import logging
|
| 7 |
+
import math
|
| 8 |
+
import time
|
| 9 |
+
import uuid
|
| 10 |
from pathlib import Path
|
| 11 |
from pydantic import BaseModel, Field, ConfigDict
|
| 12 |
+
from typing import List, Optional, Any, Dict
|
|
|
|
| 13 |
from datetime import datetime, timezone
|
| 14 |
|
| 15 |
+
from agents import run_pipeline, generate_social_post
|
| 16 |
+
|
| 17 |
|
| 18 |
ROOT_DIR = Path(__file__).parent
|
| 19 |
+
load_dotenv(ROOT_DIR / ".env")
|
| 20 |
|
| 21 |
+
mongo_url = os.environ["MONGO_URL"]
|
|
|
|
| 22 |
client = AsyncIOMotorClient(mongo_url)
|
| 23 |
+
db = client[os.environ["DB_NAME"]]
|
| 24 |
|
| 25 |
+
app = FastAPI(title="ForgeSight API")
|
|
|
|
|
|
|
|
|
|
| 26 |
api_router = APIRouter(prefix="/api")
|
| 27 |
|
| 28 |
|
| 29 |
+
# ------------------------- Models -------------------------
|
| 30 |
+
class InspectionCreate(BaseModel):
|
| 31 |
+
image_base64: str = Field(..., description="Raw base64 (no data URI prefix)")
|
| 32 |
+
notes: Optional[str] = ""
|
| 33 |
+
product_spec: Optional[str] = ""
|
| 34 |
+
source: Optional[str] = "upload" # upload | sample
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class InspectionSummary(BaseModel):
|
| 38 |
+
model_config = ConfigDict(extra="ignore")
|
| 39 |
+
id: str
|
| 40 |
+
created_at: str
|
| 41 |
+
verdict: str
|
| 42 |
+
confidence: float
|
| 43 |
+
headline: str
|
| 44 |
+
defect_count: int
|
| 45 |
+
priority: str
|
| 46 |
+
source: str
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class JournalCreate(BaseModel):
|
| 50 |
+
title: str
|
| 51 |
+
body: str
|
| 52 |
+
tags: List[str] = []
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class JournalEntry(BaseModel):
|
| 56 |
+
id: str
|
| 57 |
+
created_at: str
|
| 58 |
+
title: str
|
| 59 |
+
body: str
|
| 60 |
+
tags: List[str]
|
| 61 |
+
x_post: str
|
| 62 |
+
linkedin_post: str
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# ------------------------- Helpers -------------------------
|
| 66 |
+
def _now_iso() -> str:
|
| 67 |
+
return datetime.now(timezone.utc).isoformat()
|
| 68 |
|
|
|
|
|
|
|
| 69 |
|
| 70 |
+
def _summarize(inspection: Dict[str, Any]) -> Dict[str, Any]:
|
| 71 |
+
agents = inspection.get("transcript", {}).get("agents", [])
|
| 72 |
+
inspector = next((a for a in agents if a["role"] == "inspector"), None)
|
| 73 |
+
reporter = next((a for a in agents if a["role"] == "reporter"), None)
|
| 74 |
+
action = next((a for a in agents if a["role"] == "action"), None)
|
| 75 |
+
|
| 76 |
+
inspector_out = (inspector or {}).get("output", {}).get("parsed", {}) or {}
|
| 77 |
+
reporter_out = (reporter or {}).get("output", {}).get("parsed", {}) or {}
|
| 78 |
+
action_out = (action or {}).get("output", {}).get("parsed", {}) or {}
|
| 79 |
+
|
| 80 |
+
defects = inspector_out.get("defects") or []
|
| 81 |
+
return {
|
| 82 |
+
"id": inspection["id"],
|
| 83 |
+
"created_at": inspection["created_at"],
|
| 84 |
+
"verdict": inspector_out.get("verdict", "warn"),
|
| 85 |
+
"confidence": float(inspector_out.get("confidence", 0.0) or 0.0),
|
| 86 |
+
"headline": reporter_out.get("headline") or inspector_out.get("observation", "Inspection complete")[:60],
|
| 87 |
+
"defect_count": len(defects) if isinstance(defects, list) else 0,
|
| 88 |
+
"priority": action_out.get("priority", "P2"),
|
| 89 |
+
"source": inspection.get("source", "upload"),
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ------------------------- Routes -------------------------
|
| 94 |
@api_router.get("/")
|
| 95 |
async def root():
|
| 96 |
+
return {"service": "forgesight", "status": "online", "track": "AMD Hackathon — Tracks 1+2+3"}
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
@api_router.post("/inspections")
|
| 100 |
+
async def create_inspection(payload: InspectionCreate):
|
| 101 |
+
# Strip potential data URI prefix
|
| 102 |
+
img_b64 = payload.image_base64
|
| 103 |
+
if "," in img_b64 and img_b64.strip().startswith("data:"):
|
| 104 |
+
img_b64 = img_b64.split(",", 1)[1]
|
| 105 |
+
|
| 106 |
+
try:
|
| 107 |
+
transcript = await run_pipeline(
|
| 108 |
+
image_base64=img_b64,
|
| 109 |
+
notes=payload.notes or "",
|
| 110 |
+
product_spec=payload.product_spec or "",
|
| 111 |
+
)
|
| 112 |
+
except Exception as e:
|
| 113 |
+
logger.exception("Agent pipeline failed")
|
| 114 |
+
raise HTTPException(status_code=500, detail=f"Agent pipeline failed: {str(e)}")
|
| 115 |
+
|
| 116 |
+
inspection = {
|
| 117 |
+
"id": str(uuid.uuid4()),
|
| 118 |
+
"created_at": _now_iso(),
|
| 119 |
+
"notes": payload.notes or "",
|
| 120 |
+
"product_spec": payload.product_spec or "",
|
| 121 |
+
"source": payload.source or "upload",
|
| 122 |
+
"transcript": transcript,
|
| 123 |
+
}
|
| 124 |
+
# Do NOT persist image_base64 to keep docs small; store SHA/size if needed
|
| 125 |
+
doc = {**inspection}
|
| 126 |
+
await db.inspections.insert_one(doc)
|
| 127 |
+
|
| 128 |
+
return {"id": inspection["id"], "created_at": inspection["created_at"], "transcript": transcript, "summary": _summarize(inspection)}
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
@api_router.get("/inspections")
|
| 132 |
+
async def list_inspections(limit: int = 50):
|
| 133 |
+
cursor = db.inspections.find({}, {"_id": 0}).sort("created_at", -1).limit(limit)
|
| 134 |
+
items = []
|
| 135 |
+
async for doc in cursor:
|
| 136 |
+
items.append(_summarize(doc))
|
| 137 |
+
return {"items": items, "total": len(items)}
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@api_router.get("/inspections/{inspection_id}")
|
| 141 |
+
async def get_inspection(inspection_id: str):
|
| 142 |
+
doc = await db.inspections.find_one({"id": inspection_id}, {"_id": 0})
|
| 143 |
+
if not doc:
|
| 144 |
+
raise HTTPException(status_code=404, detail="Inspection not found")
|
| 145 |
+
return {**doc, "summary": _summarize(doc)}
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
@api_router.get("/metrics")
|
| 149 |
+
async def metrics():
|
| 150 |
+
cursor = db.inspections.find({}, {"_id": 0})
|
| 151 |
+
total = 0
|
| 152 |
+
verdict_counts = {"pass": 0, "warn": 0, "fail": 0}
|
| 153 |
+
defect_type_counts: Dict[str, int] = {}
|
| 154 |
+
confidences: List[float] = []
|
| 155 |
+
async for doc in cursor:
|
| 156 |
+
total += 1
|
| 157 |
+
summary = _summarize(doc)
|
| 158 |
+
v = summary["verdict"] if summary["verdict"] in verdict_counts else "warn"
|
| 159 |
+
verdict_counts[v] += 1
|
| 160 |
+
confidences.append(summary["confidence"])
|
| 161 |
+
agents = doc.get("transcript", {}).get("agents", [])
|
| 162 |
+
inspector = next((a for a in agents if a["role"] == "inspector"), None)
|
| 163 |
+
defects = ((inspector or {}).get("output", {}).get("parsed", {}) or {}).get("defects") or []
|
| 164 |
+
if isinstance(defects, list):
|
| 165 |
+
for d in defects:
|
| 166 |
+
if isinstance(d, dict):
|
| 167 |
+
t = (d.get("type") or "unknown").lower()
|
| 168 |
+
defect_type_counts[t] = defect_type_counts.get(t, 0) + 1
|
| 169 |
+
|
| 170 |
+
avg_conf = sum(confidences) / len(confidences) if confidences else 0.0
|
| 171 |
+
top_defects = sorted(defect_type_counts.items(), key=lambda x: x[1], reverse=True)[:6]
|
| 172 |
+
quality_score = 0
|
| 173 |
+
if total > 0:
|
| 174 |
+
quality_score = round(100 * (verdict_counts["pass"] + 0.5 * verdict_counts["warn"]) / total)
|
| 175 |
+
|
| 176 |
+
return {
|
| 177 |
+
"total_inspections": total,
|
| 178 |
+
"verdict_counts": verdict_counts,
|
| 179 |
+
"avg_confidence": round(avg_conf, 3),
|
| 180 |
+
"top_defects": [{"type": t, "count": c} for t, c in top_defects],
|
| 181 |
+
"quality_score": quality_score,
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
@api_router.get("/telemetry")
|
| 186 |
+
async def telemetry():
|
| 187 |
+
"""Simulated MI300X telemetry. Labeled as SIMULATED in the UI."""
|
| 188 |
+
t = time.time()
|
| 189 |
+
gpu_util = 62 + 30 * math.sin(t / 4.0) # 32 - 92
|
| 190 |
+
vram_gb_total = 192.0 # MI300X HBM3
|
| 191 |
+
vram_used = 88 + 20 * math.sin(t / 7.0)
|
| 192 |
+
tokens_per_sec = 2850 + 450 * math.sin(t / 3.0)
|
| 193 |
+
power_w = 620 + 80 * math.sin(t / 5.0)
|
| 194 |
+
temp_c = 58 + 7 * math.sin(t / 6.0)
|
| 195 |
+
return {
|
| 196 |
+
"simulated": True,
|
| 197 |
+
"device": "AMD Instinct MI300X",
|
| 198 |
+
"gpu_util_pct": round(max(0, min(100, gpu_util)), 1),
|
| 199 |
+
"vram_used_gb": round(max(0, vram_used), 1),
|
| 200 |
+
"vram_total_gb": vram_gb_total,
|
| 201 |
+
"tokens_per_sec": int(max(0, tokens_per_sec)),
|
| 202 |
+
"power_watts": int(max(0, power_w)),
|
| 203 |
+
"temp_c": round(max(0, temp_c), 1),
|
| 204 |
+
"ts": _now_iso(),
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
@api_router.get("/blueprint")
|
| 209 |
+
async def blueprint():
|
| 210 |
+
return {
|
| 211 |
+
"stack": [
|
| 212 |
+
{
|
| 213 |
+
"layer": "Hardware",
|
| 214 |
+
"title": "AMD Instinct MI300X",
|
| 215 |
+
"detail": "192 GB HBM3 · 5.3 TB/s memory bandwidth · 8× GPU node",
|
| 216 |
+
"why": "Massive VRAM enables serving 70B-class Qwen-VL models without sharding.",
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
"layer": "Runtime",
|
| 220 |
+
"title": "ROCm 6.2",
|
| 221 |
+
"detail": "Open compute runtime · HIP · MIOpen · RCCL",
|
| 222 |
+
"why": "PyTorch + vLLM run natively on MI300X via ROCm.",
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"layer": "Serving",
|
| 226 |
+
"title": "vLLM on ROCm",
|
| 227 |
+
"detail": "PagedAttention · continuous batching · OpenAI-compatible API",
|
| 228 |
+
"why": "High-throughput multimodal inference for the agent pipeline.",
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
"layer": "Model",
|
| 232 |
+
"title": "Qwen2-VL-72B (fine-tuned)",
|
| 233 |
+
"detail": "LoRA fine-tune on defect-image + work-order pairs via Optimum-AMD",
|
| 234 |
+
"why": "Domain-specialized vision reasoning beats zero-shot generic VLMs.",
|
| 235 |
+
},
|
| 236 |
+
{
|
| 237 |
+
"layer": "Agents",
|
| 238 |
+
"title": "Inspector → Diagnostician → Action → Reporter",
|
| 239 |
+
"detail": "Sequential multi-agent with structured JSON hand-offs",
|
| 240 |
+
"why": "Interpretable, auditable pipeline for industrial QC.",
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"layer": "Product",
|
| 244 |
+
"title": "ForgeSight Console",
|
| 245 |
+
"detail": "React + FastAPI · live transcript · defect feed · build journal",
|
| 246 |
+
"why": "End-to-end demonstrable app shipped for the hackathon.",
|
| 247 |
+
},
|
| 248 |
+
],
|
| 249 |
+
"finetune_recipe": {
|
| 250 |
+
"base_model": "Qwen/Qwen2-VL-72B-Instruct",
|
| 251 |
+
"dataset": "ForgeSight-QC-10K (proprietary defect-image ↔ work-order pairs)",
|
| 252 |
+
"method": "QLoRA r=64 · Optimum-AMD · bf16",
|
| 253 |
+
"hardware": "1× MI300X node (8 GPUs)",
|
| 254 |
+
"expected_wall_clock": "~6h for 3 epochs on 10K pairs",
|
| 255 |
+
"serve_with": "vLLM 0.6+ on ROCm",
|
| 256 |
+
},
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
@api_router.get("/journal")
|
| 261 |
+
async def list_journal():
|
| 262 |
+
cursor = db.journal.find({}, {"_id": 0}).sort("created_at", -1)
|
| 263 |
+
items = [doc async for doc in cursor]
|
| 264 |
+
return {"items": items, "total": len(items)}
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
@api_router.post("/journal")
|
| 268 |
+
async def create_journal(payload: JournalCreate):
|
| 269 |
+
try:
|
| 270 |
+
social = await generate_social_post(payload.title, payload.body)
|
| 271 |
+
except Exception:
|
| 272 |
+
logger.exception("Social gen failed; storing without drafts")
|
| 273 |
+
social = {"x_post": "", "linkedin_post": ""}
|
| 274 |
+
|
| 275 |
+
entry = {
|
| 276 |
+
"id": str(uuid.uuid4()),
|
| 277 |
+
"created_at": _now_iso(),
|
| 278 |
+
"title": payload.title,
|
| 279 |
+
"body": payload.body,
|
| 280 |
+
"tags": payload.tags or [],
|
| 281 |
+
"x_post": social.get("x_post", ""),
|
| 282 |
+
"linkedin_post": social.get("linkedin_post", ""),
|
| 283 |
+
}
|
| 284 |
+
await db.journal.insert_one({**entry})
|
| 285 |
+
return entry
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
@api_router.post("/journal/seed")
|
| 289 |
+
async def seed_journal():
|
| 290 |
+
"""Idempotent seed of initial build-journal entries."""
|
| 291 |
+
existing = await db.journal.count_documents({})
|
| 292 |
+
if existing > 0:
|
| 293 |
+
return {"seeded": 0, "reason": "already seeded"}
|
| 294 |
+
|
| 295 |
+
seeds = [
|
| 296 |
+
{
|
| 297 |
+
"title": "Kickoff: ForgeSight on AMD Developer Cloud",
|
| 298 |
+
"body": "Spun up an MI300X instance on AMD Developer Cloud. First impression: zero CUDA-lock-in, ROCm + PyTorch just worked. Targeting all three hackathon tracks with one agentic multimodal QC copilot.",
|
| 299 |
+
"tags": ["kickoff", "amd", "rocm"],
|
| 300 |
+
},
|
| 301 |
+
{
|
| 302 |
+
"title": "Multi-agent pipeline wired end-to-end",
|
| 303 |
+
"body": "Inspector → Diagnostician → Action → Reporter. Each agent produces strict JSON so hand-offs stay auditable. Running on Claude Sonnet 4.5 today, swapping to Qwen2-VL on MI300X next.",
|
| 304 |
+
"tags": ["agents", "pipeline", "qwen"],
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
"title": "Fine-tune recipe: QLoRA on Qwen2-VL with Optimum-AMD",
|
| 308 |
+
"body": "Drafted the LoRA fine-tune path for 10K defect-image ↔ work-order pairs. Expecting ~6h wall-clock on a single MI300X node. vLLM-ROCm will serve the result.",
|
| 309 |
+
"tags": ["fine-tuning", "qlora", "optimum-amd"],
|
| 310 |
+
},
|
| 311 |
+
]
|
| 312 |
+
for s in seeds:
|
| 313 |
+
try:
|
| 314 |
+
social = await generate_social_post(s["title"], s["body"])
|
| 315 |
+
except Exception:
|
| 316 |
+
social = {"x_post": "", "linkedin_post": ""}
|
| 317 |
+
await db.journal.insert_one({
|
| 318 |
+
"id": str(uuid.uuid4()),
|
| 319 |
+
"created_at": _now_iso(),
|
| 320 |
+
**s,
|
| 321 |
+
"x_post": social.get("x_post", ""),
|
| 322 |
+
"linkedin_post": social.get("linkedin_post", ""),
|
| 323 |
+
})
|
| 324 |
+
return {"seeded": len(seeds)}
|
| 325 |
+
|
| 326 |
+
|
| 327 |
app.include_router(api_router)
|
| 328 |
|
| 329 |
app.add_middleware(
|
| 330 |
CORSMiddleware,
|
| 331 |
allow_credentials=True,
|
| 332 |
+
allow_origins=os.environ.get("CORS_ORIGINS", "*").split(","),
|
| 333 |
allow_methods=["*"],
|
| 334 |
allow_headers=["*"],
|
| 335 |
)
|
| 336 |
|
| 337 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
| 338 |
+
logger = logging.getLogger("forgesight")
|
| 339 |
+
|
|
|
|
|
|
|
|
|
|
| 340 |
|
| 341 |
@app.on_event("shutdown")
|
| 342 |
async def shutdown_db_client():
|
| 343 |
+
client.close()
|
frontend/public/index.html
CHANGED
|
@@ -21,7 +21,7 @@
|
|
| 21 |
work correctly both with client-side routing and a non-root public URL.
|
| 22 |
Learn how to configure a non-root public URL by running `npm run build`.
|
| 23 |
-->
|
| 24 |
-
<title>
|
| 25 |
<script>window.addEventListener("error",function(e){if(e.error instanceof DOMException&&e.error.name==="DataCloneError"&&e.message&&e.message.includes("PerformanceServerTiming")){e.stopImmediatePropagation();e.preventDefault()}},true);</script>
|
| 26 |
<script src="https://assets.emergent.sh/scripts/emergent-main.js"></script>
|
| 27 |
</head>
|
|
|
|
| 21 |
work correctly both with client-side routing and a non-root public URL.
|
| 22 |
Learn how to configure a non-root public URL by running `npm run build`.
|
| 23 |
-->
|
| 24 |
+
<title>ForgeSight · Multimodal QC Copilot · AMD MI300X</title>
|
| 25 |
<script>window.addEventListener("error",function(e){if(e.error instanceof DOMException&&e.error.name==="DataCloneError"&&e.message&&e.message.includes("PerformanceServerTiming")){e.stopImmediatePropagation();e.preventDefault()}},true);</script>
|
| 26 |
<script src="https://assets.emergent.sh/scripts/emergent-main.js"></script>
|
| 27 |
</head>
|
frontend/src/App.css
CHANGED
|
@@ -1,34 +1,194 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
}
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
}
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
}
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
| 34 |
}
|
|
|
|
| 1 |
+
/* ForgeSight — industrial brutalist theme */
|
| 2 |
+
|
| 3 |
+
@import url('https://fonts.googleapis.com/css2?family=Chivo:wght@300;400;700;900&family=IBM+Plex+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap');
|
| 4 |
+
|
| 5 |
+
:root {
|
| 6 |
+
--fs-bg: #0A0A0A;
|
| 7 |
+
--fs-surface: #141416;
|
| 8 |
+
--fs-surface-2: #1a1a1d;
|
| 9 |
+
--fs-border: rgba(255, 255, 255, 0.1);
|
| 10 |
+
--fs-border-strong: rgba(255, 255, 255, 0.2);
|
| 11 |
+
--fs-red: #ED1C24;
|
| 12 |
+
--fs-red-hot: #FF3B30;
|
| 13 |
+
--fs-amber: #F59E0B;
|
| 14 |
+
--fs-green: #10B981;
|
| 15 |
+
--fs-text: #FFFFFF;
|
| 16 |
+
--fs-text-mute: #A1A1AA;
|
| 17 |
+
--fs-text-dim: #71717A;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.App {
|
| 21 |
+
font-family: 'IBM Plex Sans', system-ui, sans-serif;
|
| 22 |
+
background: var(--fs-bg);
|
| 23 |
+
color: var(--fs-text);
|
| 24 |
+
letter-spacing: 0.005em;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.font-display { font-family: 'Chivo', sans-serif; }
|
| 28 |
+
.font-mono { font-family: 'JetBrains Mono', ui-monospace, monospace; }
|
| 29 |
+
|
| 30 |
+
/* Selection */
|
| 31 |
+
::selection {
|
| 32 |
+
background: var(--fs-red);
|
| 33 |
+
color: #fff;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* Scrollbar */
|
| 37 |
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
| 38 |
+
::-webkit-scrollbar-track { background: #0A0A0A; }
|
| 39 |
+
::-webkit-scrollbar-thumb { background: #2a2a2d; border: 2px solid #0A0A0A; }
|
| 40 |
+
::-webkit-scrollbar-thumb:hover { background: var(--fs-red); }
|
| 41 |
+
|
| 42 |
+
/* Industrial scanline overlay for agent console */
|
| 43 |
+
.fs-scanlines {
|
| 44 |
+
background-image: repeating-linear-gradient(
|
| 45 |
+
to bottom,
|
| 46 |
+
transparent 0px,
|
| 47 |
+
transparent 2px,
|
| 48 |
+
rgba(255, 255, 255, 0.015) 2px,
|
| 49 |
+
rgba(255, 255, 255, 0.015) 3px
|
| 50 |
+
);
|
| 51 |
}
|
| 52 |
|
| 53 |
+
/* Grid dots texture */
|
| 54 |
+
.fs-grid-dots {
|
| 55 |
+
background-image: radial-gradient(rgba(255, 255, 255, 0.06) 1px, transparent 1px);
|
| 56 |
+
background-size: 24px 24px;
|
| 57 |
}
|
| 58 |
|
| 59 |
+
/* Technical corner tick marks */
|
| 60 |
+
.fs-corners {
|
| 61 |
+
position: relative;
|
| 62 |
+
}
|
| 63 |
+
.fs-corners::before,
|
| 64 |
+
.fs-corners::after {
|
| 65 |
+
content: '';
|
| 66 |
+
position: absolute;
|
| 67 |
+
width: 10px;
|
| 68 |
+
height: 10px;
|
| 69 |
+
border-color: var(--fs-red);
|
| 70 |
+
border-style: solid;
|
| 71 |
+
border-width: 0;
|
| 72 |
+
}
|
| 73 |
+
.fs-corners::before {
|
| 74 |
+
top: -1px; left: -1px;
|
| 75 |
+
border-top-width: 1px; border-left-width: 1px;
|
| 76 |
+
}
|
| 77 |
+
.fs-corners::after {
|
| 78 |
+
bottom: -1px; right: -1px;
|
| 79 |
+
border-bottom-width: 1px; border-right-width: 1px;
|
| 80 |
}
|
| 81 |
|
| 82 |
+
/* Blinking terminal cursor */
|
| 83 |
+
.fs-cursor::after {
|
| 84 |
+
content: '▊';
|
| 85 |
+
display: inline-block;
|
| 86 |
+
margin-left: 2px;
|
| 87 |
+
color: var(--fs-red);
|
| 88 |
+
animation: fs-blink 1s steps(2) infinite;
|
| 89 |
+
}
|
| 90 |
+
@keyframes fs-blink {
|
| 91 |
+
0%, 50% { opacity: 1; }
|
| 92 |
+
51%, 100% { opacity: 0; }
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/* Boot / entrance fade + slide */
|
| 96 |
+
.fs-rise {
|
| 97 |
+
animation: fs-rise 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
|
| 98 |
+
}
|
| 99 |
+
@keyframes fs-rise {
|
| 100 |
+
from { opacity: 0; transform: translateY(8px); }
|
| 101 |
+
to { opacity: 1; transform: translateY(0); }
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/* Pill button */
|
| 105 |
+
.fs-btn {
|
| 106 |
+
font-family: 'JetBrains Mono', monospace;
|
| 107 |
+
font-size: 0.75rem;
|
| 108 |
+
letter-spacing: 0.14em;
|
| 109 |
+
text-transform: uppercase;
|
| 110 |
+
padding: 0.65rem 1.1rem;
|
| 111 |
+
border: 1px solid var(--fs-border-strong);
|
| 112 |
+
background: transparent;
|
| 113 |
+
color: #fff;
|
| 114 |
+
transition: background-color 180ms ease, border-color 180ms ease, color 180ms ease, transform 180ms ease;
|
| 115 |
+
cursor: pointer;
|
| 116 |
+
}
|
| 117 |
+
.fs-btn:hover { border-color: var(--fs-red); color: var(--fs-red); }
|
| 118 |
+
.fs-btn-primary {
|
| 119 |
+
background: var(--fs-red);
|
| 120 |
+
border-color: var(--fs-red);
|
| 121 |
+
color: #fff;
|
| 122 |
+
}
|
| 123 |
+
.fs-btn-primary:hover { background: var(--fs-red-hot); border-color: var(--fs-red-hot); color: #fff; }
|
| 124 |
+
|
| 125 |
+
/* Tag chip */
|
| 126 |
+
.fs-chip {
|
| 127 |
+
font-family: 'JetBrains Mono', monospace;
|
| 128 |
+
font-size: 0.65rem;
|
| 129 |
+
letter-spacing: 0.2em;
|
| 130 |
+
text-transform: uppercase;
|
| 131 |
+
padding: 0.2rem 0.55rem;
|
| 132 |
+
border: 1px solid var(--fs-border-strong);
|
| 133 |
+
color: var(--fs-text-mute);
|
| 134 |
+
}
|
| 135 |
+
.fs-chip-pass { color: var(--fs-green); border-color: rgba(16, 185, 129, 0.4); }
|
| 136 |
+
.fs-chip-warn { color: var(--fs-amber); border-color: rgba(245, 158, 11, 0.4); }
|
| 137 |
+
.fs-chip-fail { color: var(--fs-red); border-color: rgba(237, 28, 36, 0.5); }
|
| 138 |
+
|
| 139 |
+
/* Label */
|
| 140 |
+
.fs-label {
|
| 141 |
+
font-family: 'JetBrains Mono', monospace;
|
| 142 |
+
font-size: 0.7rem;
|
| 143 |
+
letter-spacing: 0.22em;
|
| 144 |
+
text-transform: uppercase;
|
| 145 |
+
color: var(--fs-text-dim);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/* Dashed divider */
|
| 149 |
+
.fs-hr {
|
| 150 |
+
border: none;
|
| 151 |
+
border-top: 1px dashed var(--fs-border);
|
| 152 |
+
margin: 0;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/* Hero headline stroke effect */
|
| 156 |
+
.fs-stroke {
|
| 157 |
+
-webkit-text-stroke: 1px rgba(255, 255, 255, 0.25);
|
| 158 |
+
color: transparent;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
/* Drop zone */
|
| 162 |
+
.fs-drop {
|
| 163 |
+
border: 1px dashed var(--fs-border-strong);
|
| 164 |
+
transition: border-color 180ms ease, background-color 180ms ease;
|
| 165 |
+
}
|
| 166 |
+
.fs-drop.fs-drop-active {
|
| 167 |
+
border-color: var(--fs-red);
|
| 168 |
+
background-color: rgba(237, 28, 36, 0.05);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
/* Minor utility */
|
| 172 |
+
.fs-mono-small { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; letter-spacing: 0.1em; }
|
| 173 |
+
|
| 174 |
+
/* Telemetry bar */
|
| 175 |
+
.fs-bar {
|
| 176 |
+
height: 4px;
|
| 177 |
+
background: var(--fs-border);
|
| 178 |
+
overflow: hidden;
|
| 179 |
+
}
|
| 180 |
+
.fs-bar > div {
|
| 181 |
+
height: 100%;
|
| 182 |
+
background: var(--fs-red);
|
| 183 |
+
transition: width 400ms cubic-bezier(0.16, 1, 0.3, 1);
|
| 184 |
}
|
| 185 |
|
| 186 |
+
/* Keyboard style */
|
| 187 |
+
kbd {
|
| 188 |
+
font-family: 'JetBrains Mono', monospace;
|
| 189 |
+
font-size: 0.65rem;
|
| 190 |
+
padding: 2px 6px;
|
| 191 |
+
border: 1px solid var(--fs-border-strong);
|
| 192 |
+
background: #18181b;
|
| 193 |
+
color: var(--fs-text-mute);
|
| 194 |
}
|
frontend/src/App.js
CHANGED
|
@@ -1,51 +1,26 @@
|
|
| 1 |
-
import { useEffect } from "react";
|
| 2 |
import "@/App.css";
|
| 3 |
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
| 4 |
-
import
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
try {
|
| 12 |
-
const response = await axios.get(`${API}/`);
|
| 13 |
-
console.log(response.data.message);
|
| 14 |
-
} catch (e) {
|
| 15 |
-
console.error(e, `errored out requesting / api`);
|
| 16 |
-
}
|
| 17 |
-
};
|
| 18 |
-
|
| 19 |
-
useEffect(() => {
|
| 20 |
-
helloWorldApi();
|
| 21 |
-
}, []);
|
| 22 |
-
|
| 23 |
-
return (
|
| 24 |
-
<div>
|
| 25 |
-
<header className="App-header">
|
| 26 |
-
<a
|
| 27 |
-
className="App-link"
|
| 28 |
-
href="https://emergent.sh"
|
| 29 |
-
target="_blank"
|
| 30 |
-
rel="noopener noreferrer"
|
| 31 |
-
>
|
| 32 |
-
<img src="https://avatars.githubusercontent.com/in/1201222?s=120&u=2686cf91179bbafbc7a71bfbc43004cf9ae1acea&v=4" />
|
| 33 |
-
</a>
|
| 34 |
-
<p className="mt-5">Building something incredible ~!</p>
|
| 35 |
-
</header>
|
| 36 |
-
</div>
|
| 37 |
-
);
|
| 38 |
-
};
|
| 39 |
|
| 40 |
function App() {
|
| 41 |
return (
|
| 42 |
-
<div className="App">
|
| 43 |
<BrowserRouter>
|
|
|
|
| 44 |
<Routes>
|
| 45 |
-
<Route path="/" element={<
|
| 46 |
-
|
| 47 |
-
<
|
|
|
|
|
|
|
| 48 |
</Routes>
|
|
|
|
| 49 |
</BrowserRouter>
|
| 50 |
</div>
|
| 51 |
);
|
|
|
|
|
|
|
| 1 |
import "@/App.css";
|
| 2 |
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
| 3 |
+
import { Toaster } from "@/components/ui/sonner";
|
| 4 |
+
import Nav from "@/components/Nav";
|
| 5 |
+
import Landing from "@/pages/Landing";
|
| 6 |
+
import Console from "@/pages/Console";
|
| 7 |
+
import Feed from "@/pages/Feed";
|
| 8 |
+
import Blueprint from "@/pages/Blueprint";
|
| 9 |
+
import Journal from "@/pages/Journal";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
function App() {
|
| 12 |
return (
|
| 13 |
+
<div className="App min-h-screen bg-[#0A0A0A] text-white">
|
| 14 |
<BrowserRouter>
|
| 15 |
+
<Nav />
|
| 16 |
<Routes>
|
| 17 |
+
<Route path="/" element={<Landing />} />
|
| 18 |
+
<Route path="/console" element={<Console />} />
|
| 19 |
+
<Route path="/feed" element={<Feed />} />
|
| 20 |
+
<Route path="/blueprint" element={<Blueprint />} />
|
| 21 |
+
<Route path="/journal" element={<Journal />} />
|
| 22 |
</Routes>
|
| 23 |
+
<Toaster theme="dark" position="bottom-right" />
|
| 24 |
</BrowserRouter>
|
| 25 |
</div>
|
| 26 |
);
|
frontend/src/components/AgentTranscript.jsx
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
import { Eye, Stethoscope, Wrench, FileText } from "lucide-react";
|
| 3 |
+
|
| 4 |
+
const ICONS = {
|
| 5 |
+
inspector: Eye,
|
| 6 |
+
diagnostician: Stethoscope,
|
| 7 |
+
action: Wrench,
|
| 8 |
+
reporter: FileText,
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Pseudo-streams each agent's raw output character-by-character so the user
|
| 13 |
+
* sees live "thinking". The backend returns the full transcript in one shot.
|
| 14 |
+
*/
|
| 15 |
+
export default function AgentTranscript({ transcript, onDone }) {
|
| 16 |
+
const [visibleIndex, setVisibleIndex] = useState(-1);
|
| 17 |
+
const [visibleChars, setVisibleChars] = useState(0);
|
| 18 |
+
|
| 19 |
+
useEffect(() => {
|
| 20 |
+
if (!transcript) return;
|
| 21 |
+
setVisibleIndex(0);
|
| 22 |
+
setVisibleChars(0);
|
| 23 |
+
}, [transcript]);
|
| 24 |
+
|
| 25 |
+
useEffect(() => {
|
| 26 |
+
if (!transcript || visibleIndex < 0) return;
|
| 27 |
+
const agents = transcript.agents || [];
|
| 28 |
+
if (visibleIndex >= agents.length) {
|
| 29 |
+
onDone && onDone();
|
| 30 |
+
return;
|
| 31 |
+
}
|
| 32 |
+
const current = agents[visibleIndex];
|
| 33 |
+
const raw = formatRaw(current);
|
| 34 |
+
if (visibleChars < raw.length) {
|
| 35 |
+
const speed = Math.max(4, Math.floor(raw.length / 140));
|
| 36 |
+
const id = setTimeout(() => setVisibleChars((c) => Math.min(raw.length, c + speed)), 16);
|
| 37 |
+
return () => clearTimeout(id);
|
| 38 |
+
}
|
| 39 |
+
const next = setTimeout(() => {
|
| 40 |
+
setVisibleIndex((i) => i + 1);
|
| 41 |
+
setVisibleChars(0);
|
| 42 |
+
}, 320);
|
| 43 |
+
return () => clearTimeout(next);
|
| 44 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 45 |
+
}, [transcript, visibleIndex, visibleChars]);
|
| 46 |
+
|
| 47 |
+
if (!transcript) return null;
|
| 48 |
+
const agents = transcript.agents || [];
|
| 49 |
+
|
| 50 |
+
return (
|
| 51 |
+
<div className="space-y-0 border border-white/10 bg-[#0d0d10] fs-scanlines" data-testid="agent-transcript">
|
| 52 |
+
{agents.map((a, idx) => {
|
| 53 |
+
const Icon = ICONS[a.role] || Eye;
|
| 54 |
+
const active = idx === visibleIndex;
|
| 55 |
+
const done = idx < visibleIndex;
|
| 56 |
+
const raw = formatRaw(a);
|
| 57 |
+
const shown = done ? raw : active ? raw.slice(0, visibleChars) : "";
|
| 58 |
+
return (
|
| 59 |
+
<div
|
| 60 |
+
key={a.role}
|
| 61 |
+
className={`border-b border-white/10 last:border-b-0 p-5 transition-colors ${
|
| 62 |
+
active ? "bg-[#141416]" : "bg-transparent"
|
| 63 |
+
}`}
|
| 64 |
+
data-testid={`agent-block-${a.role}`}
|
| 65 |
+
>
|
| 66 |
+
<div className="flex items-center justify-between mb-3">
|
| 67 |
+
<div className="flex items-center gap-3">
|
| 68 |
+
<div
|
| 69 |
+
className={`w-7 h-7 flex items-center justify-center border ${
|
| 70 |
+
active || done ? "border-[#ED1C24] text-[#ED1C24]" : "border-white/20 text-zinc-500"
|
| 71 |
+
}`}
|
| 72 |
+
>
|
| 73 |
+
<Icon className="w-3.5 h-3.5" />
|
| 74 |
+
</div>
|
| 75 |
+
<div>
|
| 76 |
+
<div className="font-display font-bold tracking-tight text-sm">{a.label}</div>
|
| 77 |
+
<div className="fs-mono-small text-zinc-500">{a.model}</div>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
<StatusPill state={done ? "done" : active ? "active" : "pending"} />
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<pre className="font-mono text-[12.5px] leading-relaxed text-zinc-300 whitespace-pre-wrap break-words">
|
| 84 |
+
{shown}
|
| 85 |
+
{active && <span className="fs-cursor" />}
|
| 86 |
+
{!done && !active && <span className="text-zinc-600">awaiting upstream…</span>}
|
| 87 |
+
</pre>
|
| 88 |
+
</div>
|
| 89 |
+
);
|
| 90 |
+
})}
|
| 91 |
+
</div>
|
| 92 |
+
);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
function StatusPill({ state }) {
|
| 96 |
+
if (state === "done") return <span className="fs-chip fs-chip-pass">complete</span>;
|
| 97 |
+
if (state === "active") return <span className="fs-chip fs-chip-fail">streaming</span>;
|
| 98 |
+
return <span className="fs-chip">queued</span>;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
function formatRaw(agent) {
|
| 102 |
+
const parsed = agent?.output?.parsed;
|
| 103 |
+
if (parsed && !parsed._raw) {
|
| 104 |
+
try {
|
| 105 |
+
return JSON.stringify(parsed, null, 2);
|
| 106 |
+
} catch {
|
| 107 |
+
return agent?.output?.raw || "";
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
return agent?.output?.raw || "";
|
| 111 |
+
}
|
frontend/src/components/Nav.jsx
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NavLink, useLocation } from "react-router-dom";
|
| 2 |
+
import { Cpu } from "lucide-react";
|
| 3 |
+
|
| 4 |
+
const items = [
|
| 5 |
+
{ to: "/", label: "Home", key: "home" },
|
| 6 |
+
{ to: "/console", label: "Console", key: "console" },
|
| 7 |
+
{ to: "/feed", label: "Feed", key: "feed" },
|
| 8 |
+
{ to: "/blueprint", label: "Blueprint", key: "blueprint" },
|
| 9 |
+
{ to: "/journal", label: "Journal", key: "journal" },
|
| 10 |
+
];
|
| 11 |
+
|
| 12 |
+
export default function Nav() {
|
| 13 |
+
const { pathname } = useLocation();
|
| 14 |
+
return (
|
| 15 |
+
<nav
|
| 16 |
+
className="sticky top-0 z-40 w-full bg-[#0A0A0A]/90 backdrop-blur-md border-b border-white/10"
|
| 17 |
+
data-testid="top-nav"
|
| 18 |
+
>
|
| 19 |
+
<div className="mx-auto max-w-[1400px] px-6 h-14 flex items-center justify-between">
|
| 20 |
+
<NavLink to="/" className="flex items-center gap-3" data-testid="nav-logo">
|
| 21 |
+
<div className="w-6 h-6 border border-[#ED1C24] flex items-center justify-center">
|
| 22 |
+
<div className="w-1.5 h-1.5 bg-[#ED1C24]" />
|
| 23 |
+
</div>
|
| 24 |
+
<span className="font-display font-black tracking-tighter text-lg">FORGESIGHT</span>
|
| 25 |
+
<span className="fs-label hidden md:inline">v0.1 · hackathon build</span>
|
| 26 |
+
</NavLink>
|
| 27 |
+
|
| 28 |
+
<div className="hidden md:flex items-center gap-1 border border-white/10 p-1">
|
| 29 |
+
{items.map((it) => {
|
| 30 |
+
const active = pathname === it.to;
|
| 31 |
+
return (
|
| 32 |
+
<NavLink
|
| 33 |
+
key={it.key}
|
| 34 |
+
to={it.to}
|
| 35 |
+
data-testid={`nav-${it.key}`}
|
| 36 |
+
className={`px-3 py-1.5 text-xs font-mono uppercase tracking-[0.18em] transition-colors ${
|
| 37 |
+
active ? "bg-[#ED1C24] text-white" : "text-zinc-400 hover:text-white hover:bg-white/5"
|
| 38 |
+
}`}
|
| 39 |
+
>
|
| 40 |
+
{it.label}
|
| 41 |
+
</NavLink>
|
| 42 |
+
);
|
| 43 |
+
})}
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<div className="hidden lg:flex items-center gap-2 border border-white/10 px-3 py-1.5">
|
| 47 |
+
<Cpu className="w-3.5 h-3.5 text-[#ED1C24]" />
|
| 48 |
+
<span className="fs-mono-small text-zinc-400">POWERED BY</span>
|
| 49 |
+
<span className="fs-mono-small text-white">AMD INSTINCT MI300X</span>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
</nav>
|
| 53 |
+
);
|
| 54 |
+
}
|
frontend/src/components/TelemetryWidget.jsx
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
import { api } from "@/lib/api";
|
| 3 |
+
import { Activity } from "lucide-react";
|
| 4 |
+
|
| 5 |
+
export default function TelemetryWidget() {
|
| 6 |
+
const [t, setT] = useState(null);
|
| 7 |
+
|
| 8 |
+
useEffect(() => {
|
| 9 |
+
let alive = true;
|
| 10 |
+
const tick = async () => {
|
| 11 |
+
try {
|
| 12 |
+
const { data } = await api.get("/telemetry");
|
| 13 |
+
if (alive) setT(data);
|
| 14 |
+
} catch {}
|
| 15 |
+
};
|
| 16 |
+
tick();
|
| 17 |
+
const id = setInterval(tick, 1500);
|
| 18 |
+
return () => {
|
| 19 |
+
alive = false;
|
| 20 |
+
clearInterval(id);
|
| 21 |
+
};
|
| 22 |
+
}, []);
|
| 23 |
+
|
| 24 |
+
const vramPct = t ? (t.vram_used_gb / t.vram_total_gb) * 100 : 0;
|
| 25 |
+
|
| 26 |
+
return (
|
| 27 |
+
<div className="border border-white/10 bg-[#141416] p-5 fs-corners" data-testid="telemetry-widget">
|
| 28 |
+
<div className="flex items-center justify-between mb-4">
|
| 29 |
+
<div className="flex items-center gap-2">
|
| 30 |
+
<Activity className="w-3.5 h-3.5 text-[#ED1C24]" />
|
| 31 |
+
<span className="fs-label">Live Telemetry</span>
|
| 32 |
+
</div>
|
| 33 |
+
<span className="fs-chip fs-chip-warn" data-testid="telemetry-simulated-chip">SIMULATED</span>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<div className="font-mono text-xs text-zinc-500 mb-3">
|
| 37 |
+
{t ? t.device : "AMD Instinct MI300X"}
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
<div className="space-y-3">
|
| 41 |
+
<Row label="GPU Util" value={t ? `${t.gpu_util_pct.toFixed(1)}%` : "—"} pct={t?.gpu_util_pct || 0} />
|
| 42 |
+
<Row
|
| 43 |
+
label="VRAM"
|
| 44 |
+
value={t ? `${t.vram_used_gb.toFixed(1)} / ${t.vram_total_gb} GB` : "—"}
|
| 45 |
+
pct={vramPct}
|
| 46 |
+
/>
|
| 47 |
+
<Row label="Tokens/sec" value={t ? t.tokens_per_sec.toLocaleString() : "—"} pct={t ? (t.tokens_per_sec / 4000) * 100 : 0} />
|
| 48 |
+
<Row label="Power" value={t ? `${t.power_watts} W` : "—"} pct={t ? (t.power_watts / 750) * 100 : 0} />
|
| 49 |
+
<Row label="Temp" value={t ? `${t.temp_c.toFixed(1)} °C` : "—"} pct={t ? (t.temp_c / 90) * 100 : 0} />
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function Row({ label, value, pct }) {
|
| 56 |
+
return (
|
| 57 |
+
<div>
|
| 58 |
+
<div className="flex items-baseline justify-between">
|
| 59 |
+
<span className="fs-mono-small text-zinc-500 uppercase">{label}</span>
|
| 60 |
+
<span className="font-mono text-sm text-white tabular-nums">{value}</span>
|
| 61 |
+
</div>
|
| 62 |
+
<div className="fs-bar mt-1.5">
|
| 63 |
+
<div style={{ width: `${Math.max(2, Math.min(100, pct))}%` }} />
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
);
|
| 67 |
+
}
|
frontend/src/index.css
CHANGED
|
@@ -2,74 +2,41 @@
|
|
| 2 |
@tailwind components;
|
| 3 |
@tailwind utilities;
|
| 4 |
|
| 5 |
-
body {
|
| 6 |
margin: 0;
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
sans-serif;
|
| 11 |
-webkit-font-smoothing: antialiased;
|
| 12 |
-moz-osx-font-smoothing: grayscale;
|
| 13 |
}
|
| 14 |
|
| 15 |
code {
|
| 16 |
-
font-family:
|
| 17 |
-
source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
| 18 |
}
|
| 19 |
|
| 20 |
@layer base {
|
| 21 |
:root {
|
| 22 |
-
--background: 0 0%
|
| 23 |
-
--foreground: 0 0% 3.9%;
|
| 24 |
-
--card: 0 0% 100%;
|
| 25 |
-
--card-foreground: 0 0% 3.9%;
|
| 26 |
-
--popover: 0 0% 100%;
|
| 27 |
-
--popover-foreground: 0 0% 3.9%;
|
| 28 |
-
--primary: 0 0% 9%;
|
| 29 |
-
--primary-foreground: 0 0% 98%;
|
| 30 |
-
--secondary: 0 0% 96.1%;
|
| 31 |
-
--secondary-foreground: 0 0% 9%;
|
| 32 |
-
--muted: 0 0% 96.1%;
|
| 33 |
-
--muted-foreground: 0 0% 45.1%;
|
| 34 |
-
--accent: 0 0% 96.1%;
|
| 35 |
-
--accent-foreground: 0 0% 9%;
|
| 36 |
-
--destructive: 0 84.2% 60.2%;
|
| 37 |
-
--destructive-foreground: 0 0% 98%;
|
| 38 |
-
--border: 0 0% 89.8%;
|
| 39 |
-
--input: 0 0% 89.8%;
|
| 40 |
-
--ring: 0 0% 3.9%;
|
| 41 |
-
--chart-1: 12 76% 61%;
|
| 42 |
-
--chart-2: 173 58% 39%;
|
| 43 |
-
--chart-3: 197 37% 24%;
|
| 44 |
-
--chart-4: 43 74% 66%;
|
| 45 |
-
--chart-5: 27 87% 67%;
|
| 46 |
-
--radius: 0.5rem;
|
| 47 |
-
}
|
| 48 |
-
.dark {
|
| 49 |
-
--background: 0 0% 3.9%;
|
| 50 |
--foreground: 0 0% 98%;
|
| 51 |
-
--card:
|
| 52 |
--card-foreground: 0 0% 98%;
|
| 53 |
-
--popover:
|
| 54 |
--popover-foreground: 0 0% 98%;
|
| 55 |
-
--primary:
|
| 56 |
-
--primary-foreground: 0 0%
|
| 57 |
-
--secondary:
|
| 58 |
--secondary-foreground: 0 0% 98%;
|
| 59 |
-
--muted:
|
| 60 |
-
--muted-foreground:
|
| 61 |
-
--accent:
|
| 62 |
--accent-foreground: 0 0% 98%;
|
| 63 |
-
--destructive:
|
| 64 |
--destructive-foreground: 0 0% 98%;
|
| 65 |
-
--border: 0 0%
|
| 66 |
-
--input: 0 0%
|
| 67 |
-
--ring:
|
| 68 |
-
--
|
| 69 |
-
--chart-2: 160 60% 45%;
|
| 70 |
-
--chart-3: 30 80% 55%;
|
| 71 |
-
--chart-4: 280 65% 60%;
|
| 72 |
-
--chart-5: 340 75% 55%;
|
| 73 |
}
|
| 74 |
}
|
| 75 |
|
|
@@ -86,30 +53,12 @@ code {
|
|
| 86 |
[data-debug-wrapper="true"] {
|
| 87 |
display: contents !important;
|
| 88 |
}
|
| 89 |
-
|
| 90 |
[data-debug-wrapper="true"] > * {
|
| 91 |
-
margin-left: inherit;
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
padding-top: inherit;
|
| 98 |
-
padding-bottom: inherit;
|
| 99 |
-
column-gap: inherit;
|
| 100 |
-
row-gap: inherit;
|
| 101 |
-
gap: inherit;
|
| 102 |
-
border-left-width: inherit;
|
| 103 |
-
border-right-width: inherit;
|
| 104 |
-
border-top-width: inherit;
|
| 105 |
-
border-bottom-width: inherit;
|
| 106 |
-
border-left-style: inherit;
|
| 107 |
-
border-right-style: inherit;
|
| 108 |
-
border-top-style: inherit;
|
| 109 |
-
border-bottom-style: inherit;
|
| 110 |
-
border-left-color: inherit;
|
| 111 |
-
border-right-color: inherit;
|
| 112 |
-
border-top-color: inherit;
|
| 113 |
-
border-bottom-color: inherit;
|
| 114 |
}
|
| 115 |
}
|
|
|
|
| 2 |
@tailwind components;
|
| 3 |
@tailwind utilities;
|
| 4 |
|
| 5 |
+
html, body {
|
| 6 |
margin: 0;
|
| 7 |
+
background: #0A0A0A;
|
| 8 |
+
color: #fff;
|
| 9 |
+
font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
|
|
| 10 |
-webkit-font-smoothing: antialiased;
|
| 11 |
-moz-osx-font-smoothing: grayscale;
|
| 12 |
}
|
| 13 |
|
| 14 |
code {
|
| 15 |
+
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
|
|
| 16 |
}
|
| 17 |
|
| 18 |
@layer base {
|
| 19 |
:root {
|
| 20 |
+
--background: 0 0% 4%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
--foreground: 0 0% 98%;
|
| 22 |
+
--card: 240 5% 8%;
|
| 23 |
--card-foreground: 0 0% 98%;
|
| 24 |
+
--popover: 240 5% 8%;
|
| 25 |
--popover-foreground: 0 0% 98%;
|
| 26 |
+
--primary: 358 85% 52%;
|
| 27 |
+
--primary-foreground: 0 0% 100%;
|
| 28 |
+
--secondary: 240 5% 12%;
|
| 29 |
--secondary-foreground: 0 0% 98%;
|
| 30 |
+
--muted: 240 5% 12%;
|
| 31 |
+
--muted-foreground: 240 5% 65%;
|
| 32 |
+
--accent: 240 5% 14%;
|
| 33 |
--accent-foreground: 0 0% 98%;
|
| 34 |
+
--destructive: 358 85% 52%;
|
| 35 |
--destructive-foreground: 0 0% 98%;
|
| 36 |
+
--border: 0 0% 15%;
|
| 37 |
+
--input: 0 0% 15%;
|
| 38 |
+
--ring: 358 85% 52%;
|
| 39 |
+
--radius: 0.125rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
}
|
| 41 |
}
|
| 42 |
|
|
|
|
| 53 |
[data-debug-wrapper="true"] {
|
| 54 |
display: contents !important;
|
| 55 |
}
|
|
|
|
| 56 |
[data-debug-wrapper="true"] > * {
|
| 57 |
+
margin-left: inherit; margin-right: inherit; margin-top: inherit; margin-bottom: inherit;
|
| 58 |
+
padding-left: inherit; padding-right: inherit; padding-top: inherit; padding-bottom: inherit;
|
| 59 |
+
column-gap: inherit; row-gap: inherit; gap: inherit;
|
| 60 |
+
border-left-width: inherit; border-right-width: inherit; border-top-width: inherit; border-bottom-width: inherit;
|
| 61 |
+
border-left-style: inherit; border-right-style: inherit; border-top-style: inherit; border-bottom-style: inherit;
|
| 62 |
+
border-left-color: inherit; border-right-color: inherit; border-top-color: inherit; border-bottom-color: inherit;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
}
|
| 64 |
}
|
frontend/src/lib/api.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import axios from "axios";
|
| 2 |
+
|
| 3 |
+
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL;
|
| 4 |
+
export const API = `${BACKEND_URL}/api`;
|
| 5 |
+
|
| 6 |
+
export const api = axios.create({ baseURL: API, timeout: 180000 });
|
| 7 |
+
|
| 8 |
+
export const fileToBase64 = (file) =>
|
| 9 |
+
new Promise((resolve, reject) => {
|
| 10 |
+
const reader = new FileReader();
|
| 11 |
+
reader.onload = () => {
|
| 12 |
+
const str = reader.result;
|
| 13 |
+
const comma = str.indexOf(",");
|
| 14 |
+
resolve(comma >= 0 ? str.slice(comma + 1) : str);
|
| 15 |
+
};
|
| 16 |
+
reader.onerror = reject;
|
| 17 |
+
reader.readAsDataURL(file);
|
| 18 |
+
});
|
frontend/src/pages/Blueprint.jsx
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
import { api } from "@/lib/api";
|
| 3 |
+
import { Cpu, HardDrive, Server, BookOpen, Bot, Rocket, ArrowDown } from "lucide-react";
|
| 4 |
+
|
| 5 |
+
const LAYER_ICONS = {
|
| 6 |
+
Hardware: Cpu,
|
| 7 |
+
Runtime: HardDrive,
|
| 8 |
+
Serving: Server,
|
| 9 |
+
Model: BookOpen,
|
| 10 |
+
Agents: Bot,
|
| 11 |
+
Product: Rocket,
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
const BLUEPRINT_IMG = "https://static.prod-images.emergentagent.com/jobs/d5829a2e-bc03-4880-adcd-73acc809a3bd/images/7251062dc0e36ea4218374b05cc959bc4e6c55a2cf4789a8a2cbc38db6392916.png";
|
| 15 |
+
|
| 16 |
+
export default function Blueprint() {
|
| 17 |
+
const [data, setData] = useState(null);
|
| 18 |
+
|
| 19 |
+
useEffect(() => {
|
| 20 |
+
api.get("/blueprint").then(({ data }) => setData(data)).catch(() => {});
|
| 21 |
+
}, []);
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
<div className="mx-auto max-w-[1400px] px-6 py-10" data-testid="blueprint-page">
|
| 25 |
+
<header className="mb-10 grid md:grid-cols-2 gap-10">
|
| 26 |
+
<div>
|
| 27 |
+
<div className="fs-label mb-3">§ BLUEPRINT · DEPLOYMENT STACK</div>
|
| 28 |
+
<h1 className="font-display font-black tracking-tighter text-4xl md:text-5xl">
|
| 29 |
+
The exact stack<br />we ship on MI300X.
|
| 30 |
+
</h1>
|
| 31 |
+
<p className="text-zinc-400 mt-4 max-w-lg">
|
| 32 |
+
Six layers. Zero CUDA lock-in. Every choice is justified against the constraints
|
| 33 |
+
of a factory-floor deployment: latency, privacy, and model memory footprint.
|
| 34 |
+
</p>
|
| 35 |
+
</div>
|
| 36 |
+
<div className="relative border border-white/10 overflow-hidden min-h-[240px]">
|
| 37 |
+
<img src={BLUEPRINT_IMG} alt="MI300X architecture" className="w-full h-full object-cover" />
|
| 38 |
+
<div className="absolute inset-0 bg-black/40" />
|
| 39 |
+
<div className="absolute bottom-3 left-3 fs-chip fs-chip-fail bg-black/70">AMD INSTINCT MI300X</div>
|
| 40 |
+
</div>
|
| 41 |
+
</header>
|
| 42 |
+
|
| 43 |
+
{/* Stack layers */}
|
| 44 |
+
<section className="mb-16">
|
| 45 |
+
<div className="fs-label mb-6">Stack · top to bottom</div>
|
| 46 |
+
<div className="border-l-2 border-[#ED1C24] pl-0">
|
| 47 |
+
{data?.stack?.map((layer, i) => {
|
| 48 |
+
const Icon = LAYER_ICONS[layer.layer] || Cpu;
|
| 49 |
+
return (
|
| 50 |
+
<div key={i} className="relative">
|
| 51 |
+
<div className="grid md:grid-cols-12 gap-6 border-b border-white/10 p-6 hover:bg-[#141416] transition-colors">
|
| 52 |
+
<div className="md:col-span-2 flex items-start gap-3">
|
| 53 |
+
<div className="w-9 h-9 border border-[#ED1C24] text-[#ED1C24] flex items-center justify-center">
|
| 54 |
+
<Icon className="w-4 h-4" />
|
| 55 |
+
</div>
|
| 56 |
+
<div>
|
| 57 |
+
<div className="fs-mono-small text-zinc-500">LAYER {String(i + 1).padStart(2, "0")}</div>
|
| 58 |
+
<div className="font-display font-bold text-sm">{layer.layer}</div>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
<div className="md:col-span-4">
|
| 62 |
+
<div className="font-display font-black tracking-tight text-xl">{layer.title}</div>
|
| 63 |
+
<div className="font-mono text-xs text-zinc-500 mt-1">{layer.detail}</div>
|
| 64 |
+
</div>
|
| 65 |
+
<div className="md:col-span-6 text-sm text-zinc-400 leading-relaxed">{layer.why}</div>
|
| 66 |
+
</div>
|
| 67 |
+
{i < (data?.stack?.length || 0) - 1 && (
|
| 68 |
+
<div className="flex justify-start pl-4 -mt-2 -mb-2">
|
| 69 |
+
<ArrowDown className="w-3.5 h-3.5 text-[#ED1C24]" />
|
| 70 |
+
</div>
|
| 71 |
+
)}
|
| 72 |
+
</div>
|
| 73 |
+
);
|
| 74 |
+
})}
|
| 75 |
+
</div>
|
| 76 |
+
</section>
|
| 77 |
+
|
| 78 |
+
{/* Fine-tune recipe */}
|
| 79 |
+
{data?.finetune_recipe && (
|
| 80 |
+
<section className="border border-white/10 bg-[#141416] p-8 fs-corners" data-testid="finetune-recipe">
|
| 81 |
+
<div className="flex items-end justify-between mb-6 flex-wrap gap-3">
|
| 82 |
+
<div>
|
| 83 |
+
<div className="fs-label mb-2">§ FINE-TUNE RECIPE · TRACK 2</div>
|
| 84 |
+
<h2 className="font-display font-black tracking-tighter text-2xl md:text-3xl">QLoRA on Qwen2-VL</h2>
|
| 85 |
+
</div>
|
| 86 |
+
<span className="fs-chip fs-chip-fail">MI300X · 8× GPU</span>
|
| 87 |
+
</div>
|
| 88 |
+
<div className="grid md:grid-cols-2 gap-0 border-t border-l border-white/10">
|
| 89 |
+
<Cell k="BASE MODEL" v={data.finetune_recipe.base_model} />
|
| 90 |
+
<Cell k="DATASET" v={data.finetune_recipe.dataset} />
|
| 91 |
+
<Cell k="METHOD" v={data.finetune_recipe.method} />
|
| 92 |
+
<Cell k="HARDWARE" v={data.finetune_recipe.hardware} />
|
| 93 |
+
<Cell k="WALL CLOCK" v={data.finetune_recipe.expected_wall_clock} />
|
| 94 |
+
<Cell k="SERVING" v={data.finetune_recipe.serve_with} />
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<pre className="mt-8 font-mono text-[12px] leading-relaxed text-zinc-300 bg-[#0A0A0A] border border-white/10 p-5 overflow-x-auto">{`# ForgeSight fine-tune — MI300X + ROCm
|
| 98 |
+
docker run --device=/dev/kfd --device=/dev/dri \\
|
| 99 |
+
--security-opt seccomp=unconfined --group-add video \\
|
| 100 |
+
rocm/pytorch:latest
|
| 101 |
+
|
| 102 |
+
pip install "transformers>=4.45" "peft" "bitsandbytes" \\
|
| 103 |
+
"optimum-amd" "datasets" "accelerate" "vllm"
|
| 104 |
+
|
| 105 |
+
# train
|
| 106 |
+
accelerate launch --mixed_precision bf16 train_qlora.py \\
|
| 107 |
+
--base Qwen/Qwen2-VL-72B-Instruct \\
|
| 108 |
+
--data forgesight/qc-10k \\
|
| 109 |
+
--lora_r 64 --lora_alpha 128 \\
|
| 110 |
+
--epochs 3 --batch_size 4 --grad_accum 8
|
| 111 |
+
|
| 112 |
+
# serve
|
| 113 |
+
vllm serve forgesight/qwen2-vl-72b-qc \\
|
| 114 |
+
--tensor-parallel-size 8 --dtype bfloat16 --port 8000`}</pre>
|
| 115 |
+
</section>
|
| 116 |
+
)}
|
| 117 |
+
</div>
|
| 118 |
+
);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
function Cell({ k, v }) {
|
| 122 |
+
return (
|
| 123 |
+
<div className="border-r border-b border-white/10 p-5">
|
| 124 |
+
<div className="fs-label mb-2">{k}</div>
|
| 125 |
+
<div className="font-mono text-sm text-white break-words">{v}</div>
|
| 126 |
+
</div>
|
| 127 |
+
);
|
| 128 |
+
}
|
frontend/src/pages/Console.jsx
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useCallback, useRef, useState } from "react";
|
| 2 |
+
import { Upload, Image as ImageIcon, PlayCircle, RotateCcw } from "lucide-react";
|
| 3 |
+
import { toast } from "sonner";
|
| 4 |
+
import { api, fileToBase64 } from "@/lib/api";
|
| 5 |
+
import TelemetryWidget from "@/components/TelemetryWidget";
|
| 6 |
+
import AgentTranscript from "@/components/AgentTranscript";
|
| 7 |
+
|
| 8 |
+
export default function Console() {
|
| 9 |
+
const [file, setFile] = useState(null);
|
| 10 |
+
const [preview, setPreview] = useState("");
|
| 11 |
+
const [notes, setNotes] = useState("");
|
| 12 |
+
const [spec, setSpec] = useState("");
|
| 13 |
+
const [loading, setLoading] = useState(false);
|
| 14 |
+
const [result, setResult] = useState(null);
|
| 15 |
+
const [dragActive, setDragActive] = useState(false);
|
| 16 |
+
const inputRef = useRef(null);
|
| 17 |
+
|
| 18 |
+
const handleFile = useCallback((f) => {
|
| 19 |
+
if (!f) return;
|
| 20 |
+
if (!/(jpe?g|png|webp)$/i.test(f.type.split("/")[1] || f.name.split(".").pop())) {
|
| 21 |
+
toast.error("Only JPEG / PNG / WEBP images are supported");
|
| 22 |
+
return;
|
| 23 |
+
}
|
| 24 |
+
setFile(f);
|
| 25 |
+
setPreview(URL.createObjectURL(f));
|
| 26 |
+
setResult(null);
|
| 27 |
+
}, []);
|
| 28 |
+
|
| 29 |
+
const onDrop = (e) => {
|
| 30 |
+
e.preventDefault();
|
| 31 |
+
setDragActive(false);
|
| 32 |
+
const f = e.dataTransfer.files?.[0];
|
| 33 |
+
handleFile(f);
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
const runInspection = async () => {
|
| 37 |
+
if (!file) {
|
| 38 |
+
toast.error("Upload an image first");
|
| 39 |
+
return;
|
| 40 |
+
}
|
| 41 |
+
setLoading(true);
|
| 42 |
+
setResult(null);
|
| 43 |
+
try {
|
| 44 |
+
const image_base64 = await fileToBase64(file);
|
| 45 |
+
const { data } = await api.post("/inspections", {
|
| 46 |
+
image_base64,
|
| 47 |
+
notes,
|
| 48 |
+
product_spec: spec,
|
| 49 |
+
source: "upload",
|
| 50 |
+
});
|
| 51 |
+
setResult(data);
|
| 52 |
+
toast.success("Inspection complete");
|
| 53 |
+
} catch (e) {
|
| 54 |
+
toast.error(e?.response?.data?.detail || "Inspection failed");
|
| 55 |
+
} finally {
|
| 56 |
+
setLoading(false);
|
| 57 |
+
}
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
const reset = () => {
|
| 61 |
+
setFile(null);
|
| 62 |
+
setPreview("");
|
| 63 |
+
setNotes("");
|
| 64 |
+
setSpec("");
|
| 65 |
+
setResult(null);
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
+
const summary = result?.summary;
|
| 69 |
+
|
| 70 |
+
return (
|
| 71 |
+
<div className="mx-auto max-w-[1400px] px-6 py-10" data-testid="console-page">
|
| 72 |
+
<header className="mb-8">
|
| 73 |
+
<div className="fs-label mb-3">§ CONSOLE · REAL-TIME INFERENCE</div>
|
| 74 |
+
<h1 className="font-display font-black tracking-tighter text-4xl md:text-5xl">
|
| 75 |
+
Inspection Console
|
| 76 |
+
</h1>
|
| 77 |
+
<p className="text-zinc-400 mt-3 max-w-2xl">
|
| 78 |
+
Upload a product / assembly-line image. Four agents will collaborate to deliver a verdict.
|
| 79 |
+
</p>
|
| 80 |
+
</header>
|
| 81 |
+
|
| 82 |
+
<div className="grid lg:grid-cols-12 gap-6">
|
| 83 |
+
{/* LEFT — input */}
|
| 84 |
+
<div className="lg:col-span-5 space-y-6">
|
| 85 |
+
<div className="border border-white/10 bg-[#141416] p-5 fs-corners">
|
| 86 |
+
<div className="flex items-center justify-between mb-4">
|
| 87 |
+
<span className="fs-label">Specimen</span>
|
| 88 |
+
{file && (
|
| 89 |
+
<button
|
| 90 |
+
onClick={reset}
|
| 91 |
+
className="fs-chip hover:text-white hover:border-white/40 inline-flex items-center gap-1"
|
| 92 |
+
data-testid="reset-btn"
|
| 93 |
+
>
|
| 94 |
+
<RotateCcw className="w-3 h-3" /> Reset
|
| 95 |
+
</button>
|
| 96 |
+
)}
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
{!preview ? (
|
| 100 |
+
<div
|
| 101 |
+
onDragOver={(e) => {
|
| 102 |
+
e.preventDefault();
|
| 103 |
+
setDragActive(true);
|
| 104 |
+
}}
|
| 105 |
+
onDragLeave={() => setDragActive(false)}
|
| 106 |
+
onDrop={onDrop}
|
| 107 |
+
className={`fs-drop ${dragActive ? "fs-drop-active" : ""} h-64 flex flex-col items-center justify-center cursor-pointer`}
|
| 108 |
+
onClick={() => inputRef.current?.click()}
|
| 109 |
+
data-testid="drop-zone"
|
| 110 |
+
>
|
| 111 |
+
<Upload className="w-8 h-8 text-zinc-500 mb-3" />
|
| 112 |
+
<div className="font-mono text-sm text-zinc-300">Drop image here</div>
|
| 113 |
+
<div className="fs-mono-small text-zinc-500 mt-1">or click to browse · JPG · PNG · WEBP</div>
|
| 114 |
+
<input
|
| 115 |
+
ref={inputRef}
|
| 116 |
+
type="file"
|
| 117 |
+
accept="image/jpeg,image/png,image/webp"
|
| 118 |
+
className="hidden"
|
| 119 |
+
onChange={(e) => handleFile(e.target.files?.[0])}
|
| 120 |
+
data-testid="file-input"
|
| 121 |
+
/>
|
| 122 |
+
</div>
|
| 123 |
+
) : (
|
| 124 |
+
<div className="relative border border-white/10">
|
| 125 |
+
<img src={preview} alt="specimen" className="w-full h-64 object-cover" data-testid="preview-img" />
|
| 126 |
+
<div className="absolute top-2 left-2 fs-chip bg-black/80">
|
| 127 |
+
<ImageIcon className="w-3 h-3 inline mr-1" />
|
| 128 |
+
{file?.name?.slice(0, 28)}
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
)}
|
| 132 |
+
|
| 133 |
+
<div className="mt-5 space-y-3">
|
| 134 |
+
<div>
|
| 135 |
+
<div className="fs-label mb-2">Operator Notes</div>
|
| 136 |
+
<textarea
|
| 137 |
+
value={notes}
|
| 138 |
+
onChange={(e) => setNotes(e.target.value)}
|
| 139 |
+
rows={2}
|
| 140 |
+
placeholder="e.g. batch B-124, shift 2, CNC line 3…"
|
| 141 |
+
className="w-full bg-[#0A0A0A] border border-white/10 focus:border-[#ED1C24] outline-none px-3 py-2 font-mono text-sm text-white placeholder-zinc-600"
|
| 142 |
+
data-testid="notes-input"
|
| 143 |
+
/>
|
| 144 |
+
</div>
|
| 145 |
+
<div>
|
| 146 |
+
<div className="fs-label mb-2">Product Spec (optional)</div>
|
| 147 |
+
<textarea
|
| 148 |
+
value={spec}
|
| 149 |
+
onChange={(e) => setSpec(e.target.value)}
|
| 150 |
+
rows={2}
|
| 151 |
+
placeholder="e.g. aluminum 6061 bracket, max surface defect 0.2mm…"
|
| 152 |
+
className="w-full bg-[#0A0A0A] border border-white/10 focus:border-[#ED1C24] outline-none px-3 py-2 font-mono text-sm text-white placeholder-zinc-600"
|
| 153 |
+
data-testid="spec-input"
|
| 154 |
+
/>
|
| 155 |
+
</div>
|
| 156 |
+
<button
|
| 157 |
+
disabled={loading || !file}
|
| 158 |
+
onClick={runInspection}
|
| 159 |
+
className="fs-btn fs-btn-primary w-full inline-flex items-center justify-center gap-2 disabled:opacity-40 disabled:cursor-not-allowed"
|
| 160 |
+
data-testid="run-inspection-btn"
|
| 161 |
+
>
|
| 162 |
+
{loading ? (
|
| 163 |
+
<>Running pipeline<span className="fs-cursor" /></>
|
| 164 |
+
) : (
|
| 165 |
+
<>
|
| 166 |
+
<PlayCircle className="w-4 h-4" /> Run inspection
|
| 167 |
+
</>
|
| 168 |
+
)}
|
| 169 |
+
</button>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
|
| 173 |
+
<TelemetryWidget />
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
{/* RIGHT — transcript */}
|
| 177 |
+
<div className="lg:col-span-7 space-y-6">
|
| 178 |
+
{summary && (
|
| 179 |
+
<div className="border border-white/10 bg-[#141416] p-5 grid grid-cols-2 md:grid-cols-4 gap-4 fs-rise" data-testid="summary-panel">
|
| 180 |
+
<SummaryStat label="Verdict" value={summary.verdict.toUpperCase()} kind={summary.verdict} />
|
| 181 |
+
<SummaryStat label="Confidence" value={`${Math.round(summary.confidence * 100)}%`} />
|
| 182 |
+
<SummaryStat label="Defects" value={summary.defect_count} />
|
| 183 |
+
<SummaryStat label="Priority" value={summary.priority} />
|
| 184 |
+
</div>
|
| 185 |
+
)}
|
| 186 |
+
|
| 187 |
+
{result ? (
|
| 188 |
+
<AgentTranscript transcript={result.transcript} />
|
| 189 |
+
) : (
|
| 190 |
+
<EmptyTranscript loading={loading} />
|
| 191 |
+
)}
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
| 194 |
+
</div>
|
| 195 |
+
);
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
function SummaryStat({ label, value, kind }) {
|
| 199 |
+
const color =
|
| 200 |
+
kind === "pass" ? "text-[#10B981]" :
|
| 201 |
+
kind === "warn" ? "text-[#F59E0B]" :
|
| 202 |
+
kind === "fail" ? "text-[#ED1C24]" : "text-white";
|
| 203 |
+
return (
|
| 204 |
+
<div>
|
| 205 |
+
<div className="fs-label mb-1">{label}</div>
|
| 206 |
+
<div className={`font-display font-black text-2xl tabular-nums ${color}`}>{value}</div>
|
| 207 |
+
</div>
|
| 208 |
+
);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
function EmptyTranscript({ loading }) {
|
| 212 |
+
return (
|
| 213 |
+
<div className="border border-white/10 bg-[#0d0d10] fs-scanlines p-10 text-center" data-testid="empty-transcript">
|
| 214 |
+
<div className="fs-label mb-4">Awaiting specimen</div>
|
| 215 |
+
<div className="font-mono text-sm text-zinc-500 max-w-md mx-auto">
|
| 216 |
+
{loading ? (
|
| 217 |
+
<>
|
| 218 |
+
Running 4-agent pipeline
|
| 219 |
+
<span className="fs-cursor" />
|
| 220 |
+
</>
|
| 221 |
+
) : (
|
| 222 |
+
<>Upload an image and hit <kbd>Run inspection</kbd>. Agents stream their reasoning live.</>
|
| 223 |
+
)}
|
| 224 |
+
</div>
|
| 225 |
+
</div>
|
| 226 |
+
);
|
| 227 |
+
}
|
frontend/src/pages/Feed.jsx
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
import { Link } from "react-router-dom";
|
| 3 |
+
import { api } from "@/lib/api";
|
| 4 |
+
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from "recharts";
|
| 5 |
+
import { AlertTriangle, CheckCircle2, XCircle, TrendingUp } from "lucide-react";
|
| 6 |
+
|
| 7 |
+
export default function Feed() {
|
| 8 |
+
const [metrics, setMetrics] = useState(null);
|
| 9 |
+
const [items, setItems] = useState([]);
|
| 10 |
+
|
| 11 |
+
const load = async () => {
|
| 12 |
+
try {
|
| 13 |
+
const [m, l] = await Promise.all([api.get("/metrics"), api.get("/inspections")]);
|
| 14 |
+
setMetrics(m.data);
|
| 15 |
+
setItems(l.data.items || []);
|
| 16 |
+
} catch {}
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
useEffect(() => {
|
| 20 |
+
load();
|
| 21 |
+
}, []);
|
| 22 |
+
|
| 23 |
+
const verdictChart = metrics
|
| 24 |
+
? [
|
| 25 |
+
{ name: "pass", value: metrics.verdict_counts.pass, color: "#10B981" },
|
| 26 |
+
{ name: "warn", value: metrics.verdict_counts.warn, color: "#F59E0B" },
|
| 27 |
+
{ name: "fail", value: metrics.verdict_counts.fail, color: "#ED1C24" },
|
| 28 |
+
]
|
| 29 |
+
: [];
|
| 30 |
+
|
| 31 |
+
return (
|
| 32 |
+
<div className="mx-auto max-w-[1400px] px-6 py-10" data-testid="feed-page">
|
| 33 |
+
<header className="mb-8 flex flex-wrap items-end justify-between gap-4">
|
| 34 |
+
<div>
|
| 35 |
+
<div className="fs-label mb-3">§ FEED · DASHBOARD</div>
|
| 36 |
+
<h1 className="font-display font-black tracking-tighter text-4xl md:text-5xl">
|
| 37 |
+
Defect Feed
|
| 38 |
+
</h1>
|
| 39 |
+
<p className="text-zinc-400 mt-3">Every inspection. Live quality score.</p>
|
| 40 |
+
</div>
|
| 41 |
+
<Link to="/console" className="fs-btn fs-btn-primary" data-testid="feed-run-btn">
|
| 42 |
+
+ New inspection
|
| 43 |
+
</Link>
|
| 44 |
+
</header>
|
| 45 |
+
|
| 46 |
+
{/* Metrics grid */}
|
| 47 |
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-0 border-t border-l border-white/10 mb-8">
|
| 48 |
+
<Kpi label="Total inspections" value={metrics?.total_inspections ?? "—"} icon={TrendingUp} />
|
| 49 |
+
<Kpi label="Quality score" value={metrics ? `${metrics.quality_score}%` : "—"} icon={CheckCircle2} accent />
|
| 50 |
+
<Kpi label="Avg confidence" value={metrics ? `${Math.round(metrics.avg_confidence * 100)}%` : "—"} icon={AlertTriangle} />
|
| 51 |
+
<Kpi label="Fail rate" value={metrics && metrics.total_inspections ? `${Math.round((metrics.verdict_counts.fail / metrics.total_inspections) * 100)}%` : "—"} icon={XCircle} />
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<div className="grid lg:grid-cols-2 gap-6 mb-8">
|
| 55 |
+
{/* Verdict chart */}
|
| 56 |
+
<div className="border border-white/10 bg-[#141416] p-5 fs-corners">
|
| 57 |
+
<div className="flex items-center justify-between mb-4">
|
| 58 |
+
<span className="fs-label">Verdict distribution</span>
|
| 59 |
+
<span className="fs-mono-small text-zinc-500">ALL TIME</span>
|
| 60 |
+
</div>
|
| 61 |
+
<div className="h-56">
|
| 62 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 63 |
+
<BarChart data={verdictChart}>
|
| 64 |
+
<XAxis dataKey="name" stroke="#71717A" tick={{ fontFamily: "JetBrains Mono", fontSize: 11 }} axisLine={{ stroke: "#27272A" }} tickLine={false} />
|
| 65 |
+
<YAxis stroke="#71717A" tick={{ fontFamily: "JetBrains Mono", fontSize: 11 }} axisLine={{ stroke: "#27272A" }} tickLine={false} />
|
| 66 |
+
<Tooltip
|
| 67 |
+
cursor={{ fill: "rgba(237,28,36,0.08)" }}
|
| 68 |
+
contentStyle={{ background: "#0A0A0A", border: "1px solid #27272A", fontFamily: "JetBrains Mono" }}
|
| 69 |
+
labelStyle={{ color: "#fff" }}
|
| 70 |
+
/>
|
| 71 |
+
<Bar dataKey="value">
|
| 72 |
+
{verdictChart.map((entry, i) => (
|
| 73 |
+
<Cell key={i} fill={entry.color} />
|
| 74 |
+
))}
|
| 75 |
+
</Bar>
|
| 76 |
+
</BarChart>
|
| 77 |
+
</ResponsiveContainer>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
{/* Top defects */}
|
| 82 |
+
<div className="border border-white/10 bg-[#141416] p-5 fs-corners">
|
| 83 |
+
<div className="flex items-center justify-between mb-4">
|
| 84 |
+
<span className="fs-label">Top defect types</span>
|
| 85 |
+
</div>
|
| 86 |
+
{metrics && metrics.top_defects.length ? (
|
| 87 |
+
<div className="space-y-2.5">
|
| 88 |
+
{metrics.top_defects.map((d) => {
|
| 89 |
+
const max = metrics.top_defects[0].count || 1;
|
| 90 |
+
const pct = (d.count / max) * 100;
|
| 91 |
+
return (
|
| 92 |
+
<div key={d.type}>
|
| 93 |
+
<div className="flex items-baseline justify-between mb-1">
|
| 94 |
+
<span className="font-mono text-xs text-zinc-300">{d.type}</span>
|
| 95 |
+
<span className="font-mono text-xs text-zinc-500 tabular-nums">{d.count}</span>
|
| 96 |
+
</div>
|
| 97 |
+
<div className="fs-bar">
|
| 98 |
+
<div style={{ width: `${Math.max(8, pct)}%` }} />
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
);
|
| 102 |
+
})}
|
| 103 |
+
</div>
|
| 104 |
+
) : (
|
| 105 |
+
<div className="h-40 flex items-center justify-center font-mono text-sm text-zinc-600">
|
| 106 |
+
No inspections yet.
|
| 107 |
+
</div>
|
| 108 |
+
)}
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
{/* Table */}
|
| 113 |
+
<div className="border border-white/10 bg-[#141416]">
|
| 114 |
+
<div className="flex items-center justify-between px-5 py-4 border-b border-white/10">
|
| 115 |
+
<span className="fs-label">Inspection log · {items.length}</span>
|
| 116 |
+
</div>
|
| 117 |
+
{items.length === 0 ? (
|
| 118 |
+
<div className="p-10 text-center font-mono text-sm text-zinc-500">
|
| 119 |
+
No inspections yet. Head to the Console to run the first one.
|
| 120 |
+
</div>
|
| 121 |
+
) : (
|
| 122 |
+
<div className="overflow-x-auto">
|
| 123 |
+
<table className="w-full text-sm" data-testid="inspections-table">
|
| 124 |
+
<thead>
|
| 125 |
+
<tr className="text-left border-b border-white/10">
|
| 126 |
+
<Th>Time</Th>
|
| 127 |
+
<Th>Verdict</Th>
|
| 128 |
+
<Th>Headline</Th>
|
| 129 |
+
<Th>Defects</Th>
|
| 130 |
+
<Th>Priority</Th>
|
| 131 |
+
<Th>Confidence</Th>
|
| 132 |
+
</tr>
|
| 133 |
+
</thead>
|
| 134 |
+
<tbody>
|
| 135 |
+
{items.map((it) => (
|
| 136 |
+
<tr key={it.id} className="border-b border-white/5 hover:bg-white/[0.02]">
|
| 137 |
+
<Td mono>{new Date(it.created_at).toLocaleString()}</Td>
|
| 138 |
+
<Td>
|
| 139 |
+
<span className={`fs-chip fs-chip-${it.verdict}`}>{it.verdict}</span>
|
| 140 |
+
</Td>
|
| 141 |
+
<Td>{it.headline}</Td>
|
| 142 |
+
<Td mono>{it.defect_count}</Td>
|
| 143 |
+
<Td mono>{it.priority}</Td>
|
| 144 |
+
<Td mono>{Math.round(it.confidence * 100)}%</Td>
|
| 145 |
+
</tr>
|
| 146 |
+
))}
|
| 147 |
+
</tbody>
|
| 148 |
+
</table>
|
| 149 |
+
</div>
|
| 150 |
+
)}
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
function Kpi({ label, value, icon: Icon, accent }) {
|
| 157 |
+
return (
|
| 158 |
+
<div className="border-r border-b border-white/10 p-5">
|
| 159 |
+
<div className="flex items-center justify-between mb-3">
|
| 160 |
+
<span className="fs-label">{label}</span>
|
| 161 |
+
<Icon className={`w-3.5 h-3.5 ${accent ? "text-[#ED1C24]" : "text-zinc-500"}`} />
|
| 162 |
+
</div>
|
| 163 |
+
<div className={`font-display font-black text-3xl md:text-4xl tracking-tighter ${accent ? "text-[#ED1C24]" : "text-white"}`}>
|
| 164 |
+
{value}
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
);
|
| 168 |
+
}
|
| 169 |
+
function Th({ children }) {
|
| 170 |
+
return <th className="px-5 py-3 fs-label font-normal">{children}</th>;
|
| 171 |
+
}
|
| 172 |
+
function Td({ children, mono }) {
|
| 173 |
+
return <td className={`px-5 py-3 ${mono ? "font-mono text-xs" : "text-sm"} text-zinc-300`}>{children}</td>;
|
| 174 |
+
}
|
frontend/src/pages/Journal.jsx
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
import { api } from "@/lib/api";
|
| 3 |
+
import { toast } from "sonner";
|
| 4 |
+
import { Twitter, Linkedin, Copy, Plus, Sparkles } from "lucide-react";
|
| 5 |
+
|
| 6 |
+
export default function Journal() {
|
| 7 |
+
const [items, setItems] = useState([]);
|
| 8 |
+
const [title, setTitle] = useState("");
|
| 9 |
+
const [body, setBody] = useState("");
|
| 10 |
+
const [tags, setTags] = useState("");
|
| 11 |
+
const [busy, setBusy] = useState(false);
|
| 12 |
+
|
| 13 |
+
const load = async () => {
|
| 14 |
+
try {
|
| 15 |
+
const { data } = await api.get("/journal");
|
| 16 |
+
setItems(data.items || []);
|
| 17 |
+
if ((data.items || []).length === 0) {
|
| 18 |
+
await api.post("/journal/seed");
|
| 19 |
+
const r = await api.get("/journal");
|
| 20 |
+
setItems(r.data.items || []);
|
| 21 |
+
}
|
| 22 |
+
} catch {}
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
useEffect(() => {
|
| 26 |
+
load();
|
| 27 |
+
}, []);
|
| 28 |
+
|
| 29 |
+
const submit = async () => {
|
| 30 |
+
if (!title.trim() || !body.trim()) {
|
| 31 |
+
toast.error("Title + body required");
|
| 32 |
+
return;
|
| 33 |
+
}
|
| 34 |
+
setBusy(true);
|
| 35 |
+
try {
|
| 36 |
+
const { data } = await api.post("/journal", {
|
| 37 |
+
title,
|
| 38 |
+
body,
|
| 39 |
+
tags: tags.split(",").map((t) => t.trim()).filter(Boolean),
|
| 40 |
+
});
|
| 41 |
+
setItems((prev) => [data, ...prev]);
|
| 42 |
+
setTitle("");
|
| 43 |
+
setBody("");
|
| 44 |
+
setTags("");
|
| 45 |
+
toast.success("Milestone logged + social drafts generated");
|
| 46 |
+
} catch (e) {
|
| 47 |
+
toast.error(e?.response?.data?.detail || "Failed to log milestone");
|
| 48 |
+
} finally {
|
| 49 |
+
setBusy(false);
|
| 50 |
+
}
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
const copy = async (text, label) => {
|
| 54 |
+
try {
|
| 55 |
+
await navigator.clipboard.writeText(text);
|
| 56 |
+
toast.success(`${label} copied`);
|
| 57 |
+
} catch {
|
| 58 |
+
toast.error("Copy failed");
|
| 59 |
+
}
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
return (
|
| 63 |
+
<div className="mx-auto max-w-[1400px] px-6 py-10" data-testid="journal-page">
|
| 64 |
+
<header className="mb-8">
|
| 65 |
+
<div className="fs-label mb-3">§ JOURNAL · BUILD-IN-PUBLIC</div>
|
| 66 |
+
<h1 className="font-display font-black tracking-tighter text-4xl md:text-5xl">Build Journal</h1>
|
| 67 |
+
<p className="text-zinc-400 mt-3 max-w-2xl">
|
| 68 |
+
Every milestone auto-drafts social posts — X + LinkedIn — ready to ship, hashtags and AMD / lablab mentions baked in.
|
| 69 |
+
</p>
|
| 70 |
+
</header>
|
| 71 |
+
|
| 72 |
+
<div className="grid lg:grid-cols-12 gap-6">
|
| 73 |
+
{/* Composer */}
|
| 74 |
+
<aside className="lg:col-span-4">
|
| 75 |
+
<div className="border border-white/10 bg-[#141416] p-5 fs-corners sticky top-20" data-testid="journal-composer">
|
| 76 |
+
<div className="flex items-center gap-2 mb-4">
|
| 77 |
+
<Sparkles className="w-3.5 h-3.5 text-[#ED1C24]" />
|
| 78 |
+
<span className="fs-label">New milestone</span>
|
| 79 |
+
</div>
|
| 80 |
+
<div className="space-y-3">
|
| 81 |
+
<input
|
| 82 |
+
value={title}
|
| 83 |
+
onChange={(e) => setTitle(e.target.value)}
|
| 84 |
+
placeholder="Title…"
|
| 85 |
+
className="w-full bg-[#0A0A0A] border border-white/10 focus:border-[#ED1C24] outline-none px-3 py-2 font-mono text-sm"
|
| 86 |
+
data-testid="journal-title-input"
|
| 87 |
+
/>
|
| 88 |
+
<textarea
|
| 89 |
+
value={body}
|
| 90 |
+
onChange={(e) => setBody(e.target.value)}
|
| 91 |
+
rows={5}
|
| 92 |
+
placeholder="What happened today?"
|
| 93 |
+
className="w-full bg-[#0A0A0A] border border-white/10 focus:border-[#ED1C24] outline-none px-3 py-2 font-mono text-sm"
|
| 94 |
+
data-testid="journal-body-input"
|
| 95 |
+
/>
|
| 96 |
+
<input
|
| 97 |
+
value={tags}
|
| 98 |
+
onChange={(e) => setTags(e.target.value)}
|
| 99 |
+
placeholder="tags, comma, separated"
|
| 100 |
+
className="w-full bg-[#0A0A0A] border border-white/10 focus:border-[#ED1C24] outline-none px-3 py-2 font-mono text-sm"
|
| 101 |
+
data-testid="journal-tags-input"
|
| 102 |
+
/>
|
| 103 |
+
<button
|
| 104 |
+
disabled={busy}
|
| 105 |
+
onClick={submit}
|
| 106 |
+
className="fs-btn fs-btn-primary w-full inline-flex items-center justify-center gap-2 disabled:opacity-50"
|
| 107 |
+
data-testid="journal-submit-btn"
|
| 108 |
+
>
|
| 109 |
+
{busy ? (
|
| 110 |
+
<>Generating drafts<span className="fs-cursor" /></>
|
| 111 |
+
) : (
|
| 112 |
+
<>
|
| 113 |
+
<Plus className="w-4 h-4" /> Log + draft posts
|
| 114 |
+
</>
|
| 115 |
+
)}
|
| 116 |
+
</button>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</aside>
|
| 120 |
+
|
| 121 |
+
{/* Timeline */}
|
| 122 |
+
<section className="lg:col-span-8 space-y-5" data-testid="journal-timeline">
|
| 123 |
+
{items.length === 0 && (
|
| 124 |
+
<div className="border border-white/10 bg-[#141416] p-10 text-center font-mono text-sm text-zinc-500">
|
| 125 |
+
No entries yet. Log your first milestone →
|
| 126 |
+
</div>
|
| 127 |
+
)}
|
| 128 |
+
{items.map((e) => (
|
| 129 |
+
<article key={e.id} className="border border-white/10 bg-[#141416] p-6 fs-rise" data-testid={`journal-entry-${e.id}`}>
|
| 130 |
+
<div className="flex items-center justify-between mb-3 flex-wrap gap-2">
|
| 131 |
+
<div className="flex items-center gap-2">
|
| 132 |
+
<span className="fs-chip fs-chip-fail">{new Date(e.created_at).toLocaleDateString()}</span>
|
| 133 |
+
{e.tags?.map((t) => (
|
| 134 |
+
<span key={t} className="fs-chip">#{t}</span>
|
| 135 |
+
))}
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
<h3 className="font-display font-black tracking-tight text-xl mb-2">{e.title}</h3>
|
| 139 |
+
<p className="text-sm text-zinc-300 leading-relaxed whitespace-pre-line">{e.body}</p>
|
| 140 |
+
|
| 141 |
+
<div className="grid md:grid-cols-2 gap-3 mt-5">
|
| 142 |
+
{e.x_post && (
|
| 143 |
+
<SocialCard
|
| 144 |
+
icon={Twitter}
|
| 145 |
+
label="X POST"
|
| 146 |
+
text={e.x_post}
|
| 147 |
+
onCopy={() => copy(e.x_post, "X post")}
|
| 148 |
+
testid={`x-post-${e.id}`}
|
| 149 |
+
/>
|
| 150 |
+
)}
|
| 151 |
+
{e.linkedin_post && (
|
| 152 |
+
<SocialCard
|
| 153 |
+
icon={Linkedin}
|
| 154 |
+
label="LINKEDIN POST"
|
| 155 |
+
text={e.linkedin_post}
|
| 156 |
+
onCopy={() => copy(e.linkedin_post, "LinkedIn post")}
|
| 157 |
+
testid={`li-post-${e.id}`}
|
| 158 |
+
/>
|
| 159 |
+
)}
|
| 160 |
+
</div>
|
| 161 |
+
</article>
|
| 162 |
+
))}
|
| 163 |
+
</section>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
function SocialCard({ icon: Icon, label, text, onCopy, testid }) {
|
| 170 |
+
return (
|
| 171 |
+
<div className="border border-white/10 bg-[#0A0A0A] p-4" data-testid={testid}>
|
| 172 |
+
<div className="flex items-center justify-between mb-2">
|
| 173 |
+
<div className="flex items-center gap-2">
|
| 174 |
+
<Icon className="w-3.5 h-3.5 text-[#ED1C24]" />
|
| 175 |
+
<span className="fs-label">{label}</span>
|
| 176 |
+
</div>
|
| 177 |
+
<button
|
| 178 |
+
onClick={onCopy}
|
| 179 |
+
className="fs-chip hover:text-white hover:border-white/40 inline-flex items-center gap-1"
|
| 180 |
+
>
|
| 181 |
+
<Copy className="w-3 h-3" /> copy
|
| 182 |
+
</button>
|
| 183 |
+
</div>
|
| 184 |
+
<div className="font-mono text-xs text-zinc-300 leading-relaxed whitespace-pre-line">{text}</div>
|
| 185 |
+
</div>
|
| 186 |
+
);
|
| 187 |
+
}
|
frontend/src/pages/Landing.jsx
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Link } from "react-router-dom";
|
| 2 |
+
import { ArrowRight, Activity, Eye, Cpu, Megaphone, Layers, Zap } from "lucide-react";
|
| 3 |
+
|
| 4 |
+
const HERO = "https://static.prod-images.emergentagent.com/jobs/d5829a2e-bc03-4880-adcd-73acc809a3bd/images/184a8bf32b150669152ea3aa72546730d8caad845b1b8eb0233eeb35e4255eeb.png";
|
| 5 |
+
|
| 6 |
+
export default function Landing() {
|
| 7 |
+
return (
|
| 8 |
+
<div className="text-white" data-testid="landing-page">
|
| 9 |
+
{/* HERO */}
|
| 10 |
+
<section className="relative border-b border-white/10 overflow-hidden">
|
| 11 |
+
<div
|
| 12 |
+
className="absolute inset-0 bg-cover bg-center"
|
| 13 |
+
style={{ backgroundImage: `url(${HERO})` }}
|
| 14 |
+
/>
|
| 15 |
+
<div className="absolute inset-0 bg-black/70" />
|
| 16 |
+
<div className="absolute inset-0 fs-grid-dots opacity-40" />
|
| 17 |
+
|
| 18 |
+
<div className="relative mx-auto max-w-[1400px] px-6 py-24 md:py-32 lg:py-40">
|
| 19 |
+
<div className="flex items-center gap-2 mb-8 fs-rise">
|
| 20 |
+
<span className="fs-chip fs-chip-fail">LIVE DEMO</span>
|
| 21 |
+
<span className="fs-chip">AMD HACKATHON · TRACKS 1 · 2 · 3</span>
|
| 22 |
+
<span className="fs-chip hidden md:inline-flex">QWEN CATEGORY</span>
|
| 23 |
+
</div>
|
| 24 |
+
|
| 25 |
+
<h1 className="font-display font-black tracking-tighter leading-[0.88] text-5xl md:text-7xl lg:text-8xl max-w-4xl fs-rise">
|
| 26 |
+
MULTIMODAL<br />
|
| 27 |
+
<span className="fs-stroke">QUALITY-CONTROL</span><br />
|
| 28 |
+
COPILOT.
|
| 29 |
+
</h1>
|
| 30 |
+
|
| 31 |
+
<p className="mt-10 max-w-2xl text-zinc-300 text-base md:text-lg leading-relaxed fs-rise">
|
| 32 |
+
ForgeSight ships a 4-agent pipeline that inspects assembly-line images,
|
| 33 |
+
diagnoses root cause, drafts work orders, and publishes reports — fine-tuned
|
| 34 |
+
on <span className="text-white font-semibold">Qwen2-VL</span> and served on
|
| 35 |
+
<span className="text-white font-semibold"> AMD Instinct MI300X</span> via ROCm + vLLM.
|
| 36 |
+
</p>
|
| 37 |
+
|
| 38 |
+
<div className="mt-10 flex flex-wrap items-center gap-3 fs-rise">
|
| 39 |
+
<Link
|
| 40 |
+
to="/console"
|
| 41 |
+
className="fs-btn fs-btn-primary inline-flex items-center gap-2"
|
| 42 |
+
data-testid="hero-cta-console"
|
| 43 |
+
>
|
| 44 |
+
Run a live inspection <ArrowRight className="w-3.5 h-3.5" />
|
| 45 |
+
</Link>
|
| 46 |
+
<Link to="/blueprint" className="fs-btn inline-flex items-center gap-2" data-testid="hero-cta-blueprint">
|
| 47 |
+
See the MI300X blueprint
|
| 48 |
+
</Link>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<div className="mt-16 grid grid-cols-2 md:grid-cols-4 gap-0 max-w-3xl fs-rise border-t border-white/10">
|
| 52 |
+
<Stat k="192 GB" v="HBM3 per GPU" />
|
| 53 |
+
<Stat k="5.3 TB/s" v="memory bandwidth" />
|
| 54 |
+
<Stat k="4" v="cooperating agents" />
|
| 55 |
+
<Stat k="~6 h" v="QLoRA wall-clock" />
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
</section>
|
| 59 |
+
|
| 60 |
+
{/* MANIFESTO */}
|
| 61 |
+
<section className="border-b border-white/10">
|
| 62 |
+
<div className="mx-auto max-w-[1400px] px-6 py-20 grid md:grid-cols-12 gap-10">
|
| 63 |
+
<div className="md:col-span-4">
|
| 64 |
+
<div className="fs-label mb-4">§ 01 · THESIS</div>
|
| 65 |
+
<h2 className="font-display font-black tracking-tighter text-3xl md:text-4xl leading-[0.95]">
|
| 66 |
+
Generic VLMs<br />don't know what<br />a bad weld looks like.
|
| 67 |
+
</h2>
|
| 68 |
+
</div>
|
| 69 |
+
<div className="md:col-span-8 text-zinc-300 space-y-5 text-base leading-relaxed">
|
| 70 |
+
<p>
|
| 71 |
+
Manufacturing QC is context-heavy, privacy-sensitive, and latency-critical.
|
| 72 |
+
Zero-shot vision models hallucinate on defects they've never seen.
|
| 73 |
+
</p>
|
| 74 |
+
<p>
|
| 75 |
+
ForgeSight fine-tunes <span className="text-white">Qwen2-VL</span> on
|
| 76 |
+
domain pairs of <span className="font-mono text-[#ED1C24]">{"{defect image → work order}"}</span>,
|
| 77 |
+
then serves the result with <span className="text-white">vLLM on ROCm</span> across an
|
| 78 |
+
MI300X node. The massive 192 GB HBM3 per device lets us keep the full 72B model resident
|
| 79 |
+
without sharding overhead — real-time inference on the factory floor.
|
| 80 |
+
</p>
|
| 81 |
+
<p>
|
| 82 |
+
Around the model, four cooperating agents (Inspector → Diagnostician → Action → Reporter)
|
| 83 |
+
produce strict-JSON hand-offs so every verdict is auditable.
|
| 84 |
+
</p>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
</section>
|
| 88 |
+
|
| 89 |
+
{/* FEATURES */}
|
| 90 |
+
<section className="border-b border-white/10">
|
| 91 |
+
<div className="mx-auto max-w-[1400px] px-6 py-20">
|
| 92 |
+
<div className="flex items-end justify-between mb-12">
|
| 93 |
+
<div>
|
| 94 |
+
<div className="fs-label mb-4">§ 02 · MODULES</div>
|
| 95 |
+
<h2 className="font-display font-black tracking-tighter text-3xl md:text-4xl">What's in the box.</h2>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-0 border-t border-l border-white/10">
|
| 100 |
+
<Feat icon={Eye} title="Inspection Console" body="Drag, drop, inspect. Watch four agents stream strict-JSON hand-offs in real time." to="/console" testid="feat-console" />
|
| 101 |
+
<Feat icon={Activity} title="Defect Feed" body="Every inspection, quality score, and defect-type breakdown — charted live." to="/feed" testid="feat-feed" />
|
| 102 |
+
<Feat icon={Layers} title="Deployment Blueprint" body="The exact stack: MI300X → ROCm → vLLM → Qwen2-VL fine-tune recipe." to="/blueprint" testid="feat-blueprint" />
|
| 103 |
+
<Feat icon={Megaphone} title="Build Journal" body="Every milestone auto-drafts X and LinkedIn posts. Ship it, tell the story." to="/journal" testid="feat-journal" />
|
| 104 |
+
<Feat icon={Cpu} title="Live GPU Telemetry" body="Simulated MI300X util, VRAM, tokens/sec — the factory-floor HMI feel." to="/console" testid="feat-telemetry" />
|
| 105 |
+
<Feat icon={Zap} title="Qwen-first" body="Qwen2-VL for vision, Qwen2.5 for text reasoning. Built for the Qwen category prize." to="/blueprint" testid="feat-qwen" />
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
</section>
|
| 109 |
+
|
| 110 |
+
{/* FOOTER CTA */}
|
| 111 |
+
<section className="border-b border-white/10">
|
| 112 |
+
<div className="mx-auto max-w-[1400px] px-6 py-20 text-center">
|
| 113 |
+
<div className="fs-label mb-4">§ 03 · NEXT</div>
|
| 114 |
+
<h2 className="font-display font-black tracking-tighter text-4xl md:text-6xl leading-[0.9]">
|
| 115 |
+
Upload an image.<br />
|
| 116 |
+
<span className="text-[#ED1C24]">Get a verdict in 20 seconds.</span>
|
| 117 |
+
</h2>
|
| 118 |
+
<div className="mt-10 flex items-center justify-center gap-3">
|
| 119 |
+
<Link to="/console" className="fs-btn fs-btn-primary inline-flex items-center gap-2" data-testid="footer-cta-console">
|
| 120 |
+
Open Inspection Console <ArrowRight className="w-3.5 h-3.5" />
|
| 121 |
+
</Link>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
</section>
|
| 125 |
+
|
| 126 |
+
<footer className="border-b border-white/10">
|
| 127 |
+
<div className="mx-auto max-w-[1400px] px-6 py-10 flex flex-col md:flex-row items-center justify-between gap-4">
|
| 128 |
+
<div className="fs-mono-small text-zinc-500">FORGESIGHT · BUILT FOR AMD + LABLAB HACKATHON · FEB 2026</div>
|
| 129 |
+
<div className="flex items-center gap-3">
|
| 130 |
+
<span className="fs-chip">#AMDHACKATHON</span>
|
| 131 |
+
<span className="fs-chip">#ROCM</span>
|
| 132 |
+
<span className="fs-chip">#QWEN</span>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
</footer>
|
| 136 |
+
</div>
|
| 137 |
+
);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
function Stat({ k, v }) {
|
| 141 |
+
return (
|
| 142 |
+
<div className="border-r border-b border-white/10 last:border-r-0 p-4">
|
| 143 |
+
<div className="font-display font-black text-2xl md:text-3xl tracking-tighter">{k}</div>
|
| 144 |
+
<div className="fs-mono-small text-zinc-500 uppercase mt-1">{v}</div>
|
| 145 |
+
</div>
|
| 146 |
+
);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
function Feat({ icon: Icon, title, body, to, testid }) {
|
| 150 |
+
return (
|
| 151 |
+
<Link
|
| 152 |
+
to={to}
|
| 153 |
+
data-testid={testid}
|
| 154 |
+
className="group border-r border-b border-white/10 p-8 hover:bg-[#141416] transition-colors block"
|
| 155 |
+
>
|
| 156 |
+
<Icon className="w-5 h-5 text-[#ED1C24] mb-5" />
|
| 157 |
+
<div className="font-display font-bold text-lg mb-2">{title}</div>
|
| 158 |
+
<div className="text-sm text-zinc-400 leading-relaxed">{body}</div>
|
| 159 |
+
<div className="mt-6 fs-mono-small text-zinc-500 group-hover:text-[#ED1C24] transition-colors inline-flex items-center gap-1">
|
| 160 |
+
OPEN <ArrowRight className="w-3 h-3" />
|
| 161 |
+
</div>
|
| 162 |
+
</Link>
|
| 163 |
+
);
|
| 164 |
+
}
|
frontend/yarn.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
yarn.lock
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
| 2 |
+
# yarn lockfile v1
|
| 3 |
+
|
| 4 |
+
|