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 { createOpenAI } from '@ai-sdk/openai';
|
||||||
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
||||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
||||||
import { GoogleGenAI } from '@google/genai';
|
|
||||||
import { Job } from '../db/models.js';
|
import { Job } from '../db/models.js';
|
||||||
import { broadcast } from '../ws/broadcast.js';
|
import { broadcast } from '../ws/broadcast.js';
|
||||||
import { findModel, DEFAULT_MODEL_ID, normalizeModelId } from '../models.js';
|
import { findModel, DEFAULT_MODEL_ID, normalizeModelId } from '../models.js';
|
||||||
@@ -24,10 +23,6 @@ const google = createGoogleGenerativeAI({
|
|||||||
apiKey: process.env.GOOGLE_API_KEY,
|
apiKey: process.env.GOOGLE_API_KEY,
|
||||||
});
|
});
|
||||||
|
|
||||||
const geminiApi = new GoogleGenAI({
|
|
||||||
apiKey: process.env.GOOGLE_API_KEY,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ollama Cloud exposes an OpenAI-compatible /v1 endpoint.
|
// Ollama Cloud exposes an OpenAI-compatible /v1 endpoint.
|
||||||
// Using @ai-sdk/openai-compatible avoids the local-Ollama schema validation
|
// Using @ai-sdk/openai-compatible avoids the local-Ollama schema validation
|
||||||
// in ollama-ai-provider which requires fields (eval_duration etc.) that
|
// in ollama-ai-provider which requires fields (eval_duration etc.) that
|
||||||
@@ -59,23 +54,6 @@ function resolveModelMeta(modelId) {
|
|||||||
return { normalized, meta };
|
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
|
// p-queue: in-process queue, no external server needed
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -110,7 +88,7 @@ async function runJob(jobId) {
|
|||||||
await setStatus(job, 'running');
|
await setStatus(job, 'running');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { meta } = resolveModelMeta(job.model);
|
resolveModelMeta(job.model);
|
||||||
|
|
||||||
const { text, usage } = await generateText({
|
const { text, usage } = await generateText({
|
||||||
model: resolveModel(job.model),
|
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": {
|
"scripts": {
|
||||||
"dev": "vite build --watch & node --watch index.js",
|
"dev": "vite build --watch & node --watch index.js",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"start": "node index.js"
|
"start": "node index.js",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^3.0.71",
|
"@ai-sdk/anthropic": "^3.0.71",
|
||||||
@@ -28,6 +30,12 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@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"
|
"vite": "^6.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ export function useJobSocket(onMessage) {
|
|||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
try {
|
try {
|
||||||
onMessageRef.current(JSON.parse(e.data));
|
onMessageRef.current(JSON.parse(e.data));
|
||||||
} catch (_) {}
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
|
|||||||
201
src/styles.css
201
src/styles.css
@@ -4,31 +4,39 @@
|
|||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg: #0d0d0d;
|
--bg: #0d0d0d;
|
||||||
--surface: #141414;
|
--surface: #141414;
|
||||||
--surface2: #1c1c1c;
|
--surface2: #1c1c1c;
|
||||||
--border: #2a2a2a;
|
--border: #2a2a2a;
|
||||||
--border2: #383838;
|
--border2: #383838;
|
||||||
--accent: #e8a020;
|
--accent: #e8a020;
|
||||||
--accent-dim:#7a5510;
|
--accent-dim: #7a5510;
|
||||||
--text: #e8e0d0;
|
--text: #e8e0d0;
|
||||||
--text-muted:#7a7060;
|
--text-muted: #7a7060;
|
||||||
--text-dim: #696152;
|
--text-dim: #696152;
|
||||||
--green: #4caf6a;
|
--green: #4caf6a;
|
||||||
--red: #d94f4f;
|
--red: #d94f4f;
|
||||||
--blue: #4a8fd4;
|
--blue: #4a8fd4;
|
||||||
|
|
||||||
--radius: 4px;
|
--radius: 4px;
|
||||||
--radius-lg: 8px;
|
--radius-lg: 8px;
|
||||||
--font-ui: 'Aldrich', system-ui, sans-serif;
|
--font-ui: 'Aldrich', system-ui, sans-serif;
|
||||||
--font-mono: 'JetBrains Mono', 'Fira Mono', monospace;
|
--font-mono: 'JetBrains Mono', 'Fira Mono', monospace;
|
||||||
|
|
||||||
--transition: 150ms ease;
|
--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 {
|
body {
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
@@ -40,10 +48,23 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Scrollbar ─────────────────────────────────────────────── */
|
/* ── Scrollbar ─────────────────────────────────────────────── */
|
||||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
::-webkit-scrollbar {
|
||||||
::-webkit-scrollbar-track { background: var(--bg); }
|
width: 6px;
|
||||||
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
|
height: 6px;
|
||||||
::-webkit-scrollbar-thumb:hover { background: var(--accent-dim); }
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border2);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Layout ─────────────────────────────────────────────────── */
|
/* ── Layout ─────────────────────────────────────────────────── */
|
||||||
.app {
|
.app {
|
||||||
@@ -89,8 +110,17 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-eye {
|
@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 ───────────────────────────────────────────────────── */
|
/* ── Main ───────────────────────────────────────────────────── */
|
||||||
@@ -161,9 +191,20 @@ body {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-placeholder svg { color: var(--text-dim); }
|
.drop-placeholder svg {
|
||||||
.drop-placeholder span { font-size: 0.9rem; font-weight: 600; }
|
color: var(--text-dim);
|
||||||
.drop-placeholder small { font-family: var(--font-mono); font-size: 0.72rem; 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 {
|
.drop-preview {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -197,9 +238,18 @@ body {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-input::placeholder { color: var(--text-dim); }
|
.prompt-input::placeholder {
|
||||||
.prompt-input:focus { border-color: var(--accent-dim); }
|
color: var(--text-dim);
|
||||||
.prompt-input:disabled { opacity: 0.5; cursor: not-allowed; }
|
}
|
||||||
|
|
||||||
|
.prompt-input:focus {
|
||||||
|
border-color: var(--accent-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-input:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Submit button ──────────────────────────────────────────── */
|
/* ── Submit button ──────────────────────────────────────────── */
|
||||||
.submit-btn {
|
.submit-btn {
|
||||||
@@ -222,9 +272,19 @@ body {
|
|||||||
height: fit-content;
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-btn:hover:not(:disabled) { opacity: 0.88; transform: translateY(-1px); }
|
.submit-btn:hover:not(:disabled) {
|
||||||
.submit-btn:active:not(:disabled) { transform: translateY(0); }
|
opacity: 0.88;
|
||||||
.submit-btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Form error ─────────────────────────────────────────────── */
|
/* ── Form error ─────────────────────────────────────────────── */
|
||||||
.form-error {
|
.form-error {
|
||||||
@@ -249,7 +309,11 @@ body {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Jobs section ───────────────────────────────────────────── */
|
/* ── Jobs section ───────────────────────────────────────────── */
|
||||||
.jobs-section {
|
.jobs-section {
|
||||||
@@ -311,13 +375,28 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes card-in {
|
@keyframes card-in {
|
||||||
from { opacity: 0; transform: translateY(6px); }
|
from {
|
||||||
to { opacity: 1; transform: none; }
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.job-card.status-running { border-color: var(--accent-dim); }
|
.job-card.status-running {
|
||||||
.job-card.status-done { border-color: var(--border); }
|
border-color: var(--accent-dim);
|
||||||
.job-card.status-error { border-color: rgba(217, 79, 79, 0.35); }
|
}
|
||||||
|
|
||||||
|
.job-card.status-done {
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-card.status-error {
|
||||||
|
border-color: rgba(217, 79, 79, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
.job-header {
|
.job-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -341,21 +420,24 @@ body {
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.status-queued {
|
.status-badge.status-queued {
|
||||||
background: rgba(122, 112, 96, 0.12);
|
background: rgba(122, 112, 96, 0.12);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
border-color: var(--border2);
|
border-color: var(--border2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.status-running {
|
.status-badge.status-running {
|
||||||
background: rgba(232, 160, 32, 0.1);
|
background: rgba(232, 160, 32, 0.1);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
border-color: var(--accent-dim);
|
border-color: var(--accent-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.status-done {
|
.status-badge.status-done {
|
||||||
background: rgba(76, 175, 106, 0.1);
|
background: rgba(76, 175, 106, 0.1);
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
border-color: rgba(76, 175, 106, 0.3);
|
border-color: rgba(76, 175, 106, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge.status-error {
|
.status-badge.status-error {
|
||||||
background: rgba(217, 79, 79, 0.1);
|
background: rgba(217, 79, 79, 0.1);
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
@@ -406,10 +488,22 @@ body {
|
|||||||
|
|
||||||
/* ── Responsive ─────────────────────────────────────────────── */
|
/* ── Responsive ─────────────────────────────────────────────── */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.app-main { padding: 20px 16px 48px; }
|
.app-main {
|
||||||
.prompt-row { flex-direction: column; }
|
padding: 20px 16px 48px;
|
||||||
.submit-btn { width: 100%; justify-content: center; }
|
}
|
||||||
.header-inner { padding: 12px 16px; }
|
|
||||||
|
.prompt-row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Expandable error detail ────────────────────────────────── */
|
/* ── Expandable error detail ────────────────────────────────── */
|
||||||
@@ -447,7 +541,9 @@ body {
|
|||||||
transition: opacity var(--transition);
|
transition: opacity var(--transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.job-error-toggle:hover { opacity: 1; }
|
.job-error-toggle:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.job-error-detail {
|
.job-error-detail {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
@@ -497,9 +593,18 @@ body {
|
|||||||
padding-right: 32px;
|
padding-right: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-select:focus { border-color: var(--accent-dim); }
|
.model-select:focus {
|
||||||
.model-select:hover { border-color: var(--border2); }
|
border-color: var(--accent-dim);
|
||||||
.model-select:disabled { opacity: 0.5; cursor: not-allowed; }
|
}
|
||||||
|
|
||||||
|
.model-select:hover {
|
||||||
|
border-color: var(--border2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-select:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.model-select optgroup {
|
.model-select optgroup {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -529,4 +634,4 @@ body {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
max-width: 160px;
|
max-width: 160px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user