Mr. Drew's Assignment Creator — Docker share build
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>
This commit is contained in:
+144
@@ -0,0 +1,144 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user