From 30fe16506f5703d64dc12182a0568dff8f610f15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:38:03 +0000 Subject: [PATCH 1/5] Initial plan From 7dce8c587a01c102dc2064577c75abd36038774a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:43:47 +0000 Subject: [PATCH 2/5] Phase 1: Extract db-mapper helper to reduce duplication in db.ts Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- src/lib/db-mapper.ts | 43 ++++++++++++++++++ src/lib/db.ts | 105 +++---------------------------------------- 2 files changed, 50 insertions(+), 98 deletions(-) create mode 100644 src/lib/db-mapper.ts diff --git a/src/lib/db-mapper.ts b/src/lib/db-mapper.ts new file mode 100644 index 0000000..a25aa2d --- /dev/null +++ b/src/lib/db-mapper.ts @@ -0,0 +1,43 @@ +/** + * Database row-to-object mapping utilities + * Handles conversion of SQL query results to typed objects + */ + +/** + * Maps a SQL query result row to a typed object + * Handles special conversions for boolean and JSON fields + */ +export function mapRowToObject(row: any[], columns: string[]): T { + const obj: any = {} + + columns.forEach((col, idx) => { + const value = row[idx] + + // Convert integer boolean fields to actual booleans + if (col === 'hasPreview' || col === 'isDefault') { + obj[col] = value === 1 + } + // Parse JSON string fields + else if (col === 'inputParameters') { + obj[col] = value ? JSON.parse(value as string) : undefined + } + // All other fields pass through as-is + else { + obj[col] = value + } + }) + + return obj as T +} + +/** + * Maps multiple SQL result rows to an array of typed objects + */ +export function mapRowsToObjects(results: any[]): T[] { + if (results.length === 0) return [] + + const columns = results[0].columns + const values = results[0].values + + return values.map(row => mapRowToObject(row, columns)) +} diff --git a/src/lib/db.ts b/src/lib/db.ts index 1cf35ab..db4de79 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,6 +1,7 @@ import initSqlJs, { Database } from 'sql.js' import type { Snippet, SnippetTemplate } from './types' import { getStorageConfig, FlaskStorageAdapter, loadStorageConfig } from './storage' +import { mapRowToObject, mapRowsToObjects } from './db-mapper' let dbInstance: Database | null = null let sqlInstance: any = null @@ -304,27 +305,9 @@ export async function getAllSnippets(): Promise { } const db = await initDB() - const results = db.exec('SELECT * FROM snippets ORDER BY updatedAt DESC') - if (results.length === 0) return [] - - const columns = results[0].columns - const values = results[0].values - - return values.map(row => { - const snippet: any = {} - columns.forEach((col, idx) => { - if (col === 'hasPreview') { - snippet[col] = row[idx] === 1 - } else if (col === 'inputParameters') { - snippet[col] = row[idx] ? JSON.parse(row[idx] as string) : undefined - } else { - snippet[col] = row[idx] - } - }) - return snippet as Snippet - }) + return mapRowsToObjects(results) } export async function getSnippet(id: string): Promise { @@ -334,7 +317,6 @@ export async function getSnippet(id: string): Promise { } const db = await initDB() - const results = db.exec('SELECT * FROM snippets WHERE id = ?', [id]) if (results.length === 0 || results[0].values.length === 0) return null @@ -342,18 +324,7 @@ export async function getSnippet(id: string): Promise { const columns = results[0].columns const row = results[0].values[0] - const snippet: any = {} - columns.forEach((col, idx) => { - if (col === 'hasPreview') { - snippet[col] = row[idx] === 1 - } else if (col === 'inputParameters') { - snippet[col] = row[idx] ? JSON.parse(row[idx] as string) : undefined - } else { - snippet[col] = row[idx] - } - }) - - return snippet as Snippet + return mapRowToObject(row, columns) } export async function createSnippet(snippet: Snippet): Promise { @@ -431,27 +402,9 @@ export async function deleteSnippet(id: string): Promise { export async function getAllTemplates(): Promise { const db = await initDB() - const results = db.exec('SELECT * FROM snippet_templates') - if (results.length === 0) return [] - - const columns = results[0].columns - const values = results[0].values - - return values.map(row => { - const template: any = {} - columns.forEach((col, idx) => { - if (col === 'hasPreview') { - template[col] = row[idx] === 1 - } else if (col === 'inputParameters') { - template[col] = row[idx] ? JSON.parse(row[idx] as string) : undefined - } else { - template[col] = row[idx] - } - }) - return template as SnippetTemplate - }) + return mapRowsToObjects(results) } export async function createTemplate(template: SnippetTemplate): Promise { @@ -957,25 +910,9 @@ export async function getAllNamespaces(): Promise } const db = await initDB() - const results = db.exec('SELECT * FROM namespaces ORDER BY isDefault DESC, name ASC') - if (results.length === 0) return [] - - const columns = results[0].columns - const values = results[0].values - - return values.map(row => { - const namespace: any = {} - columns.forEach((col, idx) => { - if (col === 'isDefault') { - namespace[col] = row[idx] === 1 - } else { - namespace[col] = row[idx] - } - }) - return namespace - }) + return mapRowsToObjects(results) } export async function createNamespace(name: string): Promise { @@ -1057,32 +994,13 @@ export async function ensureDefaultNamespace(): Promise { export async function getSnippetsByNamespace(namespaceId: string): Promise { const db = await initDB() - const results = db.exec('SELECT * FROM snippets WHERE namespaceId = ? OR (namespaceId IS NULL AND ? = (SELECT id FROM namespaces WHERE isDefault = 1)) ORDER BY updatedAt DESC', [namespaceId, namespaceId]) - if (results.length === 0) return [] - - const columns = results[0].columns - const values = results[0].values - - return values.map(row => { - const snippet: any = {} - columns.forEach((col, idx) => { - if (col === 'hasPreview') { - snippet[col] = row[idx] === 1 - } else if (col === 'inputParameters') { - snippet[col] = row[idx] ? JSON.parse(row[idx] as string) : undefined - } else { - snippet[col] = row[idx] - } - }) - return snippet as Snippet - }) + return mapRowsToObjects(results) } export async function getNamespaceById(id: string): Promise { const db = await initDB() - const results = db.exec('SELECT * FROM namespaces WHERE id = ?', [id]) if (results.length === 0 || results[0].values.length === 0) return null @@ -1090,16 +1008,7 @@ export async function getNamespaceById(id: string): Promise { - if (col === 'isDefault') { - namespace[col] = row[idx] === 1 - } else { - namespace[col] = row[idx] - } - }) - - return namespace + return mapRowToObject(row, columns) } export async function moveSnippetToNamespace(snippetId: string, targetNamespaceId: string): Promise { From 8e8cd556cf7450ef25ecf96bdfcfd923ed4e7a28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:46:52 +0000 Subject: [PATCH 3/5] Phase 3: Extract settings state to useSettingsState hook, reduce SettingsPage from 321 to 100 LOC Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- src/hooks/useSettingsState.ts | 282 ++++++++++++++++++++++++++++++++++ src/pages/SettingsPage.tsx | 271 +++----------------------------- 2 files changed, 307 insertions(+), 246 deletions(-) create mode 100644 src/hooks/useSettingsState.ts diff --git a/src/hooks/useSettingsState.ts b/src/hooks/useSettingsState.ts new file mode 100644 index 0000000..3540379 --- /dev/null +++ b/src/hooks/useSettingsState.ts @@ -0,0 +1,282 @@ +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' + +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 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 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) + } + } + + 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) + } + + 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() + } + + 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' + }) + + 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 { + stats, + loading, + storageBackend, + setStorageBackend, + flaskUrl, + setFlaskUrl, + flaskConnectionStatus, + setFlaskConnectionStatus, + testingConnection, + envVarSet, + schemaHealth, + checkingSchema, + handleExport, + handleImport, + handleClear, + handleSeed, + formatBytes, + handleTestConnection, + handleSaveStorageConfig, + handleMigrateToFlask, + handleMigrateToIndexedDB, + checkSchemaHealth, + } +} diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 9633197..50142c8 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -1,13 +1,4 @@ -import { useState, useEffect } from 'react' import { motion } from 'framer-motion' -import { getDatabaseStats, exportDatabase, importDatabase, clearDatabase, seedDatabase, getAllSnippets, validateDatabaseSchema } from '@/lib/db' -import { toast } from 'sonner' -import { - saveStorageConfig, - loadStorageConfig, - FlaskStorageAdapter, - type StorageBackend -} from '@/lib/storage' import { PersistenceSettings } from '@/components/demo/PersistenceSettings' import { SchemaHealthCard } from '@/components/settings/SchemaHealthCard' import { BackendAutoConfigCard } from '@/components/settings/BackendAutoConfigCard' @@ -15,245 +6,33 @@ import { StorageBackendCard } from '@/components/settings/StorageBackendCard' import { DatabaseStatsCard } from '@/components/settings/DatabaseStatsCard' import { StorageInfoCard } from '@/components/settings/StorageInfoCard' import { DatabaseActionsCard } from '@/components/settings/DatabaseActionsCard' +import { useSettingsState } from '@/hooks/useSettingsState' export function SettingsPage() { - 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 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 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) - } - } - - 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) - } - - 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() - } - - 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' - }) - - 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') - } - } + const { + stats, + loading, + storageBackend, + setStorageBackend, + flaskUrl, + setFlaskUrl, + flaskConnectionStatus, + setFlaskConnectionStatus, + testingConnection, + envVarSet, + schemaHealth, + checkingSchema, + handleExport, + handleImport, + handleClear, + handleSeed, + formatBytes, + handleTestConnection, + handleSaveStorageConfig, + handleMigrateToFlask, + handleMigrateToIndexedDB, + checkSchemaHealth, + } = useSettingsState() return ( Date: Sat, 17 Jan 2026 22:48:51 +0000 Subject: [PATCH 4/5] Phase 4: Extract Monaco config to separate module, reduce MonacoEditor from 249 to 70 LOC Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- .../features/snippet-editor/MonacoEditor.tsx | 183 +-------------- src/lib/monaco-config.ts | 208 ++++++++++++++++++ 2 files changed, 210 insertions(+), 181 deletions(-) create mode 100644 src/lib/monaco-config.ts diff --git a/src/components/features/snippet-editor/MonacoEditor.tsx b/src/components/features/snippet-editor/MonacoEditor.tsx index 0a4d66b..56551b1 100644 --- a/src/components/features/snippet-editor/MonacoEditor.tsx +++ b/src/components/features/snippet-editor/MonacoEditor.tsx @@ -1,5 +1,6 @@ import { lazy, Suspense } from 'react' import { Skeleton } from '@/components/ui/skeleton' +import { configureMonacoTypeScript, getMonacoLanguage } from '@/lib/monaco-config' import type { Monaco } from '@monaco-editor/react' const Editor = lazy(() => import('@monaco-editor/react')) @@ -21,161 +22,7 @@ function EditorLoadingSkeleton({ height = '400px' }: { height?: string }) { } function handleEditorBeforeMount(monaco: Monaco) { - monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ - target: monaco.languages.typescript.ScriptTarget.Latest, - allowNonTsExtensions: true, - moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, - module: monaco.languages.typescript.ModuleKind.ESNext, - noEmit: true, - esModuleInterop: true, - jsx: monaco.languages.typescript.JsxEmit.React, - reactNamespace: 'React', - allowJs: true, - typeRoots: ['node_modules/@types'], - }) - - monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ - target: monaco.languages.typescript.ScriptTarget.Latest, - allowNonTsExtensions: true, - moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, - module: monaco.languages.typescript.ModuleKind.ESNext, - noEmit: true, - esModuleInterop: true, - jsx: monaco.languages.typescript.JsxEmit.React, - reactNamespace: 'React', - allowJs: true, - typeRoots: ['node_modules/@types'], - }) - - monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ - noSemanticValidation: false, - noSyntaxValidation: false, - }) - - monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ - noSemanticValidation: false, - noSyntaxValidation: false, - }) - - const reactTypes = ` - declare module 'react' { - export function useState(initialState: T | (() => T)): [T, (newState: T | ((prevState: T) => T)) => void]; - export function useEffect(effect: () => void | (() => void), deps?: any[]): void; - export function useCallback any>(callback: T, deps: any[]): T; - export function useMemo(factory: () => T, deps: any[]): T; - export function useRef(initialValue: T): { current: T }; - export function useContext(context: React.Context): T; - export function useReducer(reducer: (state: S, action: A) => S, initialState: S): [S, (action: A) => void]; - export interface Context { Provider: any; Consumer: any; } - export function createContext(defaultValue: T): Context; - export type FC

= (props: P) => JSX.Element | null; - export type ReactNode = JSX.Element | string | number | boolean | null | undefined; - export interface CSSProperties { [key: string]: any; } - } - - declare namespace JSX { - interface Element {} - interface IntrinsicElements { - [elemName: string]: any; - } - } - ` - - const shadcnTypes = ` - declare module '@/components/ui/button' { - export interface ButtonProps { - variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'; - size?: 'default' | 'sm' | 'lg' | 'icon'; - asChild?: boolean; - className?: string; - children?: React.ReactNode; - onClick?: () => void; - type?: 'button' | 'submit' | 'reset'; - } - export function Button(props: ButtonProps): JSX.Element; - } - - declare module '@/components/ui/card' { - export interface CardProps { - className?: string; - children?: React.ReactNode; - } - export function Card(props: CardProps): JSX.Element; - export function CardHeader(props: CardProps): JSX.Element; - export function CardTitle(props: CardProps): JSX.Element; - export function CardDescription(props: CardProps): JSX.Element; - export function CardContent(props: CardProps): JSX.Element; - export function CardFooter(props: CardProps): JSX.Element; - } - - declare module '@/components/ui/input' { - export interface InputProps { - type?: string; - placeholder?: string; - value?: string; - onChange?: (e: any) => void; - className?: string; - } - export function Input(props: InputProps): JSX.Element; - } - - declare module '@/components/ui/badge' { - export interface BadgeProps { - variant?: 'default' | 'secondary' | 'destructive' | 'outline'; - className?: string; - children?: React.ReactNode; - } - export function Badge(props: BadgeProps): JSX.Element; - } - - declare module '@/components/ui/checkbox' { - export interface CheckboxProps { - checked?: boolean; - onCheckedChange?: (checked: boolean) => void; - className?: string; - } - export function Checkbox(props: CheckboxProps): JSX.Element; - } - - declare module '@/components/ui/switch' { - export interface SwitchProps { - checked?: boolean; - onCheckedChange?: (checked: boolean) => void; - className?: string; - } - export function Switch(props: SwitchProps): JSX.Element; - } - - declare module '@/lib/utils' { - export function cn(...inputs: any[]): string; - } - - declare module '@phosphor-icons/react' { - export interface IconProps { - size?: number; - weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'; - color?: string; - className?: string; - } - export function Plus(props: IconProps): JSX.Element; - export function Minus(props: IconProps): JSX.Element; - export function Check(props: IconProps): JSX.Element; - export function X(props: IconProps): JSX.Element; - export function Trash(props: IconProps): JSX.Element; - export function PencilSimple(props: IconProps): JSX.Element; - export function MagnifyingGlass(props: IconProps): JSX.Element; - export function Heart(props: IconProps): JSX.Element; - export function Star(props: IconProps): JSX.Element; - export function User(props: IconProps): JSX.Element; - export function ShoppingCart(props: IconProps): JSX.Element; - [key: string]: (props: IconProps) => JSX.Element; - } - ` - - monaco.languages.typescript.typescriptDefaults.addExtraLib(reactTypes, 'file:///node_modules/@types/react/index.d.ts') - monaco.languages.typescript.javascriptDefaults.addExtraLib(reactTypes, 'file:///node_modules/@types/react/index.d.ts') - monaco.languages.typescript.typescriptDefaults.addExtraLib(shadcnTypes, 'file:///node_modules/@types/shadcn/index.d.ts') - monaco.languages.typescript.javascriptDefaults.addExtraLib(shadcnTypes, 'file:///node_modules/@types/shadcn/index.d.ts') + configureMonacoTypeScript(monaco) } export function MonacoEditor({ @@ -221,29 +68,3 @@ export function MonacoEditor({ ) } - -function getMonacoLanguage(language: string): string { - const languageMap: Record = { - 'JavaScript': 'javascript', - 'TypeScript': 'typescript', - 'JSX': 'javascript', - 'TSX': 'typescript', - 'Python': 'python', - 'Java': 'java', - 'C++': 'cpp', - 'C#': 'csharp', - 'Ruby': 'ruby', - 'Go': 'go', - 'Rust': 'rust', - 'PHP': 'php', - 'Swift': 'swift', - 'Kotlin': 'kotlin', - 'HTML': 'html', - 'CSS': 'css', - 'SQL': 'sql', - 'Bash': 'shell', - 'Other': 'plaintext', - } - - return languageMap[language] || 'plaintext' -} diff --git a/src/lib/monaco-config.ts b/src/lib/monaco-config.ts new file mode 100644 index 0000000..a69ab8a --- /dev/null +++ b/src/lib/monaco-config.ts @@ -0,0 +1,208 @@ +/** + * Monaco Editor TypeScript/React type definitions and compiler options + * These definitions are loaded into Monaco to provide IntelliSense for React and shadcn components + */ + +import type { Monaco } from '@monaco-editor/react' + +/** + * TypeScript compiler options for both TypeScript and JavaScript files + */ +export const compilerOptions = { + target: 2, // ScriptTarget.Latest + allowNonTsExtensions: true, + moduleResolution: 2, // ModuleResolutionKind.NodeJs + module: 99, // ModuleKind.ESNext + noEmit: true, + esModuleInterop: true, + jsx: 2, // JsxEmit.React + reactNamespace: 'React', + allowJs: true, + typeRoots: ['node_modules/@types'], +} + +/** + * Diagnostics options for TypeScript/JavaScript validation + */ +export const diagnosticsOptions = { + noSemanticValidation: false, + noSyntaxValidation: false, +} + +/** + * React type definitions for Monaco IntelliSense + */ +export const reactTypes = ` + declare module 'react' { + export function useState(initialState: T | (() => T)): [T, (newState: T | ((prevState: T) => T)) => void]; + export function useEffect(effect: () => void | (() => void), deps?: any[]): void; + export function useCallback any>(callback: T, deps: any[]): T; + export function useMemo(factory: () => T, deps: any[]): T; + export function useRef(initialValue: T): { current: T }; + export function useContext(context: React.Context): T; + export function useReducer(reducer: (state: S, action: A) => S, initialState: S): [S, (action: A) => void]; + export interface Context { Provider: any; Consumer: any; } + export function createContext(defaultValue: T): Context; + export type FC

= (props: P) => JSX.Element | null; + export type ReactNode = JSX.Element | string | number | boolean | null | undefined; + export interface CSSProperties { [key: string]: any; } + } + + declare namespace JSX { + interface Element {} + interface IntrinsicElements { + [elemName: string]: any; + } + } +` + +/** + * shadcn/ui component type definitions for Monaco IntelliSense + */ +export const shadcnTypes = ` + declare module '@/components/ui/button' { + export interface ButtonProps { + variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'; + size?: 'default' | 'sm' | 'lg' | 'icon'; + asChild?: boolean; + className?: string; + children?: React.ReactNode; + onClick?: () => void; + type?: 'button' | 'submit' | 'reset'; + } + export function Button(props: ButtonProps): JSX.Element; + } + + declare module '@/components/ui/card' { + export interface CardProps { + className?: string; + children?: React.ReactNode; + } + export function Card(props: CardProps): JSX.Element; + export function CardHeader(props: CardProps): JSX.Element; + export function CardTitle(props: CardProps): JSX.Element; + export function CardDescription(props: CardProps): JSX.Element; + export function CardContent(props: CardProps): JSX.Element; + export function CardFooter(props: CardProps): JSX.Element; + } + + declare module '@/components/ui/input' { + export interface InputProps { + type?: string; + placeholder?: string; + value?: string; + onChange?: (e: any) => void; + className?: string; + } + export function Input(props: InputProps): JSX.Element; + } + + declare module '@/components/ui/badge' { + export interface BadgeProps { + variant?: 'default' | 'secondary' | 'destructive' | 'outline'; + className?: string; + children?: React.ReactNode; + } + export function Badge(props: BadgeProps): JSX.Element; + } + + declare module '@/components/ui/checkbox' { + export interface CheckboxProps { + checked?: boolean; + onCheckedChange?: (checked: boolean) => void; + className?: string; + } + export function Checkbox(props: CheckboxProps): JSX.Element; + } + + declare module '@/components/ui/switch' { + export interface SwitchProps { + checked?: boolean; + onCheckedChange?: (checked: boolean) => void; + className?: string; + } + export function Switch(props: SwitchProps): JSX.Element; + } + + declare module '@/lib/utils' { + export function cn(...inputs: any[]): string; + } + + declare module '@phosphor-icons/react' { + export interface IconProps { + size?: number; + weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'; + color?: string; + className?: string; + } + export function Plus(props: IconProps): JSX.Element; + export function Minus(props: IconProps): JSX.Element; + export function Check(props: IconProps): JSX.Element; + export function X(props: IconProps): JSX.Element; + export function Trash(props: IconProps): JSX.Element; + export function PencilSimple(props: IconProps): JSX.Element; + export function MagnifyingGlass(props: IconProps): JSX.Element; + export function Heart(props: IconProps): JSX.Element; + export function Star(props: IconProps): JSX.Element; + export function User(props: IconProps): JSX.Element; + export function ShoppingCart(props: IconProps): JSX.Element; + [key: string]: (props: IconProps) => JSX.Element; + } +` + +/** + * Configures Monaco editor with TypeScript/React support + */ +export function configureMonacoTypeScript(monaco: Monaco) { + // Set compiler options for TypeScript + monaco.languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions as any) + + // Set compiler options for JavaScript + monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions as any) + + // Set diagnostics options for TypeScript + monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(diagnosticsOptions) + + // Set diagnostics options for JavaScript + monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(diagnosticsOptions) + + // Add React type definitions + monaco.languages.typescript.typescriptDefaults.addExtraLib(reactTypes, 'file:///node_modules/@types/react/index.d.ts') + monaco.languages.typescript.javascriptDefaults.addExtraLib(reactTypes, 'file:///node_modules/@types/react/index.d.ts') + + // Add shadcn component type definitions + monaco.languages.typescript.typescriptDefaults.addExtraLib(shadcnTypes, 'file:///node_modules/@types/shadcn/index.d.ts') + monaco.languages.typescript.javascriptDefaults.addExtraLib(shadcnTypes, 'file:///node_modules/@types/shadcn/index.d.ts') +} + +/** + * Language mapping for Monaco editor + */ +export const languageMap: Record = { + 'JavaScript': 'javascript', + 'TypeScript': 'typescript', + 'JSX': 'javascript', + 'TSX': 'typescript', + 'Python': 'python', + 'Java': 'java', + 'C++': 'cpp', + 'C#': 'csharp', + 'Ruby': 'ruby', + 'Go': 'go', + 'Rust': 'rust', + 'PHP': 'php', + 'Swift': 'swift', + 'Kotlin': 'kotlin', + 'HTML': 'html', + 'CSS': 'css', + 'SQL': 'sql', + 'Bash': 'shell', + 'Other': 'plaintext', +} + +/** + * Converts a high-level language name to Monaco language ID + */ +export function getMonacoLanguage(language: string): string { + return languageMap[language] || 'plaintext' +} From 50d1d27a97b469fb484691223f1e54a288e4dd32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:51:13 +0000 Subject: [PATCH 5/5] Address code review feedback: add error handling, clarify magic numbers, document reload usage Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- src/hooks/useSettingsState.ts | 2 ++ src/lib/db-mapper.ts | 13 +++++++++++-- src/lib/monaco-config.ts | 5 +++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/hooks/useSettingsState.ts b/src/hooks/useSettingsState.ts index 3540379..4fd97b7 100644 --- a/src/hooks/useSettingsState.ts +++ b/src/hooks/useSettingsState.ts @@ -246,6 +246,8 @@ export function useSettingsState() { 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`) diff --git a/src/lib/db-mapper.ts b/src/lib/db-mapper.ts index a25aa2d..b561c27 100644 --- a/src/lib/db-mapper.ts +++ b/src/lib/db-mapper.ts @@ -17,9 +17,18 @@ export function mapRowToObject(row: any[], columns: string[]): T { if (col === 'hasPreview' || col === 'isDefault') { obj[col] = value === 1 } - // Parse JSON string fields + // Parse JSON string fields with error handling else if (col === 'inputParameters') { - obj[col] = value ? JSON.parse(value as string) : undefined + if (value) { + try { + obj[col] = JSON.parse(value as string) + } catch (error) { + console.warn(`Failed to parse JSON for ${col}:`, error) + obj[col] = undefined + } + } else { + obj[col] = undefined + } } // All other fields pass through as-is else { diff --git a/src/lib/monaco-config.ts b/src/lib/monaco-config.ts index a69ab8a..d2bf314 100644 --- a/src/lib/monaco-config.ts +++ b/src/lib/monaco-config.ts @@ -7,6 +7,11 @@ import type { Monaco } from '@monaco-editor/react' /** * TypeScript compiler options for both TypeScript and JavaScript files + * Note: Values are enum numbers since Monaco enums aren't directly importable: + * - target: 2 = ScriptTarget.Latest + * - moduleResolution: 2 = ModuleResolutionKind.NodeJs + * - module: 99 = ModuleKind.ESNext + * - jsx: 2 = JsxEmit.React */ export const compilerOptions = { target: 2, // ScriptTarget.Latest