This commit is contained in:
1 parent
626ec138e4
commit
d6a7d10e3a
8 files changed
+529
-236
No files matched your search
+198
-103
@@ -14,6 +14,8 @@
|
||||
.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; }
|
||||
@@ -24,77 +26,30 @@
|
||||
<body class="p-8">
|
||||
<div id="app" class="max-w-5xl mx-auto">
|
||||
|
||||
<!-- En-tête -->
|
||||
<!-- 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>
|
||||
<span class="text-xs text-gray-400">v2.1.0</span>
|
||||
<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</button>
|
||||
<button @click="currentTab = 'studio'" :class="currentTab === 'studio' ? 'tab-active' : 'tab-inactive'" class="tab-btn">🎬 Studio Multimédia</button>
|
||||
<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 : STUDIO MULTIMÉDIA (ÉTAPE 2) -->
|
||||
<div v-if="currentTab === 'studio'">
|
||||
<div class="card">
|
||||
<h2 class="text-xl font-bold text-purple-400 mb-4">Générateur Média (HuggingFace)</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 (Description)</label>
|
||||
<input type="text" v-model="mediaPrompt" class="input-box" placeholder="A futuristic cyberpunk city raining at night..." @keyup.enter="generateMedia">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-gray-400">Type de Média</label>
|
||||
<select v-model="mediaType" class="input-box">
|
||||
<option value="image">🖼️ Image (Stable Diffusion)</option>
|
||||
<option value="voice">🎙️ Voix (Bark TTS)</option>
|
||||
<option value="music">🎵 Musique (MusicGen)</option>
|
||||
<option value="video">🎥 Vidéo (ZeroScope/Damo)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<button @click="generateMedia" :disabled="isGenerating || !mediaPrompt" class="btn-primary">
|
||||
{{ isGenerating ? '⏳ Génération en cours (Patientez)...' : '✨ Générer le Média' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Affichage du Résultat -->
|
||||
<div v-if="generatedMediaUrl" class="card text-center">
|
||||
<h3 class="font-bold text-gray-300 mb-4">Résultat :</h3>
|
||||
|
||||
<img v-if="generatedMediaType === 'image'" :src="generatedMediaUrl" class="max-w-full h-auto mx-auto rounded border border-gray-600 shadow-lg">
|
||||
|
||||
<audio v-if="['music', 'voice'].includes(generatedMediaType)" controls class="w-full mx-auto">
|
||||
<source :src="generatedMediaUrl" type="audio/wav">
|
||||
Votre navigateur ne supporte pas l'audio.
|
||||
</audio>
|
||||
|
||||
<video v-if="generatedMediaType === 'video'" controls class="max-w-full h-auto mx-auto rounded shadow-lg">
|
||||
<source :src="generatedMediaUrl" type="video/mp4">
|
||||
Votre navigateur ne supporte pas la vidéo.
|
||||
</video>
|
||||
|
||||
<div class="mt-6">
|
||||
<a :href="generatedMediaUrl" download class="text-[#00e5ff] hover:underline text-sm font-bold">⬇️ Télécharger le fichier</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ONGLET : ADMINISTRATION (ÉTAPE 1) -->
|
||||
<!-- ONGLET : ADMINISTRATION & CATALOGUE DE MODÈLES -->
|
||||
<div v-if="currentTab === 'admin'">
|
||||
<!-- Clés API -->
|
||||
<div class="card">
|
||||
<h2 class="text-xl font-bold text-purple-400 mb-4">🔑 Clés API Principales</h2>
|
||||
<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>
|
||||
@@ -104,37 +59,43 @@
|
||||
<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>
|
||||
<label class="text-xs font-bold text-gray-400">Gemini API Key</label>
|
||||
<input type="password" v-model="config.api_keys.gemini" class="input-box" placeholder="AIzaSy...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fournisseurs Personnalisés -->
|
||||
<!-- 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-amber-400">🔌 Fournisseurs Personnalisés</h2>
|
||||
<button @click="addProvider" class="btn-primary text-sm py-1 px-3">+ Ajouter</button>
|
||||
</div>
|
||||
<div v-for="(provider, index) in config.custom_providers" :key="index" class="p-4 bg-slate-800 rounded border border-slate-700 mb-4">
|
||||
<div class="flex justify-between mb-2">
|
||||
<h3 class="font-bold text-sm">Fournisseur #{{ index + 1 }}</h3>
|
||||
<button @click="removeProvider(index)" class="btn-danger">Supprimer</button>
|
||||
<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 class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
</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>
|
||||
<label class="text-xs text-gray-400">Nom (Ex: HuggingFace)</label>
|
||||
<input type="text" v-model="provider.name" class="input-box">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-400">URL de Base</label>
|
||||
<input type="text" v-model="provider.base_url" class="input-box" placeholder="https://api...">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-400">Clé API</label>
|
||||
<input type="password" v-model="provider.api_key" class="input-box" placeholder="hf_...">
|
||||
<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>
|
||||
@@ -144,26 +105,121 @@
|
||||
</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, onMounted } = Vue;
|
||||
const { createApp, ref, computed, onMounted } = Vue;
|
||||
createApp({
|
||||
setup() {
|
||||
const currentTab = ref('studio');
|
||||
const config = ref({ api_keys: {}, smtp: {}, custom_providers: [] });
|
||||
const currentTab = ref('admin');
|
||||
const config = ref({ api_keys: {}, custom_providers: [] });
|
||||
const notification = ref("");
|
||||
|
||||
// Variables Studio
|
||||
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 generatedMediaType = 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 () => {
|
||||
@@ -177,13 +233,19 @@
|
||||
setTimeout(() => notification.value = "", 3000);
|
||||
};
|
||||
|
||||
const addProvider = () => { config.value.custom_providers.push({ name: "", base_url: "", api_key: "" }); };
|
||||
const removeProvider = (index) => { config.value.custom_providers.splice(index, 1); };
|
||||
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;
|
||||
generatedMediaUrl.value = null;
|
||||
try {
|
||||
const res = await fetch('/api/media/generate', {
|
||||
method: 'POST',
|
||||
@@ -191,23 +253,56 @@
|
||||
body: JSON.stringify({ prompt: mediaPrompt.value, media_type: mediaType.value })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
generatedMediaUrl.value = data.url;
|
||||
generatedMediaType.value = data.type;
|
||||
} else {
|
||||
alert(data.detail);
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Erreur de connexion au serveur.");
|
||||
} finally {
|
||||
isGenerating.value = false;
|
||||
}
|
||||
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, saveConfig, addProvider, removeProvider,
|
||||
mediaPrompt, mediaType, isGenerating, generatedMediaUrl, generatedMediaType, generateMedia
|
||||
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');
|
||||
|
||||
Reference in new issue
Block a user