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