Merge pull request #5 from johndoe6345789/copilot/refactor-large-components

Refactor large components into focused modules
This commit is contained in:
2026-01-17 22:54:39 +00:00
committed by GitHub
6 changed files with 583 additions and 525 deletions

View File

@@ -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<T>(initialState: T | (() => T)): [T, (newState: T | ((prevState: T) => T)) => void];
export function useEffect(effect: () => void | (() => void), deps?: any[]): void;
export function useCallback<T extends (...args: any[]) => any>(callback: T, deps: any[]): T;
export function useMemo<T>(factory: () => T, deps: any[]): T;
export function useRef<T>(initialValue: T): { current: T };
export function useContext<T>(context: React.Context<T>): T;
export function useReducer<S, A>(reducer: (state: S, action: A) => S, initialState: S): [S, (action: A) => void];
export interface Context<T> { Provider: any; Consumer: any; }
export function createContext<T>(defaultValue: T): Context<T>;
export type FC<P = {}> = (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({
</Suspense>
)
}
function getMonacoLanguage(language: string): string {
const languageMap: Record<string, string> = {
'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'
}

View File

@@ -0,0 +1,284 @@
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<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 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<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)
}
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'
})
// 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 {
stats,
loading,
storageBackend,
setStorageBackend,
flaskUrl,
setFlaskUrl,
flaskConnectionStatus,
setFlaskConnectionStatus,
testingConnection,
envVarSet,
schemaHealth,
checkingSchema,
handleExport,
handleImport,
handleClear,
handleSeed,
formatBytes,
handleTestConnection,
handleSaveStorageConfig,
handleMigrateToFlask,
handleMigrateToIndexedDB,
checkSchemaHealth,
}
}

52
src/lib/db-mapper.ts Normal file
View File

@@ -0,0 +1,52 @@
/**
* 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<T>(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 with error handling
else if (col === 'inputParameters') {
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 {
obj[col] = value
}
})
return obj as T
}
/**
* Maps multiple SQL result rows to an array of typed objects
*/
export function mapRowsToObjects<T>(results: any[]): T[] {
if (results.length === 0) return []
const columns = results[0].columns
const values = results[0].values
return values.map(row => mapRowToObject<T>(row, columns))
}

View File

@@ -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<Snippet[]> {
}
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<Snippet>(results)
}
export async function getSnippet(id: string): Promise<Snippet | null> {
@@ -334,7 +317,6 @@ export async function getSnippet(id: string): Promise<Snippet | null> {
}
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<Snippet | null> {
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<Snippet>(row, columns)
}
export async function createSnippet(snippet: Snippet): Promise<void> {
@@ -431,27 +402,9 @@ export async function deleteSnippet(id: string): Promise<void> {
export async function getAllTemplates(): Promise<SnippetTemplate[]> {
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<SnippetTemplate>(results)
}
export async function createTemplate(template: SnippetTemplate): Promise<void> {
@@ -957,25 +910,9 @@ export async function getAllNamespaces(): Promise<import('./types').Namespace[]>
}
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<import('./types').Namespace>(results)
}
export async function createNamespace(name: string): Promise<import('./types').Namespace> {
@@ -1057,32 +994,13 @@ export async function ensureDefaultNamespace(): Promise<void> {
export async function getSnippetsByNamespace(namespaceId: string): Promise<Snippet[]> {
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<Snippet>(results)
}
export async function getNamespaceById(id: string): Promise<import('./types').Namespace | null> {
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<import('./types').Na
const columns = results[0].columns
const row = results[0].values[0]
const namespace: any = {}
columns.forEach((col, idx) => {
if (col === 'isDefault') {
namespace[col] = row[idx] === 1
} else {
namespace[col] = row[idx]
}
})
return namespace
return mapRowToObject<import('./types').Namespace>(row, columns)
}
export async function moveSnippetToNamespace(snippetId: string, targetNamespaceId: string): Promise<void> {

213
src/lib/monaco-config.ts Normal file
View File

@@ -0,0 +1,213 @@
/**
* 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
* 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
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<T>(initialState: T | (() => T)): [T, (newState: T | ((prevState: T) => T)) => void];
export function useEffect(effect: () => void | (() => void), deps?: any[]): void;
export function useCallback<T extends (...args: any[]) => any>(callback: T, deps: any[]): T;
export function useMemo<T>(factory: () => T, deps: any[]): T;
export function useRef<T>(initialValue: T): { current: T };
export function useContext<T>(context: React.Context<T>): T;
export function useReducer<S, A>(reducer: (state: S, action: A) => S, initialState: S): [S, (action: A) => void];
export interface Context<T> { Provider: any; Consumer: any; }
export function createContext<T>(defaultValue: T): Context<T>;
export type FC<P = {}> = (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<string, string> = {
'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'
}

View File

@@ -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<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 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<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)
}
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 (
<motion.div