Prathamesh Bhamare commited on
Commit ·
77f1d1a
1
Parent(s): d496e2d
feat: wire frontend buttons to API + fix HF Spaces config
Browse files- Wire all desktop.html buttons to FastAPI endpoints (predict, health, docs)
- Wire all mobile.html buttons with same API integration
- Add interactive terminal: editable input, POST /predict/f1, render results
- Add health indicator polling GET /health every 30s
- Add smooth-scroll navigation with section anchors
- Add toast notification system for success/error feedback
- Add mobile viewport detection in index.html
- Add HF Spaces YAML frontmatter to README.md (fixes config error)
- Update CORS to support file:// protocol for local dev
- Remove deleted ui/ folder references from Dockerfiles
- Remove Streamlit service from docker-compose and nginx
- Update drift report output path to data_output/
This view is limited to 50 files because it contains too many changes. See raw diff
- Dockerfile +0 -1
- Dockerfile.hf +0 -1
- README.md +12 -12
- Screenshot 2026-06-11 123347.png +3 -0
- api/main.py +1 -1
- docker-compose.yml +0 -26
- frontend/.gitignore +0 -41
- frontend/AGENTS.md +0 -5
- frontend/CLAUDE.md +0 -1
- frontend/README.md +0 -36
- frontend/app/favicon.ico +0 -0
- frontend/app/globals.css +0 -76
- frontend/app/layout.js +0 -14
- frontend/app/page.js +0 -33
- frontend/app/page.module.css +0 -365
- frontend/components/saas/DriverComparison.tsx +0 -40
- frontend/components/saas/Hero.tsx +0 -21
- frontend/components/saas/Metrics.tsx +0 -30
- frontend/components/saas/Navbar.tsx +0 -42
- frontend/components/saas/PrimaryActions.tsx +0 -93
- frontend/components/saas/ShapExplanation.tsx +0 -52
- frontend/components/saas/Telemetry.tsx +0 -46
- frontend/components/saas/UpcomingGP.tsx +0 -40
- frontend/components/ui/button.tsx +0 -52
- frontend/components/ui/popover.tsx +0 -40
- frontend/components/ui/ruixen-query-box.tsx +0 -80
- frontend/components/ui/textarea.tsx +0 -21
- frontend/desktop.html +550 -0
- frontend/eslint.config.mjs +0 -16
- frontend/hooks/use-auto-resize-textarea.ts +0 -57
- frontend/index.html +33 -0
- frontend/jsconfig.json +0 -7
- frontend/lib/utils.ts +0 -6
- frontend/mobile.html +541 -0
- frontend/next.config.mjs +0 -6
- frontend/package-lock.json +0 -0
- frontend/package.json +0 -32
- frontend/postcss.config.mjs +0 -7
- frontend/public/file.svg +0 -1
- frontend/public/globe.svg +0 -1
- frontend/public/metrics/confusion_matrix.svg +0 -984
- frontend/public/metrics/feature_importance.svg +0 -2058
- frontend/public/metrics/pr_curve.svg +0 -2038
- frontend/public/metrics/roc_curve.svg +0 -1497
- frontend/public/next.svg +0 -1
- frontend/public/vercel.svg +0 -1
- frontend/public/window.svg +0 -1
- frontend/tsconfig.json +0 -26
- ml/drift_detection.py +1 -1
- mlruns/models/kronector-f1-lgbm/version-1/meta.yaml +8 -0
Dockerfile
CHANGED
|
@@ -29,7 +29,6 @@ COPY --chown=1000:1000 api/ ./api/
|
|
| 29 |
COPY --chown=1000:1000 ml/ ./ml/
|
| 30 |
COPY --chown=1000:1000 data/ ./data/
|
| 31 |
COPY --chown=1000:1000 scripts/ ./scripts/
|
| 32 |
-
COPY --chown=1000:1000 ui/ ./ui/
|
| 33 |
COPY --chown=1000:1000 run_api.py run_all.py ./
|
| 34 |
|
| 35 |
# Copy pre-trained model artifacts and data
|
|
|
|
| 29 |
COPY --chown=1000:1000 ml/ ./ml/
|
| 30 |
COPY --chown=1000:1000 data/ ./data/
|
| 31 |
COPY --chown=1000:1000 scripts/ ./scripts/
|
|
|
|
| 32 |
COPY --chown=1000:1000 run_api.py run_all.py ./
|
| 33 |
|
| 34 |
# Copy pre-trained model artifacts and data
|
Dockerfile.hf
CHANGED
|
@@ -30,7 +30,6 @@ COPY --chown=1000:1000 api/ ./api/
|
|
| 30 |
COPY --chown=1000:1000 ml/ ./ml/
|
| 31 |
COPY --chown=1000:1000 data/ ./data/
|
| 32 |
COPY --chown=1000:1000 scripts/ ./scripts/
|
| 33 |
-
COPY --chown=1000:1000 ui/ ./ui/
|
| 34 |
COPY --chown=1000:1000 run_api.py run_all.py ./
|
| 35 |
|
| 36 |
# Copy pre-trained model artifacts and data
|
|
|
|
| 30 |
COPY --chown=1000:1000 ml/ ./ml/
|
| 31 |
COPY --chown=1000:1000 data/ ./data/
|
| 32 |
COPY --chown=1000:1000 scripts/ ./scripts/
|
|
|
|
| 33 |
COPY --chown=1000:1000 run_api.py run_all.py ./
|
| 34 |
|
| 35 |
# Copy pre-trained model artifacts and data
|
README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
<div align="center">
|
| 2 |
|
| 3 |
# Kronector: F1 Race Outcome Prediction & MLOps System
|
|
@@ -58,11 +68,7 @@ To prove the model captures complex non-linear relationships rather than just pr
|
|
| 58 |
| Pole Position Wins | ~42% |
|
| 59 |
|
| 60 |
### Feature Importance & ROC Proofs
|
| 61 |
-
*Mathematical proof that the model relies on logical racing factors.*
|
| 62 |
-
<div align="center">
|
| 63 |
-
<img src="frontend/public/metrics/feature_importance.svg" width="48%">
|
| 64 |
-
<img src="frontend/public/metrics/roc_curve.svg" width="48%">
|
| 65 |
-
</div>
|
| 66 |
|
| 67 |
---
|
| 68 |
|
|
@@ -87,7 +93,6 @@ graph TD
|
|
| 87 |
subgraph Serving
|
| 88 |
E --> G[FastAPI Backend]
|
| 89 |
G --> H[Multi-Agent LLM]
|
| 90 |
-
H --> I[React Frontend]
|
| 91 |
end
|
| 92 |
```
|
| 93 |
|
|
@@ -125,17 +130,12 @@ python -m data.build_driver_map
|
|
| 125 |
python -m scripts.auto_retrain_pipeline
|
| 126 |
```
|
| 127 |
|
| 128 |
-
### 3. Run the API
|
| 129 |
Create a `.env` file with `GROQ_API_KEY=your_key` and the `KRONECTOR_MODEL_RUN_ID` outputted by the training script.
|
| 130 |
|
| 131 |
```bash
|
| 132 |
# Start backend
|
| 133 |
python -m uvicorn api.main:app --reload
|
| 134 |
-
|
| 135 |
-
# Start frontend (in a new terminal)
|
| 136 |
-
cd frontend
|
| 137 |
-
npm install
|
| 138 |
-
npm run dev
|
| 139 |
```
|
| 140 |
|
| 141 |
---
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Kronector
|
| 3 |
+
emoji: 🏎️
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: gray
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
<div align="center">
|
| 12 |
|
| 13 |
# Kronector: F1 Race Outcome Prediction & MLOps System
|
|
|
|
| 68 |
| Pole Position Wins | ~42% |
|
| 69 |
|
| 70 |
### Feature Importance & ROC Proofs
|
| 71 |
+
*Mathematical proof that the model relies on logical racing factors can be regenerated with `python -m scripts.generate_model_proofs`.*
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
---
|
| 74 |
|
|
|
|
| 93 |
subgraph Serving
|
| 94 |
E --> G[FastAPI Backend]
|
| 95 |
G --> H[Multi-Agent LLM]
|
|
|
|
| 96 |
end
|
| 97 |
```
|
| 98 |
|
|
|
|
| 130 |
python -m scripts.auto_retrain_pipeline
|
| 131 |
```
|
| 132 |
|
| 133 |
+
### 3. Run the API
|
| 134 |
Create a `.env` file with `GROQ_API_KEY=your_key` and the `KRONECTOR_MODEL_RUN_ID` outputted by the training script.
|
| 135 |
|
| 136 |
```bash
|
| 137 |
# Start backend
|
| 138 |
python -m uvicorn api.main:app --reload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
```
|
| 140 |
|
| 141 |
---
|
Screenshot 2026-06-11 123347.png
ADDED
|
Git LFS Details
|
api/main.py
CHANGED
|
@@ -185,7 +185,7 @@ import os
|
|
| 185 |
|
| 186 |
_cors_origins = os.getenv(
|
| 187 |
"CORS_ORIGINS",
|
| 188 |
-
"http://localhost:3000,http://localhost:8501,http://localhost:5173",
|
| 189 |
).split(",")
|
| 190 |
|
| 191 |
app.add_middleware(
|
|
|
|
| 185 |
|
| 186 |
_cors_origins = os.getenv(
|
| 187 |
"CORS_ORIGINS",
|
| 188 |
+
"http://localhost:3000,http://localhost:8501,http://localhost:5173,http://localhost:8080,http://localhost:8000,null",
|
| 189 |
).split(",")
|
| 190 |
|
| 191 |
app.add_middleware(
|
docker-compose.yml
CHANGED
|
@@ -3,7 +3,6 @@
|
|
| 3 |
# ============================================================================
|
| 4 |
# Services:
|
| 5 |
# api — FastAPI prediction server (port 8000)
|
| 6 |
-
# streamlit — Drift monitoring dashboard (port 8501)
|
| 7 |
# mlflow — MLflow tracking server (port 5000)
|
| 8 |
# nginx — Reverse proxy (ports 80/443)
|
| 9 |
#
|
|
@@ -39,31 +38,6 @@ services:
|
|
| 39 |
networks:
|
| 40 |
- kronector-net
|
| 41 |
|
| 42 |
-
# --------------------------------------------------------------------------
|
| 43 |
-
# Streamlit — Drift monitoring dashboard
|
| 44 |
-
# --------------------------------------------------------------------------
|
| 45 |
-
streamlit:
|
| 46 |
-
build:
|
| 47 |
-
context: .
|
| 48 |
-
target: production
|
| 49 |
-
container_name: kronector-dashboard
|
| 50 |
-
command: >
|
| 51 |
-
streamlit run ui/monitoring_dashboard.py
|
| 52 |
-
--server.port=8501
|
| 53 |
-
--server.address=0.0.0.0
|
| 54 |
-
--server.headless=true
|
| 55 |
-
--browser.gatherUsageStats=false
|
| 56 |
-
ports:
|
| 57 |
-
- "8501:8501"
|
| 58 |
-
env_file:
|
| 59 |
-
- .env
|
| 60 |
-
volumes:
|
| 61 |
-
- model_data:/app/mlruns
|
| 62 |
-
- app_data:/app/data_output
|
| 63 |
-
restart: unless-stopped
|
| 64 |
-
networks:
|
| 65 |
-
- kronector-net
|
| 66 |
-
|
| 67 |
# --------------------------------------------------------------------------
|
| 68 |
# MLflow — Experiment tracking & model registry
|
| 69 |
# --------------------------------------------------------------------------
|
|
|
|
| 3 |
# ============================================================================
|
| 4 |
# Services:
|
| 5 |
# api — FastAPI prediction server (port 8000)
|
|
|
|
| 6 |
# mlflow — MLflow tracking server (port 5000)
|
| 7 |
# nginx — Reverse proxy (ports 80/443)
|
| 8 |
#
|
|
|
|
| 38 |
networks:
|
| 39 |
- kronector-net
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
# --------------------------------------------------------------------------
|
| 42 |
# MLflow — Experiment tracking & model registry
|
| 43 |
# --------------------------------------------------------------------------
|
frontend/.gitignore
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
-
|
| 3 |
-
# dependencies
|
| 4 |
-
/node_modules
|
| 5 |
-
/.pnp
|
| 6 |
-
.pnp.*
|
| 7 |
-
.yarn/*
|
| 8 |
-
!.yarn/patches
|
| 9 |
-
!.yarn/plugins
|
| 10 |
-
!.yarn/releases
|
| 11 |
-
!.yarn/versions
|
| 12 |
-
|
| 13 |
-
# testing
|
| 14 |
-
/coverage
|
| 15 |
-
|
| 16 |
-
# next.js
|
| 17 |
-
/.next/
|
| 18 |
-
/out/
|
| 19 |
-
|
| 20 |
-
# production
|
| 21 |
-
/build
|
| 22 |
-
|
| 23 |
-
# misc
|
| 24 |
-
.DS_Store
|
| 25 |
-
*.pem
|
| 26 |
-
|
| 27 |
-
# debug
|
| 28 |
-
npm-debug.log*
|
| 29 |
-
yarn-debug.log*
|
| 30 |
-
yarn-error.log*
|
| 31 |
-
.pnpm-debug.log*
|
| 32 |
-
|
| 33 |
-
# env files (can opt-in for committing if needed)
|
| 34 |
-
.env*
|
| 35 |
-
|
| 36 |
-
# vercel
|
| 37 |
-
.vercel
|
| 38 |
-
|
| 39 |
-
# typescript
|
| 40 |
-
*.tsbuildinfo
|
| 41 |
-
next-env.d.ts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/AGENTS.md
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
<!-- BEGIN:nextjs-agent-rules -->
|
| 2 |
-
# This is NOT the Next.js you know
|
| 3 |
-
|
| 4 |
-
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
| 5 |
-
<!-- END:nextjs-agent-rules -->
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/CLAUDE.md
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
@AGENTS.md
|
|
|
|
|
|
frontend/README.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
| 2 |
-
|
| 3 |
-
## Getting Started
|
| 4 |
-
|
| 5 |
-
First, run the development server:
|
| 6 |
-
|
| 7 |
-
```bash
|
| 8 |
-
npm run dev
|
| 9 |
-
# or
|
| 10 |
-
yarn dev
|
| 11 |
-
# or
|
| 12 |
-
pnpm dev
|
| 13 |
-
# or
|
| 14 |
-
bun dev
|
| 15 |
-
```
|
| 16 |
-
|
| 17 |
-
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
| 18 |
-
|
| 19 |
-
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
|
| 20 |
-
|
| 21 |
-
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
| 22 |
-
|
| 23 |
-
## Learn More
|
| 24 |
-
|
| 25 |
-
To learn more about Next.js, take a look at the following resources:
|
| 26 |
-
|
| 27 |
-
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
| 28 |
-
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
| 29 |
-
|
| 30 |
-
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
| 31 |
-
|
| 32 |
-
## Deploy on Vercel
|
| 33 |
-
|
| 34 |
-
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
| 35 |
-
|
| 36 |
-
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/app/favicon.ico
DELETED
|
Binary file (25.9 kB)
|
|
|
frontend/app/globals.css
DELETED
|
@@ -1,76 +0,0 @@
|
|
| 1 |
-
@import "tailwindcss";
|
| 2 |
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap');
|
| 3 |
-
|
| 4 |
-
@theme {
|
| 5 |
-
--color-saas-bg: #000000;
|
| 6 |
-
--color-saas-surface: #0A0A0A;
|
| 7 |
-
--color-saas-card: #111111;
|
| 8 |
-
--color-saas-border: #222222;
|
| 9 |
-
--color-saas-border-hover: #333333;
|
| 10 |
-
--color-saas-text-primary: #EDEDED;
|
| 11 |
-
--color-saas-text-secondary: #888888;
|
| 12 |
-
--color-saas-accent: #FFFFFF;
|
| 13 |
-
--color-saas-blue: #0070F3;
|
| 14 |
-
--color-saas-red: #E00000;
|
| 15 |
-
--color-saas-green: #00D084;
|
| 16 |
-
|
| 17 |
-
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 18 |
-
--font-mono: 'Geist Mono', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
@layer base {
|
| 22 |
-
:root {
|
| 23 |
-
--background: 0 0% 0%;
|
| 24 |
-
--foreground: 0 0% 93%;
|
| 25 |
-
--card: 0 0% 7%;
|
| 26 |
-
--card-foreground: 0 0% 93%;
|
| 27 |
-
--popover: 0 0% 7%;
|
| 28 |
-
--popover-foreground: 0 0% 93%;
|
| 29 |
-
--primary: 0 0% 100%;
|
| 30 |
-
--primary-foreground: 0 0% 0%;
|
| 31 |
-
--secondary: 0 0% 13%;
|
| 32 |
-
--secondary-foreground: 0 0% 93%;
|
| 33 |
-
--muted: 0 0% 13%;
|
| 34 |
-
--muted-foreground: 0 0% 53%;
|
| 35 |
-
--accent: 0 0% 13%;
|
| 36 |
-
--accent-foreground: 0 0% 93%;
|
| 37 |
-
--destructive: 0 100% 44%;
|
| 38 |
-
--destructive-foreground: 0 0% 100%;
|
| 39 |
-
--border: 0 0% 13%;
|
| 40 |
-
--input: 0 0% 13%;
|
| 41 |
-
--ring: 0 0% 100%;
|
| 42 |
-
--radius: 0.5rem;
|
| 43 |
-
}
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
* {
|
| 47 |
-
box-sizing: border-box;
|
| 48 |
-
margin: 0;
|
| 49 |
-
padding: 0;
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
body {
|
| 53 |
-
background-color: var(--color-saas-bg);
|
| 54 |
-
color: var(--color-saas-text-primary);
|
| 55 |
-
font-family: var(--font-sans);
|
| 56 |
-
min-height: 100vh;
|
| 57 |
-
overflow-x: hidden;
|
| 58 |
-
-webkit-font-smoothing: antialiased;
|
| 59 |
-
-moz-osx-font-smoothing: grayscale;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
/* Elegant minimalist scrollbar */
|
| 63 |
-
::-webkit-scrollbar {
|
| 64 |
-
width: 8px;
|
| 65 |
-
height: 8px;
|
| 66 |
-
}
|
| 67 |
-
::-webkit-scrollbar-track {
|
| 68 |
-
background: transparent;
|
| 69 |
-
}
|
| 70 |
-
::-webkit-scrollbar-thumb {
|
| 71 |
-
background: var(--color-saas-border);
|
| 72 |
-
border-radius: 4px;
|
| 73 |
-
}
|
| 74 |
-
::-webkit-scrollbar-thumb:hover {
|
| 75 |
-
background: var(--color-saas-border-hover);
|
| 76 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/app/layout.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
| 1 |
-
import "./globals.css";
|
| 2 |
-
|
| 3 |
-
export const metadata = {
|
| 4 |
-
title: "KRONECTOR F1 Intelligence",
|
| 5 |
-
description: "Predict F1 race outcomes using natural language queries powered by Hugging Face and MLflow.",
|
| 6 |
-
};
|
| 7 |
-
|
| 8 |
-
export default function RootLayout({ children }) {
|
| 9 |
-
return (
|
| 10 |
-
<html lang="en">
|
| 11 |
-
<body>{children}</body>
|
| 12 |
-
</html>
|
| 13 |
-
);
|
| 14 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/app/page.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
| 1 |
-
import Navbar from "@/components/saas/Navbar";
|
| 2 |
-
import Hero from "@/components/saas/Hero";
|
| 3 |
-
import PrimaryActions from "@/components/saas/PrimaryActions";
|
| 4 |
-
import UpcomingGP from "@/components/saas/UpcomingGP";
|
| 5 |
-
import ShapExplanation from "@/components/saas/ShapExplanation";
|
| 6 |
-
import DriverComparison from "@/components/saas/DriverComparison";
|
| 7 |
-
import Telemetry from "@/components/saas/Telemetry";
|
| 8 |
-
import Metrics from "@/components/saas/Metrics";
|
| 9 |
-
|
| 10 |
-
export default function SaaSPage() {
|
| 11 |
-
return (
|
| 12 |
-
<div className="bg-[#000000] min-h-screen text-[#EDEDED] font-sans selection:bg-[#0070F3]/30">
|
| 13 |
-
<Navbar />
|
| 14 |
-
|
| 15 |
-
<main className="flex flex-col items-center w-full">
|
| 16 |
-
<Hero />
|
| 17 |
-
<PrimaryActions />
|
| 18 |
-
|
| 19 |
-
{/* Everything below the fold */}
|
| 20 |
-
<div className="w-full bg-[#0A0A0A] border-t border-[#222222]">
|
| 21 |
-
<UpcomingGP />
|
| 22 |
-
<ShapExplanation />
|
| 23 |
-
</div>
|
| 24 |
-
|
| 25 |
-
<div className="w-full">
|
| 26 |
-
<DriverComparison />
|
| 27 |
-
<Telemetry />
|
| 28 |
-
<Metrics />
|
| 29 |
-
</div>
|
| 30 |
-
</main>
|
| 31 |
-
</div>
|
| 32 |
-
);
|
| 33 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/app/page.module.css
DELETED
|
@@ -1,365 +0,0 @@
|
|
| 1 |
-
.main {
|
| 2 |
-
min-height: 100vh;
|
| 3 |
-
padding: 4rem 2rem;
|
| 4 |
-
display: flex;
|
| 5 |
-
flex-direction: column;
|
| 6 |
-
align-items: center;
|
| 7 |
-
}
|
| 8 |
-
|
| 9 |
-
.header {
|
| 10 |
-
text-align: center;
|
| 11 |
-
margin-bottom: 4rem;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
.title {
|
| 15 |
-
font-size: 4rem;
|
| 16 |
-
margin-bottom: 1rem;
|
| 17 |
-
line-height: 1.1;
|
| 18 |
-
text-shadow: var(--shadow-glow);
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
.subtitle {
|
| 22 |
-
font-size: 1.2rem;
|
| 23 |
-
color: var(--text-secondary);
|
| 24 |
-
max-width: 600px;
|
| 25 |
-
margin: 0 auto;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
/* Query Input Component */
|
| 29 |
-
.queryContainer {
|
| 30 |
-
width: 100%;
|
| 31 |
-
max-width: 800px;
|
| 32 |
-
position: relative;
|
| 33 |
-
margin-bottom: 3rem;
|
| 34 |
-
z-index: 10;
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
.inputWrapper {
|
| 38 |
-
position: relative;
|
| 39 |
-
width: 100%;
|
| 40 |
-
border-radius: var(--border-radius-lg);
|
| 41 |
-
background: var(--bg-glass);
|
| 42 |
-
backdrop-filter: blur(20px);
|
| 43 |
-
border: 1px solid rgba(0, 240, 255, 0.3);
|
| 44 |
-
box-shadow: var(--shadow-glow);
|
| 45 |
-
transition: var(--transition-smooth);
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
.inputWrapper:focus-within {
|
| 49 |
-
box-shadow: 0 0 30px rgba(0, 240, 255, 0.5);
|
| 50 |
-
border-color: var(--neon-cyan);
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
.input {
|
| 54 |
-
width: 100%;
|
| 55 |
-
background: transparent;
|
| 56 |
-
border: none;
|
| 57 |
-
padding: 1.5rem 4rem 1.5rem 2rem;
|
| 58 |
-
color: var(--text-primary);
|
| 59 |
-
font-size: 1.2rem;
|
| 60 |
-
font-family: inherit;
|
| 61 |
-
outline: none;
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
.input::placeholder {
|
| 65 |
-
color: var(--text-muted);
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
.submitBtn {
|
| 69 |
-
position: absolute;
|
| 70 |
-
right: 1rem;
|
| 71 |
-
top: 50%;
|
| 72 |
-
transform: translateY(-50%);
|
| 73 |
-
background: transparent;
|
| 74 |
-
border: none;
|
| 75 |
-
color: var(--neon-cyan);
|
| 76 |
-
cursor: pointer;
|
| 77 |
-
padding: 0.5rem;
|
| 78 |
-
display: flex;
|
| 79 |
-
align-items: center;
|
| 80 |
-
justify-content: center;
|
| 81 |
-
transition: var(--transition-fast);
|
| 82 |
-
border-radius: 50%;
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
.submitBtn:hover {
|
| 86 |
-
background: rgba(0, 240, 255, 0.1);
|
| 87 |
-
box-shadow: var(--shadow-glow);
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
.submitBtn:disabled {
|
| 91 |
-
color: var(--text-muted);
|
| 92 |
-
cursor: not-allowed;
|
| 93 |
-
background: transparent;
|
| 94 |
-
box-shadow: none;
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
/* Loader */
|
| 98 |
-
.loader {
|
| 99 |
-
border: 3px solid rgba(0, 240, 255, 0.1);
|
| 100 |
-
border-left-color: var(--neon-cyan);
|
| 101 |
-
border-radius: 50%;
|
| 102 |
-
width: 24px;
|
| 103 |
-
height: 24px;
|
| 104 |
-
animation: spin 1s linear infinite;
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
@keyframes spin {
|
| 108 |
-
0% { transform: rotate(0deg); }
|
| 109 |
-
100% { transform: rotate(360deg); }
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
/* Dashboard */
|
| 113 |
-
.dashboardGrid {
|
| 114 |
-
display: grid;
|
| 115 |
-
grid-template-columns: 1fr 2fr;
|
| 116 |
-
gap: 2rem;
|
| 117 |
-
width: 100%;
|
| 118 |
-
max-width: 1200px;
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
@media (max-width: 800px) {
|
| 122 |
-
.dashboardGrid {
|
| 123 |
-
grid-template-columns: 1fr;
|
| 124 |
-
}
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
.panel {
|
| 128 |
-
padding: 2rem;
|
| 129 |
-
display: flex;
|
| 130 |
-
flex-direction: column;
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
.panelTitle {
|
| 134 |
-
font-size: 1.2rem;
|
| 135 |
-
margin-bottom: 1.5rem;
|
| 136 |
-
color: var(--neon-cyan);
|
| 137 |
-
display: flex;
|
| 138 |
-
align-items: center;
|
| 139 |
-
gap: 0.5rem;
|
| 140 |
-
text-transform: uppercase;
|
| 141 |
-
letter-spacing: 2px;
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
/* Gauge */
|
| 145 |
-
.gaugeContainer {
|
| 146 |
-
display: flex;
|
| 147 |
-
flex-direction: column;
|
| 148 |
-
align-items: center;
|
| 149 |
-
justify-content: center;
|
| 150 |
-
padding: 2rem 0;
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
.gaugeCircle {
|
| 154 |
-
position: relative;
|
| 155 |
-
width: 200px;
|
| 156 |
-
height: 200px;
|
| 157 |
-
border-radius: 50%;
|
| 158 |
-
background: conic-gradient(var(--neon-cyan) var(--prob), var(--bg-secondary) 0deg);
|
| 159 |
-
display: flex;
|
| 160 |
-
align-items: center;
|
| 161 |
-
justify-content: center;
|
| 162 |
-
box-shadow: var(--shadow-glow);
|
| 163 |
-
margin-bottom: 1.5rem;
|
| 164 |
-
}
|
| 165 |
-
|
| 166 |
-
.gaugeCircle::before {
|
| 167 |
-
content: "";
|
| 168 |
-
position: absolute;
|
| 169 |
-
inset: 15px;
|
| 170 |
-
background: var(--bg-primary);
|
| 171 |
-
border-radius: 50%;
|
| 172 |
-
border: 1px solid rgba(0, 240, 255, 0.2);
|
| 173 |
-
}
|
| 174 |
-
|
| 175 |
-
.gaugeText {
|
| 176 |
-
position: relative;
|
| 177 |
-
font-size: 3.5rem;
|
| 178 |
-
font-weight: 800;
|
| 179 |
-
color: var(--text-primary);
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
.gaugeLabel {
|
| 183 |
-
color: var(--text-secondary);
|
| 184 |
-
font-size: 1.1rem;
|
| 185 |
-
text-align: center;
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
.rating {
|
| 189 |
-
margin-top: 1rem;
|
| 190 |
-
padding: 0.25rem 1rem;
|
| 191 |
-
border-radius: 20px;
|
| 192 |
-
font-size: 0.9rem;
|
| 193 |
-
font-weight: 600;
|
| 194 |
-
text-transform: uppercase;
|
| 195 |
-
letter-spacing: 1px;
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
.ratingGood {
|
| 199 |
-
background: rgba(0, 255, 136, 0.1);
|
| 200 |
-
color: var(--neon-green);
|
| 201 |
-
border: 1px solid var(--neon-green);
|
| 202 |
-
}
|
| 203 |
-
|
| 204 |
-
.ratingBad {
|
| 205 |
-
background: rgba(255, 42, 42, 0.1);
|
| 206 |
-
color: var(--neon-red);
|
| 207 |
-
border: 1px solid var(--neon-red);
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
/* Insight */
|
| 211 |
-
.insightText {
|
| 212 |
-
line-height: 1.8;
|
| 213 |
-
font-size: 1.1rem;
|
| 214 |
-
color: var(--text-primary);
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
.insightHighlight {
|
| 218 |
-
color: var(--neon-cyan);
|
| 219 |
-
font-weight: 600;
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
/* SHAP Bars */
|
| 223 |
-
.shapContainer {
|
| 224 |
-
display: flex;
|
| 225 |
-
flex-direction: column;
|
| 226 |
-
gap: 1rem;
|
| 227 |
-
}
|
| 228 |
-
|
| 229 |
-
.shapRow {
|
| 230 |
-
display: grid;
|
| 231 |
-
grid-template-columns: 150px 1fr 60px;
|
| 232 |
-
align-items: center;
|
| 233 |
-
gap: 1rem;
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
.shapLabel {
|
| 237 |
-
font-size: 0.9rem;
|
| 238 |
-
color: var(--text-secondary);
|
| 239 |
-
text-align: right;
|
| 240 |
-
white-space: nowrap;
|
| 241 |
-
overflow: hidden;
|
| 242 |
-
text-overflow: ellipsis;
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
.barTrack {
|
| 246 |
-
height: 12px;
|
| 247 |
-
background: rgba(255,255,255,0.05);
|
| 248 |
-
border-radius: 6px;
|
| 249 |
-
position: relative;
|
| 250 |
-
overflow: hidden;
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
.barFill {
|
| 254 |
-
height: 100%;
|
| 255 |
-
position: absolute;
|
| 256 |
-
top: 0;
|
| 257 |
-
border-radius: 6px;
|
| 258 |
-
transition: width 1s cubic-bezier(0.16, 1, 0.3, 1);
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
.barPositive {
|
| 262 |
-
background: linear-gradient(90deg, transparent, var(--neon-cyan));
|
| 263 |
-
left: 50%;
|
| 264 |
-
box-shadow: var(--shadow-glow);
|
| 265 |
-
}
|
| 266 |
-
|
| 267 |
-
.barNegative {
|
| 268 |
-
background: linear-gradient(270deg, transparent, var(--neon-red));
|
| 269 |
-
right: 50%;
|
| 270 |
-
box-shadow: var(--shadow-glow-red);
|
| 271 |
-
}
|
| 272 |
-
|
| 273 |
-
.shapValue {
|
| 274 |
-
font-size: 0.85rem;
|
| 275 |
-
font-family: var(--font-geist-mono), monospace;
|
| 276 |
-
}
|
| 277 |
-
|
| 278 |
-
/* Tabs */
|
| 279 |
-
.tabsContainer {
|
| 280 |
-
display: flex;
|
| 281 |
-
gap: 1rem;
|
| 282 |
-
margin-bottom: 2rem;
|
| 283 |
-
z-index: 10;
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
.tabBtn {
|
| 287 |
-
background: rgba(0, 0, 0, 0.3);
|
| 288 |
-
border: 1px solid rgba(0, 240, 255, 0.3);
|
| 289 |
-
color: var(--text-muted);
|
| 290 |
-
padding: 0.75rem 2rem;
|
| 291 |
-
border-radius: 20px;
|
| 292 |
-
cursor: pointer;
|
| 293 |
-
transition: var(--transition-fast);
|
| 294 |
-
font-family: inherit;
|
| 295 |
-
font-size: 1rem;
|
| 296 |
-
text-transform: uppercase;
|
| 297 |
-
letter-spacing: 1px;
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
.tabBtn:hover {
|
| 301 |
-
background: rgba(0, 240, 255, 0.1);
|
| 302 |
-
color: var(--neon-cyan);
|
| 303 |
-
}
|
| 304 |
-
|
| 305 |
-
.tabActive {
|
| 306 |
-
background: rgba(0, 240, 255, 0.15);
|
| 307 |
-
border-color: var(--neon-cyan);
|
| 308 |
-
color: var(--neon-cyan);
|
| 309 |
-
box-shadow: 0 0 15px rgba(0, 240, 255, 0.3);
|
| 310 |
-
}
|
| 311 |
-
|
| 312 |
-
/* Compare Form */
|
| 313 |
-
.compareForm {
|
| 314 |
-
display: flex;
|
| 315 |
-
flex-direction: column;
|
| 316 |
-
gap: 1.5rem;
|
| 317 |
-
width: 100%;
|
| 318 |
-
}
|
| 319 |
-
|
| 320 |
-
.compareInputsRow {
|
| 321 |
-
display: flex;
|
| 322 |
-
align-items: center;
|
| 323 |
-
justify-content: space-between;
|
| 324 |
-
gap: 1rem;
|
| 325 |
-
}
|
| 326 |
-
|
| 327 |
-
.vsText {
|
| 328 |
-
font-size: 1.5rem;
|
| 329 |
-
font-weight: 800;
|
| 330 |
-
color: var(--text-muted);
|
| 331 |
-
font-style: italic;
|
| 332 |
-
}
|
| 333 |
-
|
| 334 |
-
.compareInputsRowSmall {
|
| 335 |
-
display: flex;
|
| 336 |
-
gap: 1rem;
|
| 337 |
-
justify-content: center;
|
| 338 |
-
}
|
| 339 |
-
|
| 340 |
-
.compareSubmitBtn {
|
| 341 |
-
background: linear-gradient(90deg, rgba(0, 240, 255, 0.2), rgba(0, 240, 255, 0.05));
|
| 342 |
-
border: 1px solid var(--neon-cyan);
|
| 343 |
-
color: var(--neon-cyan);
|
| 344 |
-
padding: 1rem 2rem;
|
| 345 |
-
border-radius: var(--border-radius-lg);
|
| 346 |
-
cursor: pointer;
|
| 347 |
-
font-family: inherit;
|
| 348 |
-
font-size: 1.1rem;
|
| 349 |
-
font-weight: 600;
|
| 350 |
-
text-transform: uppercase;
|
| 351 |
-
letter-spacing: 2px;
|
| 352 |
-
transition: var(--transition-fast);
|
| 353 |
-
}
|
| 354 |
-
|
| 355 |
-
.compareSubmitBtn:hover:not(:disabled) {
|
| 356 |
-
background: rgba(0, 240, 255, 0.2);
|
| 357 |
-
box-shadow: var(--shadow-glow);
|
| 358 |
-
}
|
| 359 |
-
|
| 360 |
-
.compareSubmitBtn:disabled {
|
| 361 |
-
border-color: rgba(0, 240, 255, 0.1);
|
| 362 |
-
color: var(--text-muted);
|
| 363 |
-
cursor: not-allowed;
|
| 364 |
-
background: transparent;
|
| 365 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/saas/DriverComparison.tsx
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
| 4 |
-
|
| 5 |
-
export default function DriverComparison() {
|
| 6 |
-
const data = [
|
| 7 |
-
{ metric: 'Qualifying Pace', Norris: 98, Verstappen: 95 },
|
| 8 |
-
{ metric: 'Race Pace', Norris: 95, Verstappen: 97 },
|
| 9 |
-
{ metric: 'Win Rate', Norris: 45, Verstappen: 52 },
|
| 10 |
-
{ metric: 'Avg Finish', Norris: 2.1, Verstappen: 1.8 },
|
| 11 |
-
];
|
| 12 |
-
|
| 13 |
-
return (
|
| 14 |
-
<section id="driver-comparison" className="px-6 max-w-6xl mx-auto mb-24 pt-16">
|
| 15 |
-
<div className="flex items-center gap-2 mb-8">
|
| 16 |
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-white"><path d="M16 3h5v5M4 20L21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/></svg>
|
| 17 |
-
<h2 className="text-2xl font-semibold text-white tracking-tight">Driver Comparison Results</h2>
|
| 18 |
-
</div>
|
| 19 |
-
|
| 20 |
-
<div className="bg-[#111111] border border-[#222222] rounded-xl p-8">
|
| 21 |
-
<div className="h-[300px] w-full text-sm">
|
| 22 |
-
<ResponsiveContainer width="100%" height="100%">
|
| 23 |
-
<BarChart data={data} layout="vertical" margin={{ top: 0, right: 0, left: 20, bottom: 0 }}>
|
| 24 |
-
<CartesianGrid strokeDasharray="3 3" stroke="#222222" horizontal={false} />
|
| 25 |
-
<XAxis type="number" stroke="#888888" tick={{ fill: '#888888' }} />
|
| 26 |
-
<YAxis dataKey="metric" type="category" stroke="#888888" tick={{ fill: '#888888' }} width={100} />
|
| 27 |
-
<Tooltip
|
| 28 |
-
cursor={{fill: '#222222', opacity: 0.4}}
|
| 29 |
-
contentStyle={{ backgroundColor: '#000000', border: '1px solid #333333', color: '#fff', borderRadius: '8px' }}
|
| 30 |
-
/>
|
| 31 |
-
<Legend wrapperStyle={{ paddingTop: '20px' }} />
|
| 32 |
-
<Bar dataKey="Norris" fill="#FF8700" radius={[0, 4, 4, 0]} barSize={20} />
|
| 33 |
-
<Bar dataKey="Verstappen" fill="#0070F3" radius={[0, 4, 4, 0]} barSize={20} />
|
| 34 |
-
</BarChart>
|
| 35 |
-
</ResponsiveContainer>
|
| 36 |
-
</div>
|
| 37 |
-
</div>
|
| 38 |
-
</section>
|
| 39 |
-
);
|
| 40 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/saas/Hero.tsx
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
export default function Hero() {
|
| 4 |
-
return (
|
| 5 |
-
<section className="pt-24 pb-12 px-6 max-w-5xl mx-auto text-center">
|
| 6 |
-
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[#111111] border border-[#222222] text-xs font-medium text-[#888888] mb-8">
|
| 7 |
-
<span className="w-2 h-2 rounded-full bg-[#0070F3]"></span>
|
| 8 |
-
LightGBM Model v2.1 Deployed
|
| 9 |
-
</div>
|
| 10 |
-
|
| 11 |
-
<h1 className="text-5xl md:text-7xl font-bold tracking-tighter text-white mb-6">
|
| 12 |
-
Formula 1 <br className="hidden md:block" />
|
| 13 |
-
<span className="text-[#888888]">Intelligence Platform</span>
|
| 14 |
-
</h1>
|
| 15 |
-
|
| 16 |
-
<p className="text-lg md:text-xl text-[#888888] max-w-2xl mx-auto leading-relaxed">
|
| 17 |
-
Predict race outcomes, compare drivers, analyze telemetry, and understand every prediction with mathematically-backed explainable AI.
|
| 18 |
-
</p>
|
| 19 |
-
</section>
|
| 20 |
-
);
|
| 21 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/saas/Metrics.tsx
DELETED
|
@@ -1,30 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { Database, ShieldCheck, Cpu } from "lucide-react";
|
| 4 |
-
|
| 5 |
-
export default function Metrics() {
|
| 6 |
-
const metrics = [
|
| 7 |
-
{ label: "Model Accuracy", value: "71.8%", icon: Cpu, sub: "Out of sample (2023-2025)" },
|
| 8 |
-
{ label: "Data Coverage", value: "8.2M", icon: Database, sub: "Rows of F1 Telemetry" },
|
| 9 |
-
{ label: "System Status", value: "Healthy", icon: ShieldCheck, sub: "No data drift detected" },
|
| 10 |
-
];
|
| 11 |
-
|
| 12 |
-
return (
|
| 13 |
-
<section className="px-6 max-w-6xl mx-auto pb-24 border-t border-[#222222] pt-16 mt-16">
|
| 14 |
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
| 15 |
-
{metrics.map((m, i) => (
|
| 16 |
-
<div key={i} className="flex items-start gap-4">
|
| 17 |
-
<div className="p-3 bg-[#111111] border border-[#222222] rounded-lg">
|
| 18 |
-
<m.icon className="w-5 h-5 text-white" />
|
| 19 |
-
</div>
|
| 20 |
-
<div>
|
| 21 |
-
<div className="text-sm text-[#888888]">{m.label}</div>
|
| 22 |
-
<div className="text-xl font-semibold text-white mt-1">{m.value}</div>
|
| 23 |
-
<div className="text-xs text-[#666666] mt-1">{m.sub}</div>
|
| 24 |
-
</div>
|
| 25 |
-
</div>
|
| 26 |
-
))}
|
| 27 |
-
</div>
|
| 28 |
-
</section>
|
| 29 |
-
);
|
| 30 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/saas/Navbar.tsx
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { Activity } from "lucide-react";
|
| 4 |
-
import Link from "next/link";
|
| 5 |
-
|
| 6 |
-
export default function Navbar() {
|
| 7 |
-
return (
|
| 8 |
-
<nav className="sticky top-0 z-50 w-full border-b border-[#222222] bg-[#000000]/80 backdrop-blur-md">
|
| 9 |
-
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
|
| 10 |
-
<div className="flex items-center gap-8">
|
| 11 |
-
<Link href="/" className="flex items-center gap-2">
|
| 12 |
-
<div className="w-6 h-6 bg-white text-black flex items-center justify-center rounded-sm font-bold text-sm">
|
| 13 |
-
K
|
| 14 |
-
</div>
|
| 15 |
-
<span className="font-semibold tracking-tight text-white">Kronector</span>
|
| 16 |
-
</Link>
|
| 17 |
-
|
| 18 |
-
<div className="hidden md:flex items-center gap-6 text-sm font-medium text-[#888888]">
|
| 19 |
-
<Link href="#dashboard" className="hover:text-white transition-colors">Dashboard</Link>
|
| 20 |
-
<Link href="#predictions" className="text-white">Predictions</Link>
|
| 21 |
-
<Link href="#compare" className="hover:text-white transition-colors">Compare</Link>
|
| 22 |
-
<Link href="#telemetry" className="hover:text-white transition-colors">Telemetry</Link>
|
| 23 |
-
<Link href="#metrics" className="hover:text-white transition-colors">Metrics</Link>
|
| 24 |
-
</div>
|
| 25 |
-
</div>
|
| 26 |
-
|
| 27 |
-
<div className="flex items-center gap-6">
|
| 28 |
-
<div className="hidden sm:flex items-center gap-2 text-xs font-mono text-[#888888] bg-[#111111] px-3 py-1.5 rounded-full border border-[#222222]">
|
| 29 |
-
<span>Season 2026</span>
|
| 30 |
-
</div>
|
| 31 |
-
<div className="flex items-center gap-2 text-sm text-[#00D084] font-medium">
|
| 32 |
-
<span className="relative flex h-2 w-2">
|
| 33 |
-
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#00D084] opacity-75"></span>
|
| 34 |
-
<span className="relative inline-flex rounded-full h-2 w-2 bg-[#00D084]"></span>
|
| 35 |
-
</span>
|
| 36 |
-
Live
|
| 37 |
-
</div>
|
| 38 |
-
</div>
|
| 39 |
-
</div>
|
| 40 |
-
</nav>
|
| 41 |
-
);
|
| 42 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/saas/PrimaryActions.tsx
DELETED
|
@@ -1,93 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { ArrowRight, Search, Zap } from "lucide-react";
|
| 4 |
-
import { useState } from "react";
|
| 5 |
-
|
| 6 |
-
export default function PrimaryActions() {
|
| 7 |
-
const [query, setQuery] = useState("");
|
| 8 |
-
|
| 9 |
-
const handlePredict = (e: React.FormEvent) => {
|
| 10 |
-
e.preventDefault();
|
| 11 |
-
if (query) {
|
| 12 |
-
document.getElementById('upcoming-gp')?.scrollIntoView({ behavior: 'smooth' });
|
| 13 |
-
}
|
| 14 |
-
};
|
| 15 |
-
|
| 16 |
-
const handleCompare = (e: React.FormEvent) => {
|
| 17 |
-
e.preventDefault();
|
| 18 |
-
document.getElementById('driver-comparison')?.scrollIntoView({ behavior: 'smooth' });
|
| 19 |
-
};
|
| 20 |
-
|
| 21 |
-
return (
|
| 22 |
-
<section className="px-6 max-w-6xl mx-auto mb-24">
|
| 23 |
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 24 |
-
|
| 25 |
-
{/* Predict Card */}
|
| 26 |
-
<div className="bg-[#111111] border border-[#222222] hover:border-[#333333] transition-colors rounded-xl p-8 flex flex-col justify-between h-[320px]">
|
| 27 |
-
<div>
|
| 28 |
-
<div className="flex items-center gap-2 text-white font-medium mb-2">
|
| 29 |
-
<Zap className="w-5 h-5 text-[#0070F3]" />
|
| 30 |
-
Predict a Race Outcome
|
| 31 |
-
</div>
|
| 32 |
-
<p className="text-[#888888] text-sm mb-6">Ask any natural language question about an upcoming race.</p>
|
| 33 |
-
</div>
|
| 34 |
-
|
| 35 |
-
<form onSubmit={handlePredict} className="mt-auto">
|
| 36 |
-
<div className="relative mb-4">
|
| 37 |
-
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[#888888]" />
|
| 38 |
-
<input
|
| 39 |
-
type="text"
|
| 40 |
-
value={query}
|
| 41 |
-
onChange={(e) => setQuery(e.target.value)}
|
| 42 |
-
placeholder="Who will win the Canadian GP?"
|
| 43 |
-
className="w-full bg-[#000000] border border-[#222222] rounded-lg py-4 pl-12 pr-4 text-white placeholder:text-[#444444] focus:outline-none focus:border-[#0070F3] focus:ring-1 focus:ring-[#0070F3] transition-all text-lg"
|
| 44 |
-
/>
|
| 45 |
-
</div>
|
| 46 |
-
<button type="submit" className="w-full bg-white text-black hover:bg-gray-200 font-medium py-3 rounded-lg flex items-center justify-center gap-2 transition-colors">
|
| 47 |
-
Predict Outcome <ArrowRight className="w-4 h-4" />
|
| 48 |
-
</button>
|
| 49 |
-
<div className="flex gap-3 mt-4 text-xs text-[#666666]">
|
| 50 |
-
<span className="hover:text-[#888888] cursor-pointer">Who wins Monaco?</span>
|
| 51 |
-
<span className="hover:text-[#888888] cursor-pointer">Predict Silverstone podium</span>
|
| 52 |
-
</div>
|
| 53 |
-
</form>
|
| 54 |
-
</div>
|
| 55 |
-
|
| 56 |
-
{/* Compare Card */}
|
| 57 |
-
<div className="bg-[#111111] border border-[#222222] hover:border-[#333333] transition-colors rounded-xl p-8 flex flex-col justify-between h-[320px]">
|
| 58 |
-
<div>
|
| 59 |
-
<div className="flex items-center gap-2 text-white font-medium mb-2">
|
| 60 |
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-[#00D084]"><path d="M16 3h5v5M4 20L21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/></svg>
|
| 61 |
-
Compare Drivers
|
| 62 |
-
</div>
|
| 63 |
-
<p className="text-[#888888] text-sm mb-6">Analyze head-to-head performance across metrics.</p>
|
| 64 |
-
</div>
|
| 65 |
-
|
| 66 |
-
<form onSubmit={handleCompare} className="mt-auto">
|
| 67 |
-
<div className="flex items-center gap-4 mb-6">
|
| 68 |
-
<div className="flex-1">
|
| 69 |
-
<select className="w-full bg-[#000000] border border-[#222222] rounded-lg p-4 text-white focus:outline-none focus:border-[#00D084] appearance-none cursor-pointer">
|
| 70 |
-
<option>Norris</option>
|
| 71 |
-
<option>Verstappen</option>
|
| 72 |
-
<option>Leclerc</option>
|
| 73 |
-
</select>
|
| 74 |
-
</div>
|
| 75 |
-
<div className="text-[#888888] font-mono text-sm">VS</div>
|
| 76 |
-
<div className="flex-1">
|
| 77 |
-
<select className="w-full bg-[#000000] border border-[#222222] rounded-lg p-4 text-white focus:outline-none focus:border-[#00D084] appearance-none cursor-pointer">
|
| 78 |
-
<option>Verstappen</option>
|
| 79 |
-
<option>Norris</option>
|
| 80 |
-
<option>Leclerc</option>
|
| 81 |
-
</select>
|
| 82 |
-
</div>
|
| 83 |
-
</div>
|
| 84 |
-
<button type="submit" className="w-full bg-[#222222] text-white hover:bg-[#333333] border border-[#333333] font-medium py-3 rounded-lg flex items-center justify-center gap-2 transition-colors">
|
| 85 |
-
Compare Stats
|
| 86 |
-
</button>
|
| 87 |
-
</form>
|
| 88 |
-
</div>
|
| 89 |
-
|
| 90 |
-
</div>
|
| 91 |
-
</section>
|
| 92 |
-
);
|
| 93 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/saas/ShapExplanation.tsx
DELETED
|
@@ -1,52 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { Brain } from "lucide-react";
|
| 4 |
-
|
| 5 |
-
export default function ShapExplanation() {
|
| 6 |
-
const factors = [
|
| 7 |
-
{ name: "Grid Position", value: "+3.18", type: "positive" },
|
| 8 |
-
{ name: "Driver Form (Last 3)", value: "+1.39", type: "positive" },
|
| 9 |
-
{ name: "Team Perf Index", value: "+1.01", type: "positive" },
|
| 10 |
-
{ name: "Wet Weather Risk", value: "-0.85", type: "negative" },
|
| 11 |
-
{ name: "Reliability Concerns", value: "-1.22", type: "negative" },
|
| 12 |
-
];
|
| 13 |
-
|
| 14 |
-
return (
|
| 15 |
-
<section className="px-6 max-w-6xl mx-auto mb-24">
|
| 16 |
-
<div className="flex items-center gap-2 mb-8">
|
| 17 |
-
<Brain className="w-5 h-5 text-white" />
|
| 18 |
-
<h2 className="text-2xl font-semibold text-white tracking-tight">Why The Model Thinks This</h2>
|
| 19 |
-
</div>
|
| 20 |
-
|
| 21 |
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
| 22 |
-
<div className="bg-[#111111] border border-[#222222] rounded-xl p-8 flex flex-col justify-center items-center text-center">
|
| 23 |
-
<div className="text-sm text-[#888888] mb-2 uppercase tracking-wider">Prediction Confidence</div>
|
| 24 |
-
<div className="text-5xl font-mono font-medium text-white">92%</div>
|
| 25 |
-
<div className="text-xs text-[#00D084] mt-2 bg-[#00D084]/10 px-2 py-1 rounded">MATHEMATICALLY SOUND</div>
|
| 26 |
-
</div>
|
| 27 |
-
|
| 28 |
-
<div className="md:col-span-2 bg-[#111111] border border-[#222222] rounded-xl p-8">
|
| 29 |
-
<div className="text-sm text-[#888888] mb-6 uppercase tracking-wider">Feature Contributions (SHAP)</div>
|
| 30 |
-
|
| 31 |
-
<div className="space-y-4">
|
| 32 |
-
{factors.map((f, i) => (
|
| 33 |
-
<div key={i} className="flex justify-between items-center text-sm">
|
| 34 |
-
<span className="text-[#EDEDED]">{f.name}</span>
|
| 35 |
-
<div className="flex items-center gap-3">
|
| 36 |
-
<span className={`font-mono ${f.type === 'positive' ? 'text-[#00D084]' : 'text-[#E00000]'}`}>{f.value}</span>
|
| 37 |
-
<div className="w-32 h-1.5 bg-[#222222] rounded-full overflow-hidden flex justify-end">
|
| 38 |
-
{f.type === 'positive' ? (
|
| 39 |
-
<div className="h-full bg-[#00D084] w-full" />
|
| 40 |
-
) : (
|
| 41 |
-
<div className="h-full bg-[#E00000] w-[40%]" />
|
| 42 |
-
)}
|
| 43 |
-
</div>
|
| 44 |
-
</div>
|
| 45 |
-
</div>
|
| 46 |
-
))}
|
| 47 |
-
</div>
|
| 48 |
-
</div>
|
| 49 |
-
</div>
|
| 50 |
-
</section>
|
| 51 |
-
);
|
| 52 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/saas/Telemetry.tsx
DELETED
|
@@ -1,46 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { Activity } from "lucide-react";
|
| 4 |
-
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
| 5 |
-
|
| 6 |
-
export default function Telemetry() {
|
| 7 |
-
const data = Array.from({ length: 50 }).map((_, i) => ({
|
| 8 |
-
lap: i + 1,
|
| 9 |
-
ver: 80 + Math.random() * 2 + (i * 0.05), // Verstappen lap times
|
| 10 |
-
nor: 80.5 + Math.random() * 1.5 + (i * 0.04), // Norris lap times
|
| 11 |
-
}));
|
| 12 |
-
|
| 13 |
-
return (
|
| 14 |
-
<section className="px-6 max-w-6xl mx-auto mb-24">
|
| 15 |
-
<div className="flex items-center gap-2 mb-8">
|
| 16 |
-
<Activity className="w-5 h-5 text-white" />
|
| 17 |
-
<h2 className="text-2xl font-semibold text-white tracking-tight">Telemetry Analysis</h2>
|
| 18 |
-
</div>
|
| 19 |
-
|
| 20 |
-
<div className="bg-[#111111] border border-[#222222] rounded-xl p-8">
|
| 21 |
-
<div className="flex items-center justify-between mb-8">
|
| 22 |
-
<div className="text-sm text-[#888888]">Simulated Race Pace & Tire Degradation</div>
|
| 23 |
-
<div className="flex gap-4 text-sm font-medium">
|
| 24 |
-
<div className="flex items-center gap-2"><div className="w-3 h-3 rounded-full bg-[#0070F3]" /> Verstappen</div>
|
| 25 |
-
<div className="flex items-center gap-2"><div className="w-3 h-3 rounded-full bg-[#FF8700]" /> Norris</div>
|
| 26 |
-
</div>
|
| 27 |
-
</div>
|
| 28 |
-
|
| 29 |
-
<div className="h-[300px] w-full text-sm">
|
| 30 |
-
<ResponsiveContainer width="100%" height="100%">
|
| 31 |
-
<LineChart data={data} margin={{ top: 5, right: 0, left: -20, bottom: 0 }}>
|
| 32 |
-
<CartesianGrid strokeDasharray="3 3" stroke="#222222" vertical={false} />
|
| 33 |
-
<XAxis dataKey="lap" stroke="#888888" tick={{ fill: '#888888' }} />
|
| 34 |
-
<YAxis domain={['auto', 'auto']} stroke="#888888" tick={{ fill: '#888888' }} />
|
| 35 |
-
<Tooltip
|
| 36 |
-
contentStyle={{ backgroundColor: '#000000', border: '1px solid #333333', color: '#fff', borderRadius: '8px' }}
|
| 37 |
-
/>
|
| 38 |
-
<Line type="monotone" dataKey="ver" name="Verstappen (s)" stroke="#0070F3" strokeWidth={2} dot={false} />
|
| 39 |
-
<Line type="monotone" dataKey="nor" name="Norris (s)" stroke="#FF8700" strokeWidth={2} dot={false} />
|
| 40 |
-
</LineChart>
|
| 41 |
-
</ResponsiveContainer>
|
| 42 |
-
</div>
|
| 43 |
-
</div>
|
| 44 |
-
</section>
|
| 45 |
-
);
|
| 46 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/saas/UpcomingGP.tsx
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { CloudRain, MapPin, Trophy } from "lucide-react";
|
| 4 |
-
|
| 5 |
-
export default function UpcomingGP() {
|
| 6 |
-
const predictions = [
|
| 7 |
-
{ name: "Norris", prob: 42, color: "bg-[#FF8700]" },
|
| 8 |
-
{ name: "Verstappen", prob: 31, color: "bg-[#0070F3]" },
|
| 9 |
-
{ name: "Leclerc", prob: 17, color: "bg-[#E00000]" },
|
| 10 |
-
{ name: "Piastri", prob: 10, color: "bg-[#FF8700]" },
|
| 11 |
-
];
|
| 12 |
-
|
| 13 |
-
return (
|
| 14 |
-
<section id="upcoming-gp" className="px-6 max-w-6xl mx-auto mb-16 pt-16">
|
| 15 |
-
<div className="flex items-center gap-2 mb-8">
|
| 16 |
-
<Trophy className="w-5 h-5 text-white" />
|
| 17 |
-
<h2 className="text-2xl font-semibold text-white tracking-tight">Predicted Podium</h2>
|
| 18 |
-
</div>
|
| 19 |
-
|
| 20 |
-
<div className="bg-[#111111] border border-[#222222] rounded-xl p-8">
|
| 21 |
-
<div className="flex items-center gap-6 mb-8 text-sm text-[#888888] border-b border-[#222222] pb-6">
|
| 22 |
-
<span className="flex items-center gap-2"><MapPin className="w-4 h-4" /> Montreal, Canada</span>
|
| 23 |
-
<span className="flex items-center gap-2"><CloudRain className="w-4 h-4" /> 40% Rain Probability</span>
|
| 24 |
-
</div>
|
| 25 |
-
|
| 26 |
-
<div className="space-y-6">
|
| 27 |
-
{predictions.map((p, i) => (
|
| 28 |
-
<div key={i} className="flex items-center gap-6">
|
| 29 |
-
<div className="w-24 text-sm font-medium text-white">{p.name}</div>
|
| 30 |
-
<div className="flex-1 h-3 bg-[#222222] rounded-full overflow-hidden">
|
| 31 |
-
<div className={`h-full ${p.color} transition-all duration-1000 ease-out`} style={{ width: `${p.prob}%` }} />
|
| 32 |
-
</div>
|
| 33 |
-
<div className="w-12 text-right text-sm font-mono text-[#888888]">{p.prob}%</div>
|
| 34 |
-
</div>
|
| 35 |
-
))}
|
| 36 |
-
</div>
|
| 37 |
-
</div>
|
| 38 |
-
</section>
|
| 39 |
-
);
|
| 40 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/ui/button.tsx
DELETED
|
@@ -1,52 +0,0 @@
|
|
| 1 |
-
import { Slot } from "@radix-ui/react-slot";
|
| 2 |
-
import { cva, type VariantProps } from "class-variance-authority";
|
| 3 |
-
import * as React from "react";
|
| 4 |
-
|
| 5 |
-
import { cn } from "@/lib/utils";
|
| 6 |
-
|
| 7 |
-
const buttonVariants = cva(
|
| 8 |
-
"inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium transition-colors outline-offset-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
| 9 |
-
{
|
| 10 |
-
variants: {
|
| 11 |
-
variant: {
|
| 12 |
-
default: "bg-primary text-primary-foreground shadow-sm shadow-black/5 hover:bg-primary/90",
|
| 13 |
-
destructive:
|
| 14 |
-
"bg-destructive text-destructive-foreground shadow-sm shadow-black/5 hover:bg-destructive/90",
|
| 15 |
-
outline:
|
| 16 |
-
"border border-input bg-background shadow-sm shadow-black/5 hover:bg-accent hover:text-accent-foreground",
|
| 17 |
-
secondary:
|
| 18 |
-
"bg-secondary text-secondary-foreground shadow-sm shadow-black/5 hover:bg-secondary/80",
|
| 19 |
-
ghost: "hover:bg-accent hover:text-accent-foreground",
|
| 20 |
-
link: "text-primary underline-offset-4 hover:underline",
|
| 21 |
-
},
|
| 22 |
-
size: {
|
| 23 |
-
default: "h-9 px-4 py-2",
|
| 24 |
-
sm: "h-8 rounded-lg px-3 text-xs",
|
| 25 |
-
lg: "h-10 rounded-lg px-8",
|
| 26 |
-
icon: "h-9 w-9",
|
| 27 |
-
},
|
| 28 |
-
},
|
| 29 |
-
defaultVariants: {
|
| 30 |
-
variant: "default",
|
| 31 |
-
size: "default",
|
| 32 |
-
},
|
| 33 |
-
},
|
| 34 |
-
);
|
| 35 |
-
|
| 36 |
-
export interface ButtonProps
|
| 37 |
-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
| 38 |
-
VariantProps<typeof buttonVariants> {
|
| 39 |
-
asChild?: boolean;
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
| 43 |
-
({ className, variant, size, asChild = false, ...props }, ref) => {
|
| 44 |
-
const Comp = asChild ? Slot : "button";
|
| 45 |
-
return (
|
| 46 |
-
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
| 47 |
-
);
|
| 48 |
-
},
|
| 49 |
-
);
|
| 50 |
-
Button.displayName = "Button";
|
| 51 |
-
|
| 52 |
-
export { Button, buttonVariants };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/ui/popover.tsx
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
| 4 |
-
import * as React from "react";
|
| 5 |
-
|
| 6 |
-
import { cn } from "@/lib/utils";
|
| 7 |
-
|
| 8 |
-
const Popover = PopoverPrimitive.Root;
|
| 9 |
-
|
| 10 |
-
const PopoverTrigger = PopoverPrimitive.Trigger;
|
| 11 |
-
|
| 12 |
-
const PopoverAnchor = PopoverPrimitive.Anchor;
|
| 13 |
-
|
| 14 |
-
const PopoverContent = React.forwardRef<
|
| 15 |
-
React.ElementRef<typeof PopoverPrimitive.Content>,
|
| 16 |
-
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
|
| 17 |
-
showArrow?: boolean;
|
| 18 |
-
}
|
| 19 |
-
>(({ className, align = "center", sideOffset = 4, showArrow = false, ...props }, ref) => (
|
| 20 |
-
<PopoverPrimitive.Portal>
|
| 21 |
-
<PopoverPrimitive.Content
|
| 22 |
-
ref={ref}
|
| 23 |
-
align={align}
|
| 24 |
-
sideOffset={sideOffset}
|
| 25 |
-
className={cn(
|
| 26 |
-
"z-50 max-h-[var(--radix-popover-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-lg shadow-black/5 outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
| 27 |
-
className,
|
| 28 |
-
)}
|
| 29 |
-
{...props}
|
| 30 |
-
>
|
| 31 |
-
{props.children}
|
| 32 |
-
{showArrow && (
|
| 33 |
-
<PopoverPrimitive.Arrow className="-my-px fill-popover drop-shadow-[0_1px_0_hsl(var(--border))]" />
|
| 34 |
-
)}
|
| 35 |
-
</PopoverPrimitive.Content>
|
| 36 |
-
</PopoverPrimitive.Portal>
|
| 37 |
-
));
|
| 38 |
-
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
| 39 |
-
|
| 40 |
-
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/ui/ruixen-query-box.tsx
DELETED
|
@@ -1,80 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { SendHorizonal } from "lucide-react";
|
| 4 |
-
import { useState } from "react";
|
| 5 |
-
import { cn } from "@/lib/utils";
|
| 6 |
-
import { Textarea } from "@/components/ui/textarea";
|
| 7 |
-
import { useAutoResizeTextarea } from "@/hooks/use-auto-resize-textarea";
|
| 8 |
-
|
| 9 |
-
export default function RuixenQueryBox({ onSubmit }: { onSubmit: (query: string) => void }) {
|
| 10 |
-
const { textareaRef, adjustHeight } = useAutoResizeTextarea({
|
| 11 |
-
minHeight: 56,
|
| 12 |
-
maxHeight: 220,
|
| 13 |
-
});
|
| 14 |
-
|
| 15 |
-
const [inputValue, setInputValue] = useState("");
|
| 16 |
-
|
| 17 |
-
const handleSend = () => {
|
| 18 |
-
if (!inputValue.trim()) return;
|
| 19 |
-
// Call the parent's submit handler instead of just console.log
|
| 20 |
-
onSubmit(inputValue);
|
| 21 |
-
setInputValue("");
|
| 22 |
-
adjustHeight(true);
|
| 23 |
-
};
|
| 24 |
-
|
| 25 |
-
return (
|
| 26 |
-
<div className="w-full py-6">
|
| 27 |
-
<div
|
| 28 |
-
className="relative w-full mx-auto bg-white rounded-2xl border shadow-sm overflow-hidden"
|
| 29 |
-
style={{
|
| 30 |
-
backgroundImage:
|
| 31 |
-
"url('https://pub-940ccf6255b54fa799a9b01050e6c227.r2.dev/ruixen_chat_gradient.png')",
|
| 32 |
-
backgroundSize: "cover",
|
| 33 |
-
backgroundPosition: "center",
|
| 34 |
-
borderColor: "var(--border-glass)"
|
| 35 |
-
}}
|
| 36 |
-
>
|
| 37 |
-
<Textarea
|
| 38 |
-
id="ai-textarea"
|
| 39 |
-
ref={textareaRef}
|
| 40 |
-
placeholder="Ask KRONECTOR about F1 stats, predictions, or telemetry..."
|
| 41 |
-
className={cn(
|
| 42 |
-
"w-full resize-none border-none bg-transparent",
|
| 43 |
-
"text-base text-white placeholder:text-gray-400",
|
| 44 |
-
"px-5 py-4 pr-14 rounded-2xl leading-[1.4]",
|
| 45 |
-
"transition-all focus-visible:ring-0 focus-visible:ring-offset-0",
|
| 46 |
-
"min-h-[56px]"
|
| 47 |
-
)}
|
| 48 |
-
value={inputValue}
|
| 49 |
-
onChange={(e) => {
|
| 50 |
-
setInputValue(e.target.value);
|
| 51 |
-
adjustHeight();
|
| 52 |
-
}}
|
| 53 |
-
onKeyDown={(e) => {
|
| 54 |
-
if (e.key === "Enter" && !e.shiftKey) {
|
| 55 |
-
e.preventDefault();
|
| 56 |
-
handleSend();
|
| 57 |
-
}
|
| 58 |
-
}}
|
| 59 |
-
/>
|
| 60 |
-
|
| 61 |
-
{/* Icon Buttons */}
|
| 62 |
-
<div className="absolute bottom-3 right-3 flex items-center gap-2">
|
| 63 |
-
<button
|
| 64 |
-
type="button"
|
| 65 |
-
onClick={handleSend}
|
| 66 |
-
disabled={!inputValue.trim()}
|
| 67 |
-
className={cn(
|
| 68 |
-
"p-2 rounded-full transition-all duration-200 border",
|
| 69 |
-
inputValue.trim()
|
| 70 |
-
? "bg-cyan-500 hover:bg-cyan-400 text-black border-transparent shadow-[0_0_15px_rgba(0,240,255,0.4)]"
|
| 71 |
-
: "bg-black/20 text-gray-500 border-white/5 cursor-not-allowed"
|
| 72 |
-
)}
|
| 73 |
-
>
|
| 74 |
-
<SendHorizonal className="w-4 h-4" />
|
| 75 |
-
</button>
|
| 76 |
-
</div>
|
| 77 |
-
</div>
|
| 78 |
-
</div>
|
| 79 |
-
);
|
| 80 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/components/ui/textarea.tsx
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
import * as React from "react";
|
| 2 |
-
|
| 3 |
-
import { cn } from "@/lib/utils";
|
| 4 |
-
|
| 5 |
-
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
|
| 6 |
-
({ className, ...props }, ref) => {
|
| 7 |
-
return (
|
| 8 |
-
<textarea
|
| 9 |
-
className={cn(
|
| 10 |
-
"flex w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground shadow-sm shadow-black/5 transition-shadow placeholder:text-muted-foreground/70 focus-visible:border-ring focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50",
|
| 11 |
-
className,
|
| 12 |
-
)}
|
| 13 |
-
ref={ref}
|
| 14 |
-
{...props}
|
| 15 |
-
/>
|
| 16 |
-
);
|
| 17 |
-
},
|
| 18 |
-
);
|
| 19 |
-
Textarea.displayName = "Textarea";
|
| 20 |
-
|
| 21 |
-
export { Textarea };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/desktop.html
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
|
| 3 |
+
<html class="dark" lang="en"><head>
|
| 4 |
+
<meta charset="utf-8"/>
|
| 5 |
+
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
| 6 |
+
<title>Kronector | Formula 1 Intelligence</title>
|
| 7 |
+
<!-- Fonts -->
|
| 8 |
+
<link href="https://fonts.googleapis.com" rel="preconnect"/>
|
| 9 |
+
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"/>
|
| 11 |
+
<!-- Material Symbols -->
|
| 12 |
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
| 13 |
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
| 14 |
+
<!-- Tailwind Config -->
|
| 15 |
+
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
| 16 |
+
<script id="tailwind-config">
|
| 17 |
+
tailwind.config = {
|
| 18 |
+
darkMode: "class",
|
| 19 |
+
theme: {
|
| 20 |
+
extend: {
|
| 21 |
+
"colors": {
|
| 22 |
+
"surface-dim": "#131313",
|
| 23 |
+
"on-primary-fixed": "#400200",
|
| 24 |
+
"secondary": "#ddfcff",
|
| 25 |
+
"primary": "#ffb4a7",
|
| 26 |
+
"on-error-container": "#ffdad6",
|
| 27 |
+
"surface-container": "#201f1f",
|
| 28 |
+
"on-secondary-fixed": "#002022",
|
| 29 |
+
"on-secondary-fixed-variant": "#004f54",
|
| 30 |
+
"tertiary-container": "#909191",
|
| 31 |
+
"inverse-on-surface": "#313030",
|
| 32 |
+
"primary-fixed-dim": "#ffb4a7",
|
| 33 |
+
"inverse-primary": "#be0e00",
|
| 34 |
+
"on-tertiary-container": "#282a2a",
|
| 35 |
+
"on-secondary-container": "#006a70",
|
| 36 |
+
"tertiary-fixed-dim": "#c6c6c7",
|
| 37 |
+
"outline": "#b08780",
|
| 38 |
+
"outline-variant": "#5f3e39",
|
| 39 |
+
"primary-container": "#ff553d",
|
| 40 |
+
"on-primary-fixed-variant": "#910900",
|
| 41 |
+
"secondary-fixed": "#74f5ff",
|
| 42 |
+
"surface-container-lowest": "#0e0e0e",
|
| 43 |
+
"on-surface-variant": "#eabcb4",
|
| 44 |
+
"tertiary-fixed": "#e2e2e2",
|
| 45 |
+
"surface-container-high": "#2a2a2a",
|
| 46 |
+
"on-tertiary-fixed-variant": "#454747",
|
| 47 |
+
"on-tertiary": "#2f3131",
|
| 48 |
+
"on-tertiary-fixed": "#1a1c1c",
|
| 49 |
+
"background": "#131313",
|
| 50 |
+
"on-error": "#690005",
|
| 51 |
+
"surface-variant": "#353534",
|
| 52 |
+
"surface-bright": "#3a3939",
|
| 53 |
+
"on-surface": "#e5e2e1",
|
| 54 |
+
"surface-container-low": "#1c1b1b",
|
| 55 |
+
"on-secondary": "#00363a",
|
| 56 |
+
"primary-fixed": "#ffdad4",
|
| 57 |
+
"on-primary-container": "#5b0300",
|
| 58 |
+
"on-primary": "#670400",
|
| 59 |
+
"surface": "#131313",
|
| 60 |
+
"secondary-container": "#00f1fe",
|
| 61 |
+
"on-background": "#e5e2e1",
|
| 62 |
+
"inverse-surface": "#e5e2e1",
|
| 63 |
+
"secondary-fixed-dim": "#00dbe7",
|
| 64 |
+
"error-container": "#93000a",
|
| 65 |
+
"surface-tint": "#ffb4a7",
|
| 66 |
+
"tertiary": "#c6c6c7",
|
| 67 |
+
"surface-container-highest": "#353534",
|
| 68 |
+
"error": "#ffb4ab"
|
| 69 |
+
},
|
| 70 |
+
"borderRadius": {
|
| 71 |
+
"DEFAULT": "0.25rem",
|
| 72 |
+
"lg": "0.5rem",
|
| 73 |
+
"xl": "0.75rem",
|
| 74 |
+
"full": "9999px"
|
| 75 |
+
},
|
| 76 |
+
"spacing": {
|
| 77 |
+
"margin-mobile": "16px",
|
| 78 |
+
"container-max": "1440px",
|
| 79 |
+
"margin-desktop": "32px",
|
| 80 |
+
"gutter": "16px",
|
| 81 |
+
"unit": "4px"
|
| 82 |
+
},
|
| 83 |
+
"fontFamily": {
|
| 84 |
+
"headline-md": ["Inter"],
|
| 85 |
+
"body-main": ["Inter"],
|
| 86 |
+
"label-caps": ["JetBrains Mono"],
|
| 87 |
+
"display-lg-mobile": ["Inter"],
|
| 88 |
+
"body-fixed": ["JetBrains Mono"],
|
| 89 |
+
"display-lg": ["Inter"]
|
| 90 |
+
},
|
| 91 |
+
"fontSize": {
|
| 92 |
+
"headline-md": ["24px", { "lineHeight": "1.3", "letterSpacing": "-0.01em", "fontWeight": "700" }],
|
| 93 |
+
"body-main": ["16px", { "lineHeight": "1.6", "letterSpacing": "0em", "fontWeight": "400" }],
|
| 94 |
+
"label-caps": ["11px", { "lineHeight": "1", "letterSpacing": "0.1em", "fontWeight": "700" }],
|
| 95 |
+
"display-lg-mobile": ["32px", { "lineHeight": "1.2", "letterSpacing": "-0.02em", "fontWeight": "800" }],
|
| 96 |
+
"body-fixed": ["14px", { "lineHeight": "1.5", "letterSpacing": "0em", "fontWeight": "400" }],
|
| 97 |
+
"display-lg": ["48px", { "lineHeight": "1.1", "letterSpacing": "-0.04em", "fontWeight": "800" }]
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
</script>
|
| 103 |
+
<style>
|
| 104 |
+
/* Custom Carbon UI Grid Pattern */
|
| 105 |
+
.bg-grid-pattern {
|
| 106 |
+
background-image:
|
| 107 |
+
linear-gradient(to right, rgba(95, 62, 57, 0.05) 1px, transparent 1px),
|
| 108 |
+
linear-gradient(to bottom, rgba(95, 62, 57, 0.05) 1px, transparent 1px);
|
| 109 |
+
background-size: 32px 32px;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/* Terminal Glow Effects */
|
| 113 |
+
.terminal-glow {
|
| 114 |
+
box-shadow: 0 0 15px rgba(0, 241, 254, 0.1);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/* SHAP Bar Animation (Simulated) */
|
| 118 |
+
@keyframes shapExpand {
|
| 119 |
+
from { width: 0; }
|
| 120 |
+
to { width: var(--target-width); }
|
| 121 |
+
}
|
| 122 |
+
.shap-bar {
|
| 123 |
+
animation: shapExpand 1.5s ease-out forwards;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
/* Flow Diagram Pulse */
|
| 127 |
+
@keyframes linePulse {
|
| 128 |
+
0% { opacity: 0.3; }
|
| 129 |
+
50% { opacity: 1; }
|
| 130 |
+
100% { opacity: 0.3; }
|
| 131 |
+
}
|
| 132 |
+
.flow-line {
|
| 133 |
+
animation: linePulse 2s infinite;
|
| 134 |
+
}
|
| 135 |
+
</style>
|
| 136 |
+
</head>
|
| 137 |
+
<body class="bg-surface-dim text-on-surface font-body-main antialiased min-h-screen flex flex-col selection:bg-secondary-container selection:text-on-secondary-container">
|
| 138 |
+
<!-- 1. TopNavBar -->
|
| 139 |
+
<header class="bg-surface-dim/80 backdrop-blur-xl w-full flex justify-between items-center px-margin-desktop h-16 z-50 border-b border-outline-variant/30 sticky top-0">
|
| 140 |
+
<div class="flex items-center gap-gutter">
|
| 141 |
+
<span class="font-display-lg text-display-lg-mobile md:text-[24px] font-extrabold uppercase tracking-tighter text-on-surface">KRONECTOR</span>
|
| 142 |
+
<div id="health-indicator" class="hidden md:flex items-center gap-2 px-3 py-1 bg-surface-container rounded-full border border-surface-variant cursor-pointer" title="Click to check API health">
|
| 143 |
+
<span id="health-dot" class="w-2 h-2 rounded-full bg-secondary-fixed-dim animate-pulse"></span>
|
| 144 |
+
<span id="health-label" class="font-label-caps text-label-caps text-secondary-fixed-dim uppercase">Checking...</span>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
<nav class="hidden md:flex gap-8">
|
| 148 |
+
<a class="text-primary font-bold border-b-2 border-primary pb-1 font-label-caps text-label-caps hover:text-secondary-fixed transition-colors duration-200" href="#section-predictions">Predictions</a>
|
| 149 |
+
<a class="text-on-surface-variant font-medium font-label-caps text-label-caps hover:text-secondary-fixed transition-colors duration-200" href="#section-features">Model Proof</a>
|
| 150 |
+
<a class="text-on-surface-variant font-medium font-label-caps text-label-caps hover:text-secondary-fixed transition-colors duration-200" href="#section-architecture">Architecture</a>
|
| 151 |
+
<a class="text-on-surface-variant font-medium font-label-caps text-label-caps hover:text-secondary-fixed transition-colors duration-200" href="#section-cta">API</a>
|
| 152 |
+
</nav>
|
| 153 |
+
<button id="nav-run-btn" class="bg-primary-container text-on-primary-container hover:bg-primary-container/80 transition-colors px-4 py-2 rounded font-label-caps text-label-caps hidden md:block active:scale-95">Run Prediction</button>
|
| 154 |
+
<!-- Mobile Menu Icon (Placeholder) -->
|
| 155 |
+
<button class="md:hidden text-on-surface">
|
| 156 |
+
<span class="material-symbols-outlined">menu</span>
|
| 157 |
+
</button>
|
| 158 |
+
</header>
|
| 159 |
+
<main class="flex-grow">
|
| 160 |
+
<!-- 2. Hero Section -->
|
| 161 |
+
<section id="section-predictions" class="relative pt-20 pb-16 px-margin-mobile md:px-margin-desktop max-w-container-max mx-auto overflow-hidden scroll-mt-20">
|
| 162 |
+
<div class="absolute inset-0 bg-grid-pattern pointer-events-none"></div>
|
| 163 |
+
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center relative z-10">
|
| 164 |
+
<!-- Text Content -->
|
| 165 |
+
<div class="lg:col-span-5 flex flex-col gap-6">
|
| 166 |
+
<div class="inline-flex items-center gap-2 px-3 py-1 bg-surface-container border border-surface-variant rounded-full w-max">
|
| 167 |
+
<span class="material-symbols-outlined text-secondary-fixed-dim text-sm" style="font-variation-settings: 'FILL' 1;">speed</span>
|
| 168 |
+
<span class="font-label-caps text-label-caps text-on-surface-variant">Telemetry Ingestion Active</span>
|
| 169 |
+
</div>
|
| 170 |
+
<h1 class="font-display-lg text-display-lg-mobile md:text-display-lg text-on-surface leading-tight">Predict the race before lights out</h1>
|
| 171 |
+
<p class="font-body-main text-body-main text-on-surface-variant">Kronector turns Formula 1 telemetry, qualifying pace, driver form, and model explainability into race-winning predictions before the grid forms.</p>
|
| 172 |
+
<div class="flex flex-wrap gap-4 pt-4">
|
| 173 |
+
<button id="hero-run-btn" class="bg-primary-container text-on-primary-container px-6 py-3 rounded hover:bg-primary/90 transition-colors font-headline-md text-[16px]">Run prediction</button>
|
| 174 |
+
<button id="hero-proof-btn" class="border border-secondary-container text-secondary-fixed-dim px-6 py-3 rounded hover:bg-secondary-container/10 transition-colors font-body-fixed text-body-fixed flex items-center gap-2">
|
| 175 |
+
View model proof
|
| 176 |
+
<span class="material-symbols-outlined text-sm">arrow_forward</span>
|
| 177 |
+
</button>
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
<!-- Product Mock (Terminal) -->
|
| 181 |
+
<div id="terminal" class="lg:col-span-7 bg-[#000000] border border-secondary-container rounded-lg terminal-glow flex flex-col overflow-hidden shadow-2xl">
|
| 182 |
+
<!-- Terminal Header -->
|
| 183 |
+
<div class="h-8 bg-surface-container flex items-center px-4 border-b border-surface-variant gap-2">
|
| 184 |
+
<div class="w-3 h-3 rounded-full bg-surface-variant"></div>
|
| 185 |
+
<div class="w-3 h-3 rounded-full bg-surface-variant"></div>
|
| 186 |
+
<div class="w-3 h-3 rounded-full bg-surface-variant"></div>
|
| 187 |
+
<span class="ml-4 font-body-fixed text-[10px] text-on-surface-variant">kronector-inference-node-01 — zsh</span>
|
| 188 |
+
<span id="terminal-confidence" class="mr-4 font-label-caps text-[10px] text-secondary-fixed-dim">Confidence Interval: —</span><span id="terminal-gp-label" class="ml-auto font-label-caps text-[10px] text-primary-container">Ready for Query</span>
|
| 189 |
+
</div>
|
| 190 |
+
<div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-px bg-surface-variant">
|
| 191 |
+
<!-- Win Probability Pane -->
|
| 192 |
+
<div id="pane-prediction" class="bg-[#000000] p-4 col-span-1 md:col-span-2 flex flex-col gap-2 overflow-x-auto custom-scrollbar">
|
| 193 |
+
<h3 class="font-label-caps text-label-caps text-secondary-fixed-dim mb-2 uppercase border-b border-surface-variant pb-1">Win Probability Model</h3>
|
| 194 |
+
<div id="prediction-content">
|
| 195 |
+
<p class="font-body-fixed text-[12px] text-on-surface-variant/60 italic">Enter a query below to get a live prediction...</p>
|
| 196 |
+
</div>
|
| 197 |
+
</div>
|
| 198 |
+
<!-- SHAP Pane -->
|
| 199 |
+
<div id="pane-shap" class="bg-[#000000] p-4 flex flex-col gap-2 border-t border-surface-variant md:border-t-0 md:border-l">
|
| 200 |
+
<h3 class="font-label-caps text-label-caps text-secondary-fixed-dim mb-2 uppercase border-b border-surface-variant pb-1">SHAP Features</h3>
|
| 201 |
+
<div id="shap-content" class="flex flex-col gap-2 font-body-fixed text-[11px]">
|
| 202 |
+
<p class="text-on-surface-variant/60 italic text-[11px]">Awaiting prediction...</p>
|
| 203 |
+
</div>
|
| 204 |
+
</div>
|
| 205 |
+
<!-- Race Radio Pane -->
|
| 206 |
+
<div id="pane-radio" class="bg-[#000000] p-4 col-span-1 md:col-span-2 border-t border-surface-variant">
|
| 207 |
+
<h3 class="font-label-caps text-label-caps text-secondary-fixed-dim mb-2 uppercase">Agentic Race-Radio</h3>
|
| 208 |
+
<div id="radio-content" class="font-body-fixed text-[12px] text-tertiary-fixed-dim flex flex-col gap-1 opacity-80">
|
| 209 |
+
<div><span class="text-on-surface-variant/50">—</span> Waiting for agentic synthesis...</div>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
</div>
|
| 213 |
+
<!-- Prompt Input -->
|
| 214 |
+
<div class="bg-surface-container border-t border-surface-variant p-3 flex items-center gap-3">
|
| 215 |
+
<span class="font-body-fixed text-secondary-fixed-dim">❯</span>
|
| 216 |
+
<input id="terminal-input" class="bg-transparent border-none text-on-surface font-body-fixed text-[13px] flex-grow focus:ring-0 p-0" type="text" placeholder="Who wins the 2026 Canadian GP?" autocomplete="off"/>
|
| 217 |
+
<button id="terminal-send" class="text-on-surface-variant hover:text-secondary-fixed-dim transition-colors">
|
| 218 |
+
<span class="material-symbols-outlined text-sm">send</span>
|
| 219 |
+
</button>
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
</section>
|
| 224 |
+
<!-- 3. Metrics Strip -->
|
| 225 |
+
<section id="section-metrics" class="border-y border-outline-variant/30 bg-surface-dim/50 py-8 px-margin-mobile md:px-margin-desktop">
|
| 226 |
+
<div class="max-w-container-max mx-auto grid grid-cols-2 md:grid-cols-4 gap-8 divide-x divide-outline-variant/30">
|
| 227 |
+
<div class="pl-4 border-l-2 border-secondary-fixed-dim flex flex-col gap-1">
|
| 228 |
+
<span class="font-headline-md text-headline-md text-on-surface">71%</span>
|
| 229 |
+
<span class="font-label-caps text-label-caps text-on-surface-variant uppercase">Winner Accuracy</span>
|
| 230 |
+
</div>
|
| 231 |
+
<div class="pl-4 md:pl-8 border-l-2 border-primary-container flex flex-col gap-1">
|
| 232 |
+
<span class="font-headline-md text-headline-md text-on-surface">0.94</span>
|
| 233 |
+
<span class="font-label-caps text-label-caps text-on-surface-variant uppercase">ROC AUC</span>
|
| 234 |
+
</div>
|
| 235 |
+
<div class="pl-4 md:pl-8 border-l-2 border-secondary-fixed-dim flex flex-col gap-1">
|
| 236 |
+
<span class="font-headline-md text-headline-md text-on-surface">4-Agent</span>
|
| 237 |
+
<span class="font-label-caps text-label-caps text-on-surface-variant uppercase">Pipeline</span>
|
| 238 |
+
</div>
|
| 239 |
+
<div class="pl-4 md:pl-8 border-l-2 border-primary-container flex flex-col gap-1">
|
| 240 |
+
<span class="font-headline-md text-headline-md text-on-surface">< 1s</span>
|
| 241 |
+
<span class="font-label-caps text-label-caps text-on-surface-variant uppercase">Inference</span>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
</section>
|
| 245 |
+
<!-- 4. Features Bento Grid -->
|
| 246 |
+
<section id="section-features" class="py-20 px-margin-mobile md:px-margin-desktop max-w-container-max mx-auto scroll-mt-20">
|
| 247 |
+
<div class="mb-12">
|
| 248 |
+
<h2 class="font-headline-md text-[28px] text-on-surface mb-2">Technical Capabilities</h2>
|
| 249 |
+
<p class="font-body-main text-on-surface-variant">Built for precision telemetry analysis and race strategy formulation.</p>
|
| 250 |
+
</div>
|
| 251 |
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
| 252 |
+
<div class="bg-surface-container border border-surface-variant rounded-lg p-6 hover:border-secondary-container transition-colors group">
|
| 253 |
+
<span class="material-symbols-outlined text-secondary-fixed-dim mb-4 text-[32px] group-hover:scale-110 transition-transform">monitoring</span>
|
| 254 |
+
<h3 class="font-body-fixed text-[16px] text-on-surface mb-2 font-bold">Race Prediction</h3>
|
| 255 |
+
<p class="font-body-main text-[14px] text-on-surface-variant leading-relaxed">Real-time probability updates as track conditions shift.</p>
|
| 256 |
+
</div>
|
| 257 |
+
<div class="bg-surface-container border border-surface-variant rounded-lg p-6 hover:border-primary-container transition-colors group">
|
| 258 |
+
<span class="material-symbols-outlined text-primary-container mb-4 text-[32px] group-hover:scale-110 transition-transform">insights</span>
|
| 259 |
+
<h3 class="font-body-fixed text-[16px] text-on-surface mb-2 font-bold">SHAP Explainability</h3>
|
| 260 |
+
<p class="font-body-main text-[14px] text-on-surface-variant leading-relaxed">Understand the 'why' behind every model output.</p>
|
| 261 |
+
</div>
|
| 262 |
+
<div class="bg-surface-container border border-surface-variant rounded-lg p-6 hover:border-secondary-container transition-colors group">
|
| 263 |
+
<span class="material-symbols-outlined text-secondary-fixed-dim mb-4 text-[32px] group-hover:scale-110 transition-transform">track_changes</span>
|
| 264 |
+
<h3 class="font-body-fixed text-[16px] text-on-surface mb-2 font-bold">Drift Monitoring</h3>
|
| 265 |
+
<p class="font-body-main text-[14px] text-on-surface-variant leading-relaxed">Live validation of model performance against track telemetry.</p>
|
| 266 |
+
</div>
|
| 267 |
+
<div class="bg-surface-container border border-surface-variant rounded-lg p-6 hover:border-primary-container transition-colors group">
|
| 268 |
+
<span class="material-symbols-outlined text-primary-container mb-4 text-[32px] group-hover:scale-110 transition-transform">record_voice_over</span>
|
| 269 |
+
<h3 class="font-body-fixed text-[16px] text-on-surface mb-2 font-bold">Agentic Race Radio</h3>
|
| 270 |
+
<p class="font-body-main text-[14px] text-on-surface-variant leading-relaxed">LLM-synthesized strategic insights from multi-agent consensus.</p>
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
</section>
|
| 274 |
+
<!-- 5. Architecture Band -->
|
| 275 |
+
<section id="section-architecture" class="py-16 bg-surface-container-lowest border-y border-outline-variant/20 overflow-x-auto custom-scrollbar scroll-mt-20">
|
| 276 |
+
<div class="px-margin-mobile md:px-margin-desktop min-w-[800px] max-w-container-max mx-auto flex flex-col items-center">
|
| 277 |
+
<h2 class="font-label-caps text-label-caps text-on-surface-variant uppercase mb-12 tracking-[0.2em]">Inference Architecture</h2>
|
| 278 |
+
<div class="flex items-center gap-2"><div class="group flex flex-col items-center gap-2"><div class="px-4 py-3 bg-surface-container-high border border-outline-variant rounded-lg flex flex-col items-center justify-center min-w-[160px] hover:border-secondary-fixed-dim transition-colors cursor-default"><span class="text-[9px] font-label-caps text-secondary-fixed-dim mb-1 uppercase tracking-widest">Ingesting</span><span class="font-body-fixed text-[12px] text-on-surface text-center">FastF1 + Jolpica</span></div></div><div class="flex items-center"><div class="h-px w-4 bg-secondary-fixed-dim/30"></div><div class="h-px w-8 bg-secondary-fixed-dim flow-line"></div><span class="material-symbols-outlined text-secondary-fixed-dim text-[16px]">chevron_right</span></div><div class="group flex flex-col items-center gap-2"><div class="px-4 py-3 bg-surface-container-high border border-outline-variant rounded-lg flex flex-col items-center justify-center min-w-[160px] hover:border-secondary-fixed-dim transition-colors cursor-default"><span class="text-[9px] font-label-caps text-secondary-fixed-dim mb-1 uppercase tracking-widest">Normalized</span><span class="font-body-fixed text-[12px] text-on-surface text-center">25+ features</span></div></div><div class="flex items-center"><div class="h-px w-4 bg-secondary-fixed-dim/30"></div><div class="h-px w-8 bg-secondary-fixed-dim flow-line" style="animation-delay: 0.5s"></div><span class="material-symbols-outlined text-secondary-fixed-dim text-[16px]">chevron_right</span></div><div class="group flex flex-col items-center gap-2"><div class="px-4 py-3 bg-surface-container-high border border-primary-container rounded-lg flex flex-col items-center justify-center min-w-[160px] shadow-[0_0_10px_rgba(255,85,61,0.2)] hover:border-primary transition-colors cursor-default"><span class="text-[9px] font-label-caps text-primary-container mb-1 uppercase tracking-widest">Trained</span><span class="font-body-fixed text-[12px] text-primary-container font-bold text-center">LightGBM + SHAP</span></div></div><div class="flex items-center"><div class="h-px w-4 bg-secondary-fixed-dim/30"></div><div class="h-px w-8 bg-secondary-fixed-dim flow-line" style="animation-delay: 1s"></div><span class="material-symbols-outlined text-secondary-fixed-dim text-[16px]">chevron_right</span></div><div class="group flex flex-col items-center gap-2"><div class="px-4 py-3 bg-surface-container-high border border-outline-variant rounded-lg flex flex-col items-center justify-center min-w-[160px] hover:border-secondary-fixed-dim transition-colors cursor-default"><span class="text-[9px] font-label-caps text-secondary-fixed-dim mb-1 uppercase tracking-widest">Serving</span><span class="font-body-fixed text-[12px] text-on-surface text-center">FastAPI p95 < 1s</span></div></div><div class="flex items-center"><div class="h-px w-4 bg-secondary-fixed-dim/30"></div><div class="h-px w-8 bg-secondary-fixed-dim flow-line" style="animation-delay: 1.5s"></div><span class="material-symbols-outlined text-secondary-fixed-dim text-[16px]">chevron_right</span></div><div class="group flex flex-col items-center gap-2"><div class="px-4 py-3 bg-surface-container-high border border-outline-variant rounded-lg flex flex-col items-center justify-center min-w-[160px] hover:border-secondary-fixed-dim transition-colors cursor-default"><span class="text-[9px] font-label-caps text-secondary-fixed-dim mb-1 uppercase tracking-widest">Synthesized</span><span class="font-body-fixed text-[12px] text-on-surface text-center">4-agent chain</span></div></div></div>
|
| 279 |
+
</div>
|
| 280 |
+
</section>
|
| 281 |
+
<!-- 6. Final CTA -->
|
| 282 |
+
<section id="section-cta" class="py-24 px-margin-mobile md:px-margin-desktop text-center relative scroll-mt-20">
|
| 283 |
+
<div class="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-surface-container-high/30 via-surface-dim to-surface-dim pointer-events-none"></div>
|
| 284 |
+
<div class="relative z-10 max-w-2xl mx-auto flex flex-col items-center">
|
| 285 |
+
<span class="material-symbols-outlined text-secondary-fixed-dim text-[48px] mb-6">flag</span>
|
| 286 |
+
<h2 class="font-display-lg text-display-lg-mobile md:text-[40px] text-on-surface mb-8">Ask the pit wall before race day</h2>
|
| 287 |
+
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
| 288 |
+
<button id="cta-launch-btn" class="bg-primary-container text-on-primary-container px-8 py-3 rounded hover:bg-primary/90 transition-colors font-headline-md text-[16px]">Launch predictor</button>
|
| 289 |
+
<button id="cta-docs-btn" class="border border-outline-variant text-on-surface px-8 py-3 rounded hover:bg-surface-container transition-colors font-body-fixed text-[14px]">Read API docs</button>
|
| 290 |
+
</div>
|
| 291 |
+
</div>
|
| 292 |
+
</section>
|
| 293 |
+
</main>
|
| 294 |
+
<!-- 7. Footer -->
|
| 295 |
+
<footer class="bg-surface-container-lowest full-width py-12 px-margin-desktop border-t border-outline-variant/20 flex flex-col md:flex-row justify-between items-start gap-gutter mt-auto">
|
| 296 |
+
<div class="flex flex-col gap-2">
|
| 297 |
+
<span class="font-display-lg text-display-lg-mobile uppercase text-on-surface-variant opacity-50 md:text-[24px]">KRONECTOR</span>
|
| 298 |
+
<p class="font-label-caps text-label-caps text-secondary-fixed-dim">KRONECTOR INTELLIGENCE. ALL SYSTEMS OPERATIONAL. © 2024</p>
|
| 299 |
+
</div>
|
| 300 |
+
<nav class="flex flex-wrap gap-6 mt-6 md:mt-0">
|
| 301 |
+
<a id="footer-api-ref" class="font-body-fixed text-body-fixed text-on-surface-variant hover:text-primary transition-opacity cursor-pointer">API Reference</a>
|
| 302 |
+
<a id="footer-docs" class="font-body-fixed text-body-fixed text-on-surface-variant hover:text-primary transition-opacity cursor-pointer">Documentation</a>
|
| 303 |
+
<a id="footer-status" class="font-body-fixed text-body-fixed text-on-surface-variant hover:text-primary transition-opacity cursor-pointer">Status</a>
|
| 304 |
+
<a class="font-body-fixed text-body-fixed text-on-surface-variant hover:text-primary transition-opacity" href="#">Terms</a>
|
| 305 |
+
</nav>
|
| 306 |
+
</footer>
|
| 307 |
+
<!-- Toast Notification -->
|
| 308 |
+
<div id="toast" class="fixed bottom-6 right-6 z-[100] px-5 py-3 rounded-lg font-body-fixed text-[13px] shadow-2xl border transition-all duration-300 opacity-0 translate-y-4 pointer-events-none" style="background:#201f1f;border-color:#5f3e39;color:#e5e2e1;"></div>
|
| 309 |
+
|
| 310 |
+
<script>
|
| 311 |
+
// ============================================================================
|
| 312 |
+
// KRONECTOR Frontend — API Integration
|
| 313 |
+
// ============================================================================
|
| 314 |
+
|
| 315 |
+
const API_BASE = 'http://localhost:8000';
|
| 316 |
+
|
| 317 |
+
// --- Utility ---
|
| 318 |
+
function $(id) { return document.getElementById(id); }
|
| 319 |
+
|
| 320 |
+
function showToast(msg, type = 'info') {
|
| 321 |
+
const t = $('toast');
|
| 322 |
+
t.textContent = msg;
|
| 323 |
+
t.style.borderColor = type === 'error' ? '#93000a' : type === 'success' ? '#00dbe7' : '#5f3e39';
|
| 324 |
+
t.classList.remove('opacity-0', 'translate-y-4', 'pointer-events-none');
|
| 325 |
+
t.classList.add('opacity-100', 'translate-y-0');
|
| 326 |
+
clearTimeout(t._timer);
|
| 327 |
+
t._timer = setTimeout(() => {
|
| 328 |
+
t.classList.add('opacity-0', 'translate-y-4', 'pointer-events-none');
|
| 329 |
+
t.classList.remove('opacity-100', 'translate-y-0');
|
| 330 |
+
}, 4000);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
function scrollToInput() {
|
| 334 |
+
const input = $('terminal-input');
|
| 335 |
+
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 336 |
+
setTimeout(() => input.focus(), 400);
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
// --- Smooth Scroll for nav links ---
|
| 340 |
+
document.querySelectorAll('a[href^="#section-"]').forEach(a => {
|
| 341 |
+
a.addEventListener('click', e => {
|
| 342 |
+
e.preventDefault();
|
| 343 |
+
const target = document.querySelector(a.getAttribute('href'));
|
| 344 |
+
if (target) target.scrollIntoView({ behavior: 'smooth' });
|
| 345 |
+
});
|
| 346 |
+
});
|
| 347 |
+
|
| 348 |
+
// --- Health Check ---
|
| 349 |
+
async function checkHealth() {
|
| 350 |
+
const dot = $('health-dot');
|
| 351 |
+
const label = $('health-label');
|
| 352 |
+
try {
|
| 353 |
+
const res = await fetch(`${API_BASE}/health`);
|
| 354 |
+
const data = await res.json();
|
| 355 |
+
if (data.status === 'healthy') {
|
| 356 |
+
dot.className = 'w-2 h-2 rounded-full bg-secondary-fixed-dim animate-pulse';
|
| 357 |
+
label.textContent = 'Live';
|
| 358 |
+
label.className = 'font-label-caps text-label-caps text-secondary-fixed-dim uppercase';
|
| 359 |
+
} else {
|
| 360 |
+
dot.className = 'w-2 h-2 rounded-full bg-primary-container animate-pulse';
|
| 361 |
+
label.textContent = 'Degraded';
|
| 362 |
+
label.className = 'font-label-caps text-label-caps text-primary-container uppercase';
|
| 363 |
+
}
|
| 364 |
+
} catch {
|
| 365 |
+
dot.className = 'w-2 h-2 rounded-full bg-error';
|
| 366 |
+
dot.classList.remove('animate-pulse');
|
| 367 |
+
label.textContent = 'Offline';
|
| 368 |
+
label.className = 'font-label-caps text-label-caps text-error uppercase';
|
| 369 |
+
}
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
// --- Prediction API ---
|
| 373 |
+
let _isLoading = false;
|
| 374 |
+
|
| 375 |
+
async function runPrediction(query) {
|
| 376 |
+
if (_isLoading) return;
|
| 377 |
+
if (!query || query.trim().length < 3) {
|
| 378 |
+
showToast('Query too short. Try something like: "Who wins the 2024 Monaco GP?"', 'error');
|
| 379 |
+
return;
|
| 380 |
+
}
|
| 381 |
+
_isLoading = true;
|
| 382 |
+
const sendBtn = $('terminal-send');
|
| 383 |
+
sendBtn.disabled = true;
|
| 384 |
+
|
| 385 |
+
// Show loading state in terminal
|
| 386 |
+
$('terminal-gp-label').textContent = 'Processing...';
|
| 387 |
+
$('terminal-confidence').textContent = 'Inference Running';
|
| 388 |
+
$('prediction-content').innerHTML = '<p class="font-body-fixed text-[12px] text-secondary-fixed-dim animate-pulse">⏳ Querying model pipeline...</p>';
|
| 389 |
+
$('shap-content').innerHTML = '<p class="text-on-surface-variant/60 italic text-[11px] animate-pulse">Computing SHAP values...</p>';
|
| 390 |
+
$('radio-content').innerHTML = '<div class="animate-pulse"><span class="text-secondary-container">[STRAT_AGENT]</span> Synthesizing response...</div>';
|
| 391 |
+
|
| 392 |
+
try {
|
| 393 |
+
const res = await fetch(`${API_BASE}/predict/f1`, {
|
| 394 |
+
method: 'POST',
|
| 395 |
+
headers: { 'Content-Type': 'application/json' },
|
| 396 |
+
body: JSON.stringify({ query: query.trim() })
|
| 397 |
+
});
|
| 398 |
+
|
| 399 |
+
if (!res.ok) {
|
| 400 |
+
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
| 401 |
+
throw new Error(err.detail || `API Error ${res.status}`);
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
const data = await res.json();
|
| 405 |
+
renderPrediction(data);
|
| 406 |
+
showToast('Prediction complete ✓', 'success');
|
| 407 |
+
} catch (err) {
|
| 408 |
+
$('prediction-content').innerHTML = `<p class="font-body-fixed text-[12px] text-error">❌ ${err.message}</p>`;
|
| 409 |
+
$('shap-content').innerHTML = '<p class="text-error text-[11px]">Error</p>';
|
| 410 |
+
$('radio-content').innerHTML = `<div><span class="text-error">[ERROR]</span> ${err.message}</div>`;
|
| 411 |
+
$('terminal-gp-label').textContent = 'Error';
|
| 412 |
+
$('terminal-confidence').textContent = '';
|
| 413 |
+
showToast(err.message, 'error');
|
| 414 |
+
} finally {
|
| 415 |
+
_isLoading = false;
|
| 416 |
+
sendBtn.disabled = false;
|
| 417 |
+
}
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
function renderPrediction(data) {
|
| 421 |
+
const m = data.metadata;
|
| 422 |
+
|
| 423 |
+
// Terminal header
|
| 424 |
+
$('terminal-confidence').textContent = data.confidence_rating
|
| 425 |
+
? `Confidence: ${data.confidence_rating}`
|
| 426 |
+
: `Win: ${data.win_probability}%`;
|
| 427 |
+
$('terminal-gp-label').textContent = `${m.season} Round ${m.round} — ${m.driver_name}`;
|
| 428 |
+
|
| 429 |
+
// Prediction pane — single driver result card
|
| 430 |
+
const probColor = data.win_probability > 50 ? 'text-primary-container' : data.win_probability > 20 ? 'text-secondary-fixed-dim' : 'text-on-surface-variant';
|
| 431 |
+
$('prediction-content').innerHTML = `
|
| 432 |
+
<table class="w-full text-left font-body-fixed text-[11px] border-separate border-spacing-y-1">
|
| 433 |
+
<thead class="text-on-surface-variant uppercase text-[9px] tracking-wider">
|
| 434 |
+
<tr>
|
| 435 |
+
<th class="px-2 py-1">Driver</th>
|
| 436 |
+
<th class="px-2 py-1">Team</th>
|
| 437 |
+
<th class="px-2 py-1">Grid</th>
|
| 438 |
+
<th class="px-2 py-1">Season</th>
|
| 439 |
+
<th class="px-2 py-1">Round</th>
|
| 440 |
+
<th class="px-2 py-1 text-right">Win %</th>
|
| 441 |
+
</tr>
|
| 442 |
+
</thead>
|
| 443 |
+
<tbody>
|
| 444 |
+
<tr class="bg-surface-container-low rounded">
|
| 445 |
+
<td class="px-2 py-1.5 text-on-surface font-bold">${m.driver_name}</td>
|
| 446 |
+
<td class="px-2 py-1.5">${m.team}</td>
|
| 447 |
+
<td class="px-2 py-1.5">P${m.grid_position}</td>
|
| 448 |
+
<td class="px-2 py-1.5">${m.season}</td>
|
| 449 |
+
<td class="px-2 py-1.5">${m.round}</td>
|
| 450 |
+
<td class="px-2 py-1.5 text-right ${probColor} font-bold text-[14px]">${data.win_probability}%</td>
|
| 451 |
+
</tr>
|
| 452 |
+
</tbody>
|
| 453 |
+
</table>
|
| 454 |
+
`;
|
| 455 |
+
|
| 456 |
+
// SHAP pane — top features as horizontal bars
|
| 457 |
+
const shapEl = $('shap-content');
|
| 458 |
+
if (data.shap_values && Object.keys(data.shap_values).length > 0) {
|
| 459 |
+
const entries = Object.entries(data.shap_values)
|
| 460 |
+
.map(([k, v]) => [k, parseFloat(v)])
|
| 461 |
+
.sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]))
|
| 462 |
+
.slice(0, 6);
|
| 463 |
+
const maxAbs = Math.max(...entries.map(e => Math.abs(e[1])), 0.001);
|
| 464 |
+
|
| 465 |
+
shapEl.innerHTML = entries.map(([feat, val]) => {
|
| 466 |
+
const pct = Math.min(Math.abs(val) / maxAbs * 100, 100).toFixed(0);
|
| 467 |
+
const color = val >= 0 ? 'bg-primary-container' : 'bg-secondary-fixed-dim';
|
| 468 |
+
const textColor = val >= 0 ? 'text-primary-container' : 'text-secondary-fixed-dim';
|
| 469 |
+
const dir = val >= 0 ? 'right-0' : 'left-0';
|
| 470 |
+
return `
|
| 471 |
+
<div class="flex items-center gap-3">
|
| 472 |
+
<span class="font-label-caps text-[9px] w-24 text-on-surface-variant/80 truncate" title="${feat}">${feat.replace(/_/g, ' ').toUpperCase()}</span>
|
| 473 |
+
<div class="flex-1 h-2 bg-surface-variant rounded-sm relative">
|
| 474 |
+
<div class="absolute ${dir} top-0 h-full ${color} rounded-sm shap-bar" style="--target-width:${pct}%;width:${pct}%"></div>
|
| 475 |
+
</div>
|
| 476 |
+
<span class="font-label-caps text-[9px] ${textColor} w-12 text-right">${val >= 0 ? '+' : ''}${val.toFixed(3)}</span>
|
| 477 |
+
</div>
|
| 478 |
+
`;
|
| 479 |
+
}).join('');
|
| 480 |
+
} else {
|
| 481 |
+
shapEl.innerHTML = '<p class="text-on-surface-variant/60 italic text-[11px]">No SHAP data available</p>';
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
// Race Radio pane — LLM explanation
|
| 485 |
+
const radioEl = $('radio-content');
|
| 486 |
+
if (data.llm_explanation) {
|
| 487 |
+
const now = new Date();
|
| 488 |
+
const ts = `${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}:${now.getSeconds().toString().padStart(2,'0')}`;
|
| 489 |
+
// Split explanation into lines for radio-style formatting
|
| 490 |
+
const lines = data.llm_explanation.split(/\n+/).filter(l => l.trim());
|
| 491 |
+
radioEl.innerHTML = lines.map((line, i) => {
|
| 492 |
+
const tag = i === 0 ? '[SYNTHESIS]' : '[STRAT_AGENT]';
|
| 493 |
+
const tagColor = i === 0 ? 'text-secondary-container' : 'text-secondary-fixed-dim';
|
| 494 |
+
return `<div><span class="text-on-surface-variant">${ts}</span> <span class="${tagColor}">${tag}</span> ${line.trim()}</div>`;
|
| 495 |
+
}).join('');
|
| 496 |
+
} else {
|
| 497 |
+
radioEl.innerHTML = '<div><span class="text-on-surface-variant/50">—</span> No synthesis available</div>';
|
| 498 |
+
}
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
// --- Button Wiring ---
|
| 502 |
+
|
| 503 |
+
// "Run Prediction" buttons → scroll to terminal input
|
| 504 |
+
$('nav-run-btn').addEventListener('click', scrollToInput);
|
| 505 |
+
$('hero-run-btn').addEventListener('click', scrollToInput);
|
| 506 |
+
$('cta-launch-btn').addEventListener('click', scrollToInput);
|
| 507 |
+
|
| 508 |
+
// "View model proof" → scroll to features section
|
| 509 |
+
$('hero-proof-btn').addEventListener('click', () => {
|
| 510 |
+
document.querySelector('#section-features').scrollIntoView({ behavior: 'smooth' });
|
| 511 |
+
});
|
| 512 |
+
|
| 513 |
+
// "Read API docs" → open Swagger
|
| 514 |
+
$('cta-docs-btn').addEventListener('click', () => {
|
| 515 |
+
window.open(`${API_BASE}/docs`, '_blank');
|
| 516 |
+
});
|
| 517 |
+
|
| 518 |
+
// Terminal send button + Enter key
|
| 519 |
+
$('terminal-send').addEventListener('click', () => {
|
| 520 |
+
runPrediction($('terminal-input').value);
|
| 521 |
+
});
|
| 522 |
+
$('terminal-input').addEventListener('keydown', (e) => {
|
| 523 |
+
if (e.key === 'Enter') runPrediction($('terminal-input').value);
|
| 524 |
+
});
|
| 525 |
+
|
| 526 |
+
// Health indicator click → re-check
|
| 527 |
+
$('health-indicator').addEventListener('click', () => {
|
| 528 |
+
checkHealth();
|
| 529 |
+
showToast('Health check triggered', 'info');
|
| 530 |
+
});
|
| 531 |
+
|
| 532 |
+
// Footer links
|
| 533 |
+
$('footer-api-ref').addEventListener('click', () => window.open(`${API_BASE}/docs`, '_blank'));
|
| 534 |
+
$('footer-docs').addEventListener('click', () => window.open(`${API_BASE}/redoc`, '_blank'));
|
| 535 |
+
$('footer-status').addEventListener('click', async () => {
|
| 536 |
+
try {
|
| 537 |
+
const res = await fetch(`${API_BASE}/health`);
|
| 538 |
+
const data = await res.json();
|
| 539 |
+
showToast(`Status: ${data.status} | Model: ${data.model_loaded ? '✓' : '✗'} | Data: ${data.data_available ? '✓' : '✗'}`, data.status === 'healthy' ? 'success' : 'error');
|
| 540 |
+
} catch {
|
| 541 |
+
showToast('API unreachable', 'error');
|
| 542 |
+
}
|
| 543 |
+
});
|
| 544 |
+
|
| 545 |
+
// --- Init ---
|
| 546 |
+
checkHealth();
|
| 547 |
+
// Re-check health every 30s
|
| 548 |
+
setInterval(checkHealth, 30000);
|
| 549 |
+
</script>
|
| 550 |
+
</body></html>
|
frontend/eslint.config.mjs
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
import { defineConfig, globalIgnores } from "eslint/config";
|
| 2 |
-
import nextVitals from "eslint-config-next/core-web-vitals";
|
| 3 |
-
|
| 4 |
-
const eslintConfig = defineConfig([
|
| 5 |
-
...nextVitals,
|
| 6 |
-
// Override default ignores of eslint-config-next.
|
| 7 |
-
globalIgnores([
|
| 8 |
-
// Default ignores of eslint-config-next:
|
| 9 |
-
".next/**",
|
| 10 |
-
"out/**",
|
| 11 |
-
"build/**",
|
| 12 |
-
"next-env.d.ts",
|
| 13 |
-
]),
|
| 14 |
-
]);
|
| 15 |
-
|
| 16 |
-
export default eslintConfig;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/hooks/use-auto-resize-textarea.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
| 1 |
-
import { useEffect, useRef, useCallback } from "react";
|
| 2 |
-
|
| 3 |
-
interface UseAutoResizeTextareaProps {
|
| 4 |
-
minHeight: number;
|
| 5 |
-
maxHeight?: number;
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
export function useAutoResizeTextarea({
|
| 9 |
-
minHeight,
|
| 10 |
-
maxHeight,
|
| 11 |
-
}: UseAutoResizeTextareaProps) {
|
| 12 |
-
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
| 13 |
-
|
| 14 |
-
const adjustHeight = useCallback(
|
| 15 |
-
(reset?: boolean) => {
|
| 16 |
-
const textarea = textareaRef.current;
|
| 17 |
-
if (!textarea) return;
|
| 18 |
-
|
| 19 |
-
if (reset) {
|
| 20 |
-
textarea.style.height = `${minHeight}px`;
|
| 21 |
-
return;
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
// Temporarily shrink to get the right scrollHeight
|
| 25 |
-
textarea.style.height = `${minHeight}px`;
|
| 26 |
-
|
| 27 |
-
// Calculate new height
|
| 28 |
-
const newHeight = Math.max(
|
| 29 |
-
minHeight,
|
| 30 |
-
Math.min(
|
| 31 |
-
textarea.scrollHeight,
|
| 32 |
-
maxHeight ?? Number.POSITIVE_INFINITY
|
| 33 |
-
)
|
| 34 |
-
);
|
| 35 |
-
|
| 36 |
-
textarea.style.height = `${newHeight}px`;
|
| 37 |
-
},
|
| 38 |
-
[minHeight, maxHeight]
|
| 39 |
-
);
|
| 40 |
-
|
| 41 |
-
useEffect(() => {
|
| 42 |
-
// Set initial height
|
| 43 |
-
const textarea = textareaRef.current;
|
| 44 |
-
if (textarea) {
|
| 45 |
-
textarea.style.height = `${minHeight}px`;
|
| 46 |
-
}
|
| 47 |
-
}, [minHeight]);
|
| 48 |
-
|
| 49 |
-
// Adjust height on window resize
|
| 50 |
-
useEffect(() => {
|
| 51 |
-
const handleResize = () => adjustHeight();
|
| 52 |
-
window.addEventListener("resize", handleResize);
|
| 53 |
-
return () => window.removeEventListener("resize", handleResize);
|
| 54 |
-
}, [adjustHeight]);
|
| 55 |
-
|
| 56 |
-
return { textareaRef, adjustHeight };
|
| 57 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/index.html
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>Kronector | F1 Intelligence</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
margin: 0;
|
| 10 |
+
min-height: 100vh;
|
| 11 |
+
display: grid;
|
| 12 |
+
place-items: center;
|
| 13 |
+
background: #0a0a0a;
|
| 14 |
+
color: #f4f4f4;
|
| 15 |
+
font-family: 'Inter', Arial, sans-serif;
|
| 16 |
+
}
|
| 17 |
+
a { color: #00f2ff; }
|
| 18 |
+
</style>
|
| 19 |
+
<script>
|
| 20 |
+
// Auto-redirect based on viewport width
|
| 21 |
+
(function() {
|
| 22 |
+
var target = window.innerWidth <= 768 ? 'mobile.html' : 'desktop.html';
|
| 23 |
+
window.location.replace(target);
|
| 24 |
+
})();
|
| 25 |
+
</script>
|
| 26 |
+
</head>
|
| 27 |
+
<body>
|
| 28 |
+
<main>
|
| 29 |
+
<p>Opening Kronector...</p>
|
| 30 |
+
<p><a href="desktop.html">Desktop</a> · <a href="mobile.html">Mobile</a></p>
|
| 31 |
+
</main>
|
| 32 |
+
</body>
|
| 33 |
+
</html>
|
frontend/jsconfig.json
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"compilerOptions": {
|
| 3 |
-
"paths": {
|
| 4 |
-
"@/*": ["./*"]
|
| 5 |
-
}
|
| 6 |
-
}
|
| 7 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/lib/utils.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
import { clsx, type ClassValue } from "clsx"
|
| 2 |
-
import { twMerge } from "tailwind-merge"
|
| 3 |
-
|
| 4 |
-
export function cn(...inputs: ClassValue[]) {
|
| 5 |
-
return twMerge(clsx(inputs))
|
| 6 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/mobile.html
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
|
| 3 |
+
<html class="dark" lang="en"><head>
|
| 4 |
+
<meta charset="utf-8"/>
|
| 5 |
+
<meta content="width=device-width, initial-scale=1.0, viewport-fit=cover" name="viewport"/>
|
| 6 |
+
<title>KRONECTOR | F1 Intelligence</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"/>
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
| 11 |
+
<script id="tailwind-config">
|
| 12 |
+
tailwind.config = {
|
| 13 |
+
darkMode: "class",
|
| 14 |
+
theme: {
|
| 15 |
+
extend: {
|
| 16 |
+
"colors": {
|
| 17 |
+
"surface": "#131313",
|
| 18 |
+
"primary": "#ffb4a7",
|
| 19 |
+
"on-surface-variant": "#eabcb4",
|
| 20 |
+
"on-primary-container": "#5b0300",
|
| 21 |
+
"secondary-fixed": "#74f5ff",
|
| 22 |
+
"primary-fixed-dim": "#ffb4a7",
|
| 23 |
+
"surface-tint": "#ffb4a7",
|
| 24 |
+
"secondary": "#ddfcff",
|
| 25 |
+
"secondary-fixed-dim": "#00dbe7",
|
| 26 |
+
"on-tertiary-container": "#282a2a",
|
| 27 |
+
"on-surface": "#e5e2e1",
|
| 28 |
+
"surface-container-lowest": "#0e0e0e",
|
| 29 |
+
"on-secondary-container": "#006a70",
|
| 30 |
+
"inverse-primary": "#be0e00",
|
| 31 |
+
"on-tertiary-fixed-variant": "#454747",
|
| 32 |
+
"on-secondary-fixed": "#002022",
|
| 33 |
+
"background": "#131313",
|
| 34 |
+
"secondary-container": "#00f1fe",
|
| 35 |
+
"on-primary-fixed": "#400200",
|
| 36 |
+
"surface-container-high": "#2a2a2a",
|
| 37 |
+
"surface-container-highest": "#353534",
|
| 38 |
+
"tertiary-fixed-dim": "#c6c6c7",
|
| 39 |
+
"primary-container": "#ff553d",
|
| 40 |
+
"on-background": "#e5e2e1",
|
| 41 |
+
"surface-container": "#201f1f",
|
| 42 |
+
"error": "#ffb4ab",
|
| 43 |
+
"surface-dim": "#131313",
|
| 44 |
+
"surface-bright": "#3a3939",
|
| 45 |
+
"on-primary": "#670400",
|
| 46 |
+
"on-secondary-fixed-variant": "#004f54",
|
| 47 |
+
"tertiary-fixed": "#e2e2e2",
|
| 48 |
+
"tertiary": "#c6c6c7",
|
| 49 |
+
"surface-variant": "#353534",
|
| 50 |
+
"on-tertiary-fixed": "#1a1c1c",
|
| 51 |
+
"on-primary-fixed-variant": "#910900",
|
| 52 |
+
"surface-container-low": "#1c1b1b",
|
| 53 |
+
"primary-fixed": "#ffdad4",
|
| 54 |
+
"outline-variant": "#5f3e39",
|
| 55 |
+
"error-container": "#93000a",
|
| 56 |
+
"tertiary-container": "#909191",
|
| 57 |
+
"on-tertiary": "#2f3131",
|
| 58 |
+
"inverse-surface": "#e5e2e1",
|
| 59 |
+
"inverse-on-surface": "#313030",
|
| 60 |
+
"outline": "#b08780",
|
| 61 |
+
"on-error": "#690005",
|
| 62 |
+
"on-error-container": "#ffdad6",
|
| 63 |
+
"on-secondary": "#00363a",
|
| 64 |
+
"race-red": "#ff1801",
|
| 65 |
+
"neon-cyan": "#00dbe7"
|
| 66 |
+
},
|
| 67 |
+
"borderRadius": {
|
| 68 |
+
"DEFAULT": "0.25rem",
|
| 69 |
+
"lg": "0.5rem",
|
| 70 |
+
"xl": "0.75rem",
|
| 71 |
+
"full": "9999px"
|
| 72 |
+
},
|
| 73 |
+
"spacing": {
|
| 74 |
+
"container-max": "1440px",
|
| 75 |
+
"unit": "4px",
|
| 76 |
+
"margin-desktop": "32px",
|
| 77 |
+
"margin-mobile": "16px",
|
| 78 |
+
"gutter": "16px"
|
| 79 |
+
},
|
| 80 |
+
"fontFamily": {
|
| 81 |
+
"headline-md": ["Inter"],
|
| 82 |
+
"label-caps": ["JetBrains Mono"],
|
| 83 |
+
"display-lg-mobile": ["Inter"],
|
| 84 |
+
"body-fixed": ["JetBrains Mono"],
|
| 85 |
+
"display-lg": ["Inter"],
|
| 86 |
+
"body-main": ["Inter"]
|
| 87 |
+
},
|
| 88 |
+
"fontSize": {
|
| 89 |
+
"headline-md": ["24px", {"lineHeight": "1.3", "letterSpacing": "-0.01em", "fontWeight": "700"}],
|
| 90 |
+
"label-caps": ["11px", {"lineHeight": "1", "letterSpacing": "0.1em", "fontWeight": "700"}],
|
| 91 |
+
"display-lg-mobile": ["32px", {"lineHeight": "1.2", "letterSpacing": "-0.02em", "fontWeight": "800"}],
|
| 92 |
+
"body-fixed": ["14px", {"lineHeight": "1.5", "letterSpacing": "0em", "fontWeight": "400"}],
|
| 93 |
+
"display-lg": ["48px", {"lineHeight": "1.1", "letterSpacing": "-0.04em", "fontWeight": "800"}],
|
| 94 |
+
"body-main": ["16px", {"lineHeight": "1.6", "letterSpacing": "0em", "fontWeight": "400"}]
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
},
|
| 98 |
+
}
|
| 99 |
+
</script>
|
| 100 |
+
<style>
|
| 101 |
+
body { background-color: #0A0A0A; color: #e5e2e1; }
|
| 102 |
+
.carbon-border { border: 1px solid #2D2D2D; }
|
| 103 |
+
.metric-accent-red { border-left: 2px solid #ff1801; }
|
| 104 |
+
.metric-accent-cyan { border-left: 2px solid #00dbe7; }
|
| 105 |
+
.terminal-header { background: linear-gradient(to bottom, #1A1A1A, #111111); }
|
| 106 |
+
.zebra-list div:nth-child(even) { background-color: #151515; }
|
| 107 |
+
.pulse-node { animation: pulse 2s infinite; }
|
| 108 |
+
@keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
|
| 109 |
+
</style>
|
| 110 |
+
<style>
|
| 111 |
+
body {
|
| 112 |
+
min-height: max(884px, 100dvh);
|
| 113 |
+
}
|
| 114 |
+
</style>
|
| 115 |
+
</head>
|
| 116 |
+
<body class="font-body-main selection:bg-race-red selection:text-white overflow-x-hidden">
|
| 117 |
+
<!-- TopAppBar -->
|
| 118 |
+
<header class="fixed top-0 w-full z-50 backdrop-blur-md bg-surface/80 border-b border-outline-variant flex justify-between items-center px-margin-mobile h-14 w-full">
|
| 119 |
+
<div class="flex items-center gap-3">
|
| 120 |
+
<span class="material-symbols-outlined text-primary" data-icon="menu">menu</span>
|
| 121 |
+
<span class="font-headline-md text-headline-md-mobile font-black tracking-tighter text-primary">KRONECTOR</span>
|
| 122 |
+
</div>
|
| 123 |
+
<div class="w-8 h-8 rounded-full overflow-hidden border border-outline-variant">
|
| 124 |
+
<img alt="Engineer Profile" class="w-full h-full object-cover" data-alt="A professional headshot of a technical Formula 1 race engineer wearing high-end headphones in a dark, data-driven pit wall environment. The lighting is dominated by glowing cyan telemetry screens reflecting off their glasses, creating a sharp, high-performance motorsport atmosphere with a focus on deep blacks and vibrant technical accents." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDdxk5F6y5JOwsj3vJFZHGg2ZIMdMP5AxXvxwGJhvFasyBsskV0kwijwXRDP9uIJGM4j_RLw-gxyg_3JVGPtd6ajb3-RhxilueEqcohiFz4dlFkeudrBbfZSsCrIHVyym6UUDf-9fYc4f1xuCRZD5mzdJKRwrJW128xlvWZaJ5Gc_-oM8ZsFJR0iedjp17YpWlMghL-7gr8sIKG6dZc8KknBJG3ZkiLE37hUyoCjHcRAAl6ZSJne9nMJADed0fFMCrTlk6bGdUsSqi8"/>
|
| 125 |
+
</div>
|
| 126 |
+
</header>
|
| 127 |
+
<main class="pt-20 pb-24 px-margin-mobile flex flex-col gap-10">
|
| 128 |
+
<!-- Hero Section -->
|
| 129 |
+
<section id="m-section-hero" class="flex flex-col gap-6">
|
| 130 |
+
<div class="space-y-4">
|
| 131 |
+
<h1 class="font-display-lg-mobile text-display-lg-mobile leading-tight">
|
| 132 |
+
Predict the race before <span class="text-race-red">lights out</span>
|
| 133 |
+
</h1>
|
| 134 |
+
<p class="font-body-main text-on-surface-variant leading-relaxed">
|
| 135 |
+
Kronector turns Formula 1 telemetry, qualifying pace, driver form, and model explainability into race-winning predictions before the grid forms.
|
| 136 |
+
</p>
|
| 137 |
+
</div>
|
| 138 |
+
<div class="flex flex-col gap-3">
|
| 139 |
+
<button id="m-hero-run-btn" class="bg-race-red text-white font-headline-md py-4 rounded-lg flex items-center justify-center gap-2 active:scale-95 transition-transform">
|
| 140 |
+
Run prediction
|
| 141 |
+
</button>
|
| 142 |
+
<button id="m-hero-proof-btn" class="border border-neon-cyan text-neon-cyan font-label-caps py-4 rounded-lg flex items-center justify-center gap-2 uppercase tracking-widest active:scale-95 transition-transform">
|
| 143 |
+
View model proof
|
| 144 |
+
</button>
|
| 145 |
+
</div>
|
| 146 |
+
</section>
|
| 147 |
+
<!-- Prediction Terminal -->
|
| 148 |
+
<section id="m-section-terminal" class="carbon-border rounded-lg bg-[#111111] overflow-hidden flex flex-col scroll-mt-20">
|
| 149 |
+
<div class="terminal-header p-3 border-b border-[#2D2D2D] flex justify-between items-center">
|
| 150 |
+
<div class="flex gap-1.5">
|
| 151 |
+
<div class="w-2.5 h-2.5 rounded-full bg-[#333]"></div>
|
| 152 |
+
<div class="w-2.5 h-2.5 rounded-full bg-[#333]"></div>
|
| 153 |
+
<div class="w-2.5 h-2.5 rounded-full bg-[#333]"></div>
|
| 154 |
+
</div>
|
| 155 |
+
<span id="m-terminal-label" class="font-label-caps text-[10px] text-on-surface-variant/60 uppercase">READY FOR QUERY</span>
|
| 156 |
+
</div>
|
| 157 |
+
<!-- Conditions Bar -->
|
| 158 |
+
<div id="m-conditions-bar" class="grid grid-cols-3 divide-x divide-[#2D2D2D] border-b border-[#2D2D2D]">
|
| 159 |
+
<div class="p-3 flex flex-col gap-1">
|
| 160 |
+
<span class="font-label-caps text-[9px] text-on-surface-variant/50">DRIVER</span>
|
| 161 |
+
<span id="m-cond-driver" class="font-body-fixed text-neon-cyan">—</span>
|
| 162 |
+
</div>
|
| 163 |
+
<div class="p-3 flex flex-col gap-1">
|
| 164 |
+
<span class="font-label-caps text-[9px] text-on-surface-variant/50">TEAM</span>
|
| 165 |
+
<span id="m-cond-team" class="font-body-fixed text-on-surface">—</span>
|
| 166 |
+
</div>
|
| 167 |
+
<div class="p-3 flex flex-col gap-1">
|
| 168 |
+
<span class="font-label-caps text-[9px] text-on-surface-variant/50">WIN %</span>
|
| 169 |
+
<span id="m-cond-prob" class="font-body-fixed text-race-red">—</span>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
<!-- Driver List -->
|
| 173 |
+
<div id="m-driver-list" class="zebra-list flex flex-col">
|
| 174 |
+
<div class="p-4 flex items-center justify-center">
|
| 175 |
+
<p class="font-body-fixed text-[12px] text-on-surface-variant/60 italic">Submit a query to see predictions...</p>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
<!-- SHAP Bars -->
|
| 179 |
+
<div id="m-shap-pane" class="p-4 bg-[#0D0D0D] border-t border-[#2D2D2D] flex flex-col gap-3">
|
| 180 |
+
<span class="font-label-caps text-[10px] text-on-surface-variant/60">SHAP FEATURE INFLUENCE</span>
|
| 181 |
+
<div id="m-shap-content" class="space-y-2">
|
| 182 |
+
<p class="font-body-fixed text-[11px] text-on-surface-variant/50 italic">Awaiting prediction...</p>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
<!-- Radio Feed -->
|
| 186 |
+
<div id="m-radio-pane" class="p-4 bg-[#080808] border-t border-[#2D2D2D]">
|
| 187 |
+
<div class="flex items-center gap-2 mb-2">
|
| 188 |
+
<span class="material-symbols-outlined text-[14px] text-race-red" data-icon="settings_input_antenna">settings_input_antenna</span>
|
| 189 |
+
<span class="font-label-caps text-[10px] text-race-red">LATEST SYNTHESIS</span>
|
| 190 |
+
</div>
|
| 191 |
+
<p id="m-radio-content" class="font-body-fixed text-[12px] text-on-surface/90 italic leading-relaxed">
|
| 192 |
+
Waiting for agentic synthesis...
|
| 193 |
+
</p>
|
| 194 |
+
</div>
|
| 195 |
+
<!-- Input Bar -->
|
| 196 |
+
<div class="p-3 bg-[#1A1A1A] border-t border-[#2D2D2D] flex gap-2 items-center">
|
| 197 |
+
<input id="m-terminal-input" class="flex-1 bg-black border border-[#2D2D2D] rounded-lg px-3 py-2 text-xs font-body-fixed focus:border-neon-cyan focus:ring-0" placeholder="Who wins the 2026 Canadian GP?" type="text" autocomplete="off"/>
|
| 198 |
+
<button id="m-terminal-send" class="w-8 h-8 flex items-center justify-center bg-race-red rounded-lg">
|
| 199 |
+
<span class="material-symbols-outlined text-white text-sm" data-icon="send">send</span>
|
| 200 |
+
</button>
|
| 201 |
+
</div>
|
| 202 |
+
</section>
|
| 203 |
+
<!-- Metrics Grid -->
|
| 204 |
+
<section id="m-section-metrics" class="grid grid-cols-2 gap-4 scroll-mt-20">
|
| 205 |
+
<div class="carbon-border rounded-lg p-4 metric-accent-cyan flex flex-col gap-1">
|
| 206 |
+
<span class="font-body-fixed font-bold text-lg">71%</span>
|
| 207 |
+
<span class="font-label-caps text-[9px] text-on-surface-variant/60 uppercase">Winner Accuracy</span>
|
| 208 |
+
</div>
|
| 209 |
+
<div class="carbon-border rounded-lg p-4 metric-accent-red flex flex-col gap-1">
|
| 210 |
+
<span class="font-body-fixed font-bold text-lg">0.94</span>
|
| 211 |
+
<span class="font-label-caps text-[9px] text-on-surface-variant/60 uppercase">ROC AUC</span>
|
| 212 |
+
</div>
|
| 213 |
+
<div class="carbon-border rounded-lg p-4 metric-accent-cyan flex flex-col gap-1">
|
| 214 |
+
<span class="font-body-fixed font-bold text-lg">4-Agent</span>
|
| 215 |
+
<span class="font-label-caps text-[9px] text-on-surface-variant/60 uppercase">Pipeline</span>
|
| 216 |
+
</div>
|
| 217 |
+
<div class="carbon-border rounded-lg p-4 metric-accent-red flex flex-col gap-1">
|
| 218 |
+
<span class="font-body-fixed font-bold text-lg">< 1s</span>
|
| 219 |
+
<span class="font-label-caps text-[9px] text-on-surface-variant/60 uppercase">Inference</span>
|
| 220 |
+
</div>
|
| 221 |
+
</section>
|
| 222 |
+
<!-- Technical Capabilities -->
|
| 223 |
+
<section id="m-section-capabilities" class="flex flex-col gap-4 scroll-mt-20">
|
| 224 |
+
<h2 class="font-label-caps text-on-surface-variant/50 uppercase tracking-widest text-[10px]">Technical Core</h2>
|
| 225 |
+
<div class="space-y-3">
|
| 226 |
+
<div class="carbon-border rounded-lg p-5 flex gap-4 items-start">
|
| 227 |
+
<span class="material-symbols-outlined text-race-red" data-icon="query_stats">query_stats</span>
|
| 228 |
+
<div class="space-y-1">
|
| 229 |
+
<h3 class="font-headline-md text-sm text-on-surface">Race Prediction</h3>
|
| 230 |
+
<p class="font-body-fixed text-xs text-on-surface-variant leading-normal">XGBoost & LSTM hybrid ensemble trained on 20 years of historical race data and live lap telemetry.</p>
|
| 231 |
+
</div>
|
| 232 |
+
</div>
|
| 233 |
+
<div class="carbon-border rounded-lg p-5 flex gap-4 items-start">
|
| 234 |
+
<span class="material-symbols-outlined text-neon-cyan" data-icon="explain">expand</span>
|
| 235 |
+
<div class="space-y-1">
|
| 236 |
+
<h3 class="font-headline-md text-sm text-on-surface">SHAP Explainability</h3>
|
| 237 |
+
<p class="font-body-fixed text-xs text-on-surface-variant leading-normal">Deep-dive into every prediction. Understand exactly how tire age or air pressure influenced the model's output.</p>
|
| 238 |
+
</div>
|
| 239 |
+
</div>
|
| 240 |
+
<div class="carbon-border rounded-lg p-5 flex gap-4 items-start">
|
| 241 |
+
<span class="material-symbols-outlined text-race-red" data-icon="monitoring">monitoring</span>
|
| 242 |
+
<div class="space-y-1">
|
| 243 |
+
<h3 class="font-headline-md text-sm text-on-surface">Drift Monitoring</h3>
|
| 244 |
+
<p class="font-body-fixed text-xs text-on-surface-variant leading-normal">Real-time detection of concept drift as track conditions evolve beyond the initial training baseline.</p>
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
<div class="carbon-border rounded-lg p-5 flex gap-4 items-start">
|
| 248 |
+
<span class="material-symbols-outlined text-neon-cyan" data-icon="speaker_group">speaker_group</span>
|
| 249 |
+
<div class="space-y-1">
|
| 250 |
+
<h3 class="font-headline-md text-sm text-on-surface">Agentic Race Radio</h3>
|
| 251 |
+
<p class="font-body-fixed text-xs text-on-surface-variant leading-normal">LLM-powered reasoning agents synthesize raw model outputs into strategic audio/text directives.</p>
|
| 252 |
+
</div>
|
| 253 |
+
</div>
|
| 254 |
+
</div>
|
| 255 |
+
</section>
|
| 256 |
+
<!-- Architecture Flow -->
|
| 257 |
+
<section id="m-section-architecture" class="flex flex-col gap-6 py-4 scroll-mt-20">
|
| 258 |
+
<h2 class="font-label-caps text-on-surface-variant/50 uppercase tracking-widest text-[10px] text-center">Pipeline Architecture</h2>
|
| 259 |
+
<div class="relative flex flex-col items-center gap-12">
|
| 260 |
+
<!-- Vertical Line Background -->
|
| 261 |
+
<div class="absolute h-full w-[1.5px] bg-gradient-to-b from-race-red via-neon-cyan to-race-red opacity-30 z-0"></div>
|
| 262 |
+
<!-- Node 1 -->
|
| 263 |
+
<div class="z-10 bg-surface carbon-border p-4 rounded-lg w-full flex items-center justify-between">
|
| 264 |
+
<div class="flex items-center gap-3">
|
| 265 |
+
<div class="w-2 h-2 rounded-full bg-neon-cyan pulse-node"></div>
|
| 266 |
+
<span class="font-label-caps text-xs">FASTF1 + JOLPICA</span>
|
| 267 |
+
</div>
|
| 268 |
+
<span class="bg-on-secondary-fixed-variant text-secondary-fixed text-[9px] px-2 py-0.5 rounded font-label-caps">INGESTING</span>
|
| 269 |
+
</div>
|
| 270 |
+
<!-- Node 2 -->
|
| 271 |
+
<div class="z-10 bg-surface carbon-border p-4 rounded-lg w-full flex items-center justify-between">
|
| 272 |
+
<div class="flex items-center gap-3">
|
| 273 |
+
<div class="w-2 h-2 rounded-full bg-white/20"></div>
|
| 274 |
+
<span class="font-label-caps text-xs">25+ FEATURE VECTOR</span>
|
| 275 |
+
</div>
|
| 276 |
+
<span class="bg-surface-container-highest text-on-surface-variant text-[9px] px-2 py-0.5 rounded font-label-caps">NORMALIZED</span>
|
| 277 |
+
</div>
|
| 278 |
+
<!-- Node 3 -->
|
| 279 |
+
<div class="z-10 bg-surface carbon-border p-4 rounded-lg w-full flex items-center justify-between">
|
| 280 |
+
<div class="flex items-center gap-3">
|
| 281 |
+
<div class="w-2 h-2 rounded-full bg-race-red pulse-node"></div>
|
| 282 |
+
<span class="font-label-caps text-xs">ENSEMBLE BACKBONE</span>
|
| 283 |
+
</div>
|
| 284 |
+
<span class="bg-error-container text-error text-[9px] px-2 py-0.5 rounded font-label-caps">TRAINED</span>
|
| 285 |
+
</div>
|
| 286 |
+
<!-- Node 4 -->
|
| 287 |
+
<div class="z-10 bg-surface carbon-border p-4 rounded-lg w-full flex items-center justify-between">
|
| 288 |
+
<div class="flex items-center gap-3">
|
| 289 |
+
<div class="w-2 h-2 rounded-full bg-neon-cyan"></div>
|
| 290 |
+
<span class="font-label-caps text-xs">TRITON INFERENCE</span>
|
| 291 |
+
</div>
|
| 292 |
+
<span class="bg-on-secondary-container text-secondary-container text-[9px] px-2 py-0.5 rounded font-label-caps">SERVING</span>
|
| 293 |
+
</div>
|
| 294 |
+
<!-- Node 5 -->
|
| 295 |
+
<div class="z-10 bg-surface carbon-border p-4 rounded-lg w-full flex items-center justify-between">
|
| 296 |
+
<div class="flex items-center gap-3">
|
| 297 |
+
<div class="w-2 h-2 rounded-full bg-white/50"></div>
|
| 298 |
+
<span class="font-label-caps text-xs">LLM PIT-WALL</span>
|
| 299 |
+
</div>
|
| 300 |
+
<span class="bg-surface-container-high text-on-surface text-[9px] px-2 py-0.5 rounded font-label-caps">SYNTHESIZED</span>
|
| 301 |
+
</div>
|
| 302 |
+
</div>
|
| 303 |
+
</section>
|
| 304 |
+
<!-- Final CTA -->
|
| 305 |
+
<section id="m-section-cta" class="bg-surface-container-low rounded-lg p-8 text-center flex flex-col gap-6 border border-outline-variant scroll-mt-20">
|
| 306 |
+
<div class="space-y-2">
|
| 307 |
+
<h2 class="font-headline-md text-display-lg-mobile">Ask the pit wall before race day</h2>
|
| 308 |
+
<p class="font-body-main text-on-surface-variant text-sm">Deploy the most advanced prediction suite in open-source motorsport intelligence.</p>
|
| 309 |
+
</div>
|
| 310 |
+
<div class="flex flex-col gap-3">
|
| 311 |
+
<button id="m-cta-launch-btn" class="bg-race-red text-white font-headline-md py-4 rounded-lg active:scale-95 transition-transform">Launch predictor</button>
|
| 312 |
+
<button id="m-cta-docs-btn" class="text-neon-cyan font-label-caps py-2 hover:underline">Read API docs</button>
|
| 313 |
+
</div>
|
| 314 |
+
</section>
|
| 315 |
+
</main>
|
| 316 |
+
<!-- Footer -->
|
| 317 |
+
<footer class="w-full py-8 mb-16 bg-surface-container-low border-t border-outline-variant flex flex-col items-center gap-4 px-margin-mobile text-center">
|
| 318 |
+
<span class="font-label-caps text-label-caps text-primary">KRONECTOR INTEL</span>
|
| 319 |
+
<div class="flex gap-6">
|
| 320 |
+
<a class="text-on-tertiary-container hover:text-on-surface font-body-fixed text-[11px] uppercase tracking-tighter" href="#">Terms</a>
|
| 321 |
+
<a class="text-on-tertiary-container hover:text-on-surface font-body-fixed text-[11px] uppercase tracking-tighter" href="#">Privacy</a>
|
| 322 |
+
<a id="m-footer-status" class="text-on-tertiary-container hover:text-on-surface font-body-fixed text-[11px] uppercase tracking-tighter cursor-pointer">API_Status</a>
|
| 323 |
+
<a id="m-footer-docs" class="text-on-tertiary-container hover:text-on-surface font-body-fixed text-[11px] uppercase tracking-tighter cursor-pointer">System_Log</a>
|
| 324 |
+
</div>
|
| 325 |
+
<p class="font-body-fixed text-[10px] text-on-surface-variant opacity-60">© 2024 KRONECTOR INTEL. ALL SYSTEMS NOMINAL.</p>
|
| 326 |
+
</footer>
|
| 327 |
+
<!-- BottomNavBar -->
|
| 328 |
+
<nav class="fixed bottom-0 w-full z-50 backdrop-blur-lg bg-surface/90 border-t border-outline-variant flex justify-around items-center h-16 px-4 pb-safe md:hidden">
|
| 329 |
+
<div id="m-nav-dashboard" class="flex flex-col items-center justify-center text-secondary-fixed-dim bg-secondary-container/20 rounded-xl p-2 active:scale-90 duration-150 cursor-pointer">
|
| 330 |
+
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
|
| 331 |
+
</div>
|
| 332 |
+
<div id="m-nav-analytics" class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary transition-colors active:scale-90 duration-150 cursor-pointer">
|
| 333 |
+
<span class="material-symbols-outlined" data-icon="analytics">analytics</span>
|
| 334 |
+
</div>
|
| 335 |
+
<div id="m-nav-radio" class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary transition-colors active:scale-90 duration-150 cursor-pointer">
|
| 336 |
+
<span class="material-symbols-outlined" data-icon="vibration">vibration</span>
|
| 337 |
+
</div>
|
| 338 |
+
<div id="m-nav-api" class="flex flex-col items-center justify-center text-on-surface-variant p-2 hover:text-primary transition-colors active:scale-90 duration-150 cursor-pointer">
|
| 339 |
+
<span class="material-symbols-outlined" data-icon="account_circle">account_circle</span>
|
| 340 |
+
</div>
|
| 341 |
+
</nav>
|
| 342 |
+
<!-- Toast Notification -->
|
| 343 |
+
<div id="m-toast" class="fixed bottom-20 left-4 right-4 z-[100] px-4 py-3 rounded-lg font-body-fixed text-[12px] shadow-2xl border transition-all duration-300 opacity-0 translate-y-4 pointer-events-none text-center" style="background:#201f1f;border-color:#5f3e39;color:#e5e2e1;"></div>
|
| 344 |
+
|
| 345 |
+
<script>
|
| 346 |
+
// ============================================================================
|
| 347 |
+
// KRONECTOR Mobile — API Integration
|
| 348 |
+
// ============================================================================
|
| 349 |
+
|
| 350 |
+
const API_BASE = 'http://localhost:8000';
|
| 351 |
+
|
| 352 |
+
// --- Utility ---
|
| 353 |
+
function $(id) { return document.getElementById(id); }
|
| 354 |
+
|
| 355 |
+
function showToast(msg, type = 'info') {
|
| 356 |
+
const t = $('m-toast');
|
| 357 |
+
t.textContent = msg;
|
| 358 |
+
t.style.borderColor = type === 'error' ? '#93000a' : type === 'success' ? '#00dbe7' : '#5f3e39';
|
| 359 |
+
t.classList.remove('opacity-0', 'translate-y-4', 'pointer-events-none');
|
| 360 |
+
t.classList.add('opacity-100', 'translate-y-0');
|
| 361 |
+
clearTimeout(t._timer);
|
| 362 |
+
t._timer = setTimeout(() => {
|
| 363 |
+
t.classList.add('opacity-0', 'translate-y-4', 'pointer-events-none');
|
| 364 |
+
t.classList.remove('opacity-100', 'translate-y-0');
|
| 365 |
+
}, 4000);
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
function scrollToInput() {
|
| 369 |
+
const input = $('m-terminal-input');
|
| 370 |
+
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 371 |
+
setTimeout(() => input.focus(), 400);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
// --- Prediction API ---
|
| 375 |
+
let _isLoading = false;
|
| 376 |
+
|
| 377 |
+
async function runPrediction(query) {
|
| 378 |
+
if (_isLoading) return;
|
| 379 |
+
if (!query || query.trim().length < 3) {
|
| 380 |
+
showToast('Query too short — try a full question', 'error');
|
| 381 |
+
return;
|
| 382 |
+
}
|
| 383 |
+
_isLoading = true;
|
| 384 |
+
$('m-terminal-send').disabled = true;
|
| 385 |
+
|
| 386 |
+
// Loading state
|
| 387 |
+
$('m-terminal-label').textContent = 'PROCESSING...';
|
| 388 |
+
$('m-cond-driver').textContent = '...';
|
| 389 |
+
$('m-cond-team').textContent = '...';
|
| 390 |
+
$('m-cond-prob').textContent = '...';
|
| 391 |
+
$('m-driver-list').innerHTML = '<div class="p-4 flex items-center justify-center"><p class="font-body-fixed text-[12px] text-neon-cyan animate-pulse">⏳ Querying pipeline...</p></div>';
|
| 392 |
+
$('m-shap-content').innerHTML = '<p class="font-body-fixed text-[11px] text-on-surface-variant/50 animate-pulse">Computing...</p>';
|
| 393 |
+
$('m-radio-content').textContent = 'Synthesizing response...';
|
| 394 |
+
|
| 395 |
+
try {
|
| 396 |
+
const res = await fetch(`${API_BASE}/predict/f1`, {
|
| 397 |
+
method: 'POST',
|
| 398 |
+
headers: { 'Content-Type': 'application/json' },
|
| 399 |
+
body: JSON.stringify({ query: query.trim() })
|
| 400 |
+
});
|
| 401 |
+
|
| 402 |
+
if (!res.ok) {
|
| 403 |
+
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
| 404 |
+
throw new Error(err.detail || `API Error ${res.status}`);
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
const data = await res.json();
|
| 408 |
+
renderMobilePrediction(data);
|
| 409 |
+
showToast('Prediction complete ✓', 'success');
|
| 410 |
+
} catch (err) {
|
| 411 |
+
$('m-terminal-label').textContent = 'ERROR';
|
| 412 |
+
$('m-driver-list').innerHTML = `<div class="p-4"><p class="font-body-fixed text-[12px] text-race-red">❌ ${err.message}</p></div>`;
|
| 413 |
+
$('m-shap-content').innerHTML = '<p class="text-race-red text-[11px]">Error</p>';
|
| 414 |
+
$('m-radio-content').textContent = err.message;
|
| 415 |
+
showToast(err.message, 'error');
|
| 416 |
+
} finally {
|
| 417 |
+
_isLoading = false;
|
| 418 |
+
$('m-terminal-send').disabled = false;
|
| 419 |
+
}
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
function renderMobilePrediction(data) {
|
| 423 |
+
const m = data.metadata;
|
| 424 |
+
|
| 425 |
+
// Header label
|
| 426 |
+
$('m-terminal-label').textContent = `${m.season} R${m.round} // ${m.driver_name.toUpperCase()}`;
|
| 427 |
+
|
| 428 |
+
// Conditions bar
|
| 429 |
+
$('m-cond-driver').textContent = m.driver_name;
|
| 430 |
+
$('m-cond-team').textContent = m.team;
|
| 431 |
+
$('m-cond-prob').textContent = `${data.win_probability}%`;
|
| 432 |
+
|
| 433 |
+
// Driver card in list
|
| 434 |
+
const prob = data.win_probability;
|
| 435 |
+
const barColor = prob > 40 ? 'bg-race-red' : prob > 15 ? 'bg-neon-cyan' : 'bg-white/40';
|
| 436 |
+
const textColor = prob > 40 ? 'text-race-red' : prob > 15 ? 'text-neon-cyan' : 'text-on-surface';
|
| 437 |
+
const borderColor = prob > 40 ? 'border-race-red' : prob > 15 ? 'border-neon-cyan' : 'border-[#555]';
|
| 438 |
+
|
| 439 |
+
$('m-driver-list').innerHTML = `
|
| 440 |
+
<div class="p-3 flex justify-between items-center border-l-2 ${borderColor}">
|
| 441 |
+
<div class="flex flex-col">
|
| 442 |
+
<span class="font-label-caps text-on-surface">${m.driver_name.toUpperCase()}</span>
|
| 443 |
+
<span class="font-label-caps text-[10px] text-on-surface-variant/50">${m.team.toUpperCase()}</span>
|
| 444 |
+
</div>
|
| 445 |
+
<div class="text-right">
|
| 446 |
+
<span class="font-body-fixed font-bold ${textColor} text-lg">${prob}%</span>
|
| 447 |
+
<div class="h-1 w-24 bg-[#2D2D2D] rounded-full mt-1 overflow-hidden">
|
| 448 |
+
<div class="h-full ${barColor}" style="width:${Math.min(prob, 100)}%"></div>
|
| 449 |
+
</div>
|
| 450 |
+
</div>
|
| 451 |
+
</div>
|
| 452 |
+
<div class="p-3 flex justify-between items-center text-on-surface-variant/60 border-l-2 border-[#333]">
|
| 453 |
+
<span class="font-label-caps text-[10px]">GRID POSITION</span>
|
| 454 |
+
<span class="font-body-fixed">P${m.grid_position}</span>
|
| 455 |
+
</div>
|
| 456 |
+
${data.confidence_rating ? `
|
| 457 |
+
<div class="p-3 flex justify-between items-center text-on-surface-variant/60 border-l-2 border-[#333]">
|
| 458 |
+
<span class="font-label-caps text-[10px]">CONFIDENCE</span>
|
| 459 |
+
<span class="font-body-fixed text-neon-cyan">${data.confidence_rating}</span>
|
| 460 |
+
</div>` : ''}
|
| 461 |
+
`;
|
| 462 |
+
|
| 463 |
+
// SHAP bars
|
| 464 |
+
const shapEl = $('m-shap-content');
|
| 465 |
+
if (data.shap_values && Object.keys(data.shap_values).length > 0) {
|
| 466 |
+
const entries = Object.entries(data.shap_values)
|
| 467 |
+
.map(([k, v]) => [k, parseFloat(v)])
|
| 468 |
+
.sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]))
|
| 469 |
+
.slice(0, 4);
|
| 470 |
+
const maxAbs = Math.max(...entries.map(e => Math.abs(e[1])), 0.001);
|
| 471 |
+
|
| 472 |
+
shapEl.innerHTML = entries.map(([feat, val]) => {
|
| 473 |
+
const pct = Math.min(Math.abs(val) / maxAbs * 100, 100).toFixed(0);
|
| 474 |
+
const isPositive = val >= 0;
|
| 475 |
+
const barBg = isPositive ? 'bg-race-red/20' : 'bg-neon-cyan/20';
|
| 476 |
+
const barFill = isPositive ? 'bg-race-red' : 'bg-neon-cyan';
|
| 477 |
+
const textCol = isPositive ? 'text-race-red' : 'text-neon-cyan';
|
| 478 |
+
const dir = isPositive ? 'right-0' : 'left-0';
|
| 479 |
+
return `
|
| 480 |
+
<div class="flex items-center gap-3">
|
| 481 |
+
<span class="font-label-caps text-[10px] w-20 text-on-surface-variant/80">${feat.replace(/_/g, ' ').toUpperCase().slice(0, 10)}</span>
|
| 482 |
+
<div class="flex-1 h-2 ${barBg} rounded-sm relative">
|
| 483 |
+
<div class="absolute ${dir} top-0 h-full w-[${pct}%] ${barFill}"></div>
|
| 484 |
+
</div>
|
| 485 |
+
<span class="font-label-caps text-[10px] ${textCol}">${val >= 0 ? '+' : ''}${val.toFixed(2)}</span>
|
| 486 |
+
</div>
|
| 487 |
+
`;
|
| 488 |
+
}).join('');
|
| 489 |
+
} else {
|
| 490 |
+
shapEl.innerHTML = '<p class="font-body-fixed text-[11px] text-on-surface-variant/50 italic">No SHAP data</p>';
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
// Radio synthesis
|
| 494 |
+
$('m-radio-content').textContent = data.llm_explanation || 'No synthesis available.';
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
// --- Button Wiring ---
|
| 498 |
+
|
| 499 |
+
// Hero buttons
|
| 500 |
+
$('m-hero-run-btn').addEventListener('click', scrollToInput);
|
| 501 |
+
$('m-hero-proof-btn').addEventListener('click', () => {
|
| 502 |
+
$('m-shap-pane').scrollIntoView({ behavior: 'smooth' });
|
| 503 |
+
});
|
| 504 |
+
|
| 505 |
+
// CTA buttons
|
| 506 |
+
$('m-cta-launch-btn').addEventListener('click', scrollToInput);
|
| 507 |
+
$('m-cta-docs-btn').addEventListener('click', () => window.open(`${API_BASE}/docs`, '_blank'));
|
| 508 |
+
|
| 509 |
+
// Terminal send + Enter
|
| 510 |
+
$('m-terminal-send').addEventListener('click', () => runPrediction($('m-terminal-input').value));
|
| 511 |
+
$('m-terminal-input').addEventListener('keydown', (e) => {
|
| 512 |
+
if (e.key === 'Enter') runPrediction($('m-terminal-input').value);
|
| 513 |
+
});
|
| 514 |
+
|
| 515 |
+
// Terminal input focus glow
|
| 516 |
+
$('m-terminal-input').addEventListener('focus', () => {
|
| 517 |
+
$('m-terminal-input').parentElement.classList.add('border-neon-cyan');
|
| 518 |
+
});
|
| 519 |
+
$('m-terminal-input').addEventListener('blur', () => {
|
| 520 |
+
$('m-terminal-input').parentElement.classList.remove('border-neon-cyan');
|
| 521 |
+
});
|
| 522 |
+
|
| 523 |
+
// Bottom nav bar
|
| 524 |
+
$('m-nav-dashboard').addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
|
| 525 |
+
$('m-nav-analytics').addEventListener('click', () => $('m-shap-pane').scrollIntoView({ behavior: 'smooth' }));
|
| 526 |
+
$('m-nav-radio').addEventListener('click', () => $('m-radio-pane').scrollIntoView({ behavior: 'smooth' }));
|
| 527 |
+
$('m-nav-api').addEventListener('click', () => window.open(`${API_BASE}/docs`, '_blank'));
|
| 528 |
+
|
| 529 |
+
// Footer links
|
| 530 |
+
$('m-footer-status').addEventListener('click', async () => {
|
| 531 |
+
try {
|
| 532 |
+
const res = await fetch(`${API_BASE}/health`);
|
| 533 |
+
const data = await res.json();
|
| 534 |
+
showToast(`${data.status} | Model: ${data.model_loaded ? '✓' : '✗'} | Data: ${data.data_available ? '✓' : '✗'}`, data.status === 'healthy' ? 'success' : 'error');
|
| 535 |
+
} catch {
|
| 536 |
+
showToast('API unreachable', 'error');
|
| 537 |
+
}
|
| 538 |
+
});
|
| 539 |
+
$('m-footer-docs').addEventListener('click', () => window.open(`${API_BASE}/docs`, '_blank'));
|
| 540 |
+
</script>
|
| 541 |
+
</body></html>
|
frontend/next.config.mjs
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
/** @type {import('next').NextConfig} */
|
| 2 |
-
const nextConfig = {
|
| 3 |
-
/* config options here */
|
| 4 |
-
};
|
| 5 |
-
|
| 6 |
-
export default nextConfig;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/package-lock.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "frontend",
|
| 3 |
-
"version": "0.1.0",
|
| 4 |
-
"private": true,
|
| 5 |
-
"scripts": {
|
| 6 |
-
"dev": "next dev",
|
| 7 |
-
"build": "next build",
|
| 8 |
-
"start": "next start",
|
| 9 |
-
"lint": "eslint"
|
| 10 |
-
},
|
| 11 |
-
"dependencies": {
|
| 12 |
-
"@radix-ui/react-popover": "^1.1.16",
|
| 13 |
-
"@radix-ui/react-slot": "^1.2.5",
|
| 14 |
-
"@types/node": "^25.9.2",
|
| 15 |
-
"@types/react": "^19.2.17",
|
| 16 |
-
"class-variance-authority": "^0.7.1",
|
| 17 |
-
"clsx": "^2.1.1",
|
| 18 |
-
"lucide-react": "^1.17.0",
|
| 19 |
-
"next": "16.2.7",
|
| 20 |
-
"react": "19.2.4",
|
| 21 |
-
"react-dom": "19.2.4",
|
| 22 |
-
"recharts": "^3.8.1",
|
| 23 |
-
"tailwind-merge": "^3.6.0",
|
| 24 |
-
"typescript": "^6.0.3"
|
| 25 |
-
},
|
| 26 |
-
"devDependencies": {
|
| 27 |
-
"@tailwindcss/postcss": "^4",
|
| 28 |
-
"eslint": "^9",
|
| 29 |
-
"eslint-config-next": "16.2.7",
|
| 30 |
-
"tailwindcss": "^4"
|
| 31 |
-
}
|
| 32 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/postcss.config.mjs
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
const config = {
|
| 2 |
-
plugins: {
|
| 3 |
-
"@tailwindcss/postcss": {},
|
| 4 |
-
},
|
| 5 |
-
};
|
| 6 |
-
|
| 7 |
-
export default config;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/public/file.svg
DELETED
frontend/public/globe.svg
DELETED
frontend/public/metrics/confusion_matrix.svg
DELETED
frontend/public/metrics/feature_importance.svg
DELETED
frontend/public/metrics/pr_curve.svg
DELETED
frontend/public/metrics/roc_curve.svg
DELETED
frontend/public/next.svg
DELETED
frontend/public/vercel.svg
DELETED
frontend/public/window.svg
DELETED
frontend/tsconfig.json
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"compilerOptions": {
|
| 3 |
-
"lib": ["dom", "dom.iterable", "esnext"],
|
| 4 |
-
"allowJs": true,
|
| 5 |
-
"skipLibCheck": true,
|
| 6 |
-
"strict": true,
|
| 7 |
-
"noEmit": true,
|
| 8 |
-
"esModuleInterop": true,
|
| 9 |
-
"module": "esnext",
|
| 10 |
-
"moduleResolution": "bundler",
|
| 11 |
-
"resolveJsonModule": true,
|
| 12 |
-
"isolatedModules": true,
|
| 13 |
-
"jsx": "preserve",
|
| 14 |
-
"incremental": true,
|
| 15 |
-
"plugins": [
|
| 16 |
-
{
|
| 17 |
-
"name": "next"
|
| 18 |
-
}
|
| 19 |
-
],
|
| 20 |
-
"paths": {
|
| 21 |
-
"@/*": ["./*"]
|
| 22 |
-
}
|
| 23 |
-
},
|
| 24 |
-
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
| 25 |
-
"exclude": ["node_modules"]
|
| 26 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ml/drift_detection.py
CHANGED
|
@@ -17,7 +17,7 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(name)s | %(level
|
|
| 17 |
def detect_drift(
|
| 18 |
data_path: str = "data_output/fastf1_races.parquet",
|
| 19 |
current_season: int = 2026,
|
| 20 |
-
report_output_path: str = "
|
| 21 |
) -> bool:
|
| 22 |
"""
|
| 23 |
Detects drift between the reference dataset (historical) and the current dataset.
|
|
|
|
| 17 |
def detect_drift(
|
| 18 |
data_path: str = "data_output/fastf1_races.parquet",
|
| 19 |
current_season: int = 2026,
|
| 20 |
+
report_output_path: str = "data_output/drift_report.html"
|
| 21 |
) -> bool:
|
| 22 |
"""
|
| 23 |
Detects drift between the reference dataset (historical) and the current dataset.
|
mlruns/models/kronector-f1-lgbm/version-1/meta.yaml
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
creation_timestamp: 1780335714328
|
| 2 |
current_stage: None
|
| 3 |
deployment_job_state: null
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
|
| 9 |
creation_timestamp: 1780335714328
|
| 10 |
current_stage: None
|
| 11 |
deployment_job_state: null
|