diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b9719f4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: Kaapi CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + lint-and-build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run Linting + run: npm run lint + + - name: Build the Project + run: npm run build diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..2312dc5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/README.md b/README.md index e4b76fc..67701c9 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ This is a thin frontend UI for [Kaapi backend](https://github.com/ProjectTech4De - [Installation](#installation) - [Start frontend server](#start-frontend-server) - [Available Scripts](#available-scripts) +- [Pre-commit Hooks](#pre-commit-hooks) - [Deploying Release on EC2 with CD](#deploying-release-on-ec2-with-cd) - [Learn More](#learn-more) @@ -91,11 +92,41 @@ Visit `http://localhost:3000` to open the app. ## Available Scripts ```bash -npm install # Install dependencies +npm install # Install dependencies (also sets up pre-commit hooks via husky) npm run dev # Run app in development mode npm run build # Create optimized production build npm run start # Start the production server -npm run lint # Run ESLint +npm run lint # Run ESLint on the entire project +npm test # Run tests +npm run test:coverage # Run tests with coverage report +``` + +--- + +## Pre-commit Hooks + +This project uses [Husky](https://typicode.github.io/husky/) and [lint-staged](https://github.com/lint-staged/lint-staged) to enforce code quality before every commit. + +### What runs on commit + +- **ESLint** is executed on all staged `*.ts`, `*.tsx`, `*.js`, and `*.jsx` files. Any lint errors must be fixed before the commit is accepted. + +### Setup + +Hooks are installed automatically when you run: + +```bash +npm install +``` + +This works because the `prepare` script in `package.json` runs `husky` after every install. + +### Skipping the hook (not recommended) + +If you need to bypass the hook in exceptional cases: + +```bash +git commit --no-verify -m "your message" ``` --- diff --git a/app/api/assistant/[assistant_id]/route.ts b/app/api/assistant/[assistant_id]/route.ts index 45458f9..b25a15f 100644 --- a/app/api/assistant/[assistant_id]/route.ts +++ b/app/api/assistant/[assistant_id]/route.ts @@ -42,10 +42,10 @@ export async function GET( } return NextResponse.json(data, { status: 200 }); - } catch (error: any) { + } catch (error: unknown) { console.error('Proxy error:', error); return NextResponse.json( - { error: 'Failed to forward request to backend', details: error.message }, + { error: 'Failed to forward request to backend', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } diff --git a/app/api/collections/[collection_id]/route.ts b/app/api/collections/[collection_id]/route.ts index 76f80e9..8892c76 100644 --- a/app/api/collections/[collection_id]/route.ts +++ b/app/api/collections/[collection_id]/route.ts @@ -35,9 +35,9 @@ export async function GET( } return NextResponse.json(data, { status: response.status }); - } catch (error: any) { + } catch (error: unknown) { return NextResponse.json( - { success: false, error: error.message, data: null }, + { success: false, error: error instanceof Error ? error.message : String(error), data: null }, { status: 500 } ); } @@ -77,9 +77,9 @@ export async function DELETE( } return NextResponse.json(data, { status: response.status }); - } catch (error: any) { + } catch (error: unknown) { return NextResponse.json( - { success: false, error: error.message, data: null }, + { success: false, error: error instanceof Error ? error.message : String(error), data: null }, { status: 500 } ); } diff --git a/app/api/collections/jobs/[job_id]/route.ts b/app/api/collections/jobs/[job_id]/route.ts index 0f8962f..37180c1 100644 --- a/app/api/collections/jobs/[job_id]/route.ts +++ b/app/api/collections/jobs/[job_id]/route.ts @@ -35,9 +35,9 @@ export async function GET( } return NextResponse.json(data, { status: response.status }); - } catch (error: any) { + } catch (error: unknown) { return NextResponse.json( - { success: false, error: error.message, data: null }, + { success: false, error: error instanceof Error ? error.message : String(error), data: null }, { status: 500 } ); } diff --git a/app/api/collections/route.ts b/app/api/collections/route.ts index 0b57c27..2d4033d 100644 --- a/app/api/collections/route.ts +++ b/app/api/collections/route.ts @@ -28,9 +28,9 @@ export async function GET(request: Request) { } return NextResponse.json(data, { status: response.status }); - } catch (error: any) { + } catch (error: unknown) { return NextResponse.json( - { success: false, error: error.message, data: null }, + { success: false, error: error instanceof Error ? error.message : String(error), data: null }, { status: 500 } ); } @@ -74,10 +74,10 @@ export async function POST(request: NextRequest) { } return NextResponse.json(data, { status: response.status }); - } catch (error: any) { + } catch (error: unknown) { console.error('Proxy error:', error); return NextResponse.json( - { error: 'Failed to forward request to backend', details: error.message }, + { error: 'Failed to forward request to backend', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } diff --git a/app/api/configs/[config_id]/route.ts b/app/api/configs/[config_id]/route.ts index c6a00e2..91ea693 100644 --- a/app/api/configs/[config_id]/route.ts +++ b/app/api/configs/[config_id]/route.ts @@ -17,7 +17,7 @@ export async function GET( const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to fetch config', data: null }, { status: 500 } @@ -47,7 +47,7 @@ export async function PATCH( const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to update config', data: null }, { status: 500 } @@ -73,7 +73,7 @@ export async function DELETE( const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to delete config', data: null }, { status: 500 } diff --git a/app/api/configs/[config_id]/versions/[version_number]/route.ts b/app/api/configs/[config_id]/versions/[version_number]/route.ts index 175053e..f03fe9a 100644 --- a/app/api/configs/[config_id]/versions/[version_number]/route.ts +++ b/app/api/configs/[config_id]/versions/[version_number]/route.ts @@ -20,7 +20,7 @@ export async function GET( const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to fetch version', data: null }, { status: 500 } @@ -49,7 +49,7 @@ export async function DELETE( const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to delete version', data: null }, { status: 500 } diff --git a/app/api/configs/[config_id]/versions/route.ts b/app/api/configs/[config_id]/versions/route.ts index 12768fe..f161cb5 100644 --- a/app/api/configs/[config_id]/versions/route.ts +++ b/app/api/configs/[config_id]/versions/route.ts @@ -17,7 +17,7 @@ export async function GET( const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to fetch versions', data: null }, { status: 500 } @@ -47,7 +47,7 @@ export async function POST( const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to create version', data: null }, { status: 500 } diff --git a/app/api/credentials/[provider]/route.ts b/app/api/credentials/[provider]/route.ts index cfc7bef..3b9ebe7 100644 --- a/app/api/credentials/[provider]/route.ts +++ b/app/api/credentials/[provider]/route.ts @@ -12,8 +12,8 @@ export async function GET( `/api/v1/credentials/provider/${provider}`, ); return NextResponse.json(data, { status }); - } catch (e: any) { - return NextResponse.json({ error: e.message }, { status: 500 }); + } catch (e: unknown) { + return NextResponse.json({ error: e instanceof Error ? e.message : String(e) }, { status: 500 }); } } @@ -33,7 +33,7 @@ export async function DELETE( } return NextResponse.json(data, { status }); - } catch (e: any) { - return NextResponse.json({ error: e.message }, { status: 500 }); + } catch (e: unknown) { + return NextResponse.json({ error: e instanceof Error ? e.message : String(e) }, { status: 500 }); } } diff --git a/app/api/credentials/route.ts b/app/api/credentials/route.ts index 70a8969..b062fd7 100644 --- a/app/api/credentials/route.ts +++ b/app/api/credentials/route.ts @@ -5,8 +5,8 @@ export async function GET(request: NextRequest) { try { const { status, data } = await apiClient(request, "/api/v1/credentials/"); return NextResponse.json(data, { status }); - } catch (e: any) { - return NextResponse.json({ error: e.message }, { status: 500 }); + } catch (e: unknown) { + return NextResponse.json({ error: e instanceof Error ? e.message : String(e) }, { status: 500 }); } } @@ -18,8 +18,8 @@ export async function POST(request: NextRequest) { body: JSON.stringify(body), }); return NextResponse.json(data, { status }); - } catch (e: any) { - return NextResponse.json({ error: e.message }, { status: 500 }); + } catch (e: unknown) { + return NextResponse.json({ error: e instanceof Error ? e.message : String(e) }, { status: 500 }); } } @@ -31,7 +31,7 @@ export async function PATCH(request: NextRequest) { body: JSON.stringify(body), }); return NextResponse.json(data, { status }); - } catch (e: any) { - return NextResponse.json({ error: e.message }, { status: 500 }); + } catch (e: unknown) { + return NextResponse.json({ error: e instanceof Error ? e.message : String(e) }, { status: 500 }); } } diff --git a/app/api/document/[document_id]/route.ts b/app/api/document/[document_id]/route.ts index f244967..74c7ded 100644 --- a/app/api/document/[document_id]/route.ts +++ b/app/api/document/[document_id]/route.ts @@ -1,4 +1,4 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { NextResponse } from 'next/server'; export async function GET(request: Request, @@ -32,9 +32,9 @@ export async function GET(request: Request, } return NextResponse.json(data, { status: 200 }); - } catch (error: any) { + } catch (error: unknown) { return NextResponse.json( - { success: false, error: error.message, data: null }, + { success: false, error: error instanceof Error ? error.message : String(error), data: null }, { status: 500 } ); } @@ -71,10 +71,10 @@ export async function DELETE(request: Request, } return NextResponse.json(data, { status: 200 }); - } catch (error: any) { + } catch (error: unknown) { console.error('Delete error:', error); return NextResponse.json( - { error: 'Failed to delete document', details: error.message }, + { error: 'Failed to delete document', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } diff --git a/app/api/document/route.ts b/app/api/document/route.ts index d63edc6..c8c90c9 100644 --- a/app/api/document/route.ts +++ b/app/api/document/route.ts @@ -28,9 +28,9 @@ export async function GET(request: Request) { } return NextResponse.json(data, { status: response.status }); - } catch (error: any) { + } catch (error: unknown) { return NextResponse.json( - { success: false, error: error.message, data: null }, + { success: false, error: error instanceof Error ? error.message : String(error), data: null }, { status: 500 } ); } @@ -74,10 +74,10 @@ export async function POST(request: NextRequest) { } return NextResponse.json(data, { status: response.status }); - } catch (error: any) { + } catch (error: unknown) { console.error('Proxy error:', error); return NextResponse.json( - { error: 'Failed to forward request to backend', details: error.message }, + { error: 'Failed to forward request to backend', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } diff --git a/app/api/evaluations/[id]/route.ts b/app/api/evaluations/[id]/route.ts index f65043b..405ce92 100644 --- a/app/api/evaluations/[id]/route.ts +++ b/app/api/evaluations/[id]/route.ts @@ -88,10 +88,10 @@ export async function GET( } return NextResponse.json(data, { status: 200 }); - } catch (error: any) { + } catch (error: unknown) { console.error('Proxy error:', error); return NextResponse.json( - { error: 'Failed to fetch evaluation', details: error.message }, + { error: 'Failed to fetch evaluation', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } diff --git a/app/api/evaluations/datasets/[dataset_id]/route.ts b/app/api/evaluations/datasets/[dataset_id]/route.ts index bf65d09..3c633b9 100644 --- a/app/api/evaluations/datasets/[dataset_id]/route.ts +++ b/app/api/evaluations/datasets/[dataset_id]/route.ts @@ -46,10 +46,10 @@ export async function GET( } return NextResponse.json(data, { status: 200 }); - } catch (error: any) { + } catch (error: unknown) { console.error('Proxy error:', error); return NextResponse.json( - { error: 'Failed to forward request to backend', details: error.message }, + { error: 'Failed to forward request to backend', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } @@ -92,7 +92,7 @@ export async function DELETE( let data; try { data = await response.json(); - } catch (e) { + } catch (_e) { // If response is not JSON, just return success data = { success: true }; } @@ -103,10 +103,10 @@ export async function DELETE( } return NextResponse.json(data, { status: 200 }); - } catch (error: any) { + } catch (error: unknown) { console.error('Proxy error:', error); return NextResponse.json( - { error: 'Failed to forward request to backend', details: error.message }, + { error: 'Failed to forward request to backend', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } diff --git a/app/api/evaluations/datasets/route.ts b/app/api/evaluations/datasets/route.ts index 0a8a3a6..9ea9b9e 100644 --- a/app/api/evaluations/datasets/route.ts +++ b/app/api/evaluations/datasets/route.ts @@ -37,10 +37,10 @@ export async function GET(request: NextRequest) { } return NextResponse.json(data, { status: 200 }); - } catch (error: any) { + } catch (error: unknown) { console.error('Proxy error:', error); return NextResponse.json( - { error: 'Failed to forward request to backend', details: error.message }, + { error: 'Failed to forward request to backend', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } @@ -89,10 +89,10 @@ export async function POST(request: NextRequest) { } return NextResponse.json(data, { status: 200 }); - } catch (error: any) { + } catch (error: unknown) { console.error('Proxy error:', error); return NextResponse.json( - { error: 'Failed to forward request to backend', details: error.message }, + { error: 'Failed to forward request to backend', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } diff --git a/app/api/evaluations/stt/datasets/[dataset_id]/route.ts b/app/api/evaluations/stt/datasets/[dataset_id]/route.ts index 6d53bf8..5f085ba 100644 --- a/app/api/evaluations/stt/datasets/[dataset_id]/route.ts +++ b/app/api/evaluations/stt/datasets/[dataset_id]/route.ts @@ -34,7 +34,7 @@ export async function GET( const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to fetch dataset', data: null }, { status: 500 } @@ -62,7 +62,7 @@ export async function DELETE( let data; try { data = await response.json(); } catch { data = { success: true }; } return NextResponse.json(data, { status: response.ok ? 200 : response.status }); - } catch (error: any) { - return NextResponse.json({ success: false, error: 'Failed to delete dataset', details: error.message }, { status: 500 }); + } catch (error: unknown) { + return NextResponse.json({ success: false, error: 'Failed to delete dataset', details: error instanceof Error ? error.message : String(error) }, { status: 500 }); } } \ No newline at end of file diff --git a/app/api/evaluations/stt/files/route.ts b/app/api/evaluations/stt/files/route.ts index ab76667..143dbaf 100644 --- a/app/api/evaluations/stt/files/route.ts +++ b/app/api/evaluations/stt/files/route.ts @@ -39,7 +39,7 @@ export async function POST(request: NextRequest) { } return NextResponse.json(data, { status: response.status }); - } catch (error: any) { + } catch (error: unknown) { console.error('Proxy error:', error); return NextResponse.json( { error: 'Failed to forward request to backend', details: error instanceof Error ? error.message : String(error) }, diff --git a/app/api/evaluations/stt/results/[result_id]/route.ts b/app/api/evaluations/stt/results/[result_id]/route.ts index 16d69d6..a336c1c 100644 --- a/app/api/evaluations/stt/results/[result_id]/route.ts +++ b/app/api/evaluations/stt/results/[result_id]/route.ts @@ -1,4 +1,4 @@ -import { NextResponse, NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; export async function GET( request: Request, @@ -17,7 +17,7 @@ export async function GET( const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to fetch results', data: null }, { status: 500 } @@ -55,7 +55,7 @@ export async function PATCH( const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to update result feedback', data: null }, { status: 500 } diff --git a/app/api/evaluations/stt/runs/[run_id]/route.ts b/app/api/evaluations/stt/runs/[run_id]/route.ts index 49bac61..3aab95d 100644 --- a/app/api/evaluations/stt/runs/[run_id]/route.ts +++ b/app/api/evaluations/stt/runs/[run_id]/route.ts @@ -1,4 +1,4 @@ -import { NextResponse, NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; export async function GET( request: Request, @@ -26,7 +26,7 @@ export async function GET( const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to fetch the run', data: null }, { status: 500 } diff --git a/app/api/evaluations/stt/samples/[sample_id]/route.ts b/app/api/evaluations/stt/samples/[sample_id]/route.ts index a9794d3..bf4cbea 100644 --- a/app/api/evaluations/stt/samples/[sample_id]/route.ts +++ b/app/api/evaluations/stt/samples/[sample_id]/route.ts @@ -37,10 +37,10 @@ export async function PATCH( data = { success: response.ok }; } return NextResponse.json(data, { status: response.status }); - } catch (error: any) { + } catch (error: unknown) { console.error('Sample update error:', error); return NextResponse.json( - { success: false, error: 'Failed to update sample', details: error.message }, + { success: false, error: 'Failed to update sample', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } diff --git a/app/api/evaluations/tts/datasets/[dataset_id]/route.ts b/app/api/evaluations/tts/datasets/[dataset_id]/route.ts index 1ce25c7..49b84d6 100644 --- a/app/api/evaluations/tts/datasets/[dataset_id]/route.ts +++ b/app/api/evaluations/tts/datasets/[dataset_id]/route.ts @@ -51,7 +51,7 @@ export async function GET( } return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to fetch dataset', data: null }, { status: 500 } @@ -79,7 +79,7 @@ export async function DELETE( let data; try { data = await response.json(); } catch { data = { success: true }; } return NextResponse.json(data, { status: response.ok ? 200 : response.status }); - } catch (error: any) { - return NextResponse.json({ success: false, error: 'Failed to delete dataset', details: error.message }, { status: 500 }); + } catch (error: unknown) { + return NextResponse.json({ success: false, error: 'Failed to delete dataset', details: error instanceof Error ? error.message : String(error) }, { status: 500 }); } } diff --git a/app/api/evaluations/tts/results/[result_id]/route.ts b/app/api/evaluations/tts/results/[result_id]/route.ts index 5620737..14489d5 100644 --- a/app/api/evaluations/tts/results/[result_id]/route.ts +++ b/app/api/evaluations/tts/results/[result_id]/route.ts @@ -17,7 +17,7 @@ export async function GET( const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to fetch results', data: null }, { status: 500 } @@ -54,7 +54,7 @@ export async function PATCH( const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to update result feedback', data: null }, { status: 500 } diff --git a/app/api/evaluations/tts/runs/[run_id]/route.ts b/app/api/evaluations/tts/runs/[run_id]/route.ts index 5ebbe00..fe5e265 100644 --- a/app/api/evaluations/tts/runs/[run_id]/route.ts +++ b/app/api/evaluations/tts/runs/[run_id]/route.ts @@ -24,7 +24,7 @@ export async function GET( const data = await response.json(); return NextResponse.json(data, { status: response.status }); - } catch (error) { + } catch (_error) { return NextResponse.json( { success: false, error: 'Failed to fetch the run', data: null }, { status: 500 } diff --git a/app/components/ConfigDrawer.tsx b/app/components/ConfigDrawer.tsx index 5070268..320ebaa 100644 --- a/app/components/ConfigDrawer.tsx +++ b/app/components/ConfigDrawer.tsx @@ -4,7 +4,8 @@ * Updated to work with backend API config structure */ -"use client" +"use client"; + import React, { useState } from 'react'; import { colors } from '@/app/lib/colors'; import { SavedConfig } from './SimplifiedConfigEditor'; @@ -31,13 +32,13 @@ interface ConfigDrawerProps { commitMessage?: string; }; selectedConfigId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any onConfigChange: (field: string, value: any) => void; onSaveConfig: () => void; onLoadConfig: (config: SavedConfig) => void; onApplyConfig: (configId: string) => void; } -// Provider-specific models const MODEL_OPTIONS = { openai: [ { value: 'gpt-4o', label: 'GPT-4o' }, @@ -59,7 +60,6 @@ const MODEL_OPTIONS = { // ], }; -// Simple diff utility function generateDiff(text1: string, text2: string): { left: DiffLine[], right: DiffLine[] } { const lines1 = text1.split('\n'); const lines2 = text2.split('\n'); @@ -140,6 +140,7 @@ export default function ConfigDrawer({ // Format timestamp - calculate relative time from UTC timestamps const formatTimestamp = (timestamp: number | string) => { + // eslint-disable-next-line react-hooks/purity const now = Date.now(); // Current time in UTC milliseconds const date = typeof timestamp === 'string' ? new Date(timestamp).getTime() // Parse UTC timestamp to milliseconds @@ -214,11 +215,12 @@ export default function ConfigDrawer({ onConfigChange('tools', newTools); }; - const updateTool = (index: number, field: keyof Tool, value: any) => { + const updateTool = (index: number, field: keyof Tool, value: unknown) => { const newTools = [...tools]; if (field === 'knowledge_base_ids') { - newTools[index][field] = [value]; + newTools[index][field] = [value as string]; } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any (newTools[index] as any)[field] = value; } onConfigChange('tools', newTools); diff --git a/app/components/ConfigModal.tsx b/app/components/ConfigModal.tsx index e0a6aee..7c8eb22 100644 --- a/app/components/ConfigModal.tsx +++ b/app/components/ConfigModal.tsx @@ -96,7 +96,9 @@ export default function ConfigModal({ isOpen, onClose, job, assistantConfig }: C // 2. Check tools array for knowledge_base_ids if (params.tools) { const toolKbIds = params.tools + // eslint-disable-next-line @typescript-eslint/no-explicit-any .filter((tool: any) => Array.isArray(tool.knowledge_base_ids) && tool.knowledge_base_ids.length > 0) + // eslint-disable-next-line @typescript-eslint/no-explicit-any .flatMap((tool: any) => tool.knowledge_base_ids); knowledgeBaseIds.push(...toolKbIds); } @@ -275,11 +277,13 @@ export default function ConfigModal({ isOpen, onClose, job, assistantConfig }: C
- {job.config.tools.map((tool, idx) => ( + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {job.config.tools.map((tool: any, idx) => ( {tool.type} ))}
- {job.config.tools.map((tool, idx) => ( + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {job.config.tools.map((tool: any, idx: number) => ( {Array.isArray(tool.knowledge_base_ids) && tool.knowledge_base_ids.length > 0 && (
diff --git a/app/components/ConfigSelector.tsx b/app/components/ConfigSelector.tsx index 5e47493..b53b0b9 100644 --- a/app/components/ConfigSelector.tsx +++ b/app/components/ConfigSelector.tsx @@ -42,6 +42,7 @@ export default function ConfigSelector({ // Reset expanded state and recheck overflow whenever selected config changes. useLayoutEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect setPromptExpanded(false); const el = promptRef.current; if (!el) return; diff --git a/app/components/DetailedResultsTable.tsx b/app/components/DetailedResultsTable.tsx index f18070c..19ff7d3 100644 --- a/app/components/DetailedResultsTable.tsx +++ b/app/components/DetailedResultsTable.tsx @@ -6,8 +6,7 @@ */ import React, { useState, useEffect } from 'react'; -import { IndividualScore, TraceScore, getScoreObject, normalizeToIndividualScores, hasSummaryScores, isNewScoreObjectV2, isGroupedFormat, GroupedTraceItem } from './types'; -import { EvalJob } from './types'; +import { TraceScore, getScoreObject, normalizeToIndividualScores, hasSummaryScores, isNewScoreObjectV2, isGroupedFormat, GroupedTraceItem, EvalJob } from '@/app/components/types'; // Helper function to format score value with color const formatScoreValue = (score: TraceScore | undefined) => { diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx index 79be552..fe5eede 100644 --- a/app/components/Sidebar.tsx +++ b/app/components/Sidebar.tsx @@ -39,6 +39,7 @@ export default function Sidebar({ collapsed, activeRoute = '/evaluations' }: Sid const saved = localStorage.getItem('sidebar-expanded-menus'); if (saved) { try { + // eslint-disable-next-line react-hooks/set-state-in-effect setExpandedMenus(JSON.parse(saved)); } catch (e) { console.error('Failed to load sidebar state:', e); diff --git a/app/components/SimplifiedConfigEditor.tsx b/app/components/SimplifiedConfigEditor.tsx index a9aface..bfb23cc 100644 --- a/app/components/SimplifiedConfigEditor.tsx +++ b/app/components/SimplifiedConfigEditor.tsx @@ -9,10 +9,10 @@ * - Backend integration for persistent config storage */ -"use client" -import { useState, useEffect } from 'react'; -import ConfigDrawer from './ConfigDrawer'; -import { useToast } from './Toast'; +"use client"; +import { useState, useEffect } from "react"; +import ConfigDrawer from "./ConfigDrawer"; +import { useToast } from "./Toast"; import { ConfigPublic, ConfigVersionPublic, @@ -23,7 +23,8 @@ import { ConfigListResponse, ConfigWithVersionResponse, ConfigVersionListResponse, -} from '@/app/lib/configTypes'; +} from "@/app/lib/configTypes"; +import { colors } from "../lib/colors"; // UI representation of a config version (flattened for easier display) export interface SavedConfig { @@ -70,27 +71,26 @@ export default function SimplifiedConfigEditor({ }: SimplifiedConfigEditorProps) { const toast = useToast(); const [savedConfigs, setSavedConfigs] = useState([]); - const [configName, setConfigName] = useState(''); - const [selectedConfigId, setSelectedConfigId] = useState(''); - const [provider, setProvider] = useState('openai'); + const [configName, setConfigName] = useState(""); + const [selectedConfigId, setSelectedConfigId] = useState(""); + const [provider, setProvider] = useState("openai"); const [temperature, setTemperature] = useState(0.7); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [tools, setTools] = useState([]); const [isLoading, setIsLoading] = useState(false); - const [isSaving, setIsSaving] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - const [commitMessage, setCommitMessage] = useState(''); + const [commitMessage, setCommitMessage] = useState(""); // Get API key from localStorage const getApiKey = (): string | null => { try { - const stored = localStorage.getItem('kaapi_api_keys'); + const stored = localStorage.getItem("kaapi_api_keys"); if (stored) { const keys = JSON.parse(stored); return keys.length > 0 ? keys[0].key : null; } } catch (e) { - console.error('Failed to get API key:', e); + console.error("Failed to get API key:", e); } return null; }; @@ -98,7 +98,7 @@ export default function SimplifiedConfigEditor({ // Flatten config versions for UI const flattenConfigVersion = ( config: ConfigPublic, - version: ConfigVersionPublic + version: ConfigVersionPublic, ): SavedConfig => { const blob = version.config_blob; const params = blob.completion.params; @@ -109,10 +109,14 @@ export default function SimplifiedConfigEditor({ // If no tools array but has flattened fields, create tools array from them // Each knowledge_base_id becomes a separate tool for UI display - if (tools.length === 0 && params.knowledge_base_ids && params.knowledge_base_ids.length > 0) { + if ( + tools.length === 0 && + params.knowledge_base_ids && + params.knowledge_base_ids.length > 0 + ) { params.knowledge_base_ids.forEach((kbId: string) => { tools.push({ - type: 'file_search', + type: "file_search", knowledge_base_ids: [kbId], // Each tool gets one ID for UI max_num_results: params.max_num_results || 20, }); @@ -125,11 +129,11 @@ export default function SimplifiedConfigEditor({ name: config.name, version: version.version, timestamp: version.inserted_at, - instructions: params.instructions || '', - modelName: params.model || '', + instructions: params.instructions || "", + modelName: params.model || "", provider: blob.completion.provider, temperature: params.temperature ?? 0.7, - vectorStoreIds: tools[0]?.knowledge_base_ids?.[0] || '', + vectorStoreIds: tools[0]?.knowledge_base_ids?.[0] || "", tools: tools, commit_message: version.commit_message, }; @@ -141,20 +145,22 @@ export default function SimplifiedConfigEditor({ setIsLoading(true); const apiKey = getApiKey(); if (!apiKey) { - console.warn('No API key found. Please add an API key in the Keystore.'); + console.warn( + "No API key found. Please add an API key in the Keystore.", + ); setIsLoading(false); return; } try { // Fetch all configs - const response = await fetch('/api/configs', { - headers: { 'X-API-KEY': apiKey }, + const response = await fetch("/api/configs", { + headers: { "X-API-KEY": apiKey }, }); const data: ConfigListResponse = await response.json(); if (!data.success || !data.data) { - console.error('Failed to fetch configs:', data.error); + console.error("Failed to fetch configs:", data.error); setIsLoading(false); return; } @@ -163,10 +169,14 @@ export default function SimplifiedConfigEditor({ const allVersions: SavedConfig[] = []; for (const config of data.data) { try { - const versionsResponse = await fetch(`/api/configs/${config.id}/versions`, { - headers: { 'X-API-KEY': apiKey }, - }); - const versionsData: ConfigVersionListResponse = await versionsResponse.json(); + const versionsResponse = await fetch( + `/api/configs/${config.id}/versions`, + { + headers: { "X-API-KEY": apiKey }, + }, + ); + const versionsData: ConfigVersionListResponse = + await versionsResponse.json(); if (versionsData.success && versionsData.data) { // Fetch full version details for each version @@ -174,26 +184,34 @@ export default function SimplifiedConfigEditor({ try { const versionResponse = await fetch( `/api/configs/${config.id}/versions/${versionItem.version}`, - { headers: { 'X-API-KEY': apiKey } } + { headers: { "X-API-KEY": apiKey } }, ); const versionData = await versionResponse.json(); if (versionData.success && versionData.data) { - allVersions.push(flattenConfigVersion(config, versionData.data)); + allVersions.push( + flattenConfigVersion(config, versionData.data), + ); } } catch (e) { - console.error(`Failed to fetch version ${versionItem.version}:`, e); + console.error( + `Failed to fetch version ${versionItem.version}:`, + e, + ); } } } } catch (e) { - console.error(`Failed to fetch versions for config ${config.id}:`, e); + console.error( + `Failed to fetch versions for config ${config.id}:`, + e, + ); } } setSavedConfigs(allVersions); } catch (e) { - console.error('Failed to load saved configs:', e); + console.error("Failed to load saved configs:", e); } finally { setIsLoading(false); } @@ -210,7 +228,7 @@ export default function SimplifiedConfigEditor({ return; } - const selectedConfig = savedConfigs.find(c => c.id === selectedConfigId); + const selectedConfig = savedConfigs.find((c) => c.id === selectedConfigId); if (!selectedConfig) { setHasUnsavedChanges(true); return; @@ -225,23 +243,29 @@ export default function SimplifiedConfigEditor({ temperature !== selectedConfig.temperature; setHasUnsavedChanges(hasChanges); - }, [selectedConfigId, instructions, modelName, vectorStoreIds, provider, temperature, savedConfigs]); + }, [ + selectedConfigId, + instructions, + modelName, + vectorStoreIds, + provider, + temperature, + savedConfigs, + ]); // Save current configuration const handleSaveConfig = async () => { if (!configName.trim()) { - toast.error('Please enter a configuration name'); + toast.error("Please enter a configuration name"); return; } const apiKey = getApiKey(); if (!apiKey) { - toast.error('No API key found. Please add an API key in the Keystore.'); + toast.error("No API key found. Please add an API key in the Keystore."); return; } - setIsSaving(true); - try { // Build config blob // Extract tools array and flatten to direct params fields for backend compatibility @@ -260,7 +284,7 @@ export default function SimplifiedConfigEditor({ const configBlob: ConfigBlob = { completion: { - provider: provider as 'openai', // | 'anthropic' | 'google', // Only OpenAI supported for now + provider: provider as "openai", // | 'anthropic' | 'google', // Only OpenAI supported for now params: { model: modelName, instructions: instructions, @@ -275,46 +299,57 @@ export default function SimplifiedConfigEditor({ }; // Check if updating existing config (same name exists) - const existingConfig = savedConfigs.find(c => c.name === configName.trim()); + const existingConfig = savedConfigs.find( + (c) => c.name === configName.trim(), + ); if (existingConfig) { // Create new version for existing config const versionCreate: ConfigVersionCreate = { config_blob: configBlob, - commit_message: commitMessage.trim() || `Updated to ${modelName} with temperature ${temperature}`, + commit_message: + commitMessage.trim() || + `Updated to ${modelName} with temperature ${temperature}`, }; - const response = await fetch(`/api/configs/${existingConfig.config_id}/versions`, { - method: 'POST', - headers: { - 'X-API-KEY': apiKey, - 'Content-Type': 'application/json', + const response = await fetch( + `/api/configs/${existingConfig.config_id}/versions`, + { + method: "POST", + headers: { + "X-API-KEY": apiKey, + "Content-Type": "application/json", + }, + body: JSON.stringify(versionCreate), }, - body: JSON.stringify(versionCreate), - }); + ); const data = await response.json(); if (!data.success) { - toast.error(`Failed to create version: ${data.error || 'Unknown error'}`); + toast.error( + `Failed to create version: ${data.error || "Unknown error"}`, + ); return; } - toast.success(`Configuration "${configName}" updated! New version created.`); + toast.success( + `Configuration "${configName}" updated! New version created.`, + ); } else { // Create new config const configCreate: ConfigCreate = { name: configName.trim(), description: `${provider} ${modelName} configuration`, config_blob: configBlob, - commit_message: commitMessage.trim() || 'Initial version', + commit_message: commitMessage.trim() || "Initial version", }; - const response = await fetch('/api/configs', { - method: 'POST', + const response = await fetch("/api/configs", { + method: "POST", headers: { - 'X-API-KEY': apiKey, - 'Content-Type': 'application/json', + "X-API-KEY": apiKey, + "Content-Type": "application/json", }, body: JSON.stringify(configCreate), }); @@ -322,7 +357,9 @@ export default function SimplifiedConfigEditor({ const data: ConfigWithVersionResponse = await response.json(); if (!data.success || !data.data) { - toast.error(`Failed to create config: ${data.error || 'Unknown error'}`); + toast.error( + `Failed to create config: ${data.error || "Unknown error"}`, + ); return; } @@ -330,8 +367,8 @@ export default function SimplifiedConfigEditor({ } // Refresh configs list - const response = await fetch('/api/configs', { - headers: { 'X-API-KEY': apiKey }, + const response = await fetch("/api/configs", { + headers: { "X-API-KEY": apiKey }, }); const data: ConfigListResponse = await response.json(); @@ -339,30 +376,36 @@ export default function SimplifiedConfigEditor({ const allVersions: SavedConfig[] = []; for (const config of data.data) { try { - const versionsResponse = await fetch(`/api/configs/${config.id}/versions`, { - headers: { 'X-API-KEY': apiKey }, - }); - const versionsData: ConfigVersionListResponse = await versionsResponse.json(); + const versionsResponse = await fetch( + `/api/configs/${config.id}/versions`, + { + headers: { "X-API-KEY": apiKey }, + }, + ); + const versionsData: ConfigVersionListResponse = + await versionsResponse.json(); if (versionsData.success && versionsData.data) { for (const versionItem of versionsData.data) { try { const versionResponse = await fetch( `/api/configs/${config.id}/versions/${versionItem.version}`, - { headers: { 'X-API-KEY': apiKey } } + { headers: { "X-API-KEY": apiKey } }, ); const versionData = await versionResponse.json(); if (versionData.success && versionData.data) { - allVersions.push(flattenConfigVersion(config, versionData.data)); + allVersions.push( + flattenConfigVersion(config, versionData.data), + ); } } catch (e) { - console.error('Failed to fetch version:', e); + console.error("Failed to fetch version:", e); } } } } catch (e) { - console.error('Failed to fetch versions:', e); + console.error("Failed to fetch versions:", e); } } setSavedConfigs(allVersions); @@ -370,12 +413,10 @@ export default function SimplifiedConfigEditor({ // Reset unsaved changes flag and commit message after successful save setHasUnsavedChanges(false); - setCommitMessage(''); + setCommitMessage(""); } catch (e) { - console.error('Failed to save config:', e); - toast.error('Failed to save configuration. Please try again.'); - } finally { - setIsSaving(false); + console.error("Failed to save config:", e); + toast.error("Failed to save configuration. Please try again."); } }; @@ -398,58 +439,59 @@ export default function SimplifiedConfigEditor({ // Handle config selection from dropdown const handleConfigSelect = (configId: string) => { - if (configId === '') { + if (configId === "") { // New config - clear config selection - setSelectedConfigId(''); - setConfigName(''); - setProvider('openai'); + setSelectedConfigId(""); + setConfigName(""); + setProvider("openai"); setTemperature(0.7); setTools([]); - onModelNameChange('gpt-4'); - onInstructionsChange('You are a helpful FAQ assistant.'); - onVectorStoreIdsChange(''); + onModelNameChange("gpt-4"); + onInstructionsChange("You are a helpful FAQ assistant."); + onVectorStoreIdsChange(""); // Notify parent to clear config selection if (onConfigSelect) { - onConfigSelect('', 0); + onConfigSelect("", 0); } return; } - const config = savedConfigs.find(c => c.id === configId); + const config = savedConfigs.find((c) => c.id === configId); if (config) { handleLoadConfig(config); } }; // Handle config changes from drawer + // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleConfigChange = (field: string, value: any) => { switch (field) { - case 'name': + case "name": setConfigName(value); break; - case 'provider': + case "provider": setProvider(value); break; - case 'modelName': + case "modelName": onModelNameChange(value); break; - case 'temperature': + case "temperature": setTemperature(value); break; - case 'vectorStoreIds': + case "vectorStoreIds": onVectorStoreIdsChange(value); break; - case 'instructions': + case "instructions": onInstructionsChange(value); break; - case 'tools': + case "tools": setTools(value); break; - case 'selectedConfigId': + case "selectedConfigId": setSelectedConfigId(value); break; - case 'commitMessage': + case "commitMessage": setCommitMessage(value); break; } @@ -457,35 +499,13 @@ export default function SimplifiedConfigEditor({ // Apply config from comparison const handleApplyConfig = (configId: string) => { - if (configId === 'current') return; - const config = savedConfigs.find(c => c.id === configId); + if (configId === "current") return; + const config = savedConfigs.find((c) => c.id === configId); if (config) { handleLoadConfig(config); } }; - // Get current selected config - const selectedConfig = savedConfigs.find(c => c.id === selectedConfigId); - - // Format timestamp - calculate relative time from UTC timestamps - const formatTimestamp = (timestamp: string | number) => { - const now = Date.now(); // Current time in UTC milliseconds - const date = typeof timestamp === 'string' - ? new Date(timestamp).getTime() // Parse UTC timestamp to milliseconds - : timestamp; - - // Calculate difference (works the same in any timezone) - const diff = now - date; - const minutes = Math.floor(diff / 60000); - const hours = Math.floor(diff / 3600000); - const days = Math.floor(diff / 86400000); - - if (minutes < 1) return 'just now'; - if (minutes < 60) return `${minutes} min ago`; - if (hours < 24) return `${hours} hr ago`; - return `${days} day${days > 1 ? 's' : ''} ago`; - }; - return ( <>
@@ -501,14 +521,22 @@ export default function SimplifiedConfigEditor({ onClick={() => setIsDrawerOpen(true)} className="px-4 py-2 rounded-md text-sm font-medium flex items-center gap-2 relative" style={{ - backgroundColor: hasUnsavedChanges ? '#fffbeb' : '#ffffff', - borderWidth: '1px', - borderColor: hasUnsavedChanges ? '#fcd34d' : '#e5e5e5', - color: hasUnsavedChanges ? '#b45309' : '#171717', - transition: 'all 0.15s ease' + backgroundColor: hasUnsavedChanges ? "#fffbeb" : colors.bg.primary, + borderWidth: "1px", + borderColor: hasUnsavedChanges ? "#fcd34d" : "#e5e5e5", + color: hasUnsavedChanges ? "#b45309" : "#171717", + transition: "all 0.15s ease", }} - onMouseEnter={(e) => e.currentTarget.style.backgroundColor = hasUnsavedChanges ? '#fef3c7' : '#fafafa'} - onMouseLeave={(e) => e.currentTarget.style.backgroundColor = hasUnsavedChanges ? '#fffbeb' : '#ffffff'} + onMouseEnter={(e) => + (e.currentTarget.style.backgroundColor = hasUnsavedChanges + ? "#fef3c7" + : "#fafafa") + } + onMouseLeave={(e) => + (e.currentTarget.style.backgroundColor = hasUnsavedChanges + ? "#fffbeb" + : "#ffffff") + } > {hasUnsavedChanges && ( @@ -519,41 +547,93 @@ export default function SimplifiedConfigEditor({ {/* Run Evaluation Button */}
@@ -667,20 +760,29 @@ export default function SimplifiedConfigEditor({
-
Provider
+
+ Provider +
{provider}
-
Model
+
+ Model +
{modelName}
-
Temperature
-
{temperature.toFixed(2)}
+
+ Temperature +
+
+ {temperature.toFixed(2)} +

- 💡 Click "⚙️ Config" to edit model settings, save configs, or compare versions + 💡 Click "⚙️ Config" to edit model settings, save + configs, or compare versions

diff --git a/app/components/evaluations/DatasetsTab.tsx b/app/components/evaluations/DatasetsTab.tsx index aec7f11..b301429 100644 --- a/app/components/evaluations/DatasetsTab.tsx +++ b/app/components/evaluations/DatasetsTab.tsx @@ -82,6 +82,7 @@ export default function DatasetsTab({ } toast.success('Dataset deleted'); loadStoredDatasets(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { toast.error(err.message || 'Failed to delete dataset'); } finally { @@ -140,6 +141,7 @@ export default function DatasetsTab({ const rows = lines.slice(1).map(parseRow); setViewModalData({ name: datasetName, headers, rows, signedUrl: signedUrl || '' }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { toast.error(err.message || 'Failed to view dataset'); } finally { diff --git a/app/components/evaluations/EvaluationsTab.tsx b/app/components/evaluations/EvaluationsTab.tsx index 94739b2..63e0b01 100644 --- a/app/components/evaluations/EvaluationsTab.tsx +++ b/app/components/evaluations/EvaluationsTab.tsx @@ -1,7 +1,6 @@ "use client" import { useState, useEffect, useCallback } from 'react'; -import { useRouter } from 'next/navigation'; import { colors } from '@/app/lib/colors'; import { APIKey } from '@/app/keystore/page'; import { Dataset } from '@/app/datasets/page'; @@ -46,7 +45,6 @@ export default function EvaluationsTab({ handleRunEvaluation, setActiveTab, }: EvaluationsTabProps) { - const router = useRouter(); const [evalJobs, setEvalJobs] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -82,8 +80,8 @@ export default function EvaluationsTab({ } const data = await response.json(); setEvalJobs(Array.isArray(data) ? data : (data.data || [])); - } catch (err: any) { - setError(err.message || 'Failed to fetch evaluation jobs'); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Failed to fetch evaluation jobs'); } finally { setIsLoading(false); } @@ -370,7 +368,7 @@ export default function EvaluationsTab({ ) : (

No {statusFilter} runs

-

No evaluation runs with status "{statusFilter}"

+

No evaluation runs with status "{statusFilter}"

); })()} diff --git a/app/components/prompt-editor/ABTestTab.tsx b/app/components/prompt-editor/ABTestTab.tsx index f1ada4c..4ae00e2 100644 --- a/app/components/prompt-editor/ABTestTab.tsx +++ b/app/components/prompt-editor/ABTestTab.tsx @@ -41,6 +41,7 @@ export default function ABTestTab({ const updateVariant = (index: number, field: keyof LegacyVariant, value: string) => { const newVariants = [...variants]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any (newVariants[index] as any)[field] = value; onVariantsChange(newVariants); }; @@ -49,16 +50,6 @@ export default function ABTestTab({ return `#${commit.id}: ${commit.message} (${commit.branch})`; }; - const getConfigByCommit = (commitId: string) => { - const commit = commits.find((c) => c.id === commitId); - if (!commit) return null; - return { - model: 'gpt-4o-mini', // Default for now - temp: 0.7, // Default for now - prompt: commit.content.split('\n')[0], - }; - }; - const getBestVariant = () => { if (!testResults || testResults.length === 0) return null; return testResults.reduce((best, current) => diff --git a/app/components/prompt-editor/ConfigDiffPane.tsx b/app/components/prompt-editor/ConfigDiffPane.tsx index 0eae5a4..5602601 100644 --- a/app/components/prompt-editor/ConfigDiffPane.tsx +++ b/app/components/prompt-editor/ConfigDiffPane.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { colors } from '@/app/lib/colors'; -import { ConfigBlob } from '@/app/configurations/prompt-editor/types'; import { SavedConfig } from '@/app/lib/useConfigs'; interface ConfigDiffPaneProps { @@ -10,8 +9,8 @@ interface ConfigDiffPaneProps { interface ConfigDiff { field: string; - oldValue: any; - newValue: any; + oldValue: unknown; + newValue: unknown; changed: boolean; } @@ -66,10 +65,12 @@ export default function ConfigDiffPane({ const hasChanges = configDiffs.length > 0; - const renderValue = (value: any): string => { + const renderValue = (value: unknown): string => { if (Array.isArray(value)) { if (value.length === 0) return '[]'; - return value.map((tool, idx) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return value.map((_idx: any) => { + const tool = _idx; if (tool.type === 'file_search') { return `File Search (${tool.knowledge_base_ids?.[0] || 'no store'})`; } diff --git a/app/components/prompt-editor/ConfigDrawer.tsx b/app/components/prompt-editor/ConfigDrawer.tsx index 2cb77dc..1c84080 100644 --- a/app/components/prompt-editor/ConfigDrawer.tsx +++ b/app/components/prompt-editor/ConfigDrawer.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { colors } from '@/app/lib/colors'; +import { Tool } from '@/app/lib/configTypes'; import { Config, Commit, LegacyVariant, TestResult } from '@/app/configurations/prompt-editor/types'; import CurrentConfigTab from './CurrentConfigTab'; import HistoryTab from './HistoryTab'; @@ -19,15 +20,14 @@ interface ConfigDrawerProps { model: string; instructions: string; temperature: number; - tools: any[]; + tools: Tool[]; configCommitMsg: string; - currentContent: string; onConfigNameChange: (name: string) => void; onProviderChange: (provider: string) => void; onModelChange: (model: string) => void; onInstructionsChange: (instructions: string) => void; onTemperatureChange: (temp: number) => void; - onToolsChange: (tools: any[]) => void; + onToolsChange: (tools: Tool[]) => void; onConfigCommitMsgChange: (msg: string) => void; onSaveConfig: () => void; onLoadConfig: (configId: string) => void; @@ -58,7 +58,6 @@ export default function ConfigDrawer({ temperature, tools, configCommitMsg, - currentContent, onConfigNameChange, onProviderChange, onModelChange, diff --git a/app/components/prompt-editor/ConfigEditorPane.tsx b/app/components/prompt-editor/ConfigEditorPane.tsx index 0c43c9c..fea263c 100644 --- a/app/components/prompt-editor/ConfigEditorPane.tsx +++ b/app/components/prompt-editor/ConfigEditorPane.tsx @@ -98,6 +98,7 @@ export default function ConfigEditorPane({ ...configBlob, completion: { ...configBlob.completion, + // eslint-disable-next-line @typescript-eslint/no-explicit-any provider: newProvider as any, params: { ...params, @@ -166,11 +167,12 @@ export default function ConfigEditorPane({ }); }; - const handleUpdateTool = (index: number, field: keyof Tool, value: any) => { + const handleUpdateTool = (index: number, field: keyof Tool, value: unknown) => { const newTools = [...tools]; if (field === 'knowledge_base_ids') { - newTools[index][field] = [value]; + newTools[index][field] = [value as string]; } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any (newTools[index] as any)[field] = value; } onConfigChange({ diff --git a/app/components/prompt-editor/CurrentConfigTab.tsx b/app/components/prompt-editor/CurrentConfigTab.tsx index 0656078..1e3a2f5 100644 --- a/app/components/prompt-editor/CurrentConfigTab.tsx +++ b/app/components/prompt-editor/CurrentConfigTab.tsx @@ -62,11 +62,12 @@ export default function CurrentConfigTab({ onToolsChange(tools.filter((_, i) => i !== index)); }; - const updateTool = (index: number, field: keyof Tool, value: any) => { + const updateTool = (index: number, field: keyof Tool, value: unknown) => { const newTools = [...tools]; if (field === 'knowledge_base_ids') { - newTools[index][field] = [value]; + newTools[index][field] = [value as string]; } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any (newTools[index] as any)[field] = value; } onToolsChange(newTools); diff --git a/app/components/prompt-editor/DiffView.tsx b/app/components/prompt-editor/DiffView.tsx index a11304d..bedc977 100644 --- a/app/components/prompt-editor/DiffView.tsx +++ b/app/components/prompt-editor/DiffView.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import { useMemo } from 'react'; import { colors } from '@/app/lib/colors'; import PromptDiffPane from './PromptDiffPane'; import ConfigDiffPane from './ConfigDiffPane'; diff --git a/app/components/prompt-editor/HistorySidebar.tsx b/app/components/prompt-editor/HistorySidebar.tsx index a1dc66d..6d2c831 100644 --- a/app/components/prompt-editor/HistorySidebar.tsx +++ b/app/components/prompt-editor/HistorySidebar.tsx @@ -59,6 +59,7 @@ export default function HistorySidebar({ // Format timestamp - calculate relative time from UTC timestamps const formatTimestamp = (timestamp: string) => { + // eslint-disable-next-line react-hooks/purity const now = Date.now(); const date = new Date(timestamp).getTime(); const diff = now - date; diff --git a/app/components/prompt-editor/HistoryTab.tsx b/app/components/prompt-editor/HistoryTab.tsx index a690968..3846e08 100644 --- a/app/components/prompt-editor/HistoryTab.tsx +++ b/app/components/prompt-editor/HistoryTab.tsx @@ -14,6 +14,7 @@ export default function HistoryTab({ onLoadConfig, }: HistoryTabProps) { const formatTimestamp = (timestamp: number) => { + // eslint-disable-next-line react-hooks/purity const now = Date.now(); const diff = now - timestamp; const minutes = Math.floor(diff / 60000); diff --git a/app/components/speech-to-text/ModelComparisonCard.tsx b/app/components/speech-to-text/ModelComparisonCard.tsx index 96e4c2d..7fe9282 100644 --- a/app/components/speech-to-text/ModelComparisonCard.tsx +++ b/app/components/speech-to-text/ModelComparisonCard.tsx @@ -54,7 +54,6 @@ const getWerLabel = (wer: number) => { export default function ModelComparisonCard({ modelId, modelName, - provider, transcript, status, error, @@ -62,9 +61,7 @@ export default function ModelComparisonCard({ lenientMetrics, isBest = false, isWorst = false, - isSelected = false, onClick, - compact = false, }: ModelComparisonCardProps) { const [isExpanded, setIsExpanded] = useState(false); const werPercent = strictMetrics ? strictMetrics.wer * 100 : null; @@ -74,6 +71,7 @@ export default function ModelComparisonCard({ // Also reset when modelId changes (new model added) useEffect(() => { if (status === 'pending') { + // eslint-disable-next-line react-hooks/set-state-in-effect setIsExpanded(false); } }, [status, modelId]); diff --git a/app/components/speech-to-text/WaveformVisualizer.tsx b/app/components/speech-to-text/WaveformVisualizer.tsx index 726ddf8..6d66006 100644 --- a/app/components/speech-to-text/WaveformVisualizer.tsx +++ b/app/components/speech-to-text/WaveformVisualizer.tsx @@ -33,6 +33,7 @@ export default function WaveformVisualizer({ if (!audioElement || isInitialized.current) return; try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext; const audioContext = new AudioContextClass(); const analyser = audioContext.createAnalyser(); diff --git a/app/components/types.ts b/app/components/types.ts index 8dad6e3..a745c2f 100644 --- a/app/components/types.ts +++ b/app/components/types.ts @@ -119,7 +119,7 @@ export interface EvalJob { config?: { model?: string; instructions?: string; - tools?: any[]; + tools?: unknown[]; include?: string[]; temperature?: number; }; diff --git a/app/configurations/page.tsx b/app/configurations/page.tsx index f2942cd..5e0124b 100644 --- a/app/configurations/page.tsx +++ b/app/configurations/page.tsx @@ -13,7 +13,7 @@ import React, { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import Sidebar from '@/app/components/Sidebar'; import { colors } from '@/app/lib/colors'; -import { useConfigs, ConfigGroup, SavedConfig } from '@/app/lib/useConfigs'; +import { useConfigs, SavedConfig } from '@/app/lib/useConfigs'; import ConfigCard from '@/app/components/ConfigCard'; import { LoaderBox } from '@/app/components/Loader'; import { EvalJob } from '@/app/components/types'; @@ -274,7 +274,7 @@ export default function ConfigLibraryPage() {

- No configs match "{searchQuery}" + No configs match "{searchQuery}"

@@ -371,44 +425,94 @@ export default function EvaluationReport() { const scoreObject = getScoreObject(job); const hasScore = !!scoreObject; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const statusColor = getStatusColor(job.status); const isNewFormat = hasSummaryScores(scoreObject); - const summaryScores = (isNewFormat && scoreObject) ? scoreObject.summary_scores || [] : []; + const summaryScores = + isNewFormat && scoreObject ? scoreObject.summary_scores || [] : []; return ( -
+
{/* Header */} -
+
-

+

{job.run_name}

- - - + + + {job.dataset_name} @@ -422,56 +526,104 @@ export default function EvaluationReport() { style={{ backgroundColor: colors.bg.secondary }} > @@ -479,7 +631,11 @@ export default function EvaluationReport() { @@ -488,9 +644,11 @@ export default function EvaluationReport() { disabled={!hasScore} className="px-3 py-1.5 rounded-md text-xs font-medium" style={{ - backgroundColor: hasScore ? colors.accent.primary : colors.bg.secondary, - color: hasScore ? '#fff' : colors.text.secondary, - cursor: hasScore ? 'pointer' : 'not-allowed', + backgroundColor: hasScore + ? colors.accent.primary + : colors.bg.secondary, + color: hasScore ? "#fff" : colors.text.secondary, + cursor: hasScore ? "pointer" : "not-allowed", }} > Export CSV @@ -499,14 +657,19 @@ export default function EvaluationReport() {
{/* Content */} -
+
- {/* Metrics */} {hasScore && isNewFormat ? (
-

+

Metrics Overview

{summaryScores.length > 0 ? (
- {summaryScores.filter(s => s.data_type === 'NUMERIC').map((summary) => ( -
-
- {summary.name} -
-
- {summary.avg !== undefined ? summary.avg.toFixed(3) : 'N/A'} + {summaryScores + .filter((s) => s.data_type === "NUMERIC") + .map((summary) => ( +
+
+ {summary.name} +
+
+ {summary.avg !== undefined + ? summary.avg.toFixed(3) + : "N/A"} +
+
+ {summary.std !== undefined && + `±${summary.std.toFixed(3)} · `} + {summary.total_pairs} pairs +
-
- {summary.std !== undefined && `±${summary.std.toFixed(3)} · `}{summary.total_pairs} pairs + ))} + {summaryScores + .filter((s) => s.data_type === "CATEGORICAL") + .map((summary) => ( +
+
+ {summary.name} +
+
+ {summary.distribution && + Object.entries(summary.distribution).map( + ([key, value]) => ( +
+ + {key} + + + {value} + +
+ ), + )} +
+
+ {summary.total_pairs} pairs +
-
- ))} - {summaryScores.filter(s => s.data_type === 'CATEGORICAL').map((summary) => ( -
-
- {summary.name} -
-
- {summary.distribution && Object.entries(summary.distribution).map(([key, value]) => ( -
- {key} - {value} -
- ))} -
-
- {summary.total_pairs} pairs -
-
- ))} + ))}
) : ( -
-

No summary scores available

+
+

+ No summary scores available +

)}
) : ( -
-

- {job.error_message || 'No results available yet'} +

+

+ {job.error_message || "No results available yet"}

)} @@ -585,12 +825,19 @@ export default function EvaluationReport() { {hasScore && (
-

+

Detailed Results

{isNewFormat && ( - - ({normalizeToIndividualScores(scoreObject).length} items) + + ({normalizeToIndividualScores(scoreObject).length}{" "} + items) )}
@@ -614,7 +861,7 @@ export default function EvaluationReport() { {showNoTracesModal && (
setShowNoTracesModal(false)} >
e.stopPropagation()} > -

+

No Langfuse Traces Available

-

+

This evaluation does not have Langfuse traces.

diff --git a/app/evaluations/page.tsx b/app/evaluations/page.tsx index ead350c..36b016e 100644 --- a/app/evaluations/page.tsx +++ b/app/evaluations/page.tsx @@ -9,7 +9,7 @@ import { useState, useEffect, useCallback, Suspense } from 'react'; import { colors } from '@/app/lib/colors'; -import { useRouter, useSearchParams } from 'next/navigation' +import { useSearchParams } from 'next/navigation' import { APIKey, STORAGE_KEY } from '@/app/keystore/page'; import { Dataset } from '@/app/datasets/page'; import Sidebar from '@/app/components/Sidebar'; @@ -24,7 +24,6 @@ type Tab = 'datasets' | 'evaluations'; const leftPanelWidth = 450; function SimplifiedEvalContent() { - const router = useRouter(); const searchParams = useSearchParams(); const toast = useToast(); @@ -265,8 +264,8 @@ function SimplifiedEvalContent() { setIsEvaluating(false); toast.success(`Evaluation created! ${evalId !== 'unknown' ? `Job ID: ${evalId}` : ''}`); return true; - } catch (error: any) { - toast.error(`Failed to run evaluation: ${error.message || 'Unknown error'}`); + } catch (error: unknown) { + toast.error(`Failed to run evaluation: ${error instanceof Error ? error.message : 'Unknown error'}`); setIsEvaluating(false); return false; } diff --git a/app/keystore/page.tsx b/app/keystore/page.tsx index 233c58a..0bdfee5 100644 --- a/app/keystore/page.tsx +++ b/app/keystore/page.tsx @@ -4,9 +4,9 @@ * Allows users to securely store and manage API keys for various LLM providers */ -"use client" +"use client"; + import { useState, useEffect } from 'react'; -import { useRouter } from 'next/navigation' import Sidebar from '../components/Sidebar'; export interface APIKey { @@ -20,7 +20,6 @@ export interface APIKey { export const STORAGE_KEY = 'kaapi_api_keys'; export default function KaapiKeystore() { - const router = useRouter(); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [apiKeys, setApiKeys] = useState([]); @@ -34,6 +33,7 @@ export default function KaapiKeystore() { const stored = localStorage.getItem(STORAGE_KEY); if (stored) { try { + // eslint-disable-next-line react-hooks/set-state-in-effect setApiKeys(JSON.parse(stored)); } catch (e) { console.error('Failed to load API keys:', e); @@ -351,7 +351,7 @@ function StoredKeysTab({ Security Note

- API keys are stored in your browser's local storage. For production use, consider implementing secure server-side storage. + API keys are stored in your browser's local storage. For production use, consider implementing secure server-side storage.

@@ -482,7 +482,7 @@ function AddKeyModal({

- API keys are stored in your browser's local storage. + API keys are stored in your browser's local storage.

diff --git a/app/knowledge-base/page.tsx b/app/knowledge-base/page.tsx index ef852fe..5c34c1b 100644 --- a/app/knowledge-base/page.tsx +++ b/app/knowledge-base/page.tsx @@ -124,7 +124,7 @@ export default function KnowledgeBasePage() { jobStatusMap: Map ): Promise => { // First try to look up cached data by collection_id - let cached = getCollectionDataByCollectionId(collection.id); + const cached = getCollectionDataByCollectionId(collection.id); let jobId = cached.job_id; let collectionJobStatus = null; @@ -242,6 +242,7 @@ export default function KnowledgeBasePage() { }; // Fetch collections + // eslint-disable-next-line react-hooks/exhaustive-deps const fetchCollections = async () => { if (!apiKey) return; @@ -749,6 +750,7 @@ export default function KnowledgeBasePage() { fetchCollections(); fetchDocuments(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [apiKey]); // Keep apiKeyRef in sync so polling always has the current key diff --git a/app/lib/configTypes.ts b/app/lib/configTypes.ts index 6fe235d..e63bbe3 100644 --- a/app/lib/configTypes.ts +++ b/app/lib/configTypes.ts @@ -19,6 +19,7 @@ export interface CompletionParams { // Backend expects these as direct fields (flattened from tools array) knowledge_base_ids?: string[]; max_num_results?: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; // Allow additional provider-specific params } @@ -89,7 +90,7 @@ export interface APIResponse { success: boolean; data: T | null; error?: string | null; - metadata?: Record | null; + metadata?: Record | null; } // Helper type for list responses diff --git a/app/settings/credentials/page.tsx b/app/settings/credentials/page.tsx index d7efab2..5e1be10 100644 --- a/app/settings/credentials/page.tsx +++ b/app/settings/credentials/page.tsx @@ -56,6 +56,7 @@ export default function CredentialsPage() { useEffect(() => { if (apiKeys.length === 0) return; loadCredentials(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [apiKeys]); // Re-populate form when provider or credentials change @@ -139,8 +140,8 @@ export default function CredentialsPage() { toast.success(`${selectedProvider.name} credentials saved`); } await loadCredentials(); - } catch (err: any) { - toast.error(err.message || "Failed to save credentials"); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : "Failed to save credentials"); } finally { setIsSaving(false); } @@ -177,8 +178,8 @@ export default function CredentialsPage() { ); toast.success(`${selectedProvider.name} credentials removed`); await loadCredentials(); - } catch (err: any) { - toast.error(err.message || "Failed to remove credentials"); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : "Failed to remove credentials"); } finally { setIsDeleting(false); } @@ -191,7 +192,11 @@ export default function CredentialsPage() { const handleToggleVisibility = (key: string) => { setVisibleFields((prev) => { const next = new Set(prev); - next.has(key) ? next.delete(key) : next.add(key); + if (next.has(key)) { + next.delete(key); + } else { + next.add(key); + } return next; }); }; diff --git a/app/speech-to-text/page.tsx b/app/speech-to-text/page.tsx index df1237f..20c85c7 100644 --- a/app/speech-to-text/page.tsx +++ b/app/speech-to-text/page.tsx @@ -15,8 +15,7 @@ import Loader, { LoaderBox } from '@/app/components/Loader'; import { useToast } from '@/app/components/Toast'; import { APIKey, STORAGE_KEY } from '@/app/keystore/page'; import WaveformVisualizer from '@/app/components/speech-to-text/WaveformVisualizer'; -import { computeWordDiff, DiffSegment } from '@/app/components/speech-to-text/TranscriptionDiffViewer'; -import { formatDate } from '@/app/components/utils'; +import { computeWordDiff } from '@/app/components/speech-to-text/TranscriptionDiffViewer'; import ErrorModal from '@/app/components/ErrorModal'; type Tab = 'datasets' | 'evaluations'; @@ -43,6 +42,7 @@ interface Dataset { object_store_url: string | null; dataset_metadata: { sample_count?: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; }; organization_id: number; @@ -62,6 +62,7 @@ interface STTRun { status: string; total_items: number; score: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } | null; error_message: string | null; @@ -77,6 +78,7 @@ interface STTResult { provider: string; status: string; score: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } | null; is_correct: boolean | null; @@ -173,11 +175,13 @@ function AudioPlayer({ {/* Waveform Visualizer */}
+ {/* eslint-disable react-hooks/refs -- accessing ref in render for waveform display */} + {/* eslint-enable react-hooks/refs */}
@@ -309,36 +313,14 @@ const DEFAULT_LANGUAGES: Language[] = [ { id: 2, code: 'hi', name: 'Hindi' }, ]; -// Helper function to map language ID to language name -const getLanguageName = (languageId: number | null, languages: Language[] = DEFAULT_LANGUAGES): string => { - const lang = languages.find(l => l.id === languageId); - return lang ? lang.name : languageId ? 'Unknown' : 'N/A'; -}; - -// Helper function to map language code to language ID -const getLanguageId = (languageCode: string, languages: Language[] = DEFAULT_LANGUAGES): number => { - const lang = languages.find(l => l.code === languageCode); - return lang ? lang.id : 1; -}; - - export default function SpeechToTextPage() { const toast = useToast(); - // Tab state const [activeTab, setActiveTab] = useState('datasets'); - - // UI State const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [leftPanelWidth] = useState(450); - - // API Keys const [apiKeys, setApiKeys] = useState([]); - - // Languages const [languages, setLanguages] = useState([]); - - // Dataset form (Tab 1) const [datasetName, setDatasetName] = useState(''); const [datasetDescription, setDatasetDescription] = useState(''); const [datasetLanguageId, setDatasetLanguageId] = useState(1); @@ -394,6 +376,7 @@ export default function SpeechToTextPage() { const data = await response.json(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any let rawList: any[] = []; if (Array.isArray(data)) { rawList = data; @@ -406,7 +389,9 @@ export default function SpeechToTextPage() { } const languagesList: Language[] = rawList + // eslint-disable-next-line @typescript-eslint/no-explicit-any .filter((l: any) => l.is_active !== false) + // eslint-disable-next-line @typescript-eslint/no-explicit-any .map((l: any) => ({ id: l.id, code: l.locale || l.code || '', @@ -500,6 +485,7 @@ export default function SpeechToTextPage() { if (activeTab === 'evaluations') { loadRuns(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [apiKeys, activeTab]); // Handle audio file selection and upload @@ -772,6 +758,7 @@ export default function SpeechToTextPage() { // Enrich results with sample data (filename, ground truth, signed URL) // The structure is: data.results[].sample contains all sample information + // eslint-disable-next-line @typescript-eslint/no-explicit-any resultsList = resultsList.map((result: any) => { const sample = result.sample; @@ -1013,8 +1000,6 @@ function DatasetsTab({ isCreating, handleCreateDataset, datasets, - isLoadingDatasets, - loadDatasets, apiKeys, languages, toast, @@ -1031,6 +1016,7 @@ function DatasetsTab({ ground_truth: string; language_id: number; signed_url?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any sample_metadata?: { original_filename?: string; [key: string]: any }; }[]; } | null>(null); @@ -1056,8 +1042,8 @@ function DatasetsTab({ return; } setViewModalData({ name: datasetName, datasetId, samples }); - } catch (err: any) { - toast.error(err.message || 'Failed to view dataset'); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : 'Failed to view dataset'); } finally { setViewingId(null); } @@ -1084,8 +1070,8 @@ function DatasetsTab({ ...prev, samples: prev.samples.map(s => s.id === sampleId ? { ...s, [field]: value } : s), } : null); - } catch (err: any) { - toast.error(err.message || 'Failed to update sample'); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : 'Failed to update sample'); } finally { setSavingSampleId(null); } @@ -1618,6 +1604,7 @@ interface EvaluationsTabProps { isLoadingResults: boolean; loadResults: (runId: number) => void; apiKeys: APIKey[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any toast: any; setActiveTab: (tab: Tab) => void; } @@ -1646,6 +1633,7 @@ function EvaluationsTab({ loadResults, apiKeys, toast, + // eslint-disable-next-line @typescript-eslint/no-unused-vars setActiveTab, }: EvaluationsTabProps) { const [statusFilter, setStatusFilter] = useState('all'); @@ -2006,7 +1994,7 @@ function EvaluationsTab({ > {/* Tab navigation */}
- {metrics.map((m, idx) => ( + {metrics.map((m, _idx) => (
); })() diff --git a/app/text-to-speech/page.tsx b/app/text-to-speech/page.tsx index c308b88..47081f6 100644 --- a/app/text-to-speech/page.tsx +++ b/app/text-to-speech/page.tsx @@ -10,7 +10,6 @@ import { useState, useEffect, useRef } from 'react'; import { colors } from '@/app/lib/colors'; import Sidebar from '@/app/components/Sidebar'; import TabNavigation from '@/app/components/TabNavigation'; -import StatusBadge from '@/app/components/StatusBadge'; import Loader, { LoaderBox } from '@/app/components/Loader'; import { getStatusColor } from '@/app/components/utils'; import { useToast } from '@/app/components/Toast'; @@ -44,6 +43,7 @@ interface TTSDataset { object_store_url: string | null; dataset_metadata: { sample_count?: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; }; organization_id: number; @@ -62,12 +62,14 @@ interface TTSRun { status: string; total_items: number; score: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } | null; error_message: string | null; run_metadata: { voice_name?: string; style_prompt?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } | null; organization_id: number; @@ -85,6 +87,7 @@ interface TTSResult { provider: string; status: string; score: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } | null; is_correct: boolean | null; @@ -292,6 +295,7 @@ export default function TextToSpeechPage() { const data = await response.json(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any let rawList: any[] = []; if (Array.isArray(data)) { rawList = data; @@ -304,7 +308,9 @@ export default function TextToSpeechPage() { } const languagesList: Language[] = rawList + // eslint-disable-next-line @typescript-eslint/no-explicit-any .filter((l: any) => l.is_active !== false) + // eslint-disable-next-line @typescript-eslint/no-explicit-any .map((l: any) => ({ id: l.id, code: l.locale || l.code || '', @@ -397,6 +403,7 @@ export default function TextToSpeechPage() { if (activeTab === 'evaluations') { loadRuns(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [apiKeys, activeTab]); // Add a new text sample @@ -562,6 +569,7 @@ export default function TextToSpeechPage() { } // Enrich results with signed URL for audio playback + // eslint-disable-next-line @typescript-eslint/no-explicit-any resultsList = resultsList.map((result: any) => { const signedUrl = result.signed_url || result.sample?.signed_url || ''; return { @@ -662,7 +670,6 @@ export default function TextToSpeechPage() { }} apiKeys={apiKeys} datasets={datasets} - loadDatasets={loadDatasets} toast={toast} /> ) : ( @@ -753,7 +760,6 @@ interface DatasetsTabProps { resetForm: () => void; apiKeys: APIKey[]; datasets: TTSDataset[]; - loadDatasets: () => void; toast: ReturnType; } @@ -775,7 +781,6 @@ function DatasetsTab({ resetForm, apiKeys, datasets, - loadDatasets, toast, }: DatasetsTabProps) { const [viewingId, setViewingId] = useState(null); @@ -856,8 +861,8 @@ function DatasetsTab({ const rows = lines.slice(1).map(parseRow); setViewModalData({ name: datasetName, headers, rows }); - } catch (err: any) { - toast.error(err.message || 'Failed to view dataset'); + } catch (err: unknown) { + toast.error(err instanceof Error ? err.message : 'Failed to view dataset'); } finally { setViewingId(null); } @@ -1199,6 +1204,7 @@ interface EvaluationsTabProps { isLoadingResults: boolean; loadResults: (runId: number) => void; apiKeys: APIKey[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any toast: any; setActiveTab: (tab: Tab) => void; } @@ -1227,6 +1233,7 @@ function EvaluationsTab({ loadResults, apiKeys, toast, + // eslint-disable-next-line @typescript-eslint/no-unused-vars setActiveTab, }: EvaluationsTabProps) { const [statusFilter, setStatusFilter] = useState('all'); @@ -1246,11 +1253,17 @@ function EvaluationsTab({ }; }, [openScoreInfo]); - const updateFeedback = async (resultId: number, isCorrect: boolean | null, comment?: string, score?: { [key: string]: any }) => { + const updateFeedback = async (resultId: number, isCorrect: boolean | null, comment?: string, score?: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any + }) => { if (apiKeys.length === 0) return; try { - const payload: { is_correct?: boolean | null; comment?: string; score?: { [key: string]: any } } = {}; + const payload: { is_correct?: boolean | null; comment?: string; score?: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any + } } = {}; if (isCorrect !== undefined) payload.is_correct = isCorrect; if (comment !== undefined) payload.comment = comment; if (score !== undefined) payload.score = score; @@ -1921,7 +1934,7 @@ function EvaluationsTab({ ) : (

No {statusFilter} runs

-

No evaluation runs with status "{statusFilter}"

+

No evaluation runs with status "{statusFilter}"

); })() diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..fbb6290 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,7 +12,24 @@ const eslintConfig = defineConfig([ "out/**", "build/**", "next-env.d.ts", + // Generated files: + "coverage/**", ]), + { + rules: { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { + "varsIgnorePattern": "^_", + "argsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + }], + "react/no-unescaped-entities": "error", + "no-duplicate-imports": "error", + "prefer-const": "error", + "no-var": "error", + "no-console": ["warn", { allow: ["warn", "error"] }], + }, + }, ]); export default eslintConfig; diff --git a/eslint.config.mts b/eslint.config.mts deleted file mode 100644 index 18e261e..0000000 --- a/eslint.config.mts +++ /dev/null @@ -1,11 +0,0 @@ -import js from "@eslint/js"; -import globals from "globals"; -import tseslint from "typescript-eslint"; -import pluginReact from "eslint-plugin-react"; -import { defineConfig } from "eslint/config"; -export default defineConfig([ - { files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } }, - tseslint.configs.recommended, - pluginReact.configs.flat.recommended, - -]); diff --git a/package-lock.json b/package-lock.json index 99e36f8..3b17e39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,8 @@ "eslint-config-next": "16.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.5.0", + "husky": "^9.1.7", + "lint-staged": "^16.4.0", "tailwindcss": "^4", "typescript": "^5", "typescript-eslint": "^8.49.0" @@ -75,7 +77,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1583,7 +1584,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1643,7 +1643,6 @@ "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", @@ -2143,7 +2142,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2485,7 +2483,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -2597,6 +2594,56 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2623,6 +2670,23 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2734,7 +2798,6 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -2885,6 +2948,19 @@ "node": ">=10.13.0" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -3091,7 +3167,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3277,7 +3352,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3511,6 +3585,13 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3713,6 +3794,19 @@ "node": ">=6.9.0" } }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3957,6 +4051,22 @@ "hermes-estree": "0.25.1" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4854,6 +4964,117 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lint-staged": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.3", + "listr2": "^9.0.5", + "picomatch": "^4.0.3", + "string-argv": "^0.3.2", + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4877,6 +5098,131 @@ "dev": true, "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4944,6 +5290,19 @@ "node": ">=8.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5449,7 +5808,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5459,7 +5817,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5597,6 +5954,39 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5608,6 +5998,13 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5915,6 +6312,65 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5945,6 +6401,16 @@ "node": ">= 0.4" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6058,6 +6524,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6164,6 +6659,16 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6205,7 +6710,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6368,7 +6872,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6634,6 +7137,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6653,7 +7172,6 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 0a453c6..2c83544 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "prepare": "husky" }, "dependencies": { "date-fns": "^4.1.0", @@ -27,8 +29,15 @@ "eslint-config-next": "16.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.5.0", + "husky": "^9.1.7", + "lint-staged": "^16.4.0", "tailwindcss": "^4", "typescript": "^5", "typescript-eslint": "^8.49.0" + }, + "lint-staged": { + "**/*.{js,ts,jsx,tsx}": [ + "eslint --fix" + ] } }