File size: 6,731 Bytes
7e2f74d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | import React, { useState, useEffect } from 'react';
import { Cpu, ArrowLeft, AlertCircle, CheckCircle } from 'lucide-react';
import InputForm from './components/InputForm';
import RepoTree from './components/RepoTree';
import Dashboard from './components/Dashboard';
import { apiUrl } from './api';
const LOADING_STEPS = [
{ title: 'Access Verification', desc: 'Checking GitHub repository accessibility...' },
{ title: 'Workspace Isolation', desc: 'Cloning repository into isolated sandbox...' },
{ title: 'Static Profiling', desc: 'Scanning files, parsing package manifests...' },
{ title: 'Gemini Reasoning', desc: 'Analyzing codebase with Gemini 2.5 Flash...' },
{ title: 'Knowledge Indexing', desc: 'Building semantic vector index (ChromaDB)...' },
];
export default function App() {
const [appState, setAppState] = useState('idle'); // idle | loading | success | error
const [analysisResult, setAnalysisResult] = useState(null);
const [error, setError] = useState(null);
const [currentStep, setCurrentStep] = useState(0);
useEffect(() => {
let interval;
if (appState === 'loading') {
setCurrentStep(0);
interval = setInterval(() => {
setCurrentStep(prev => (prev < LOADING_STEPS.length - 2 ? prev + 1 : prev));
}, 3800);
}
return () => clearInterval(interval);
}, [appState]);
const handleStartAnalysis = async (formData) => {
setAppState('loading');
setError(null);
setAnalysisResult(null);
// API key is now server-side from .env β only send if user explicitly provided one
const headers = {};
if (formData.apiKey) headers['x-gemini-key'] = formData.apiKey;
try {
let response;
if (formData.type === 'url') {
setCurrentStep(0);
response = await fetch(apiUrl('/api/analyze-url'), {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body: JSON.stringify({ url: formData.url, token: formData.token || null }),
});
} else {
setCurrentStep(1);
const fd = new FormData();
fd.append('file', formData.file);
response = await fetch(apiUrl('/api/analyze-zip'), { method: 'POST', headers, body: fd });
}
const resData = await response.json();
if (!response.ok) throw new Error(resData.detail || 'Analysis failed.');
setCurrentStep(4);
setTimeout(() => {
setAnalysisResult({ ...resData, apiKey: formData.apiKey || null });
setAppState('success');
}, 700);
} catch (e) {
setError(e.message);
setAppState('error');
}
};
const handleReset = () => {
setAppState('idle');
setAnalysisResult(null);
setError(null);
};
return (
<div className="app-container">
{/* ββ Header ββ */}
<header className="app-header">
<div className="brand-section">
<div className="brand-icon">
<Cpu size={20} color="#fff" />
</div>
<div>
<div className="brand-title">Repository Intelligence</div>
<div className="brand-subtitle">AI-Powered Code Analysis Platform</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
{appState === 'success' && (
<>
<span style={{ fontSize: '0.78rem', color: 'var(--text-muted)' }}>
{analysisResult?.project_name}
</span>
<button className="btn-secondary" onClick={handleReset}>
<ArrowLeft size={14} /> New Analysis
</button>
</>
)}
</div>
</header>
{/* ββ Main ββ */}
<main className="main-content">
{/* IDLE */}
{appState === 'idle' && (
<InputForm onSubmit={handleStartAnalysis} loading={false} />
)}
{/* ERROR */}
{appState === 'error' && (
<div style={{ maxWidth: '680px', width: '100%', margin: '0 auto' }}>
<div className="error-panel">
<AlertCircle size={20} style={{ flexShrink: 0 }} />
<div>
<div className="error-title">Analysis Failed</div>
<p style={{ fontSize: '0.88rem', marginTop: '0.25rem' }}>{error}</p>
</div>
</div>
<InputForm onSubmit={handleStartAnalysis} loading={false} />
</div>
)}
{/* LOADING */}
{appState === 'loading' && (
<div className="glass-panel progress-panel">
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
<div className="spinner" />
<h2 className="section-title" style={{ marginBottom: '0.5rem' }}>
Generating Intelligence Report
</h2>
<p className="section-desc" style={{ margin: 0 }}>
Scanning codebase, extracting semantics, and building your knowledge base.
This may take up to a minute for large repositories.
</p>
</div>
<div className="progress-steps">
{LOADING_STEPS.map((step, idx) => {
const state = idx < currentStep ? 'completed' : idx === currentStep ? 'active' : 'pending';
return (
<div key={idx} className={`progress-step ${state}`}>
<div className={`step-icon ${state}`}>
{state === 'completed' ? <CheckCircle size={14} /> : idx + 1}
</div>
<div>
<div style={{
fontWeight: 600,
fontSize: '0.88rem',
color: state === 'active' ? 'var(--text-primary)' : 'var(--text-secondary)'
}}>
{step.title}
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
{step.desc}
</div>
</div>
</div>
);
})}
</div>
</div>
)}
{/* SUCCESS */}
{appState === 'success' && analysisResult && (
<div className="dashboard-grid">
<aside className="glass-panel sidebar-panel">
<RepoTree
tree={analysisResult.tree}
title={analysisResult.project_name?.split('/').pop()}
/>
</aside>
<section>
<Dashboard analysisResult={analysisResult} />
</section>
</div>
)}
</main>
</div>
);
}
|