feat: refactor ESLint configuration and update dependencies; improve error handling and code structure

This commit is contained in:
2026-01-19 23:55:01 +00:00
parent 1cbcb2051f
commit f25e5e0597
25 changed files with 102 additions and 110 deletions

View File

@@ -1,3 +0,0 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}

36
eslint.config.mjs Normal file
View File

@@ -0,0 +1,36 @@
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import nextPlugin from '@next/eslint-plugin-next'
import reactHooks from 'eslint-plugin-react-hooks'
import globals from 'globals'
export default [
{
ignores: ['node_modules', '.next', 'dist', 'coverage'],
},
js.configs.recommended,
...tseslint.configs.recommended,
nextPlugin.configs['core-web-vitals'],
{
name: 'react-hooks/custom',
plugins: {
'react-hooks': reactHooks,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-hooks/set-state-in-effect': 'off',
'react-hooks/purity': 'off',
},
},
{
rules: {
'@next/next/no-page-custom-font': 'off',
},
},
{
files: ['*.config.js', '*.config.cjs', '*.config.mjs', 'next.config.js'],
languageOptions: {
globals: globals.node,
},
},
]

View File

@@ -1,3 +1,4 @@
/* eslint-env node */
/** @type {import('next').NextConfig} */
const nextConfig = {
output: process.env.BUILD_STATIC ? 'export' : 'standalone',

17
package-lock.json generated
View File

@@ -49,7 +49,7 @@
"marked": "^15.0.7",
"next": "16.1.3",
"next-themes": "^0.4.6",
"pyodide": "^0.29.1",
"pyodide": "^0.27.0",
"react": "^19.2.3",
"react-dom": "^19.0.0",
"react-error-boundary": "^6.0.0",
@@ -3450,12 +3450,6 @@
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/emscripten": {
"version": "1.41.5",
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz",
"integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -7406,12 +7400,11 @@
}
},
"node_modules/pyodide": {
"version": "0.29.1",
"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.29.1.tgz",
"integrity": "sha512-mpk9jtkiM7Ugh1r9P9dbR8vKrmf0lED32hBZq+Fn1kkkBiUoOjSsJEWcyprugICpiFpIXpUOf80ZrvFXkQMk2g==",
"license": "MPL-2.0",
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.27.0.tgz",
"integrity": "sha512-0O992noCKqv8lPw4+QFWw8d8yrNWVtQ6zPhWNg/RNYbFohiwmV6SGlVh5fWk/4pOxyhgHbayq+ur8JoR/r5U9A==",
"license": "Apache-2.0",
"dependencies": {
"@types/emscripten": "^1.41.4",
"ws": "^8.5.0"
},
"engines": {

View File

@@ -7,7 +7,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint .",
"kill": "fuser -k 5000/tcp"
},
"dependencies": {
@@ -52,7 +52,7 @@
"marked": "^15.0.7",
"next": "16.1.3",
"next-themes": "^0.4.6",
"pyodide": "^0.29.1",
"pyodide": "^0.27.0",
"react": "^19.2.3",
"react-dom": "^19.0.0",
"react-error-boundary": "^6.0.0",

View File

@@ -4,8 +4,8 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useAppDispatch } from '@/store/hooks'
import { createSnippet, updateSnippet, deleteSnippet } from '@/store/slices/snippetsSlice'
import { FloppyDisk, Plus, Pencil, Trash } from '@phosphor-icons/react'
import { createSnippet } from '@/store/slices/snippetsSlice'
import { FloppyDisk, Plus } from '@phosphor-icons/react'
import { toast } from 'sonner'
export function PersistenceExample() {

View File

@@ -39,6 +39,7 @@ export function AIErrorHelper({ error, context, className }: AIErrorHelperProps)
const result = await analyzeErrorWithAI(errorMessage, errorStack, context)
setAnalysis(result)
} catch (err) {
console.error('AI analysis failed', err)
setAnalysisError('Unable to analyze error. The AI service may be temporarily unavailable.')
} finally {
setIsAnalyzing(false)
@@ -105,4 +106,3 @@ export function AIErrorHelper({ error, context, className }: AIErrorHelperProps)
</>
)
}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useCallback } from 'react'
import {
Select,
SelectContent,
@@ -30,11 +30,7 @@ export function NamespaceSelector({ selectedNamespaceId, onNamespaceChange }: Na
const [namespaceToDelete, setNamespaceToDelete] = useState<Namespace | null>(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
loadNamespaces()
}, [])
const loadNamespaces = async () => {
const loadNamespaces = useCallback(async () => {
try {
const loadedNamespaces = await getAllNamespaces()
setNamespaces(loadedNamespaces)
@@ -49,7 +45,11 @@ export function NamespaceSelector({ selectedNamespaceId, onNamespaceChange }: Na
console.error('Failed to load namespaces:', error)
toast.error('Failed to load namespaces')
}
}
}, [onNamespaceChange, selectedNamespaceId])
useEffect(() => {
loadNamespaces()
}, [loadNamespaces])
const handleCreateNamespace = async () => {
if (!newNamespaceName.trim()) {

View File

@@ -59,8 +59,8 @@ export function PythonOutput({ code }: PythonOutputProps) {
}, [initializePyodide])
useEffect(() => {
const codeToCheck = code.toLowerCase()
setHasInput(codeToCheck.includes('input('))
const hasInputCall = /\binput\s*\(/i.test(code)
setHasInput(hasInputCall)
}, [code])
const statusTone = initError

View File

@@ -43,34 +43,24 @@ export function SnippetCard({
try {
const loadedNamespaces = await getAllNamespaces()
setNamespaces(loadedNamespaces)
} catch (error) {
console.error('Failed to load namespaces:', error)
} catch {
console.error('Failed to load namespaces')
}
}
const snippetData = useMemo(() => {
try {
const code = snippet?.code || ''
const description = snippet?.description || ''
const maxLength = appConfig.codePreviewMaxLength
const isTruncated = code.length > maxLength
const displayCode = isTruncated ? code.slice(0, maxLength) + '...' : code
const code = snippet?.code || ''
const description = snippet?.description || ''
const maxLength = appConfig.codePreviewMaxLength
const isTruncated = code.length > maxLength
const displayCode = isTruncated ? code.slice(0, maxLength) + '...' : code
return {
description,
displayCode,
fullCode: code,
isTruncated,
hasPreview: snippet?.hasPreview || false
}
} catch (error) {
return {
description: '',
displayCode: '',
fullCode: '',
isTruncated: false,
hasPreview: false
}
return {
description,
displayCode,
fullCode: code,
isTruncated,
hasPreview: snippet?.hasPreview || false
}
}, [snippet])
@@ -92,6 +82,7 @@ export function SnippetCard({
}
const handleView = (e: React.MouseEvent) => {
e.stopPropagation()
if (selectionMode) {
handleToggleSelect()
} else {
@@ -135,7 +126,6 @@ export function SnippetCard({
)
}
const currentNamespace = namespaces.find(n => n.id === snippet.namespaceId)
const availableNamespaces = namespaces.filter(n => n.id !== snippet.namespaceId)
return (

View File

@@ -1,4 +1,4 @@
import { useEffect, useState, useMemo } from 'react'
import { useMemo } from 'react'
import * as React from 'react'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AIErrorHelper } from '@/components/error/AIErrorHelper'
@@ -15,24 +15,17 @@ interface ReactPreviewProps {
}
export function ReactPreview({ code, language, functionName, inputParameters }: ReactPreviewProps) {
const [error, setError] = useState<string | null>(null)
const [Component, setComponent] = useState<React.ComponentType | null>(null)
useEffect(() => {
setError(null)
setComponent(null)
const { Component, error } = useMemo(() => {
const isReactCode = ['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language)
if (!isReactCode) {
return
return { Component: null, error: null }
}
try {
const transformedComponent = transformReactCode(code, functionName)
setComponent(() => transformedComponent)
return { Component: transformedComponent, error: null }
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to render preview')
return { Component: null, error: err instanceof Error ? err.message : 'Failed to render preview' }
}
}, [code, language, functionName])

View File

@@ -1,17 +1,10 @@
import { useState, useEffect } from 'react'
import { Database, CloudCheck } from '@phosphor-icons/react'
import { getStorageConfig } from '@/lib/storage'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
export function BackendIndicator() {
const [backend, setBackend] = useState<'indexeddb' | 'flask'>('indexeddb')
const [isEnvConfigured, setIsEnvConfigured] = useState(false)
useEffect(() => {
const config = getStorageConfig()
setBackend(config.backend)
setIsEnvConfigured(Boolean(process.env.NEXT_PUBLIC_FLASK_BACKEND_URL))
}, [])
const { backend } = getStorageConfig()
const isEnvConfigured = Boolean(process.env.NEXT_PUBLIC_FLASK_BACKEND_URL)
if (backend === 'indexeddb') {
return (

View File

@@ -1,5 +1,3 @@
import { ComponentShowcase } from '@/components/demo/ComponentShowcase'
import { organismsCodeSnippets } from '@/lib/component-code-snippets'
import { Snippet } from '@/lib/types'
import { NavigationBarsShowcase } from './showcases/NavigationBarsShowcase'
import { DataTablesShowcase } from './showcases/DataTablesShowcase'

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from 'react';
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
@@ -8,18 +8,10 @@ import { Label } from '@/components/ui/label';
import { Eye, EyeClosed, Key } from '@phosphor-icons/react';
export function OpenAISettingsCard() {
const [apiKey, setApiKey] = useState('');
const [apiKey, setApiKey] = useState(() => (typeof window !== 'undefined' ? localStorage.getItem('openai_api_key') || '' : ''));
const [showKey, setShowKey] = useState(false);
const [saved, setSaved] = useState(false);
useEffect(() => {
// Load the API key from localStorage on mount
const storedKey = localStorage.getItem('openai_api_key');
if (storedKey) {
setApiKey(storedKey);
}
}, []);
const handleSave = () => {
if (apiKey.trim()) {
localStorage.setItem('openai_api_key', apiKey.trim());

View File

@@ -1,6 +1,5 @@
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import {
Bell,

View File

@@ -1,6 +1,6 @@
"use client"
import { CSSProperties, ComponentProps, useMemo } from "react"
import { CSSProperties, ComponentProps } from "react"
import { cn } from "@/lib/utils"
import { Skeleton } from "@/components/ui/skeleton"
@@ -11,10 +11,8 @@ export function SidebarMenuSkeleton({
}: ComponentProps<"div"> & {
showIcon?: boolean
}) {
// Random width between 50 to 90%.
const width = useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
// Use a stable width so skeletons don't change across renders.
const width = "70%"
return (
<div

View File

@@ -3,7 +3,10 @@ import { useEffect, useState } from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined)
const [isMobile, setIsMobile] = useState<boolean | undefined>(() => {
if (typeof window === "undefined") return undefined
return window.innerWidth < MOBILE_BREAKPOINT
})
useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
@@ -11,7 +14,6 @@ export function useIsMobile() {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])

View File

@@ -14,7 +14,6 @@ export function usePythonTerminal() {
const [isInitializing, setIsInitializing] = useState(!isPyodideReady())
const [inputValue, setInputValue] = useState('')
const [waitingForInput, setWaitingForInput] = useState(false)
const [inputPrompt, setInputPrompt] = useState('')
const inputResolveRef = useRef<((value: string) => void) | null>(null)
useEffect(() => {
@@ -42,7 +41,6 @@ export function usePythonTerminal() {
const handleInputPrompt = (prompt: string): Promise<string> => {
return new Promise((resolve) => {
setInputPrompt(prompt)
addLine('input-prompt', prompt)
setWaitingForInput(true)
inputResolveRef.current = resolve

View File

@@ -98,7 +98,7 @@ export async function createTemplate(snippet: Omit<Snippet, 'id' | 'createdAt' |
await createSnippet(template);
}
export async function syncTemplatesFromJSON(templates: any[]): Promise<void> {
export async function syncTemplatesFromJSON(templates: unknown[]): Promise<void> {
// This would sync predefined templates - implement as needed
console.log('Syncing templates', templates.length);
}

View File

@@ -13,7 +13,7 @@ import type { Monaco } from '@monaco-editor/react'
* - module: 99 = ModuleKind.ESNext
* - jsx: 2 = JsxEmit.React
*/
export const compilerOptions = {
export const compilerOptions: Monaco['languages']['typescript']['CompilerOptions'] = {
target: 2, // ScriptTarget.Latest
allowNonTsExtensions: true,
moduleResolution: 2, // ModuleResolutionKind.NodeJs
@@ -29,7 +29,7 @@ export const compilerOptions = {
/**
* Diagnostics options for TypeScript/JavaScript validation
*/
export const diagnosticsOptions = {
export const diagnosticsOptions: Monaco['languages']['typescript']['DiagnosticsOptions'] = {
noSemanticValidation: false,
noSyntaxValidation: false,
}
@@ -160,10 +160,10 @@ export const shadcnTypes = `
*/
export function configureMonacoTypeScript(monaco: Monaco) {
// Set compiler options for TypeScript
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions as any)
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions)
// Set compiler options for JavaScript
monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions as any)
monaco.languages.typescript.javascriptDefaults.setCompilerOptions(compilerOptions)
// Set diagnostics options for TypeScript
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(diagnosticsOptions)

View File

@@ -1,11 +1,11 @@
import { InputParameter } from '@/lib/types'
export function parseInputParameters(inputParameters?: InputParameter[]): Record<string, any> {
export function parseInputParameters(inputParameters?: InputParameter[]): Record<string, unknown> {
if (!inputParameters || inputParameters.length === 0) {
return {}
}
const parsedProps: Record<string, any> = {}
const parsedProps: Record<string, unknown> = {}
inputParameters.forEach((param) => {
try {

View File

@@ -46,7 +46,9 @@ sys.stderr = StringIO()
`)
try {
const result = pyodide.runPython(code)
const result = await pyodide.runPythonAsync(code)
// Flush and collect stdout/stderr after async execution
const stdout = pyodide.runPython('sys.stdout.getvalue()')
const stderr = pyodide.runPython('sys.stderr.getvalue()')

View File

@@ -89,7 +89,7 @@ export class FlaskStorageAdapter {
signal: AbortSignal.timeout(5000)
})
return response.ok
} catch (error) {
} catch {
return false
}
}
@@ -102,8 +102,8 @@ export class FlaskStorageAdapter {
if (!response.ok) {
throw new Error(`Failed to fetch snippets: ${response.statusText}`)
}
const data = await response.json()
return data.map((s: any) => ({
const data: Snippet[] = await response.json()
return data.map((s) => ({
...s,
createdAt: typeof s.createdAt === 'string' ? new Date(s.createdAt).getTime() : s.createdAt,
updatedAt: typeof s.updatedAt === 'string' ? new Date(s.updatedAt).getTime() : s.updatedAt

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState } from 'react'
import {
type PersistenceConfig,
getPersistenceConfig,

View File

@@ -154,11 +154,11 @@ const snippetsSlice = createSlice({
state.items = state.items.filter(s => s.id !== action.payload)
})
.addCase(moveSnippet.fulfilled, (state, action) => {
const { snippetId, targetNamespaceId } = action.payload
const { snippetId } = action.payload
state.items = state.items.filter(s => s.id !== snippetId)
})
.addCase(bulkMoveSnippets.fulfilled, (state, action) => {
const { snippetIds, targetNamespaceId } = action.payload
const { snippetIds } = action.payload
state.items = state.items.filter(s => !snippetIds.includes(s.id))
state.selectedIds = []
state.selectionMode = false