Formatting and linting

This commit is contained in:
2026-04-23 15:21:40 -05:00
parent 7191c68390
commit 47c9648f4a
6 changed files with 3101 additions and 76 deletions

83
eslint.config.js Normal file
View File

@@ -0,0 +1,83 @@
import js from '@eslint/js';
import globals from 'globals';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import reactRefreshPlugin from 'eslint-plugin-react-refresh';
export default [
{
ignores: [
'**/node_modules/**',
'**/dist/**',
'**/.vite/**',
'**/coverage/**',
'**/*.min.*',
],
},
// Base JS rules
js.configs.recommended,
{
rules: {
'no-empty': ['error', { allowEmptyCatch: true }],
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
},
// Node (server/build) files
{
files: ['**/*.js', '**/*.mjs'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.node,
},
},
rules: {
// This repo uses `console.log` for boot logs.
'no-console': 'off',
},
},
// Browser + React files
{
files: ['src/**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parserOptions: {
ecmaFeatures: { jsx: true },
},
globals: {
...globals.browser,
},
},
plugins: {
react: reactPlugin,
'react-hooks': reactHooksPlugin,
'react-refresh': reactRefreshPlugin,
},
settings: {
react: { version: 'detect' },
},
rules: {
...reactPlugin.configs.recommended.rules,
...reactHooksPlugin.configs.recommended.rules,
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
];

View File

@@ -4,7 +4,6 @@ import { createAnthropic } from '@ai-sdk/anthropic';
import { createOpenAI } from '@ai-sdk/openai';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { GoogleGenAI } from '@google/genai';
import { Job } from '../db/models.js';
import { broadcast } from '../ws/broadcast.js';
import { findModel, DEFAULT_MODEL_ID, normalizeModelId } from '../models.js';
@@ -24,10 +23,6 @@ const google = createGoogleGenerativeAI({
apiKey: process.env.GOOGLE_API_KEY,
});
const geminiApi = new GoogleGenAI({
apiKey: process.env.GOOGLE_API_KEY,
});
// Ollama Cloud exposes an OpenAI-compatible /v1 endpoint.
// Using @ai-sdk/openai-compatible avoids the local-Ollama schema validation
// in ollama-ai-provider which requires fields (eval_duration etc.) that
@@ -59,23 +54,6 @@ function resolveModelMeta(modelId) {
return { normalized, meta };
}
function dataUrlToInlineData(dataUrl) {
if (!dataUrl || typeof dataUrl !== 'string') return null;
// Expected: data:<mime>;base64,<data>
if (!dataUrl.startsWith('data:')) return null;
const comma = dataUrl.indexOf(',');
if (comma < 0) return null;
const header = dataUrl.slice(5, comma); // drop "data:"
const data = dataUrl.slice(comma + 1);
const isBase64 = header.includes(';base64');
const mimeType = header.split(';')[0] || 'application/octet-stream';
if (!isBase64 || !data) return null;
return { mimeType, data };
}
// ---------------------------------------------------------------------------
// p-queue: in-process queue, no external server needed
// ---------------------------------------------------------------------------
@@ -110,7 +88,7 @@ async function runJob(jobId) {
await setStatus(job, 'running');
try {
const { meta } = resolveModelMeta(job.model);
resolveModelMeta(job.model);
const { text, usage } = await generateText({
model: resolveModel(job.model),

2855
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,9 @@
"scripts": {
"dev": "vite build --watch & node --watch index.js",
"build": "vite build",
"start": "node index.js"
"start": "node index.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.71",
@@ -28,6 +30,12 @@
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"@eslint/js": "^9.0.0",
"eslint": "^9.0.0",
"eslint-plugin-react": "^7.0.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.0",
"globals": "^15.0.0",
"vite": "^6.0.7"
}
}

View File

@@ -17,7 +17,9 @@ export function useJobSocket(onMessage) {
ws.onmessage = (e) => {
try {
onMessageRef.current(JSON.parse(e.data));
} catch (_) {}
} catch {
return;
}
};
ws.onclose = () => {

View File

@@ -4,31 +4,39 @@
============================================================ */
:root {
--bg: #0d0d0d;
--surface: #141414;
--surface2: #1c1c1c;
--border: #2a2a2a;
--border2: #383838;
--accent: #e8a020;
--accent-dim:#7a5510;
--text: #e8e0d0;
--text-muted:#7a7060;
--text-dim: #696152;
--green: #4caf6a;
--red: #d94f4f;
--blue: #4a8fd4;
--bg: #0d0d0d;
--surface: #141414;
--surface2: #1c1c1c;
--border: #2a2a2a;
--border2: #383838;
--accent: #e8a020;
--accent-dim: #7a5510;
--text: #e8e0d0;
--text-muted: #7a7060;
--text-dim: #696152;
--green: #4caf6a;
--red: #d94f4f;
--blue: #4a8fd4;
--radius: 4px;
--radius: 4px;
--radius-lg: 8px;
--font-ui: 'Aldrich', system-ui, sans-serif;
--font-ui: 'Aldrich', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Mono', monospace;
--transition: 150ms ease;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html { font-size: 15px; }
html {
font-size: 15px;
}
body {
background: var(--bg);
@@ -40,10 +48,23 @@ body {
}
/* ── Scrollbar ─────────────────────────────────────────────── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent-dim); }
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--bg);
}
::-webkit-scrollbar-thumb {
background: var(--border2);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-dim);
}
/* ── Layout ─────────────────────────────────────────────────── */
.app {
@@ -89,8 +110,17 @@ body {
}
@keyframes pulse-eye {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(0.88); }
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(0.88);
}
}
/* ── Main ───────────────────────────────────────────────────── */
@@ -161,9 +191,20 @@ body {
pointer-events: none;
}
.drop-placeholder svg { color: var(--text-dim); }
.drop-placeholder span { font-size: 0.9rem; font-weight: 600; }
.drop-placeholder small { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-dim); }
.drop-placeholder svg {
color: var(--text-dim);
}
.drop-placeholder span {
font-size: 0.9rem;
font-weight: 600;
}
.drop-placeholder small {
font-family: var(--font-mono);
font-size: 0.72rem;
color: var(--text-dim);
}
.drop-preview {
width: 100%;
@@ -197,9 +238,18 @@ body {
outline: none;
}
.prompt-input::placeholder { color: var(--text-dim); }
.prompt-input:focus { border-color: var(--accent-dim); }
.prompt-input:disabled { opacity: 0.5; cursor: not-allowed; }
.prompt-input::placeholder {
color: var(--text-dim);
}
.prompt-input:focus {
border-color: var(--accent-dim);
}
.prompt-input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ── Submit button ──────────────────────────────────────────── */
.submit-btn {
@@ -222,9 +272,19 @@ body {
height: fit-content;
}
.submit-btn:hover:not(:disabled) { opacity: 0.88; transform: translateY(-1px); }
.submit-btn:active:not(:disabled) { transform: translateY(0); }
.submit-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.submit-btn:hover:not(:disabled) {
opacity: 0.88;
transform: translateY(-1px);
}
.submit-btn:active:not(:disabled) {
transform: translateY(0);
}
.submit-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
/* ── Form error ─────────────────────────────────────────────── */
.form-error {
@@ -249,7 +309,11 @@ body {
flex-shrink: 0;
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ── Jobs section ───────────────────────────────────────────── */
.jobs-section {
@@ -311,13 +375,28 @@ body {
}
@keyframes card-in {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: none; }
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: none;
}
}
.job-card.status-running { border-color: var(--accent-dim); }
.job-card.status-done { border-color: var(--border); }
.job-card.status-error { border-color: rgba(217, 79, 79, 0.35); }
.job-card.status-running {
border-color: var(--accent-dim);
}
.job-card.status-done {
border-color: var(--border);
}
.job-card.status-error {
border-color: rgba(217, 79, 79, 0.35);
}
.job-header {
display: flex;
@@ -341,21 +420,24 @@ body {
border: 1px solid transparent;
}
.status-badge.status-queued {
.status-badge.status-queued {
background: rgba(122, 112, 96, 0.12);
color: var(--text-muted);
border-color: var(--border2);
}
.status-badge.status-running {
background: rgba(232, 160, 32, 0.1);
color: var(--accent);
border-color: var(--accent-dim);
}
.status-badge.status-done {
background: rgba(76, 175, 106, 0.1);
color: var(--green);
border-color: rgba(76, 175, 106, 0.3);
}
.status-badge.status-error {
background: rgba(217, 79, 79, 0.1);
color: var(--red);
@@ -406,10 +488,22 @@ body {
/* ── Responsive ─────────────────────────────────────────────── */
@media (max-width: 600px) {
.app-main { padding: 20px 16px 48px; }
.prompt-row { flex-direction: column; }
.submit-btn { width: 100%; justify-content: center; }
.header-inner { padding: 12px 16px; }
.app-main {
padding: 20px 16px 48px;
}
.prompt-row {
flex-direction: column;
}
.submit-btn {
width: 100%;
justify-content: center;
}
.header-inner {
padding: 12px 16px;
}
}
/* ── Expandable error detail ────────────────────────────────── */
@@ -447,7 +541,9 @@ body {
transition: opacity var(--transition);
}
.job-error-toggle:hover { opacity: 1; }
.job-error-toggle:hover {
opacity: 1;
}
.job-error-detail {
font-family: var(--font-mono);
@@ -497,9 +593,18 @@ body {
padding-right: 32px;
}
.model-select:focus { border-color: var(--accent-dim); }
.model-select:hover { border-color: var(--border2); }
.model-select:disabled { opacity: 0.5; cursor: not-allowed; }
.model-select:focus {
border-color: var(--accent-dim);
}
.model-select:hover {
border-color: var(--border2);
}
.model-select:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.model-select optgroup {
color: var(--text-muted);