Files
MULTI-IA-CODAGE/frontend/index.html
T
xavier d6a7d10e3a
Build and Push Docker Image / build-and-push (push) Failing after 27s
MAJ majeur
2026-10-06 13:59:17 +02:00

311 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AETHAS38 - Orchestrateur</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body { background-color: #0f172a; color: #f8fafc; }
.card { background-color: #1e293b; border: 1px solid #333; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
.input-box { width: 100%; padding: 10px; border-radius: 5px; background: #2a2a35; border: 1px solid #444; color: white; margin-top: 5px; outline: none; }
.input-box:focus { border-color: #00e5ff; }
.btn-primary { background-color: #00e5ff; color: #0f172a; font-weight: bold; padding: 10px 20px; border-radius: 5px; cursor: pointer; transition: 0.2s; }
.btn-primary:hover { background-color: #38bdf8; }
.btn-primary:disabled { background-color: #334155; color: #94a3b8; cursor: not-allowed; }
.btn-export { background-color: #334155; color: #38bdf8; font-size: 11px; font-weight: bold; padding: 4px 8px; border-radius: 4px; cursor: pointer; border: 1px solid #475569; }
.btn-export:hover { background-color: #475569; color: #00e5ff; }
.btn-danger { background-color: #f43f5e; color: white; padding: 5px 10px; border-radius: 5px; cursor: pointer; font-size: 12px; }
.tab-btn { padding: 10px 20px; font-weight: bold; cursor: pointer; transition: 0.2s; border-bottom: 2px solid transparent; }
.tab-active { color: #00e5ff; border-bottom-color: #00e5ff; }
.tab-inactive { color: #64748b; }
.tab-inactive:hover { color: #94a3b8; }
</style>
</head>
<body class="p-8">
<div id="app" class="max-w-5xl mx-auto">
<!-- En-tête & Suivi financier OpenRouter -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-[#00e5ff]">🤖 AETHAS38</h1>
<div class="bg-slate-800 border border-slate-700 px-4 py-2 rounded text-xs flex space-x-4">
<span>💰 OpenRouter - Crédit : <strong class="text-emerald-400">{{ orCredits.limit }}</strong></span>
<span>Consommation : <strong class="text-amber-400">{{ orCredits.usage }}</strong></span>
</div>
</div>
<!-- Navigation Onglets -->
<div class="flex space-x-2 mb-8 border-b border-gray-700">
<button @click="currentTab = 'admin'" :class="currentTab === 'admin' ? 'tab-active' : 'tab-inactive'" class="tab-btn">⚙️ Administration & Modèles</button>
<button @click="currentTab = 'studio'" :class="currentTab === 'studio' ? 'tab-active' : 'tab-inactive'" class="tab-btn">🎬 Studio Média</button>
<button @click="currentTab = 'documents'" :class="currentTab === 'documents' ? 'tab-active' : 'tab-inactive'" class="tab-btn">📂 Documents & Exports</button>
</div>
<div v-if="notification" class="mb-4 p-4 rounded bg-emerald-900/50 border border-emerald-500 text-emerald-400">
{{ notification }}
</div>
<!-- ONGLET : ADMINISTRATION & CATALOGUE DE MODÈLES -->
<div v-if="currentTab === 'admin'">
<div class="card">
<h2 class="text-xl font-bold text-purple-400 mb-4">🔑 Clés API & Suivi</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="text-xs font-bold text-gray-400">OpenRouter API Key</label>
<input type="password" v-model="config.api_keys.openrouter" class="input-box" placeholder="sk-or-v1-...">
</div>
<div>
<label class="text-xs font-bold text-gray-400">Groq API Key</label>
<input type="password" v-model="config.api_keys.groq" class="input-box" placeholder="gsk_...">
</div>
</div>
</div>
<!-- Gestion et Choix des Modèles (Gratuit / Payant & Catégorie) -->
<div class="card">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-cyan-400">🎛️ Sélection & Filtrage des Modèles</h2>
<div class="space-x-2">
<select v-model="modelFilterPricing" class="bg-slate-800 text-xs text-gray-300 p-2 rounded border border-gray-700">
<option value="all">Tous les tarifs</option>
<option value="free">🟢 Gratuit uniquement</option>
<option value="paid">🟠 Payant</option>
</select>
<button @click="exportModelsJson" class="btn-export">Exporter Catalogue JSON</button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<label class="text-xs text-gray-400">Filtrer par Catégorie / Spécialité</label>
<input type="text" v-model="modelSearchCat" class="input-box text-xs" placeholder="Ex: Texte, Vision, Code...">
</div>
<div class="md:col-span-2">
<label class="text-xs text-gray-400">Modèle Actif Sélectionné</label>
<input type="text" v-model="selectedActiveModel" class="input-box text-xs text-emerald-400 font-bold" readonly>
</div>
</div>
<div class="bg-slate-900 p-4 rounded border border-gray-700 max-h-60 overflow-y-auto text-xs space-y-2">
<div v-for="m in filteredModels" :key="m.id" @click="selectedActiveModel = m.id" class="p-2 bg-slate-800 rounded hover:bg-slate-700 cursor-pointer flex justify-between items-center">
<div>
<strong class="text-cyan-300">{{ m.name }}</strong> <span class="text-gray-500">({{ m.id }})</span>
<p class="text-gray-400">{{ m.description }}</p>
</div>
<span :class="m.pricing === 'free' ? 'text-emerald-400 bg-emerald-950 px-2 py-1 rounded text-[10px]' : 'text-amber-400 bg-amber-950 px-2 py-1 rounded text-[10px]'">
{{ m.pricing === 'free' ? 'GRATUIT' : 'PAYANT' }}
</span>
</div>
</div>
</div>
<div class="flex justify-end mt-6">
<button @click="saveConfig" class="btn-primary text-lg">💾 Sauvegarder la Configuration</button>
</div>
</div>
<!-- ONGLET : STUDIO MÉDIA -->
<div v-if="currentTab === 'studio'">
<div class="card">
<h2 class="text-xl font-bold text-purple-400 mb-4">🎬 Studio Multimédia</h2>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
<div class="md:col-span-3">
<label class="text-xs font-bold text-gray-400">Votre Prompt</label>
<input type="text" v-model="mediaPrompt" class="input-box" placeholder="A futuristic city...">
</div>
<div>
<label class="text-xs font-bold text-gray-400">Type</label>
<select v-model="mediaType" class="input-box">
<option value="image">🖼️ Image</option>
<option value="voice">🎙️ Voix</option>
<option value="music">🎵 Musique</option>
</select>
</div>
</div>
<div class="flex justify-end">
<button @click="generateMedia" :disabled="isGenerating || !mediaPrompt" class="btn-primary">
{{ isGenerating ? '⏳ Génération...' : '✨ Générer' }}
</button>
</div>
</div>
<div v-if="generatedMediaUrl" class="card text-center">
<h3 class="font-bold text-gray-300 mb-4">Résultat :</h3>
<img :src="generatedMediaUrl" class="max-w-full h-auto mx-auto rounded border border-gray-600 shadow-lg">
<div class="mt-4"><a :href="generatedMediaUrl" download class="text-[#00e5ff] hover:underline text-sm font-bold">⬇️ Télécharger</a></div>
</div>
</div>
<!-- ONGLET : DOCUMENTS & EXPORTS -->
<div v-if="currentTab === 'documents'">
<div class="card">
<h2 class="text-xl font-bold text-blue-400 mb-4">📂 Analyse de Documents & Exports Multi-formats</h2>
<div class="mb-4">
<label class="text-xs font-bold text-gray-400">Sélectionner un fichier (PDF, Word, TXT)</label>
<input type="file" @change="handleFileUpload" class="input-box file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#00e5ff] file:text-[#0f172a] hover:file:bg-[#38bdf8] cursor-pointer">
</div>
<div class="mb-4">
<label class="text-xs font-bold text-gray-400">Instruction associée</label>
<textarea v-model="docPrompt" rows="3" class="input-box" placeholder="Ex: Résume ce document..."></textarea>
</div>
<div class="flex justify-end">
<button @click="uploadDocument" :disabled="isUploading || !selectedFile" class="btn-primary">
{{ isUploading ? '⏳ Traitement...' : '🚀 Envoyer et Analyser' }}
</button>
</div>
</div>
<div v-if="docResult" class="card">
<div class="flex justify-between items-center mb-2">
<h3 class="font-bold text-emerald-400">Rapport : {{ docResult.filename }}</h3>
<!-- Boutons d'export directs (PDF, TXT, MD, JSON) -->
<div class="flex space-x-2">
<span class="text-xs text-gray-400 self-center">Exporter en :</span>
<button @click="exportData(docResult.full_text, 'document_result', 'pdf')" class="btn-export">PDF</button>
<button @click="exportData(docResult.full_text, 'document_result', 'txt')" class="btn-export">TXT</button>
<button @click="exportData(docResult.full_text, 'document_result', 'md')" class="btn-export">MD</button>
<button @click="exportData(docResult.full_text, 'document_result', 'json')" class="btn-export">JSON</button>
</div>
</div>
<div class="bg-slate-900 p-4 rounded border border-gray-700 whitespace-pre-wrap text-sm text-gray-200">
{{ docResult.extracted_preview }}
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, computed, onMounted } = Vue;
createApp({
setup() {
const currentTab = ref('admin');
const config = ref({ api_keys: {}, custom_providers: [] });
const notification = ref("");
const orCredits = ref({ limit: "Chargement...", usage: "..." });
// Modèles et filtres
const modelFilterPricing = ref("free");
const modelSearchCat = ref("");
const selectedActiveModel = ref("groq/llama-3.1-8b-instant");
const availableModels = ref([
{ id: "groq/llama-3.1-8b-instant", name: "Groq Llama 3.1 8B", pricing: "free", category: "Texte", description: "Modèle ultra-rapide sur Groq" },
{ id: "google/gemini-1.5-flash", name: "Gemini 1.5 Flash", pricing: "free", category: "Vision / Multimodal", description: "Modèle léger Google" },
{ id: "anthropic/claude-3.5-sonnet", name: "Claude 3.5 Sonnet", pricing: "paid", category: "Texte / Code", description: "Modèle de pointe Anthropic" },
{ id: "black-forest-labs/flux-schnell", name: "Flux Schnell", pricing: "free", category: "Image", description: "Génération d'images rapide" }
]);
const filteredModels = computed(() => {
return availableModels.value.filter(m => {
const matchPricing = modelFilterPricing.value === 'all' || m.pricing === modelFilterPricing.value;
const matchCat = !modelSearchCat.value || m.category.toLowerCase().includes(modelSearchCat.value.toLowerCase()) || m.name.toLowerCase().includes(modelSearchCat.value.toLowerCase());
return matchPricing && matchCat;
});
});
const mediaPrompt = ref("");
const mediaType = ref("image");
const isGenerating = ref(false);
const generatedMediaUrl = ref(null);
const selectedFile = ref(null);
const docPrompt = ref("");
const isUploading = ref(false);
const docResult = ref(null);
const loadConfig = async () => {
const res = await fetch('/api/config');
config.value = await res.json();
const credRes = await fetch('/api/openrouter/credits');
orCredits.value = await credRes.json();
};
const saveConfig = async () => {
const res = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config.value)
});
const data = await res.json();
notification.value = data.message;
setTimeout(() => notification.value = "", 3000);
};
const exportModelsJson = () => {
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(availableModels.value, null, 4));
const downloadAnchor = document.createElement('a');
downloadAnchor.setAttribute("href", dataStr);
downloadAnchor.setAttribute("download", "aethas38_models_catalog.json");
document.body.appendChild(downloadAnchor);
downloadAnchor.click();
downloadAnchor.remove();
};
const generateMedia = async () => {
if (!mediaPrompt.value) return;
isGenerating.value = true;
try {
const res = await fetch('/api/media/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: mediaPrompt.value, media_type: mediaType.value })
});
const data = await res.json();
if (res.ok) generatedMediaUrl.value = data.url;
else alert(data.detail);
} catch (e) { alert("Erreur réseau"); }
finally { isGenerating.value = false; }
};
const handleFileUpload = (event) => { selectedFile.value = event.target.files[0]; };
const uploadDocument = async () => {
if (!selectedFile.value) return;
isUploading.value = true;
docResult.value = null;
const formData = new FormData();
formData.append("file", selectedFile.value);
formData.append("prompt", docPrompt.value);
try {
const res = await fetch('/api/documents/process', { method: 'POST', body: formData });
const data = await res.json();
if (res.ok) docResult.value = data;
else alert(data.detail || "Erreur document");
} catch (e) { alert("Erreur connexion"); }
finally { isUploading.value = false; }
};
const exportData = async (content, type, format) => {
try {
const res = await fetch('/api/export', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, format, type })
});
if (!res.ok) throw new Error("Erreur export");
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `aethas38_${type}.${format}`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (e) { alert("Erreur téléchargement export."); }
};
onMounted(loadConfig);
return {
currentTab, config, notification, orCredits, saveConfig,
modelFilterPricing, modelSearchCat, selectedActiveModel, availableModels, filteredModels, exportModelsJson,
mediaPrompt, mediaType, isGenerating, generatedMediaUrl, generateMedia,
selectedFile, docPrompt, isUploading, docResult, handleFileUpload, uploadDocument, exportData
};
}
}).mount('#app');
</script>
</body>
</html>