diff --git a/src/components/error/AIErrorHelper.tsx b/src/components/error/AIErrorHelper.tsx index 2c77e5c..4920729 100644 --- a/src/components/error/AIErrorHelper.tsx +++ b/src/components/error/AIErrorHelper.tsx @@ -10,6 +10,9 @@ import { import { Alert, AlertDescription } from '@/components/ui/alert' import { Sparkle } from '@phosphor-icons/react' import { motion, AnimatePresence } from 'framer-motion' +import { analyzeErrorWithAI } from './analyzeError' +import { MarkdownRenderer } from './MarkdownRenderer' +import { LoadingAnalysis } from './LoadingAnalysis' interface AIErrorHelperProps { error: Error | string @@ -33,20 +36,7 @@ export function AIErrorHelper({ error, context, className }: AIErrorHelperProps) setAnalysis('') try { - const contextInfo = context ? `\n\nContext: ${context}` : '' - const stackInfo = errorStack ? `\n\nStack trace: ${errorStack}` : '' - - const prompt = (window.spark.llmPrompt as any)`You are a helpful debugging assistant for a code snippet manager app. Analyze this error and provide: - -1. A clear explanation of what went wrong (in plain language) -2. Why this error likely occurred -3. 2-3 specific actionable steps to fix it - -Error message: ${errorMessage}${contextInfo}${stackInfo} - -Keep your response concise, friendly, and focused on practical solutions. Format your response with clear sections using markdown.` - - const result = await window.spark.llm(prompt, 'gpt-4o-mini') + const result = await analyzeErrorWithAI(errorMessage, errorStack, context) setAnalysis(result) } catch (err) { setAnalysisError('Unable to analyze error. The AI service may be temporarily unavailable.') @@ -98,30 +88,7 @@ Keep your response concise, friendly, and focused on practical solutions. Format - {isAnalyzing && ( -
-
- - - - Analyzing error... -
-
- {[1, 2, 3].map((i) => ( - - ))} -
-
- )} + {isAnalyzing && } {analysisError && ( @@ -130,55 +97,7 @@ Keep your response concise, friendly, and focused on practical solutions. Format )} - {analysis && ( - -
- {analysis.split('\n').map((line, idx) => { - if (line.startsWith('###')) { - return ( -

- {line.replace('###', '').trim()} -

- ) - } - if (line.startsWith('##')) { - return ( -

- {line.replace('##', '').trim()} -

- ) - } - if (line.match(/^\d+\./)) { - return ( -
- {line} -
- ) - } - if (line.startsWith('-')) { - return ( -
- {line} -
- ) - } - if (line.trim()) { - return ( -

- {line} -

- ) - } - return null - })} -
-
- )} + {analysis && }
diff --git a/src/components/error/LoadingAnalysis.tsx b/src/components/error/LoadingAnalysis.tsx new file mode 100644 index 0000000..64b0f0e --- /dev/null +++ b/src/components/error/LoadingAnalysis.tsx @@ -0,0 +1,29 @@ +import { motion } from 'framer-motion' +import { Sparkle } from '@phosphor-icons/react' + +export function LoadingAnalysis() { + return ( +
+
+ + + + Analyzing error... +
+
+ {[1, 2, 3].map((i) => ( + + ))} +
+
+ ) +} diff --git a/src/components/error/MarkdownRenderer.tsx b/src/components/error/MarkdownRenderer.tsx new file mode 100644 index 0000000..434d02d --- /dev/null +++ b/src/components/error/MarkdownRenderer.tsx @@ -0,0 +1,57 @@ +import { motion } from 'framer-motion' + +interface MarkdownRendererProps { + content: string +} + +export function MarkdownRenderer({ content }: MarkdownRendererProps) { + return ( + +
+ {content.split('\n').map((line, idx) => { + if (line.startsWith('###')) { + return ( +

+ {line.replace('###', '').trim()} +

+ ) + } + if (line.startsWith('##')) { + return ( +

+ {line.replace('##', '').trim()} +

+ ) + } + if (line.match(/^\d+\./)) { + return ( +
+ {line} +
+ ) + } + if (line.startsWith('-')) { + return ( +
+ {line} +
+ ) + } + if (line.trim()) { + return ( +

+ {line} +

+ ) + } + return null + })} +
+
+ ) +} diff --git a/src/components/error/analyzeError.ts b/src/components/error/analyzeError.ts new file mode 100644 index 0000000..de4c0fb --- /dev/null +++ b/src/components/error/analyzeError.ts @@ -0,0 +1,21 @@ +export async function analyzeErrorWithAI( + errorMessage: string, + errorStack?: string, + context?: string +): Promise { + const contextInfo = context ? `\n\nContext: ${context}` : '' + const stackInfo = errorStack ? `\n\nStack trace: ${errorStack}` : '' + + const prompt = (window.spark.llmPrompt as any)`You are a helpful debugging assistant for a code snippet manager app. Analyze this error and provide: + +1. A clear explanation of what went wrong (in plain language) +2. Why this error likely occurred +3. 2-3 specific actionable steps to fix it + +Error message: ${errorMessage}${contextInfo}${stackInfo} + +Keep your response concise, friendly, and focused on practical solutions. Format your response with clear sections using markdown.` + + const result = await window.spark.llm(prompt, 'gpt-4o-mini') + return result +} diff --git a/src/hooks/useDatabaseOperations.ts b/src/hooks/useDatabaseOperations.ts new file mode 100644 index 0000000..ef74549 --- /dev/null +++ b/src/hooks/useDatabaseOperations.ts @@ -0,0 +1,138 @@ +import { useState, useCallback } from 'react' +import { toast } from 'sonner' +import { + getDatabaseStats, + exportDatabase, + importDatabase, + clearDatabase, + seedDatabase, + validateDatabaseSchema +} from '@/lib/db' + +export function useDatabaseOperations() { + const [stats, setStats] = useState<{ + snippetCount: number + templateCount: number + storageType: 'indexeddb' | 'localstorage' | 'none' + databaseSize: number + } | null>(null) + const [loading, setLoading] = useState(true) + const [schemaHealth, setSchemaHealth] = useState<'unknown' | 'healthy' | 'corrupted'>('unknown') + const [checkingSchema, setCheckingSchema] = useState(false) + + const loadStats = useCallback(async () => { + setLoading(true) + try { + const data = await getDatabaseStats() + setStats(data) + } catch (error) { + console.error('Failed to load stats:', error) + toast.error('Failed to load database statistics') + } finally { + setLoading(false) + } + }, []) + + const checkSchemaHealth = useCallback(async () => { + setCheckingSchema(true) + try { + const result = await validateDatabaseSchema() + setSchemaHealth(result.valid ? 'healthy' : 'corrupted') + + if (!result.valid) { + console.warn('Schema validation failed:', result.issues) + } + } catch (error) { + console.error('Schema check failed:', error) + setSchemaHealth('corrupted') + } finally { + setCheckingSchema(false) + } + }, []) + + const handleExport = useCallback(async () => { + try { + const data = await exportDatabase() + const blob = new Blob([new Uint8Array(data)], { type: 'application/octet-stream' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `codesnippet-backup-${Date.now()}.db` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + toast.success('Database exported successfully') + } catch (error) { + console.error('Failed to export:', error) + toast.error('Failed to export database') + } + }, []) + + const handleImport = useCallback(async (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (!file) return + + try { + const arrayBuffer = await file.arrayBuffer() + const data = new Uint8Array(arrayBuffer) + await importDatabase(data) + toast.success('Database imported successfully') + await loadStats() + } catch (error) { + console.error('Failed to import:', error) + toast.error('Failed to import database') + } + + event.target.value = '' + }, [loadStats]) + + const handleClear = useCallback(async () => { + if (!confirm('Are you sure you want to clear all data? This cannot be undone.')) { + return + } + + try { + await clearDatabase() + toast.success('Database cleared and schema recreated successfully') + await loadStats() + await checkSchemaHealth() + } catch (error) { + console.error('Failed to clear:', error) + toast.error('Failed to clear database') + } + }, [loadStats, checkSchemaHealth]) + + const handleSeed = useCallback(async () => { + try { + await seedDatabase() + toast.success('Sample data added successfully') + await loadStats() + } catch (error) { + console.error('Failed to seed:', error) + toast.error('Failed to add sample data') + } + }, [loadStats]) + + const formatBytes = useCallback((bytes: number) => { + 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 Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i] + }, []) + + return { + stats, + loading, + schemaHealth, + checkingSchema, + loadStats, + checkSchemaHealth, + handleExport, + handleImport, + handleClear, + handleSeed, + formatBytes, + } +} diff --git a/src/hooks/useSettingsState.ts b/src/hooks/useSettingsState.ts index 4fd97b7..cf99495 100644 --- a/src/hooks/useSettingsState.ts +++ b/src/hooks/useSettingsState.ts @@ -1,260 +1,54 @@ -import { useState, useEffect } from 'react' -import { toast } from 'sonner' -import { - getDatabaseStats, - exportDatabase, - importDatabase, - clearDatabase, - seedDatabase, - getAllSnippets, - validateDatabaseSchema -} from '@/lib/db' -import { - saveStorageConfig, - loadStorageConfig, - FlaskStorageAdapter, - type StorageBackend -} from '@/lib/storage' +import { useEffect } from 'react' +import { useDatabaseOperations } from './useDatabaseOperations' +import { useStorageConfig } from './useStorageConfig' +import { useStorageMigration } from './useStorageMigration' export function useSettingsState() { - const [stats, setStats] = useState<{ - snippetCount: number - templateCount: number - storageType: 'indexeddb' | 'localstorage' | 'none' - databaseSize: number - } | null>(null) - const [loading, setLoading] = useState(true) - const [storageBackend, setStorageBackend] = useState('indexeddb') - const [flaskUrl, setFlaskUrl] = useState('') - const [flaskConnectionStatus, setFlaskConnectionStatus] = useState<'unknown' | 'connected' | 'failed'>('unknown') - const [testingConnection, setTestingConnection] = useState(false) - const [envVarSet, setEnvVarSet] = useState(false) - const [schemaHealth, setSchemaHealth] = useState<'unknown' | 'healthy' | 'corrupted'>('unknown') - const [checkingSchema, setCheckingSchema] = useState(false) + const { + stats, + loading, + schemaHealth, + checkingSchema, + loadStats, + checkSchemaHealth, + handleExport, + handleImport, + handleClear, + handleSeed, + formatBytes, + } = useDatabaseOperations() - const loadStats = async () => { - setLoading(true) - try { - const data = await getDatabaseStats() - setStats(data) - } catch (error) { - console.error('Failed to load stats:', error) - toast.error('Failed to load database statistics') - } finally { - setLoading(false) - } - } + const { + storageBackend, + setStorageBackend, + flaskUrl, + setFlaskUrl, + flaskConnectionStatus, + setFlaskConnectionStatus, + testingConnection, + envVarSet, + loadConfig, + handleTestConnection, + handleSaveStorageConfig: saveConfig, + } = useStorageConfig() - const testFlaskConnection = async (url: string) => { - setTestingConnection(true) - try { - const adapter = new FlaskStorageAdapter(url) - const connected = await adapter.testConnection() - setFlaskConnectionStatus(connected ? 'connected' : 'failed') - return connected - } catch (error) { - console.error('Connection test failed:', error) - setFlaskConnectionStatus('failed') - return false - } finally { - setTestingConnection(false) - } - } - - const checkSchemaHealth = async () => { - setCheckingSchema(true) - try { - const result = await validateDatabaseSchema() - setSchemaHealth(result.valid ? 'healthy' : 'corrupted') - - if (!result.valid) { - console.warn('Schema validation failed:', result.issues) - } - } catch (error) { - console.error('Schema check failed:', error) - setSchemaHealth('corrupted') - } finally { - setCheckingSchema(false) - } - } + const { + handleMigrateToFlask: migrateToFlask, + handleMigrateToIndexedDB, + } = useStorageMigration() useEffect(() => { loadStats() checkSchemaHealth() - const config = loadStorageConfig() - - const envFlaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL - const isEnvSet = Boolean(envFlaskUrl) - setEnvVarSet(isEnvSet) - - setStorageBackend(config.backend) - setFlaskUrl(config.flaskUrl || envFlaskUrl || 'http://localhost:5000') - }, []) - - const handleExport = async () => { - try { - const data = await exportDatabase() - const blob = new Blob([new Uint8Array(data)], { type: 'application/octet-stream' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = `codesnippet-backup-${Date.now()}.db` - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(url) - toast.success('Database exported successfully') - } catch (error) { - console.error('Failed to export:', error) - toast.error('Failed to export database') - } - } - - const handleImport = async (event: React.ChangeEvent) => { - const file = event.target.files?.[0] - if (!file) return - - try { - const arrayBuffer = await file.arrayBuffer() - const data = new Uint8Array(arrayBuffer) - await importDatabase(data) - toast.success('Database imported successfully') - await loadStats() - } catch (error) { - console.error('Failed to import:', error) - toast.error('Failed to import database') - } - - event.target.value = '' - } - - const handleClear = async () => { - if (!confirm('Are you sure you want to clear all data? This cannot be undone.')) { - return - } - - try { - await clearDatabase() - toast.success('Database cleared and schema recreated successfully') - await loadStats() - await checkSchemaHealth() - } catch (error) { - console.error('Failed to clear:', error) - toast.error('Failed to clear database') - } - } - - const handleSeed = async () => { - try { - await seedDatabase() - toast.success('Sample data added successfully') - await loadStats() - } catch (error) { - console.error('Failed to seed:', error) - toast.error('Failed to add sample data') - } - } - - const formatBytes = (bytes: number) => { - 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 Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i] - } - - const handleTestConnection = async () => { - await testFlaskConnection(flaskUrl) - } + loadConfig() + }, [loadStats, checkSchemaHealth, loadConfig]) const handleSaveStorageConfig = async () => { - if (storageBackend === 'flask') { - if (!flaskUrl) { - toast.error('Please enter a Flask backend URL') - return - } - - const connected = await testFlaskConnection(flaskUrl) - if (!connected) { - toast.error('Cannot connect to Flask backend. Please check the URL and ensure the server is running.') - return - } - } - - saveStorageConfig({ - backend: storageBackend, - flaskUrl: storageBackend === 'flask' ? flaskUrl : undefined - }) - - toast.success('Storage backend updated successfully') - await loadStats() + await saveConfig(loadStats) } const handleMigrateToFlask = async () => { - if (!flaskUrl) { - toast.error('Please enter a Flask backend URL') - return - } - - try { - const adapter = new FlaskStorageAdapter(flaskUrl) - const connected = await adapter.testConnection() - - if (!connected) { - toast.error('Cannot connect to Flask backend') - return - } - - const snippets = await getAllSnippets() - - if (snippets.length === 0) { - toast.info('No snippets to migrate') - return - } - - await adapter.migrateFromIndexedDB(snippets) - - saveStorageConfig({ - backend: 'flask', - flaskUrl - }) - - toast.success(`Successfully migrated ${snippets.length} snippets to Flask backend`) - await loadStats() - } catch (error) { - console.error('Migration failed:', error) - toast.error('Failed to migrate data to Flask backend') - } - } - - const handleMigrateToIndexedDB = async () => { - if (!flaskUrl) { - toast.error('Please enter a Flask backend URL') - return - } - - try { - const adapter = new FlaskStorageAdapter(flaskUrl) - const snippets = await adapter.migrateToIndexedDB() - - if (snippets.length === 0) { - toast.info('No snippets to migrate') - return - } - - saveStorageConfig({ - backend: 'indexeddb' - }) - - // Full page reload is necessary here to reinitialize the database layer - // with the new backend after migration from Flask to IndexedDB - window.location.reload() - - toast.success(`Successfully migrated ${snippets.length} snippets to IndexedDB`) - } catch (error) { - console.error('Migration failed:', error) - toast.error('Failed to migrate data from Flask backend') - } + await migrateToFlask(flaskUrl, loadStats) } return { @@ -282,3 +76,4 @@ export function useSettingsState() { checkSchemaHealth, } } + diff --git a/src/hooks/useStorageConfig.ts b/src/hooks/useStorageConfig.ts new file mode 100644 index 0000000..0d66b7d --- /dev/null +++ b/src/hooks/useStorageConfig.ts @@ -0,0 +1,86 @@ +import { useState, useCallback } from 'react' +import { toast } from 'sonner' +import { + saveStorageConfig, + loadStorageConfig, + FlaskStorageAdapter, + type StorageBackend +} from '@/lib/storage' + +export function useStorageConfig() { + const [storageBackend, setStorageBackend] = useState('indexeddb') + const [flaskUrl, setFlaskUrl] = useState('') + const [flaskConnectionStatus, setFlaskConnectionStatus] = useState<'unknown' | 'connected' | 'failed'>('unknown') + const [testingConnection, setTestingConnection] = useState(false) + const [envVarSet, setEnvVarSet] = useState(false) + + const testFlaskConnection = useCallback(async (url: string) => { + setTestingConnection(true) + try { + const adapter = new FlaskStorageAdapter(url) + const connected = await adapter.testConnection() + setFlaskConnectionStatus(connected ? 'connected' : 'failed') + return connected + } catch (error) { + console.error('Connection test failed:', error) + setFlaskConnectionStatus('failed') + return false + } finally { + setTestingConnection(false) + } + }, []) + + const loadConfig = useCallback(() => { + const config = loadStorageConfig() + const envFlaskUrl = import.meta.env.VITE_FLASK_BACKEND_URL + const isEnvSet = Boolean(envFlaskUrl) + + setEnvVarSet(isEnvSet) + setStorageBackend(config.backend) + setFlaskUrl(config.flaskUrl || envFlaskUrl || 'http://localhost:5000') + }, []) + + const handleTestConnection = useCallback(async () => { + await testFlaskConnection(flaskUrl) + }, [flaskUrl, testFlaskConnection]) + + const handleSaveStorageConfig = useCallback(async (onSuccess?: () => Promise) => { + if (storageBackend === 'flask') { + if (!flaskUrl) { + toast.error('Please enter a Flask backend URL') + return + } + + const connected = await testFlaskConnection(flaskUrl) + if (!connected) { + toast.error('Cannot connect to Flask backend. Please check the URL and ensure the server is running.') + return + } + } + + saveStorageConfig({ + backend: storageBackend, + flaskUrl: storageBackend === 'flask' ? flaskUrl : undefined + }) + + toast.success('Storage backend updated successfully') + + if (onSuccess) { + await onSuccess() + } + }, [storageBackend, flaskUrl, testFlaskConnection]) + + return { + storageBackend, + setStorageBackend, + flaskUrl, + setFlaskUrl, + flaskConnectionStatus, + setFlaskConnectionStatus, + testingConnection, + envVarSet, + loadConfig, + handleTestConnection, + handleSaveStorageConfig, + } +} diff --git a/src/hooks/useStorageMigration.ts b/src/hooks/useStorageMigration.ts new file mode 100644 index 0000000..96d1486 --- /dev/null +++ b/src/hooks/useStorageMigration.ts @@ -0,0 +1,84 @@ +import { useCallback } from 'react' +import { toast } from 'sonner' +import { getAllSnippets } from '@/lib/db' +import { + saveStorageConfig, + FlaskStorageAdapter +} from '@/lib/storage' + +export function useStorageMigration() { + const handleMigrateToFlask = useCallback(async (flaskUrl: string, onSuccess?: () => Promise) => { + if (!flaskUrl) { + toast.error('Please enter a Flask backend URL') + return + } + + try { + const adapter = new FlaskStorageAdapter(flaskUrl) + const connected = await adapter.testConnection() + + if (!connected) { + toast.error('Cannot connect to Flask backend') + return + } + + const snippets = await getAllSnippets() + + if (snippets.length === 0) { + toast.info('No snippets to migrate') + return + } + + await adapter.migrateFromIndexedDB(snippets) + + saveStorageConfig({ + backend: 'flask', + flaskUrl + }) + + toast.success(`Successfully migrated ${snippets.length} snippets to Flask backend`) + + if (onSuccess) { + await onSuccess() + } + } catch (error) { + console.error('Migration failed:', error) + toast.error('Failed to migrate data to Flask backend') + } + }, []) + + const handleMigrateToIndexedDB = useCallback(async (flaskUrl: string) => { + if (!flaskUrl) { + toast.error('Please enter a Flask backend URL') + return + } + + try { + const adapter = new FlaskStorageAdapter(flaskUrl) + const snippets = await adapter.migrateToIndexedDB() + + if (snippets.length === 0) { + toast.info('No snippets to migrate') + return + } + + saveStorageConfig({ + backend: 'indexeddb' + }) + + // Full page reload is necessary here to reinitialize the database layer + // with the new backend after migration from Flask to IndexedDB + window.location.reload() + + toast.success(`Successfully migrated ${snippets.length} snippets to IndexedDB`) + } catch (error) { + console.error('Migration failed:', error) + toast.error('Failed to migrate data from Flask backend') + } + }, []) + + return { + handleMigrateToFlask, + handleMigrateToIndexedDB, + } +} diff --git a/src/pages/DemoFeatureCards.tsx b/src/pages/DemoFeatureCards.tsx new file mode 100644 index 0000000..9b3652e --- /dev/null +++ b/src/pages/DemoFeatureCards.tsx @@ -0,0 +1,34 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' + +export function DemoFeatureCards() { + return ( +
+ + + Real-Time Updates + + + Watch your React components render instantly as you type. No refresh needed. + + + + + + Resizable Panels + + + Drag the center divider to adjust the editor and preview panel sizes to your preference. + + + + + + Multiple View Modes + + + Switch between code-only, split-screen, or preview-only modes with the toggle buttons. + + +
+ ) +} diff --git a/src/pages/DemoPage.tsx b/src/pages/DemoPage.tsx index 749655e..ce0a9da 100644 --- a/src/pages/DemoPage.tsx +++ b/src/pages/DemoPage.tsx @@ -3,117 +3,8 @@ import { motion } from 'framer-motion' import { SplitScreenEditor } from '@/components/features/snippet-editor/SplitScreenEditor' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Sparkle } from '@phosphor-icons/react' - -const DEMO_CODE = `function Counter() { - const [count, setCount] = React.useState(0) - - return ( -
-

- Interactive Counter -

- -
- {count} -
- -
- - - - - -
- -

- Try editing the code on the left to see live changes! -

-
- ) -} - -Counter` +import { DEMO_CODE } from './demo-constants' +import { DemoFeatureCards } from './DemoFeatureCards' export function DemoPage() { const [code, setCode] = useState(DEMO_CODE) @@ -158,34 +49,7 @@ export function DemoPage() { -
- - - Real-Time Updates - - - Watch your React components render instantly as you type. No refresh needed. - - - - - - Resizable Panels - - - Drag the center divider to adjust the editor and preview panel sizes to your preference. - - - - - - Multiple View Modes - - - Switch between code-only, split-screen, or preview-only modes with the toggle buttons. - - -
+ ) } diff --git a/src/pages/demo-constants.ts b/src/pages/demo-constants.ts new file mode 100644 index 0000000..8abcefe --- /dev/null +++ b/src/pages/demo-constants.ts @@ -0,0 +1,110 @@ +export const DEMO_CODE = `function Counter() { + const [count, setCount] = React.useState(0) + + return ( +
+

+ Interactive Counter +

+ +
+ {count} +
+ +
+ + + + + +
+ +

+ Try editing the code on the left to see live changes! +

+
+ ) +} + +Counter`