From d0bd0b8ee7da2ae1a40f35575263c931d6e42adc Mon Sep 17 00:00:00 2001 From: abhigyantrumio Date: Thu, 25 Sep 2025 07:36:59 +0530 Subject: [PATCH] Homepage UI fixes --- src/services/database-reset.service.ts | 124 +++++ src/services/ingestion.service.ts | 7 + src/ui/components/WarningDialog.tsx | 213 ++++++++ .../components/repository/RepositoryInput.tsx | 455 ++++++++++-------- src/ui/pages/HomePage.tsx | 212 ++------ src/ui/pages/HomePage/HomePage.tsx | 44 +- 6 files changed, 686 insertions(+), 369 deletions(-) create mode 100644 src/services/database-reset.service.ts create mode 100644 src/ui/components/WarningDialog.tsx diff --git a/src/services/database-reset.service.ts b/src/services/database-reset.service.ts new file mode 100644 index 000000000..90874dc1e --- /dev/null +++ b/src/services/database-reset.service.ts @@ -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 { + 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 { + 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 { + 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(); diff --git a/src/services/ingestion.service.ts b/src/services/ingestion.service.ts index 7d375b004..0c7c546d2 100644 --- a/src/services/ingestion.service.ts +++ b/src/services/ingestion.service.ts @@ -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 { 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 { 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) diff --git a/src/ui/components/WarningDialog.tsx b/src/ui/components/WarningDialog.tsx new file mode 100644 index 000000000..735886a07 --- /dev/null +++ b/src/ui/components/WarningDialog.tsx @@ -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 = ({ + 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 ( + <> +
+
+
+ {theme.icon} +

{title}

+
+ +
+ {message} +
+ +
+ + +
+
+
+ + + + ); +}; + +export default WarningDialog; diff --git a/src/ui/components/repository/RepositoryInput.tsx b/src/ui/components/repository/RepositoryInput.tsx index d898896f1..b76c4e88d 100644 --- a/src/ui/components/repository/RepositoryInput.tsx +++ b/src/ui/components/repository/RepositoryInput.tsx @@ -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 = ({ - 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(null); + const fileInputRef = useRef(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 = ({ 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 (
-
- - +
+

๐Ÿ“ฆ Upload Project ZIP File

+

Drag and drop your project ZIP file below or click to browse

- {activeTab === 'github' && ( -
-
-
- onGitHubUrlChange(e.target.value)} - disabled={disabled} - className="github-input" - required - /> - -
-
- ๐Ÿ’ก Enter a public GitHub repository URL to analyze its codebase -
-
-
- )} - - {activeTab === 'zip' && ( -
-
-
-
๐Ÿ“
+
+
+ {!selectedFile ? ( + <> +
+ {dragOver ? '๐Ÿ“ฅ' : '๐Ÿ“'} +
- Drop a ZIP file here or click to browse + {dragOver ? 'Drop your ZIP file here!' : 'Drop a ZIP file here or click to browse'}
Upload a ZIP file containing your project source code
- - +
+ ๐Ÿ“‚ Browse Files +
+ + ) : ( +
+
โœ…
+
+
{selectedFile.name}
+
{formatFileSize(selectedFile.size)}
+
+
+ +
-
+ )} + +
- )} +
+ +
+
+ ๐Ÿ’ก + Supported format: ZIP files only +
+
+ โšก + Processing typically takes just a few seconds +
+
+ ๐Ÿ”’ + Your files are processed locally in your browser +
+
diff --git a/src/ui/pages/HomePage.tsx b/src/ui/pages/HomePage.tsx index dea60b6f8..da89cd784 100644 --- a/src/ui/pages/HomePage.tsx +++ b/src/ui/pages/HomePage.tsx @@ -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(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 = () => {
)} -
-
- - updateState({ githubUrl: e.target.value })} - placeholder="https://github.com/owner/repo" - style={styles.input} - disabled={state.isProcessing} - /> - - {/* GitHub Token Input - Optional */} -
- - updateState({ githubToken: e.target.value })} - placeholder="ghp_xxxxxxxxxxxxxxxxxxxx" - style={{ - ...styles.input, - borderColor: colors.borderLight, - backgroundColor: colors.surface - }} - disabled={state.isProcessing} - /> -
- Increases rate limit from 60 to 5,000 requests/hour. - - Generate token - -
-
- - {/* GitHub Token Status */} -
- {state.githubToken ? '๐Ÿ”‘' : 'โš ๏ธ'} - {state.githubToken - ? 'GitHub token configured (5,000 requests/hour)' - : 'No GitHub token (60 requests/hour limit)'} -
- - -
- -
-
- OR -
-
- -
- - -
-
+ handleFileUpload({ target: { files: [file] } } as any)} + disabled={state.isProcessing} + />
); diff --git a/src/ui/pages/HomePage/HomePage.tsx b/src/ui/pages/HomePage/HomePage.tsx index ee9f8ead8..0d02ce86e 100644 --- a/src/ui/pages/HomePage/HomePage.tsx +++ b/src/ui/pages/HomePage/HomePage.tsx @@ -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 = () => {
{/* Engine selector removed - parsing mode controlled via .env */} @@ -87,7 +95,7 @@ const HomePage: React.FC = () => { + + {/* New Analysis Warning Dialog */} + );