scaffold react dashboard

This commit is contained in:
2025-04-12 11:16:46 +00:00
parent c8b31bedda
commit a6ced69393
14 changed files with 4635 additions and 0 deletions

24
dashboard/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
dashboard/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>dashboard</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4254
dashboard/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
dashboard/package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "dashboard",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.90.20",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.0",
"recharts": "^3.7.0",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4"
}
}

View File

@@ -0,0 +1,45 @@
const API_BASE_URL = import.meta.env.VITE_API_URL || '';
export class ApiError extends Error {
status: number;
constructor(status: number, message: string) {
super(message);
this.name = 'ApiError';
this.status = status;
}
}
export async function apiRequest<T>(
endpoint: string,
options: RequestInit = {},
): Promise<T> {
const url = `${API_BASE_URL}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!response.ok) {
const message = await response.text().catch(() => 'Request failed');
throw new ApiError(response.status, message);
}
return response.json();
}
export function buildQueryString(
params: Record<string, string | number | boolean | undefined>,
): string {
const filtered = Object.entries(params).filter(
([, value]) => value !== undefined,
);
if (filtered.length === 0) return '';
return '?' + new URLSearchParams(
filtered.map(([key, value]) => [key, String(value)]),
).toString();
}

View File

@@ -0,0 +1,45 @@
import { useQuery } from '@tanstack/react-query';
import { apiRequest, buildQueryString } from './client';
import type {
ReviewListResponse,
ReviewDetail,
DeliberationLogResponse,
ReviewFilters,
MetricsResponse,
} from '../types/api';
export function useReviews(filters: ReviewFilters = {}) {
return useQuery({
queryKey: ['reviews', filters],
queryFn: () =>
apiRequest<ReviewListResponse>(
`/api/reviews${buildQueryString(filters as Record<string, string | number | undefined>)}`,
),
});
}
export function useReview(id: string) {
return useQuery({
queryKey: ['review', id],
queryFn: () => apiRequest<ReviewDetail>(`/api/reviews/${id}`),
enabled: Boolean(id),
});
}
export function useDeliberationLog(reviewId: string) {
return useQuery({
queryKey: ['deliberation', reviewId],
queryFn: () =>
apiRequest<DeliberationLogResponse>(
`/api/reviews/${reviewId}/deliberation`,
),
enabled: Boolean(reviewId),
});
}
export function useMetrics() {
return useQuery({
queryKey: ['metrics'],
queryFn: () => apiRequest<MetricsResponse>('/api/reviews/metrics'),
});
}

5
dashboard/src/index.css Normal file
View File

@@ -0,0 +1,5 @@
@import "tailwindcss";
body {
@apply bg-gray-50 text-gray-900 antialiased;
}

10
dashboard/src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

104
dashboard/src/types/api.ts Normal file
View File

@@ -0,0 +1,104 @@
export type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info';
export type Verdict = 'approve' | 'request_changes' | 'comment';
export type ReviewStatus = 'pending' | 'running' | 'completed' | 'failed';
export type AgentName = 'security' | 'style' | 'complexity';
export type ConflictNature = 'contradictory' | 'trade_off' | 'overlapping';
export type StepType = 'merge' | 'conflict_detection' | 'synthesis' | 'verdict';
export interface ReviewSummary {
id: string;
repository: string;
pr_number: number;
pr_title: string | null;
author: string | null;
status: ReviewStatus;
verdict: Verdict | null;
verdict_confidence: number | null;
finding_count: number;
critical_count: number;
high_count: number;
total_cost_usd: number | null;
created_at: string;
completed_at: string | null;
}
export interface Finding {
id: string;
agent: AgentName;
file: string;
line_start: number;
line_end: number;
severity: Severity;
confidence: number;
title: string;
description: string;
reasoning: string;
suggestion: string | null;
references: string[];
prompt_version: string;
}
export interface Conflict {
id: string;
finding_ids: string[];
nature: ConflictNature;
description: string;
severity_weight: number;
resolution: string | null;
winning_finding_id: string | null;
}
export interface ReviewDetail extends ReviewSummary {
base_sha: string;
head_sha: string;
is_draft: boolean;
verdict_reasoning: string | null;
total_tokens: number | null;
tokens_by_agent: Record<string, number> | null;
cost_by_agent: Record<string, number> | null;
started_at: string | null;
error_message: string | null;
findings: Finding[];
conflicts: Conflict[];
}
export interface DeliberationStep {
id: string;
step_type: StepType;
timestamp: string;
description: string;
details: Record<string, unknown>;
sequence: number;
}
export interface ReviewListResponse {
items: ReviewSummary[];
total: number;
page: number;
page_size: number;
pages: number;
}
export interface DeliberationLogResponse {
review_id: string;
steps: DeliberationStep[];
}
export interface ReviewFilters {
repository?: string;
status?: ReviewStatus;
verdict?: Verdict;
author?: string;
page?: number;
page_size?: number;
}
export interface MetricsResponse {
total_reviews: number;
completed_reviews: number;
average_cost_usd: number;
verdict_counts: Record<Verdict, number>;
severity_counts: Record<Severity, number>;
reviews_by_day: { date: string; count: number }[];
cost_by_agent: Record<AgentName, number>;
}

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
dashboard/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

15
dashboard/vite.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
},
})