- Express backend with Replicate API proxy (chat, models, account, search) - React + Vite + Tailwind frontend with custom Midnight Violet color scheme - @mention autocomplete to route messages to specific models - Parallel multi-model queries with model selection in sidebar - DuckDuckGo web search context injection - Model manager UI (add/edit/remove Replicate models) - Per-model system instructions per conversation - Replicate account info display in sidebar - Conversation history with local persistence (Zustand) - Full Docker deployment (backend + nginx-served frontend) - Montserrat + Poppins fonts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
85 lines
2.7 KiB
JavaScript
85 lines
2.7 KiB
JavaScript
import express from 'express';
|
|
import { readFile, writeFile } from 'fs/promises';
|
|
import { fileURLToPath } from 'url';
|
|
import { dirname, join } from 'path';
|
|
|
|
const router = express.Router();
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const MODELS_PATH = join(__dirname, '../data/models.json');
|
|
|
|
async function loadModels() {
|
|
const raw = await readFile(MODELS_PATH, 'utf-8');
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
async function saveModels(models) {
|
|
await writeFile(MODELS_PATH, JSON.stringify(models, null, 2));
|
|
}
|
|
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const models = await loadModels();
|
|
res.json(models);
|
|
} catch (err) {
|
|
res.status(500).json({ error: 'Failed to load models' });
|
|
}
|
|
});
|
|
|
|
router.post('/', async (req, res) => {
|
|
try {
|
|
const { tag, displayName, owner, name, type, avatar, color, description, systemPromptParam, defaultInput } = req.body;
|
|
if (!tag || !owner || !name || !displayName) {
|
|
return res.status(400).json({ error: 'Missing required fields: tag, owner, name, displayName' });
|
|
}
|
|
const models = await loadModels();
|
|
if (models.find(m => m.tag === tag)) {
|
|
return res.status(409).json({ error: `Tag @${tag} already exists` });
|
|
}
|
|
const newModel = {
|
|
id: `${owner}-${name}`.replace(/[^a-z0-9-]/gi, '-'),
|
|
tag: tag.toLowerCase().replace(/[^a-z0-9]/g, ''),
|
|
displayName,
|
|
owner,
|
|
name,
|
|
type: type || 'text',
|
|
avatar: avatar || '🤖',
|
|
color: color || '#6B7280',
|
|
description: description || '',
|
|
systemPromptParam: systemPromptParam || null,
|
|
defaultInput: defaultInput || {}
|
|
};
|
|
models.push(newModel);
|
|
await saveModels(models);
|
|
res.status(201).json(newModel);
|
|
} catch (err) {
|
|
res.status(500).json({ error: 'Failed to add model' });
|
|
}
|
|
});
|
|
|
|
router.put('/:id', async (req, res) => {
|
|
try {
|
|
const models = await loadModels();
|
|
const idx = models.findIndex(m => m.id === req.params.id);
|
|
if (idx === -1) return res.status(404).json({ error: 'Model not found' });
|
|
models[idx] = { ...models[idx], ...req.body, id: models[idx].id };
|
|
await saveModels(models);
|
|
res.json(models[idx]);
|
|
} catch (err) {
|
|
res.status(500).json({ error: 'Failed to update model' });
|
|
}
|
|
});
|
|
|
|
router.delete('/:id', async (req, res) => {
|
|
try {
|
|
const models = await loadModels();
|
|
const filtered = models.filter(m => m.id !== req.params.id);
|
|
if (filtered.length === models.length) return res.status(404).json({ error: 'Model not found' });
|
|
await saveModels(filtered);
|
|
res.json({ success: true });
|
|
} catch (err) {
|
|
res.status(500).json({ error: 'Failed to delete model' });
|
|
}
|
|
});
|
|
|
|
export { router as modelsRouter, loadModels };
|