mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
Refactor remaining large components into focused modules
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{isAnalyzing && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
||||
>
|
||||
<Sparkle className="h-4 w-4" weight="fill" />
|
||||
</motion.div>
|
||||
<span className="text-sm">Analyzing error...</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0.3 }}
|
||||
animate={{ opacity: [0.3, 0.6, 0.3] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, delay: i * 0.2 }}
|
||||
className="h-4 bg-muted rounded"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isAnalyzing && <LoadingAnalysis />}
|
||||
|
||||
{analysisError && (
|
||||
<Alert variant="destructive">
|
||||
@@ -130,55 +97,7 @@ Keep your response concise, friendly, and focused on practical solutions. Format
|
||||
)}
|
||||
|
||||
<AnimatePresence>
|
||||
{analysis && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="prose prose-invert prose-sm max-w-none"
|
||||
>
|
||||
<div className="bg-card/50 rounded-lg p-4 border border-border space-y-3">
|
||||
{analysis.split('\n').map((line, idx) => {
|
||||
if (line.startsWith('###')) {
|
||||
return (
|
||||
<h3 key={idx} className="text-base font-semibold text-foreground mt-4 mb-2">
|
||||
{line.replace('###', '').trim()}
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
if (line.startsWith('##')) {
|
||||
return (
|
||||
<h2 key={idx} className="text-lg font-semibold text-foreground mt-4 mb-2">
|
||||
{line.replace('##', '').trim()}
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
if (line.match(/^\d+\./)) {
|
||||
return (
|
||||
<div key={idx} className="text-foreground/90 ml-2">
|
||||
{line}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (line.startsWith('-')) {
|
||||
return (
|
||||
<div key={idx} className="text-foreground/90 ml-4">
|
||||
{line}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (line.trim()) {
|
||||
return (
|
||||
<p key={idx} className="text-foreground/80 text-sm leading-relaxed">
|
||||
{line}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
{analysis && <MarkdownRenderer content={analysis} />}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
29
src/components/error/LoadingAnalysis.tsx
Normal file
29
src/components/error/LoadingAnalysis.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Sparkle } from '@phosphor-icons/react'
|
||||
|
||||
export function LoadingAnalysis() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
||||
>
|
||||
<Sparkle className="h-4 w-4" weight="fill" />
|
||||
</motion.div>
|
||||
<span className="text-sm">Analyzing error...</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
initial={{ opacity: 0.3 }}
|
||||
animate={{ opacity: [0.3, 0.6, 0.3] }}
|
||||
transition={{ duration: 1.5, repeat: Infinity, delay: i * 0.2 }}
|
||||
className="h-4 bg-muted rounded"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
57
src/components/error/MarkdownRenderer.tsx
Normal file
57
src/components/error/MarkdownRenderer.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
interface MarkdownRendererProps {
|
||||
content: string
|
||||
}
|
||||
|
||||
export function MarkdownRenderer({ content }: MarkdownRendererProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="prose prose-invert prose-sm max-w-none"
|
||||
>
|
||||
<div className="bg-card/50 rounded-lg p-4 border border-border space-y-3">
|
||||
{content.split('\n').map((line, idx) => {
|
||||
if (line.startsWith('###')) {
|
||||
return (
|
||||
<h3 key={idx} className="text-base font-semibold text-foreground mt-4 mb-2">
|
||||
{line.replace('###', '').trim()}
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
if (line.startsWith('##')) {
|
||||
return (
|
||||
<h2 key={idx} className="text-lg font-semibold text-foreground mt-4 mb-2">
|
||||
{line.replace('##', '').trim()}
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
if (line.match(/^\d+\./)) {
|
||||
return (
|
||||
<div key={idx} className="text-foreground/90 ml-2">
|
||||
{line}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (line.startsWith('-')) {
|
||||
return (
|
||||
<div key={idx} className="text-foreground/90 ml-4">
|
||||
{line}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (line.trim()) {
|
||||
return (
|
||||
<p key={idx} className="text-foreground/80 text-sm leading-relaxed">
|
||||
{line}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
21
src/components/error/analyzeError.ts
Normal file
21
src/components/error/analyzeError.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export async function analyzeErrorWithAI(
|
||||
errorMessage: string,
|
||||
errorStack?: string,
|
||||
context?: string
|
||||
): Promise<string> {
|
||||
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
|
||||
}
|
||||
138
src/hooks/useDatabaseOperations.ts
Normal file
138
src/hooks/useDatabaseOperations.ts
Normal file
@@ -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<HTMLInputElement>) => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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<StorageBackend>('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<HTMLInputElement>) => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
86
src/hooks/useStorageConfig.ts
Normal file
86
src/hooks/useStorageConfig.ts
Normal file
@@ -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<StorageBackend>('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<void>) => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
84
src/hooks/useStorageMigration.ts
Normal file
84
src/hooks/useStorageMigration.ts
Normal file
@@ -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<void>) => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
34
src/pages/DemoFeatureCards.tsx
Normal file
34
src/pages/DemoFeatureCards.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
export function DemoFeatureCards() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card className="border-primary/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Real-Time Updates</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Watch your React components render instantly as you type. No refresh needed.
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-accent/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Resizable Panels</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Drag the center divider to adjust the editor and preview panel sizes to your preference.
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-primary/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Multiple View Modes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Switch between code-only, split-screen, or preview-only modes with the toggle buttons.
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div style={{
|
||||
padding: '2rem',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '1.5rem'
|
||||
}}>
|
||||
<h2 style={{
|
||||
fontSize: '2rem',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #8b5cf6, #06b6d4)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
margin: 0
|
||||
}}>
|
||||
Interactive Counter
|
||||
</h2>
|
||||
|
||||
<div style={{
|
||||
fontSize: '4rem',
|
||||
fontWeight: 'bold',
|
||||
color: '#8b5cf6',
|
||||
padding: '2rem',
|
||||
background: 'rgba(139, 92, 246, 0.1)',
|
||||
borderRadius: '1rem',
|
||||
minWidth: '200px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{count}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
<button
|
||||
onClick={() => setCount(count - 1)}
|
||||
style={{
|
||||
padding: '0.75rem 2rem',
|
||||
fontSize: '1.125rem',
|
||||
fontWeight: '600',
|
||||
background: '#ef4444',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
onMouseOver={(e) => e.currentTarget.style.transform = 'scale(1.05)'}
|
||||
onMouseOut={(e) => e.currentTarget.style.transform = 'scale(1)'}
|
||||
>
|
||||
Decrement
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setCount(0)}
|
||||
style={{
|
||||
padding: '0.75rem 2rem',
|
||||
fontSize: '1.125rem',
|
||||
fontWeight: '600',
|
||||
background: '#6b7280',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
onMouseOver={(e) => e.currentTarget.style.transform = 'scale(1.05)'}
|
||||
onMouseOut={(e) => e.currentTarget.style.transform = 'scale(1)'}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setCount(count + 1)}
|
||||
style={{
|
||||
padding: '0.75rem 2rem',
|
||||
fontSize: '1.125rem',
|
||||
fontWeight: '600',
|
||||
background: '#10b981',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
onMouseOver={(e) => e.currentTarget.style.transform = 'scale(1.05)'}
|
||||
onMouseOut={(e) => e.currentTarget.style.transform = 'scale(1)'}
|
||||
>
|
||||
Increment
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p style={{
|
||||
marginTop: '1rem',
|
||||
color: '#9ca3af',
|
||||
fontSize: '0.875rem'
|
||||
}}>
|
||||
Try editing the code on the left to see live changes!
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card className="border-primary/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Real-Time Updates</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Watch your React components render instantly as you type. No refresh needed.
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-accent/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Resizable Panels</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Drag the center divider to adjust the editor and preview panel sizes to your preference.
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-primary/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Multiple View Modes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Switch between code-only, split-screen, or preview-only modes with the toggle buttons.
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<DemoFeatureCards />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
110
src/pages/demo-constants.ts
Normal file
110
src/pages/demo-constants.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
export const DEMO_CODE = `function Counter() {
|
||||
const [count, setCount] = React.useState(0)
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '2rem',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '1.5rem'
|
||||
}}>
|
||||
<h2 style={{
|
||||
fontSize: '2rem',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #8b5cf6, #06b6d4)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
margin: 0
|
||||
}}>
|
||||
Interactive Counter
|
||||
</h2>
|
||||
|
||||
<div style={{
|
||||
fontSize: '4rem',
|
||||
fontWeight: 'bold',
|
||||
color: '#8b5cf6',
|
||||
padding: '2rem',
|
||||
background: 'rgba(139, 92, 246, 0.1)',
|
||||
borderRadius: '1rem',
|
||||
minWidth: '200px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{count}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
<button
|
||||
onClick={() => setCount(count - 1)}
|
||||
style={{
|
||||
padding: '0.75rem 2rem',
|
||||
fontSize: '1.125rem',
|
||||
fontWeight: '600',
|
||||
background: '#ef4444',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
onMouseOver={(e) => e.currentTarget.style.transform = 'scale(1.05)'}
|
||||
onMouseOut={(e) => e.currentTarget.style.transform = 'scale(1)'}
|
||||
>
|
||||
Decrement
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setCount(0)}
|
||||
style={{
|
||||
padding: '0.75rem 2rem',
|
||||
fontSize: '1.125rem',
|
||||
fontWeight: '600',
|
||||
background: '#6b7280',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
onMouseOver={(e) => e.currentTarget.style.transform = 'scale(1.05)'}
|
||||
onMouseOut={(e) => e.currentTarget.style.transform = 'scale(1)'}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setCount(count + 1)}
|
||||
style={{
|
||||
padding: '0.75rem 2rem',
|
||||
fontSize: '1.125rem',
|
||||
fontWeight: '600',
|
||||
background: '#10b981',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
onMouseOver={(e) => e.currentTarget.style.transform = 'scale(1.05)'}
|
||||
onMouseOut={(e) => e.currentTarget.style.transform = 'scale(1)'}
|
||||
>
|
||||
Increment
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p style={{
|
||||
marginTop: '1rem',
|
||||
color: '#9ca3af',
|
||||
fontSize: '0.875rem'
|
||||
}}>
|
||||
Try editing the code on the left to see live changes!
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Counter`
|
||||
Reference in New Issue
Block a user