Formatting and linting
This commit is contained in:
83
eslint.config.js
Normal file
83
eslint.config.js
Normal 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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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
2855
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ export function useJobSocket(onMessage) {
|
||||
ws.onmessage = (e) => {
|
||||
try {
|
||||
onMessageRef.current(JSON.parse(e.data));
|
||||
} catch (_) {}
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
|
||||
199
src/styles.css
199
src/styles.css
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user