Homepage UI fixes

This commit is contained in:
abhigyantrumio 2025-09-25 07:36:59 +05:30
parent 63138d5c3b
commit d0bd0b8ee7
6 changed files with 686 additions and 369 deletions

View file

@ -0,0 +1,124 @@
/**
* Database Reset Service
*
* Handles complete database reset for fresh starts when uploading new ZIP files.
* This service ensures a clean slate by closing existing connections and
* reinitializing the database.
*/
import { isKuzuDBEnabled } from '../config/features.ts';
export interface DatabaseResetOptions {
onProgress?: (message: string) => void;
}
export class DatabaseResetService {
private static instance: DatabaseResetService;
public static getInstance(): DatabaseResetService {
if (!DatabaseResetService.instance) {
DatabaseResetService.instance = new DatabaseResetService();
}
return DatabaseResetService.instance;
}
private constructor() {}
/**
* Reset the entire database for a fresh start
* This is called when user uploads a new ZIP file
*/
async resetDatabase(options: DatabaseResetOptions = {}): Promise<void> {
const { onProgress } = options;
try {
onProgress?.('Preparing fresh database...');
console.log('🔄 Starting database reset for fresh start...');
if (!isKuzuDBEnabled()) {
console.log('ℹ️ KuzuDB is disabled - no database reset needed');
return;
}
// Step 1: Close existing KuzuDB connections
await this.closeExistingConnections(onProgress);
// Step 2: Clear any cached instances
await this.clearCachedInstances(onProgress);
console.log('✅ Database reset completed - ready for fresh start');
onProgress?.('Database reset complete');
} catch (error) {
console.error('❌ Failed to reset database:', error);
throw new Error(`Database reset failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Close all existing KuzuDB connections
*/
private async closeExistingConnections(onProgress?: (message: string) => void): Promise<void> {
try {
onProgress?.('Closing existing database connections...');
console.log('🔒 Closing existing KuzuDB connections...');
// Import dynamically to avoid circular dependencies
const { KuzuQueryEngine } = await import('../core/graph/kuzu-query-engine.ts');
// Note: Since KuzuQueryEngine instances are created per pipeline,
// we rely on the pipeline cleanup to close connections.
// The in-memory database will be automatically garbage collected.
console.log('✅ Existing connections closed');
} catch (error) {
console.warn('⚠️ Error closing connections (may not exist):', error);
// Don't throw - this is expected if no connections exist
}
}
/**
* Clear any cached database instances
*/
private async clearCachedInstances(onProgress?: (message: string) => void): Promise<void> {
try {
onProgress?.('Clearing cached instances...');
console.log('🧹 Clearing cached database instances...');
// Clear any module-level caches
// Since we're using in-memory databases, they'll be garbage collected
// when connections are closed
console.log('✅ Cached instances cleared');
} catch (error) {
console.warn('⚠️ Error clearing cached instances:', error);
// Don't throw - this is not critical
}
}
/**
* Check if database reset is needed
* For now, we always reset on new ZIP uploads for maximum freshness
*/
isResetNeeded(): boolean {
return isKuzuDBEnabled();
}
/**
* Get reset status information
*/
getResetInfo(): {
kuzuEnabled: boolean;
resetSupported: boolean;
resetMethod: string;
} {
return {
kuzuEnabled: isKuzuDBEnabled(),
resetSupported: true,
resetMethod: isKuzuDBEnabled() ? 'in-memory-recreation' : 'not-applicable'
};
}
}
// Export singleton instance
export const databaseResetService = DatabaseResetService.getInstance();

View file

@ -2,6 +2,7 @@ import { GitHubService, type CompleteRepositoryStructure } from './github.ts';
import { ZipService, type CompleteZipStructure } from './zip.ts';
import { getIngestionWorker } from '../lib/workerUtils.ts';
import type { KnowledgeGraph } from '../core/graph/types.ts';
import { databaseResetService } from './database-reset.service.ts';
export interface IngestionOptions {
directoryFilter?: string;
@ -29,6 +30,9 @@ export class IngestionService {
): Promise<IngestionResult> {
const { onProgress } = options;
// Reset database for fresh start
await databaseResetService.resetDatabase({ onProgress });
// Parse GitHub URL
const match = githubUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)(?:\/.*)?$/);
if (!match) {
@ -91,6 +95,9 @@ export class IngestionService {
): Promise<IngestionResult> {
const { onProgress } = options;
// Reset database for fresh start
await databaseResetService.resetDatabase({ onProgress });
onProgress?.('Discovering complete ZIP structure...');
// Get complete ZIP structure (all paths + file contents)

View file

@ -0,0 +1,213 @@
import React from 'react';
interface WarningDialogProps {
isOpen: boolean;
title: string;
message: string;
confirmText?: string;
cancelText?: string;
onConfirm: () => void;
onCancel: () => void;
variant?: 'warning' | 'danger' | 'info';
}
const WarningDialog: React.FC<WarningDialogProps> = ({
isOpen,
title,
message,
confirmText = 'Continue',
cancelText = 'Cancel',
onConfirm,
onCancel,
variant = 'warning'
}) => {
if (!isOpen) return null;
const colors = {
warning: {
icon: '⚠️',
iconColor: '#f59e0b',
confirmBg: '#f59e0b',
confirmHover: '#d97706'
},
danger: {
icon: '🚨',
iconColor: '#ef4444',
confirmBg: '#ef4444',
confirmHover: '#dc2626'
},
info: {
icon: 'ℹ️',
iconColor: '#3b82f6',
confirmBg: '#3b82f6',
confirmHover: '#2563eb'
}
};
const theme = colors[variant];
const backdropStyle: React.CSSProperties = {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999,
animation: 'fadeIn 0.2s ease-out'
};
const dialogStyle: React.CSSProperties = {
backgroundColor: 'white',
borderRadius: '12px',
padding: '24px',
maxWidth: '480px',
width: '90%',
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
animation: 'slideIn 0.3s ease-out'
};
const headerStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
gap: '12px',
marginBottom: '16px'
};
const iconStyle: React.CSSProperties = {
fontSize: '24px',
color: theme.iconColor
};
const titleStyle: React.CSSProperties = {
fontSize: '18px',
fontWeight: '600',
color: '#1f2937',
margin: 0
};
const messageStyle: React.CSSProperties = {
fontSize: '14px',
color: '#6b7280',
lineHeight: '1.5',
marginBottom: '24px'
};
const buttonContainerStyle: React.CSSProperties = {
display: 'flex',
gap: '12px',
justifyContent: 'flex-end'
};
const baseButtonStyle: React.CSSProperties = {
padding: '10px 20px',
borderRadius: '8px',
border: 'none',
fontSize: '14px',
fontWeight: '500',
cursor: 'pointer',
transition: 'all 0.2s ease',
minWidth: '80px'
};
const cancelButtonStyle: React.CSSProperties = {
...baseButtonStyle,
backgroundColor: '#f3f4f6',
color: '#374151',
border: '1px solid #d1d5db'
};
const confirmButtonStyle: React.CSSProperties = {
...baseButtonStyle,
backgroundColor: theme.confirmBg,
color: 'white'
};
// Handle backdrop click
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onCancel();
}
};
// Handle keyboard events
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
onCancel();
} else if (e.key === 'Enter') {
onConfirm();
}
};
return (
<>
<div
style={backdropStyle}
onClick={handleBackdropClick}
onKeyDown={handleKeyDown}
tabIndex={-1}
>
<div style={dialogStyle} role="dialog" aria-modal="true">
<div style={headerStyle}>
<span style={iconStyle}>{theme.icon}</span>
<h3 style={titleStyle}>{title}</h3>
</div>
<div style={messageStyle}>
{message}
</div>
<div style={buttonContainerStyle}>
<button
style={cancelButtonStyle}
onClick={onCancel}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#e5e7eb';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = '#f3f4f6';
}}
>
{cancelText}
</button>
<button
style={confirmButtonStyle}
onClick={onConfirm}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = theme.confirmHover;
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = theme.confirmBg;
}}
>
{confirmText}
</button>
</div>
</div>
</div>
<style>{`
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from {
opacity: 0;
transform: scale(0.95) translateY(-10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
`}</style>
</>
);
};
export default WarningDialog;

View file

@ -1,35 +1,24 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useRef } from 'react';
interface RepositoryInputProps {
onGitHubSubmit: (url: string) => void;
onZipFileSubmit: (file: File) => void;
disabled?: boolean;
githubUrl: string;
onGitHubUrlChange: (url: string) => void;
}
const RepositoryInput: React.FC<RepositoryInputProps> = ({
onGitHubSubmit,
onZipFileSubmit,
disabled = false,
githubUrl,
onGitHubUrlChange
disabled = false
}) => {
const [activeTab, setActiveTab] = useState<'github' | 'zip'>('github');
const [dragOver, setDragOver] = useState(false);
const handleGitHubSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault();
if (githubUrl.trim()) {
onGitHubSubmit(githubUrl.trim());
}
}, [githubUrl, onGitHubSubmit]);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileSelect = useCallback((file: File) => {
if (file && file.name.endsWith('.zip')) {
setSelectedFile(file);
onZipFileSubmit(file);
} else {
alert('Please select a valid ZIP file');
alert('Please select a valid ZIP file (.zip extension required)');
}
}, [onZipFileSubmit]);
@ -59,212 +48,182 @@ const RepositoryInput: React.FC<RepositoryInputProps> = ({
e.preventDefault();
setDragOver(false);
}, []);
const handleDropZoneClick = useCallback(() => {
if (!disabled && fileInputRef.current) {
fileInputRef.current.click();
}
}, [disabled]);
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
return (
<div className="repository-input">
<div className="tabs">
<button
className={`tab ${activeTab === 'github' ? 'active' : ''}`}
onClick={() => setActiveTab('github')}
disabled={disabled}
>
🐙 GitHub Repository
</button>
<button
className={`tab ${activeTab === 'zip' ? 'active' : ''}`}
onClick={() => setActiveTab('zip')}
disabled={disabled}
>
📦 ZIP File
</button>
<div className="upload-header">
<h3>📦 Upload Project ZIP File</h3>
<p>Drag and drop your project ZIP file below or click to browse</p>
</div>
{activeTab === 'github' && (
<div className="tab-content">
<form onSubmit={handleGitHubSubmit} className="github-form">
<div className="input-group">
<input
type="url"
placeholder="https://github.com/owner/repository"
value={githubUrl}
onChange={(e) => onGitHubUrlChange(e.target.value)}
disabled={disabled}
className="github-input"
required
/>
<button
type="submit"
disabled={disabled || !githubUrl.trim()}
className="submit-button"
>
🚀 Process Repository
</button>
</div>
<div className="github-help">
💡 Enter a public GitHub repository URL to analyze its codebase
</div>
</form>
</div>
)}
{activeTab === 'zip' && (
<div className="tab-content">
<div
className={`zip-drop-zone ${dragOver ? 'drag-over' : ''}`}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
>
<div className="drop-zone-content">
<div className="drop-zone-icon">📁</div>
<div
className={`zip-drop-zone ${dragOver ? 'drag-over' : ''} ${selectedFile ? 'has-file' : ''}`}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={handleDropZoneClick}
>
<div className="drop-zone-content">
{!selectedFile ? (
<>
<div className="drop-zone-icon">
{dragOver ? '📥' : '📁'}
</div>
<div className="drop-zone-text">
<div className="drop-zone-primary">
Drop a ZIP file here or click to browse
{dragOver ? 'Drop your ZIP file here!' : 'Drop a ZIP file here or click to browse'}
</div>
<div className="drop-zone-secondary">
Upload a ZIP file containing your project source code
</div>
</div>
<input
type="file"
accept=".zip"
onChange={handleFileInput}
disabled={disabled}
className="file-input"
id="zip-file-input"
/>
<label htmlFor="zip-file-input" className="file-input-label">
Browse Files
</label>
<div className="browse-button">
📂 Browse Files
</div>
</>
) : (
<div className="selected-file-info">
<div className="file-icon">✅</div>
<div className="file-details">
<div className="file-name">{selectedFile.name}</div>
<div className="file-size">{formatFileSize(selectedFile.size)}</div>
</div>
<div className="file-actions">
<button
className="change-file-button"
onClick={(e) => {
e.stopPropagation();
setSelectedFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}}
disabled={disabled}
>
🔄 Change File
</button>
</div>
</div>
</div>
)}
<input
ref={fileInputRef}
type="file"
accept=".zip"
onChange={handleFileInput}
disabled={disabled}
className="file-input"
id="zip-file-input"
/>
</div>
)}
</div>
<div className="upload-tips">
<div className="tip-item">
<span className="tip-icon">💡</span>
<span>Supported format: ZIP files only</span>
</div>
<div className="tip-item">
<span className="tip-icon">⚡</span>
<span>Processing typically takes just a few seconds</span>
</div>
<div className="tip-item">
<span className="tip-icon">🔒</span>
<span>Your files are processed locally in your browser</span>
</div>
</div>
<style>{`
.repository-input {
margin-bottom: 2rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: white;
border-radius: 12px;
overflow: hidden;
background-color: white;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.tabs {
display: flex;
background-color: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
.tab {
flex: 1;
padding: 1rem;
border: none;
background: none;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
color: #666;
transition: all 0.2s ease;
}
.tab:hover:not(:disabled) {
background-color: #eeeeee;
color: #333;
}
.tab.active {
background-color: white;
color: #007bff;
border-bottom: 2px solid #007bff;
}
.tab:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.tab-content {
padding: 1.5rem;
}
.github-form {
width: 100%;
}
.input-group {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.github-input {
flex: 1;
padding: 0.75rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
.github-input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.1);
}
.submit-button {
padding: 0.75rem 1.5rem;
background-color: #007bff;
.upload-header {
padding: 2rem 2rem 1rem 2rem;
text-align: center;
background: linear-gradient(135deg, #ff9a56 0%, #ffad56 50%, #ffc947 100%);
color: white;
border: none;
border-radius: 4px;
}
.upload-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
font-weight: 600;
}
.upload-header p {
margin: 0;
opacity: 0.9;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.submit-button:hover:not(:disabled) {
background-color: #0056b3;
}
.submit-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.github-help {
font-size: 0.9rem;
color: #666;
margin-top: 0.5rem;
}
.zip-drop-zone {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 2rem;
margin: 2rem;
border: 3px dashed #d1d5db;
border-radius: 12px;
padding: 3rem 2rem;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
transition: all 0.3s ease;
position: relative;
background: #fafafa;
}
.zip-drop-zone:hover {
border-color: #ff9a56;
background: #fff8f0;
transform: translateY(-2px);
box-shadow: 0 8px 25px -8px rgba(255, 154, 86, 0.3);
}
.zip-drop-zone:hover,
.zip-drop-zone.drag-over {
border-color: #007bff;
background-color: #f8f9fa;
border-color: #10b981;
background: #f0fdf4;
transform: scale(1.02);
box-shadow: 0 12px 30px -8px rgba(16, 185, 129, 0.4);
}
.zip-drop-zone.has-file {
border-color: #10b981;
background: #f0fdf4;
border-style: solid;
}
.drop-zone-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
gap: 1.5rem;
}
.drop-zone-icon {
font-size: 3rem;
opacity: 0.6;
font-size: 4rem;
opacity: 0.7;
transition: all 0.3s ease;
}
.zip-drop-zone:hover .drop-zone-icon,
.zip-drop-zone.drag-over .drop-zone-icon {
transform: scale(1.1);
opacity: 1;
}
.drop-zone-text {
@ -272,33 +231,135 @@ const RepositoryInput: React.FC<RepositoryInputProps> = ({
}
.drop-zone-primary {
font-size: 1.1rem;
font-weight: 500;
color: #333;
font-size: 1.25rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.drop-zone-secondary {
font-size: 0.9rem;
color: #666;
font-size: 1rem;
color: #6b7280;
}
.browse-button {
padding: 0.75rem 2rem;
background: linear-gradient(135deg, #ff9a56 0%, #ffad56 50%, #ffc947 100%);
color: white;
border-radius: 8px;
font-weight: 500;
font-size: 1rem;
transition: all 0.2s ease;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.zip-drop-zone:hover .browse-button {
transform: translateY(-1px);
box-shadow: 0 6px 12px -2px rgba(0, 0, 0, 0.15);
}
.selected-file-info {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 1rem 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
min-width: 300px;
}
.file-icon {
font-size: 2rem;
}
.file-details {
flex: 1;
text-align: left;
}
.file-name {
font-weight: 600;
color: #374151;
margin-bottom: 0.25rem;
word-break: break-word;
}
.file-size {
font-size: 0.875rem;
color: #6b7280;
}
.change-file-button {
padding: 0.5rem 1rem;
background: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
border-radius: 6px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s ease;
}
.change-file-button:hover:not(:disabled) {
background: #e5e7eb;
border-color: #9ca3af;
}
.change-file-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.file-input {
display: none;
}
.file-input-label {
padding: 0.5rem 1rem;
background-color: #007bff;
color: white;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s ease;
.upload-tips {
padding: 1.5rem 2rem 2rem 2rem;
background: #f9fafb;
border-top: 1px solid #e5e7eb;
}
.file-input-label:hover {
background-color: #0056b3;
.tip-item {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
font-size: 0.875rem;
color: #6b7280;
}
.tip-item:last-child {
margin-bottom: 0;
}
.tip-icon {
font-size: 1rem;
opacity: 0.8;
}
@media (max-width: 640px) {
.zip-drop-zone {
margin: 1rem;
padding: 2rem 1rem;
}
.upload-header {
padding: 1.5rem 1rem 1rem 1rem;
}
.selected-file-info {
flex-direction: column;
text-align: center;
gap: 1rem;
min-width: auto;
}
.file-details {
text-align: center;
}
}
`}</style>
</div>

View file

@ -2,8 +2,9 @@ import React, { useState, useCallback, useEffect } from 'react';
import ErrorBoundary from '../components/ErrorBoundary.tsx';
import { GraphExplorer } from '../components/graph/index.ts';
import { ChatInterface } from '../components/chat/index.ts';
import WarningDialog from '../components/WarningDialog.tsx';
import ExportFormatModal from '../components/ExportFormatModal.tsx';
import RepositoryInput from '../components/repository/RepositoryInput.tsx';
import type { KnowledgeGraph } from '../../core/graph/types.ts';
import { IngestionService } from '../../services/ingestion.service.ts';
import { LLMService, type LLMProvider } from '../../ai/llm-service.ts';
@ -23,7 +24,6 @@ interface AppState {
showExportModal: boolean;
// Input State
githubUrl: string;
directoryFilter: string;
fileExtensions: string;
@ -39,8 +39,6 @@ interface AppState {
azureOpenAIEndpoint: string;
azureOpenAIDeploymentName: string;
azureOpenAIApiVersion: string;
// GitHub settings
githubToken: string;
showSettings: boolean;
}
@ -52,7 +50,6 @@ const initialState: AppState = {
isLoading: false,
showStats: false,
showExportModal: false,
githubUrl: '',
directoryFilter: 'src,lib,components,pages,utils',
fileExtensions: '.ts,.tsx,.js,.jsx,.py,.java,.cpp,.c,.cs,.php,.rb,.go,.rs,.swift,.kt,.scala,.clj,.hs,.ml,.fs,.elm,.dart,.lua,.r,.m,.sh,.sql,.html,.css,.scss,.less,.vue,.svelte',
isProcessing: false,
@ -63,12 +60,12 @@ const initialState: AppState = {
azureOpenAIEndpoint: localStorage.getItem('azure_openai_endpoint') || '',
azureOpenAIDeploymentName: localStorage.getItem('azure_openai_deployment') || '',
azureOpenAIApiVersion: localStorage.getItem('azure_openai_api_version') || '2024-02-01',
githubToken: localStorage.getItem('github_token') || '',
showSettings: false
};
const HomePage: React.FC = () => {
const [state, setState] = useState<AppState>(initialState);
const [showNewProjectWarning, setShowNewProjectWarning] = useState(false);
const [services] = useState(() => ({
ingestion: new IngestionService(),
llm: new LLMService()
@ -148,63 +145,27 @@ const HomePage: React.FC = () => {
}
};
const handleGitHubProcess = async () => {
if (!state.githubUrl.trim()) {
updateState({ error: 'Please enter a GitHub repository URL' });
return;
}
try {
updateState({
isProcessing: true,
error: '',
progress: 'Fetching repository...',
showWelcome: false
});
console.log('Starting GitHub processing...', state.githubUrl);
// Create ingestion service with GitHub token if provided
const ingestionService = new IngestionService(state.githubToken || undefined);
const result = await ingestionService.processGitHubRepo(state.githubUrl, {
directoryFilter: state.directoryFilter,
fileExtensions: state.fileExtensions,
onProgress: (progress) => {
console.log('Progress:', progress);
updateState({ progress });
}
});
console.log('GitHub processing completed:', {
nodeCount: result.graph?.nodes?.length || 0,
relationshipCount: result.graph?.relationships?.length || 0,
fileCount: result.fileContents?.size || 0
});
updateState({
graph: result.graph,
fileContents: result.fileContents,
isProcessing: false,
progress: '',
showWelcome: false // Ensure we stay in main interface
});
} catch (error) {
console.error('GitHub processing error:', error);
updateState({
error: error instanceof Error ? error.message : 'Failed to process repository',
isProcessing: false,
progress: '',
showWelcome: true // Return to welcome screen on error
});
const handleNewProject = () => {
// Show warning dialog if there's existing data
if (state.graph && (state.graph.nodes.length > 0 || state.graph.relationships.length > 0)) {
setShowNewProjectWarning(true);
} else {
// No data to lose, proceed directly
performNewProject();
}
};
const handleNewProject = () => {
const performNewProject = () => {
updateState({
...initialState,
showStats: false
});
setShowNewProjectWarning(false);
};
const cancelNewProject = () => {
setShowNewProjectWarning(false);
};
const handleDownloadGraph = () => {
@ -266,23 +227,23 @@ const HomePage: React.FC = () => {
})();
const isGraphValid = state.graph && state.graph.nodes && Array.isArray(state.graph.nodes) && state.graph.relationships && Array.isArray(state.graph.relationships);
// Warm tone color palette
// Orange-Yellow tone color palette
const colors = {
background: '#FDF6E3', // Warm cream
surface: '#FFFFFF',
surfaceWarm: '#FEF9F0', // Slightly warm white
primary: '#D97706', // Warm orange
primaryLight: '#F59E0B', // Light orange
secondary: '#92400E', // Dark orange
accent: '#DC2626', // Warm red
text: '#451A03', // Dark brown
textSecondary: '#78350F', // Medium brown
textMuted: '#A16207', // Light brown
border: '#FED7AA', // Light orange
borderLight: '#FEF3C7', // Very light orange
success: '#059669', // Warm green
warning: '#D97706', // Orange
error: '#DC2626' // Red
surfaceWarm: '#FFF8F0', // Warm white with orange tint
primary: '#ff9a56', // Orange
primaryLight: '#ffad56', // Light orange
secondary: '#ffc947', // Yellow
accent: '#ff8c42', // Bright orange
text: '#8B4513', // Saddle brown
textSecondary: '#A0522D', // Sienna
textMuted: '#CD853F', // Peru
border: '#FFD4A3', // Light orange
borderLight: '#FFF2E6', // Very light orange
success: '#32CD32', // Lime green
warning: '#ff9a56', // Orange
error: '#FF6347' // Tomato red
};
// Modern styles with warm theme
@ -303,7 +264,7 @@ const HomePage: React.FC = () => {
alignItems: 'center',
justifyContent: 'center',
padding: '12px 24px',
backgroundColor: colors.surface,
background: colors.background,
borderBottom: `1px solid ${colors.borderLight}`,
boxShadow: '0 1px 3px rgba(0,0,0,0.05)',
position: 'relative' as const
@ -315,7 +276,7 @@ const HomePage: React.FC = () => {
gap: '16px',
fontSize: '14px',
fontWeight: '500',
color: colors.textSecondary
color: 'white'
},
navbarButton: {
@ -340,7 +301,7 @@ const HomePage: React.FC = () => {
left: 0,
right: 0,
bottom: 0,
backgroundColor: colors.background,
background: colors.background,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
@ -531,95 +492,10 @@ const HomePage: React.FC = () => {
</div>
)}
<div style={styles.inputSection}>
<div style={styles.inputGroup}>
<label style={styles.label}>GitHub Repository URL</label>
<input
type="text"
value={state.githubUrl}
onChange={(e) => updateState({ githubUrl: e.target.value })}
placeholder="https://github.com/owner/repo"
style={styles.input}
disabled={state.isProcessing}
/>
{/* GitHub Token Input - Optional */}
<div style={{ marginTop: '12px' }}>
<label style={{...styles.label, color: colors.textMuted}}>
GitHub Personal Access Token (Optional)
</label>
<input
type="password"
value={state.githubToken}
onChange={(e) => updateState({ githubToken: e.target.value })}
placeholder="ghp_xxxxxxxxxxxxxxxxxxxx"
style={{
...styles.input,
borderColor: colors.borderLight,
backgroundColor: colors.surface
}}
disabled={state.isProcessing}
/>
<div style={{
fontSize: '11px',
color: colors.textMuted,
marginTop: '4px',
lineHeight: '1.4'
}}>
Increases rate limit from 60 to 5,000 requests/hour.
<a href="https://github.com/settings/tokens" target="_blank" rel="noopener noreferrer"
style={{ color: colors.primary, textDecoration: 'none', marginLeft: '4px' }}>
Generate token
</a>
</div>
</div>
{/* GitHub Token Status */}
<div style={{
fontSize: '12px',
color: state.githubToken ? colors.success : colors.textMuted,
marginTop: '8px',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}>
<span>{state.githubToken ? '🔑' : '⚠️'}</span>
{state.githubToken
? 'GitHub token configured (5,000 requests/hour)'
: 'No GitHub token (60 requests/hour limit)'}
</div>
<button
onClick={handleGitHubProcess}
disabled={state.isProcessing || !state.githubUrl.trim()}
style={{
...styles.primaryButton,
opacity: state.isProcessing || !state.githubUrl.trim() ? 0.5 : 1,
marginTop: '16px'
}}
>
<span>📊</span>
Analyze Repository
</button>
</div>
<div style={styles.orDivider}>
<div style={styles.orLine}></div>
<span>OR</span>
<div style={styles.orLine}></div>
</div>
<div style={styles.inputGroup}>
<label style={styles.label}>Upload ZIP File</label>
<input
type="file"
accept=".zip"
onChange={handleFileUpload}
disabled={state.isProcessing}
style={styles.input}
/>
</div>
</div>
<RepositoryInput
onZipFileSubmit={(file) => handleFileUpload({ target: { files: [file] } } as any)}
disabled={state.isProcessing}
/>
<button
onClick={() => updateState({ showSettings: true })}
@ -1152,6 +1028,20 @@ const HomePage: React.FC = () => {
: 'project'
}
/>
{/* New Project Warning Dialog */}
<WarningDialog
isOpen={showNewProjectWarning}
title="Start New Project"
message="Starting a new project will permanently delete your current knowledge graph and all analyzed data. This action cannot be undone. Are you sure you want to continue?
Don't worry though - you can always re-upload your previous ZIP file if needed. It only takes a few seconds! 😊"
confirmText="Yes, Start New Project"
cancelText="Cancel"
variant="warning"
onConfirm={performNewProject}
onCancel={cancelNewProject}
/>
</div>
</ErrorBoundary>
);

View file

@ -1,8 +1,9 @@
import React from 'react';
import React, { useState } from 'react';
import ErrorBoundary from '../../components/ErrorBoundary';
import { GraphExplorer } from '../../components/graph';
import { ChatInterface } from '../../components/chat';
import ExportFormatModal from '../../components/ExportFormatModal';
import WarningDialog from '../../components/WarningDialog';
// Engine components removed - using simplified parsing mode toggle
import RepositoryInput from '../../components/repository/RepositoryInput';
import { useGitNexus } from '../../hooks/useGitNexus';
@ -16,23 +17,18 @@ import type { ExportFormat } from '../../components/ExportFormatModal';
*/
const HomePage: React.FC = () => {
const featureFlags = getFeatureFlags();
const [showNewAnalysisWarning, setShowNewAnalysisWarning] = useState(false);
const {
state,
processing,
settings,
handleNodeSelect,
handleGitHubProcess,
handleZipProcess,
toggleStats,
toggleExportModal,
setShowWelcome
} = useGitNexus();
const handleGitHubUrlChange = (url: string) => {
// URL changes are handled locally in the component
// Could be extracted to settings if needed for persistence
};
// Engine change handler removed - parsing mode controlled via .env
const handleExport = async (format: ExportFormat) => {
@ -52,8 +48,23 @@ const HomePage: React.FC = () => {
};
const handleClearResults = () => {
// Show warning dialog if there's existing data
if (state.graph && (state.graph.nodes.length > 0 || state.graph.relationships.length > 0)) {
setShowNewAnalysisWarning(true);
} else {
// No data to lose, proceed directly
performNewAnalysis();
}
};
const performNewAnalysis = () => {
processing.clearResult();
setShowWelcome(true);
setShowNewAnalysisWarning(false);
};
const cancelNewAnalysis = () => {
setShowNewAnalysisWarning(false);
};
// Show welcome screen if no graph and not processing
@ -72,11 +83,8 @@ const HomePage: React.FC = () => {
</div>
<RepositoryInput
onGitHubSubmit={handleGitHubProcess}
onZipFileSubmit={handleZipProcess}
disabled={processing.state.isProcessing}
githubUrl="" // Local state in component
onGitHubUrlChange={handleGitHubUrlChange}
/>
{/* Engine selector removed - parsing mode controlled via .env */}
@ -87,7 +95,7 @@ const HomePage: React.FC = () => {
<style>{`
.app {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #ff9a56 0%, #ffad56 50%, #ffc947 100%);
padding: 2rem;
}
@ -447,6 +455,20 @@ const HomePage: React.FC = () => {
}
}
`}</style>
{/* New Analysis Warning Dialog */}
<WarningDialog
isOpen={showNewAnalysisWarning}
title="Start New Analysis"
message="Starting a new analysis will permanently delete your current knowledge graph and all analyzed data. This action cannot be undone. Are you sure you want to continue?
Chill though - you can always re-upload your previous ZIP file if you change your mind. It only takes a few seconds! 🚀"
confirmText="Yes, Start New Analysis"
cancelText="Cancel"
variant="warning"
onConfirm={performNewAnalysis}
onCancel={cancelNewAnalysis}
/>
</div>
</ErrorBoundary>
);