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

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>;
}