nice-bill commited on
Commit
fe1519f
·
1 Parent(s): 49e124f

fastapi dockerised

Browse files
.dockerignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Git
2
+ .git
3
+ .gitignore
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ .venv/
10
+ .env
11
+
12
+ # Data & Notebooks
13
+ data/
14
+ notebooks/
15
+ *.ipynb
16
+ cache_data/
17
+ docs/
18
+
19
+ # Frontend
20
+ frontend/
21
+ node_modules/
22
+
23
+ # Local artifacts
24
+ *.csv
Dockerfile ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim-bookworm
2
+
3
+ # Install uv
4
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
5
+
6
+ # Set environment variables
7
+ # Compile bytecode prevents python from writing .pyc files to disk
8
+ ENV PYTHONDONTWRITEBYTECODE=1
9
+ ENV PYTHONUNBUFFERED=1
10
+
11
+ WORKDIR /app
12
+
13
+ # Copy dependency files first for better caching
14
+ COPY pyproject.toml uv.lock ./
15
+
16
+ # Install dependencies
17
+ # --frozen: Sync with exact versions from uv.lock
18
+ # --no-dev: Do not install development dependencies
19
+ RUN uv sync --frozen --no-dev
20
+
21
+ # Add the virtual environment to the PATH
22
+ # This ensures that 'uvicorn' and 'python' use the installed dependencies
23
+ ENV PATH="/app/.venv/bin:$PATH"
24
+
25
+ # Copy the application code
26
+ COPY src/ src/
27
+ COPY app.py .
28
+ COPY kmeans_model.pkl .
29
+ COPY wallet_power_transformer.pkl .
30
+
31
+ # Create cache directory
32
+ RUN mkdir -p cache_data
33
+
34
+ EXPOSE 8000
35
+
36
+ # Use the venv's uvicorn directly (thanks to PATH)
37
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
frontend/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
frontend/index.html ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
10
+ <title>Cluster // Protocol</title>
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script type="module" src="/src/main.jsx"></script>
15
+ </body>
16
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "axios": "^1.13.2",
14
+ "framer-motion": "^12.23.26",
15
+ "lucide-react": "^0.561.0",
16
+ "react": "^19.2.0",
17
+ "react-dom": "^19.2.0",
18
+ "recharts": "^3.5.1"
19
+ },
20
+ "devDependencies": {
21
+ "@eslint/js": "^9.39.1",
22
+ "@tailwindcss/postcss": "^4.1.18",
23
+ "@types/react": "^19.2.5",
24
+ "@types/react-dom": "^19.2.3",
25
+ "@vitejs/plugin-react": "^5.1.1",
26
+ "autoprefixer": "^10.4.22",
27
+ "eslint": "^9.39.1",
28
+ "eslint-plugin-react-hooks": "^7.0.1",
29
+ "eslint-plugin-react-refresh": "^0.4.24",
30
+ "globals": "^16.5.0",
31
+ "postcss": "^8.5.6",
32
+ "tailwindcss": "^4.1.18",
33
+ "vite": "^7.2.4"
34
+ }
35
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/public/vite.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ will-change: filter;
12
+ transition: filter 300ms;
13
+ }
14
+ .logo:hover {
15
+ filter: drop-shadow(0 0 2em #646cffaa);
16
+ }
17
+ .logo.react:hover {
18
+ filter: drop-shadow(0 0 2em #61dafbaa);
19
+ }
20
+
21
+ @keyframes logo-spin {
22
+ from {
23
+ transform: rotate(0deg);
24
+ }
25
+ to {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ a:nth-of-type(2) .logo {
32
+ animation: logo-spin infinite 20s linear;
33
+ }
34
+ }
35
+
36
+ .card {
37
+ padding: 2em;
38
+ }
39
+
40
+ .read-the-docs {
41
+ color: #888;
42
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import axios from 'axios';
3
+ import { Search, Share2, Activity, Database, Zap, Wallet, ChevronRight, Terminal, Layers, Hash } from 'lucide-react';
4
+ import { motion, AnimatePresence } from 'framer-motion';
5
+ import PersonaChart from './components/PersonaChart';
6
+ import RoastCard from './components/RoastCard';
7
+ import Logo from './assets/logo.svg';
8
+
9
+ const API_BASE = import.meta.env.VITE_API_URL || "http://localhost:8000";
10
+
11
+ function App() {
12
+ const [wallet, setWallet] = useState("");
13
+ const [status, setStatus] = useState("idle");
14
+ const [data, setData] = useState(null);
15
+ const [errorMsg, setErrorMsg] = useState("");
16
+
17
+ const analyzeWallet = async () => {
18
+ if (!wallet.startsWith("0x")) {
19
+ setErrorMsg("INVALID_ADDRESS: Must start with 0x");
20
+ return;
21
+ }
22
+
23
+ setStatus("loading");
24
+ setErrorMsg("");
25
+ setData(null);
26
+
27
+ try {
28
+ const startRes = await axios.post(`${API_BASE}/analyze/start/${wallet}`);
29
+ pollStatus(startRes.data.job_id);
30
+ } catch (err) {
31
+ console.error(err);
32
+ setErrorMsg("CONNECTION_ERR: API Unreachable");
33
+ setStatus("error");
34
+ }
35
+ };
36
+
37
+ const handleExport = () => {
38
+ if (!data) return;
39
+ const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
40
+ JSON.stringify(data, null, 2)
41
+ )}`;
42
+ const link = document.createElement("a");
43
+ link.href = jsonString;
44
+ link.download = `analysis_${data.wallet_address || "wallet"}.json`;
45
+ link.click();
46
+ };
47
+
48
+ return (
49
+ <div className="h-screen flex flex-col overflow-hidden bg-bg-main text-sm">
50
+
51
+ {/* 1. Compact Top Navigation Bar */}
52
+ <header className="h-14 border-b border-border bg-bg-panel flex items-center px-4 justify-between shrink-0">
53
+ <div className="flex items-center gap-3">
54
+ <div className="w-8 h-8 text-accent flex items-center justify-center">
55
+ <img src={Logo} alt="Cluster Protocol" className="w-full h-full text-accent" />
56
+ </div>
57
+ <h1 className="font-mono font-semibold tracking-tight text-text-primary">
58
+ CLUSTER<span className="text-text-secondary">PROTOCOL</span>
59
+ </h1>
60
+ <span className="px-2 py-0.5 rounded-full bg-border text-[10px] text-text-secondary font-mono">v2.1.0</span>
61
+ </div>
62
+
63
+ {/* Dense Search Input */}
64
+ <div className="flex items-center gap-2 w-full max-w-md">
65
+ <div className="relative flex-grow group">
66
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary">
67
+ <Terminal size={14} />
68
+ </div>
69
+ <input
70
+ type="text"
71
+ value={wallet}
72
+ onChange={(e) => setWallet(e.target.value)}
73
+ onKeyDown={(e) => e.key === 'Enter' && analyzeWallet()}
74
+ placeholder="0x..."
75
+ className="w-full bg-bg-main border border-border text-text-primary pl-9 pr-3 py-1.5 font-mono text-xs focus:outline-none focus:border-accent transition-colors rounded-sm"
76
+ disabled={status === 'loading'}
77
+ />
78
+ </div>
79
+ <button
80
+ onClick={analyzeWallet}
81
+ disabled={status === 'loading'}
82
+ className="px-4 py-1.5 bg-accent hover:bg-amber-400 text-black font-semibold text-xs uppercase tracking-wide rounded-sm transition-colors disabled:opacity-50"
83
+ >
84
+ {status === 'loading' ? "RUNNING..." : "EXECUTE"}
85
+ </button>
86
+ </div>
87
+ </header>
88
+
89
+ {/* Error Toast */}
90
+ {errorMsg && (
91
+ <div className="bg-red-900/20 border-b border-red-900/50 text-red-400 px-4 py-2 text-xs font-mono text-center">
92
+ ! {errorMsg}
93
+ </div>
94
+ )}
95
+
96
+ {/* Main Content - Flex Layout to avoid scroll */}
97
+ <main className="flex-grow flex items-center justify-center p-4 md:p-6 overflow-hidden relative">
98
+
99
+ {/* Empty State */}
100
+ {status === 'idle' && (
101
+ <div className="text-center text-text-secondary space-y-4 max-w-md">
102
+ <Layers className="w-12 h-12 mx-auto opacity-20" />
103
+ <div className="space-y-1">
104
+ <h2 className="text-text-primary font-medium">Ready to Process</h2>
105
+ <p className="text-xs">Enter a wallet address above to initiate the segmentation engine.</p>
106
+ </div>
107
+ </div>
108
+ )}
109
+
110
+ {/* Loading State */}
111
+ {status === 'loading' && (
112
+ <div className="w-64 space-y-2">
113
+ <div className="h-1 bg-border overflow-hidden rounded-full">
114
+ <div className="h-full bg-accent w-1/3 animate-[shimmer_1s_infinite_linear]"></div>
115
+ </div>
116
+ <div className="flex justify-between text-[10px] font-mono text-text-secondary uppercase">
117
+ <span>Ingesting Data</span>
118
+ <span className="animate-pulse">...</span>
119
+ </div>
120
+ </div>
121
+ )}
122
+
123
+ {/* Dashboard Grid */}
124
+ <AnimatePresence>
125
+ {status === 'success' && data && (
126
+ <motion.div
127
+ initial={{ opacity: 0, scale: 0.98 }}
128
+ animate={{ opacity: 1, scale: 1 }}
129
+ transition={{ duration: 0.2 }}
130
+ className="w-full max-w-6xl h-full grid grid-cols-1 md:grid-cols-12 gap-4 grid-rows-[auto_1fr] md:grid-rows-1"
131
+ >
132
+
133
+ {/* Col 1: Visuals (Radar) - 5 Cols */}
134
+ <div className="md:col-span-5 bg-bg-panel border border-border flex flex-col">
135
+ <div className="p-3 border-b border-border flex justify-between items-center">
136
+ <span className="text-xs font-mono text-text-primary font-semibold uppercase tracking-wider">Behavioral Topology</span>
137
+ <Activity size={12} className="text-text-secondary" />
138
+ </div>
139
+ <div className="flex-grow min-h-[250px] relative p-4">
140
+ <PersonaChart scores={data.confidence_scores} />
141
+ </div>
142
+ <div className="p-3 border-t border-border bg-bg-main">
143
+ <div className="flex items-center justify-between mb-2">
144
+ <span className="text-xs text-text-secondary">Primary Classification</span>
145
+ <span className="text-accent text-xs font-mono font-bold">{data.persona}</span>
146
+ </div>
147
+ <div className="w-full h-1.5 bg-border rounded-full overflow-hidden">
148
+ <div
149
+ className="h-full bg-accent"
150
+ style={{ width: `${Math.max(...Object.values(data.confidence_scores)) * 100}%` }}
151
+ />
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ {/* Col 2: Metrics & Insights - 7 Cols */}
157
+ <div className="md:col-span-7 flex flex-col gap-4 overflow-y-auto pr-1">
158
+
159
+ {/* Top Row: Metrics */}
160
+ <div className="grid grid-cols-3 gap-4 h-24 shrink-0">
161
+ <MetricCard
162
+ label="TX_COUNT"
163
+ value={data.stats.tx_count}
164
+ icon={<Hash size={14} />}
165
+ />
166
+ <MetricCard
167
+ label="NFT_VAL_USD"
168
+ value={`$${Math.round(data.stats.total_nft_volume_usd).toLocaleString()}`}
169
+ icon={<Database size={14} />}
170
+ />
171
+ <MetricCard
172
+ label="GAS_ETH"
173
+ value={data.stats.total_gas_spent.toFixed(4)}
174
+ icon={<Zap size={14} />}
175
+ />
176
+ </div>
177
+
178
+ {/* Bottom Row: Text Analysis */}
179
+ <div className="flex-grow bg-bg-panel border border-border flex flex-col">
180
+ <div className="p-3 border-b border-border flex justify-between items-center bg-bg-main/50">
181
+ <span className="text-xs font-mono text-text-primary font-semibold uppercase tracking-wider">Identity Narrative</span>
182
+ <div className="flex gap-2">
183
+ <div className="w-2 h-2 rounded-full bg-red-500/20 border border-red-500/50"></div>
184
+ <div className="w-2 h-2 rounded-full bg-yellow-500/20 border border-yellow-500/50"></div>
185
+ <div className="w-2 h-2 rounded-full bg-green-500/20 border border-green-500/50"></div>
186
+ </div>
187
+ </div>
188
+ <div className="p-0 flex-grow relative overflow-hidden">
189
+ <RoastCard explanation={data.explanation} />
190
+ </div>
191
+ <div className="p-3 border-t border-border flex justify-between items-center">
192
+ <button
193
+ onClick={handleExport}
194
+ className="flex items-center gap-2 text-xs text-text-secondary hover:text-white transition-colors"
195
+ >
196
+ <Share2 size={12} />
197
+ <span>EXPORT_JSON</span>
198
+ </button>
199
+ <span className="text-[10px] text-text-secondary font-mono">LATENCY: 42ms</span>
200
+ </div>
201
+ </div>
202
+
203
+ </div>
204
+
205
+ </motion.div>
206
+ )}
207
+ </AnimatePresence>
208
+ </main>
209
+ </div>
210
+ );
211
+ }
212
+
213
+ function MetricCard({ label, value, icon }) {
214
+ return (
215
+ <div className="bg-bg-panel border border-border p-3 flex flex-col justify-between">
216
+ <div className="flex justify-between items-start text-text-secondary">
217
+ <span className="text-[10px] font-mono tracking-wider">{label}</span>
218
+ {icon}
219
+ </div>
220
+ <div className="text-lg font-semibold text-text-primary tracking-tight font-mono">
221
+ {value}
222
+ </div>
223
+ </div>
224
+ );
225
+ }
226
+
227
+ export default App;
frontend/src/assets/logo.svg ADDED
frontend/src/assets/react.svg ADDED
frontend/src/components/PersonaChart.jsx ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import {
3
+ Radar,
4
+ RadarChart,
5
+ PolarGrid,
6
+ PolarAngleAxis,
7
+ PolarRadiusAxis,
8
+ ResponsiveContainer,
9
+ Tooltip
10
+ } from 'recharts';
11
+
12
+ const PersonaChart = ({ scores }) => {
13
+ const data = Object.keys(scores).map(key => ({
14
+ subject: key.split(" / ")[0].toUpperCase(), // Truncate and Uppercase for cleanliness
15
+ value: scores[key] * 100,
16
+ fullMark: 100,
17
+ }));
18
+
19
+ const CustomTooltip = ({ active, payload }) => {
20
+ if (active && payload && payload.length) {
21
+ return (
22
+ <div className="bg-black border border-zinc-700 p-2 shadow-xl">
23
+ <div className="flex justify-between items-center gap-4 mb-1 border-b border-zinc-800 pb-1">
24
+ <span className="text-[10px] font-mono text-zinc-400 uppercase">{payload[0].payload.subject}</span>
25
+ </div>
26
+ <div className="flex items-baseline gap-1">
27
+ <span className="text-amber-500 font-bold font-mono text-sm">
28
+ {payload[0].value.toFixed(1)}%
29
+ </span>
30
+ </div>
31
+ </div>
32
+ );
33
+ }
34
+ return null;
35
+ };
36
+
37
+ return (
38
+ <ResponsiveContainer width="100%" height="100%">
39
+ <RadarChart cx="50%" cy="50%" outerRadius="70%" data={data}>
40
+ <PolarGrid stroke="#27272a" />
41
+ <PolarAngleAxis
42
+ dataKey="subject"
43
+ tick={{ fill: '#71717a', fontSize: 10, fontFamily: 'JetBrains Mono', fontWeight: 500 }}
44
+ />
45
+ <PolarRadiusAxis angle={30} domain={[0, 100]} tick={false} axisLine={false} />
46
+ <Radar
47
+ name="Confidence"
48
+ dataKey="value"
49
+ stroke="#f59e0b"
50
+ strokeWidth={2}
51
+ fill="#f59e0b"
52
+ fillOpacity={0.2}
53
+ isAnimationActive={true}
54
+ />
55
+ <Tooltip content={<CustomTooltip />} cursor={{ stroke: '#f59e0b', strokeWidth: 1 }} />
56
+ </RadarChart>
57
+ </ResponsiveContainer>
58
+ );
59
+ };
60
+
61
+ export default PersonaChart;
frontend/src/components/RoastCard.jsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ const RoastCard = ({ explanation }) => {
4
+ const cleanText = explanation ? explanation.replace(/^["']|["']$/g, '') : '';
5
+
6
+ return (
7
+ <div className="h-full w-full p-4 overflow-y-auto font-mono text-xs md:text-sm leading-relaxed text-zinc-300">
8
+ <div className="mb-2 text-zinc-500 uppercase text-[10px] tracking-widest border-l-2 border-amber-500 pl-2">
9
+ Behavioral Pattern Detected
10
+ </div>
11
+ <p>
12
+ {cleanText || "Awaiting input stream..."}
13
+ </p>
14
+ <div className="mt-4 flex gap-1">
15
+ <span className="w-1 h-3 bg-amber-500 animate-pulse"></span>
16
+ <span className="w-1 h-3 bg-transparent border border-zinc-700"></span>
17
+ </div>
18
+ </div>
19
+ );
20
+ };
21
+
22
+ export default RoastCard;
frontend/src/index.css ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
2
+ @import "tailwindcss";
3
+
4
+ @theme {
5
+ --color-bg-main: #050505;
6
+ --color-bg-panel: #0a0a0a;
7
+ --color-border: #27272a;
8
+ --color-accent: #f59e0b; /* Amber-500 for a functional, industrial look */
9
+ --color-accent-dim: #78350f;
10
+ --color-text-primary: #e4e4e7;
11
+ --color-text-secondary: #a1a1aa;
12
+ }
13
+
14
+ body {
15
+ background-color: var(--color-bg-main);
16
+ color: var(--color-text-primary);
17
+ font-family: 'Inter', sans-serif;
18
+ overflow-x: hidden;
19
+ }
20
+
21
+ .font-mono {
22
+ font-family: 'JetBrains Mono', monospace;
23
+ }
24
+
25
+ /* Custom Scrollbar for dense data */
26
+ ::-webkit-scrollbar {
27
+ width: 6px;
28
+ height: 6px;
29
+ }
30
+ ::-webkit-scrollbar-track {
31
+ background: var(--color-bg-main);
32
+ }
33
+ ::-webkit-scrollbar-thumb {
34
+ background: var(--color-border);
35
+ border-radius: 3px;
36
+ }
37
+ ::-webkit-scrollbar-thumb:hover {
38
+ background: var(--color-text-secondary);
39
+ }
40
+
41
+ /* Utilities */
42
+ .panel-border {
43
+ border: 1px solid var(--color-border);
44
+ }
45
+
46
+ .dense-grid {
47
+ display: grid;
48
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
49
+ gap: 0.75rem;
50
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/tailwind.config.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {
9
+ fontFamily: {
10
+ mono: ['"Space Mono"', 'monospace'],
11
+ sans: ['"Space Mono"', 'monospace'], // Force mono everywhere for this look
12
+ },
13
+ colors: {
14
+ retro: {
15
+ bg: '#050505', // Void Black
16
+ surface: '#111111', // Dark Gray
17
+ border: '#333333', // Border Gray
18
+ primary: '#ffb000', // Amber-500 (Classic Terminal)
19
+ secondary: '#00ff41', // Matrix Green (Alternative accent)
20
+ muted: '#666666',
21
+ }
22
+ },
23
+ animation: {
24
+ 'blink': 'blink 1s step-end infinite',
25
+ 'scan': 'scan 8s linear infinite',
26
+ },
27
+ keyframes: {
28
+ blink: {
29
+ '0%, 100%': { opacity: '1' },
30
+ '50%': { opacity: '0' },
31
+ },
32
+ scan: {
33
+ '0%': { transform: 'translateY(-100%)' },
34
+ '100%': { transform: 'translateY(100%)' },
35
+ }
36
+ }
37
+ },
38
+ },
39
+ plugins: [],
40
+ }
frontend/vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })