Self-contained Dockerized build for end users. Run via docker compose; see README.md for setup. Source-only, no sample data or build artifacts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
145 lines
4.4 KiB
JavaScript
145 lines
4.4 KiB
JavaScript
// lib/store.js — single-file local database (data/db.json) with atomic writes.
|
|
// Portable: copy data/db.json to move your whole library + settings anywhere.
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
const DATA_DIR = path.join(process.cwd(), "data");
|
|
const DB_PATH = path.join(DATA_DIR, "db.json");
|
|
|
|
export const DEFAULT_SETTINGS = {
|
|
// Shown in the header of printed/exported assignments.
|
|
profile: {
|
|
teacherName: "",
|
|
className: "",
|
|
schoolName: "",
|
|
logo: "", // small data-URL image (downscaled client-side before saving)
|
|
},
|
|
provider: "ollama",
|
|
providers: {
|
|
// Env overrides let the Docker image default to the host machine's
|
|
// Ollama / LM Studio (host.docker.internal) without touching the UI.
|
|
ollama: { baseUrl: process.env.OLLAMA_BASE_URL || "http://localhost:11434", model: "" },
|
|
lmstudio: { baseUrl: process.env.LMSTUDIO_BASE_URL || "http://localhost:1234", model: "" },
|
|
openai: { apiKey: "", model: "gpt-4o-mini" },
|
|
anthropic: { apiKey: "", model: "claude-sonnet-4-6" },
|
|
google: { apiKey: "", model: "gemini-2.0-flash" },
|
|
},
|
|
generation: {
|
|
auto: true, // size maxTokens/maxSourceChars to the selected model (lib/model-caps.js)
|
|
temperature: 0.3,
|
|
maxTokens: 8000,
|
|
maxSourceChars: 24000,
|
|
verification: true,
|
|
},
|
|
};
|
|
|
|
function emptyDb() {
|
|
return { assignments: [], settings: structuredClone(DEFAULT_SETTINGS) };
|
|
}
|
|
|
|
function readDb() {
|
|
try {
|
|
if (!fs.existsSync(DB_PATH)) return emptyDb();
|
|
const raw = fs.readFileSync(DB_PATH, "utf8");
|
|
const db = JSON.parse(raw);
|
|
if (!Array.isArray(db.assignments)) db.assignments = [];
|
|
db.settings = mergeSettings(db.settings);
|
|
return db;
|
|
} catch {
|
|
// Corrupt file: keep a backup, start fresh rather than crash.
|
|
try { fs.copyFileSync(DB_PATH, DB_PATH + ".corrupt-" + Date.now()); } catch {}
|
|
return emptyDb();
|
|
}
|
|
}
|
|
|
|
function writeDb(db) {
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
const tmp = DB_PATH + ".tmp";
|
|
fs.writeFileSync(tmp, JSON.stringify(db, null, 2), "utf8");
|
|
fs.renameSync(tmp, DB_PATH);
|
|
}
|
|
|
|
export function mergeSettings(saved) {
|
|
const base = structuredClone(DEFAULT_SETTINGS);
|
|
if (!saved || typeof saved !== "object") return base;
|
|
const out = { ...base, ...saved };
|
|
out.providers = { ...base.providers };
|
|
for (const key of Object.keys(base.providers)) {
|
|
out.providers[key] = { ...base.providers[key], ...(saved.providers?.[key] || {}) };
|
|
}
|
|
out.generation = { ...base.generation, ...(saved.generation || {}) };
|
|
out.profile = { ...base.profile, ...(saved.profile || {}) };
|
|
return out;
|
|
}
|
|
|
|
// ---------- Settings ----------
|
|
export function getSettings() {
|
|
return readDb().settings;
|
|
}
|
|
|
|
export function saveSettings(settings) {
|
|
const db = readDb();
|
|
db.settings = mergeSettings(settings);
|
|
writeDb(db);
|
|
return db.settings;
|
|
}
|
|
|
|
// ---------- Assignments ----------
|
|
export function listAssignments() {
|
|
const db = readDb();
|
|
return db.assignments
|
|
.map((a) => ({
|
|
id: a.id,
|
|
title: a.title || "Untitled assignment",
|
|
assignmentType: a.assignmentType,
|
|
gradeLevel: a.gradeLevel,
|
|
subject: a.subject,
|
|
questionCount: (a.questions || []).length,
|
|
totalPoints: (a.questions || []).reduce((s, q) => s + (Number(q.points) || 0), 0),
|
|
createdAt: a.createdAt,
|
|
updatedAt: a.updatedAt,
|
|
}))
|
|
.sort((x, y) => String(y.updatedAt).localeCompare(String(x.updatedAt)));
|
|
}
|
|
|
|
export function getAssignment(id) {
|
|
return readDb().assignments.find((a) => a.id === id) || null;
|
|
}
|
|
|
|
export function createAssignment(assignment) {
|
|
const db = readDb();
|
|
const now = new Date().toISOString();
|
|
const record = {
|
|
...assignment,
|
|
id: assignment.id || "a_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7),
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
db.assignments.push(record);
|
|
writeDb(db);
|
|
return record;
|
|
}
|
|
|
|
export function updateAssignment(id, patch) {
|
|
const db = readDb();
|
|
const idx = db.assignments.findIndex((a) => a.id === id);
|
|
if (idx === -1) return null;
|
|
db.assignments[idx] = {
|
|
...db.assignments[idx],
|
|
...patch,
|
|
id,
|
|
createdAt: db.assignments[idx].createdAt,
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
writeDb(db);
|
|
return db.assignments[idx];
|
|
}
|
|
|
|
export function deleteAssignment(id) {
|
|
const db = readDb();
|
|
const before = db.assignments.length;
|
|
db.assignments = db.assignments.filter((a) => a.id !== id);
|
|
writeDb(db);
|
|
return db.assignments.length < before;
|
|
}
|