Spaces:
Sleeping
Sleeping
fastapi dockerised
Browse files- .dockerignore +24 -0
- Dockerfile +37 -0
- frontend/.gitignore +24 -0
- frontend/README.md +16 -0
- frontend/eslint.config.js +29 -0
- frontend/index.html +16 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +35 -0
- frontend/postcss.config.js +6 -0
- frontend/public/vite.svg +1 -0
- frontend/src/App.css +42 -0
- frontend/src/App.jsx +227 -0
- frontend/src/assets/logo.svg +13 -0
- frontend/src/assets/react.svg +1 -0
- frontend/src/components/PersonaChart.jsx +61 -0
- frontend/src/components/RoastCard.jsx +22 -0
- frontend/src/index.css +50 -0
- frontend/src/main.jsx +10 -0
- frontend/tailwind.config.js +40 -0
- frontend/vite.config.js +7 -0
.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 |
+
})
|