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:
Malin
2026-04-16 13:12:40 +02:00
commit 71965939a1
31 changed files with 2399 additions and 0 deletions

129
backend/routes/chat.js Normal file
View File

@@ -0,0 +1,129 @@
import express from 'express';
import { loadModels } from './models.js';
const router = express.Router();
const REPLICATE_BASE = 'https://api.replicate.com/v1';
function buildInput(model, prompt, systemPrompt, extraParams) {
const base = { ...model.defaultInput };
// Inject system prompt if model supports it
if (model.systemPromptParam && systemPrompt) {
base[model.systemPromptParam] = systemPrompt;
}
// Apply extra params from user
if (extraParams) {
Object.assign(base, extraParams);
}
base.prompt = prompt;
return base;
}
async function runPrediction(model, input, token) {
const url = `${REPLICATE_BASE}/models/${model.owner}/${model.name}/predictions`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Prefer': 'wait'
},
body: JSON.stringify({ input })
});
if (!response.ok) {
const err = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(err.detail || `HTTP ${response.status}`);
}
return response.json();
}
function extractOutput(prediction) {
const { output } = prediction;
if (!output) return '';
if (typeof output === 'string') return output;
if (Array.isArray(output)) return output.join('');
if (typeof output === 'object') return JSON.stringify(output, null, 2);
return String(output);
}
// POST /api/chat - single model call
router.post('/', async (req, res) => {
const token = process.env.REPLICATE_API_TOKEN;
if (!token) return res.status(400).json({ error: 'REPLICATE_API_TOKEN not configured' });
const { modelId, prompt, systemPrompt, searchContext, extraParams } = req.body;
if (!modelId || !prompt) return res.status(400).json({ error: 'modelId and prompt are required' });
try {
const models = await loadModels();
const model = models.find(m => m.id === modelId);
if (!model) return res.status(404).json({ error: `Model not found: ${modelId}` });
const finalPrompt = searchContext ? `${searchContext}\n\n${prompt}` : prompt;
const input = buildInput(model, finalPrompt, systemPrompt, extraParams);
const prediction = await runPrediction(model, input, token);
const content = extractOutput(prediction);
res.json({
id: prediction.id,
modelId: model.id,
modelTag: model.tag,
modelName: model.displayName,
content,
status: prediction.status,
metrics: prediction.metrics,
urls: prediction.urls
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// POST /api/chat/multi - send to multiple models in parallel
router.post('/multi', async (req, res) => {
const token = process.env.REPLICATE_API_TOKEN;
if (!token) return res.status(400).json({ error: 'REPLICATE_API_TOKEN not configured' });
const { modelIds, prompt, systemPrompt, searchContext, extraParams } = req.body;
if (!modelIds?.length || !prompt) return res.status(400).json({ error: 'modelIds and prompt are required' });
try {
const models = await loadModels();
const tasks = modelIds.map(async (modelId) => {
const model = models.find(m => m.id === modelId);
if (!model) return { modelId, error: 'Model not found' };
try {
const finalPrompt = searchContext ? `${searchContext}\n\n${prompt}` : prompt;
const input = buildInput(model, finalPrompt, systemPrompt, extraParams);
const prediction = await runPrediction(model, input, token);
const content = extractOutput(prediction);
return {
id: prediction.id,
modelId: model.id,
modelTag: model.tag,
modelName: model.displayName,
content,
status: prediction.status,
metrics: prediction.metrics
};
} catch (err) {
return { modelId, modelTag: model.tag, modelName: model.displayName, error: err.message };
}
});
const results = await Promise.all(tasks);
res.json({ results });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
export { router as chatRouter };