feat: initial IQAI multi-model AI dashboard
- 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>
This commit is contained in:
84
backend/routes/models.js
Normal file
84
backend/routes/models.js
Normal file
@@ -0,0 +1,84 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user