🐳 18/04 - 22:02 - corrige tout
Browse files- index.html +143 -28
index.html
CHANGED
|
@@ -3,11 +3,10 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>
|
| 7 |
-
<script src="https:
|
| 8 |
-
|
| 9 |
-
<script src="https:
|
| 10 |
-
<script src="https:=======cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 11 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 12 |
<script>
|
| 13 |
tailwind.config = {
|
|
@@ -171,7 +170,7 @@
|
|
| 171 |
<label class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2 block">Modèle IA</label>
|
| 172 |
<div class="relative">
|
| 173 |
<button id="model-dropdown-btn" onclick="toggleModelDropdown()" class="w-full flex items-center justify-between px-3 py-2.5 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-[#12121c] hover:border-brand-400 dark:hover:border-brand-500 transition-all text-sm">
|
| 174 |
-
<span id="selected-model-display" class="truncate">
|
| 175 |
<i data-lucide="chevron-down" class="w-4 h-4 text-gray-400 flex-shrink-0 ml-2"></i>
|
| 176 |
</button>
|
| 177 |
<div id="model-dropdown" class="hidden absolute top-full left-0 right-0 mt-2 bg-white dark:bg-[#12121c] border border-gray-200 dark:border-gray-700 rounded-xl shadow-xl z-50 max-h-64 overflow-hidden flex flex-col">
|
|
@@ -200,7 +199,6 @@
|
|
| 200 |
|
| 201 |
<!-- Chat History -->
|
| 202 |
<div class="flex-1 overflow-y-auto p-3 space-y-1" id="chat-history">
|
| 203 |
-
<!-- Chat history items will be inserted here -->
|
| 204 |
</div>
|
| 205 |
|
| 206 |
<!-- Sidebar Footer -->
|
|
@@ -302,6 +300,7 @@
|
|
| 302 |
let currentConversationId = null;
|
| 303 |
let isGenerating = false;
|
| 304 |
let currentAbortController = null;
|
|
|
|
| 305 |
|
| 306 |
// ==================== MODELS ====================
|
| 307 |
const MODELS = [
|
|
@@ -346,14 +345,58 @@
|
|
| 346 |
{ id: 'ibm-granite/granite-4.0-h-micro', name: 'Granite 4.0', provider: 'IBM' },
|
| 347 |
];
|
| 348 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
// ==================== INIT ====================
|
| 350 |
function init() {
|
| 351 |
lucide.createIcons();
|
| 352 |
renderChatHistory();
|
| 353 |
renderModelList();
|
| 354 |
updateThemeIcons();
|
|
|
|
| 355 |
setupInputListener();
|
| 356 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
if (conversations.length > 0) {
|
| 358 |
loadConversation(conversations[0].id);
|
| 359 |
}
|
|
@@ -441,7 +484,7 @@
|
|
| 441 |
function selectModel(modelId) {
|
| 442 |
currentModel = modelId;
|
| 443 |
const model = MODELS.find(m => m.id === modelId);
|
| 444 |
-
document.getElementById('selected-model-display').textContent = model ? `${model.provider}/${model.name}` : modelId;
|
| 445 |
document.getElementById('chat-model-label').textContent = modelId;
|
| 446 |
toggleModelDropdown();
|
| 447 |
renderModelList();
|
|
@@ -496,7 +539,6 @@
|
|
| 496 |
renderChatHistory();
|
| 497 |
renderMessages();
|
| 498 |
document.getElementById('user-input').focus();
|
| 499 |
-
// Close sidebar on mobile
|
| 500 |
if (window.innerWidth < 1024) toggleSidebar();
|
| 501 |
}
|
| 502 |
|
|
@@ -535,13 +577,11 @@
|
|
| 535 |
|
| 536 |
if (!conv || conv.messages.length === 0) {
|
| 537 |
document.getElementById('welcome-screen').style.display = 'flex';
|
| 538 |
-
// Remove all messages except welcome
|
| 539 |
container.querySelectorAll('.message-bubble').forEach(el => el.remove());
|
| 540 |
return;
|
| 541 |
}
|
| 542 |
|
| 543 |
document.getElementById('welcome-screen').style.display = 'none';
|
| 544 |
-
// Clear and re-render
|
| 545 |
container.querySelectorAll('.message-bubble').forEach(el => el.remove());
|
| 546 |
|
| 547 |
const isDark = document.documentElement.classList.contains('dark');
|
|
@@ -656,12 +696,89 @@
|
|
| 656 |
}
|
| 657 |
}
|
| 658 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
// ==================== SEND MESSAGE ====================
|
| 660 |
async function sendMessage() {
|
| 661 |
const input = document.getElementById('user-input');
|
| 662 |
const prompt = input.value.trim();
|
| 663 |
if (!prompt || isGenerating) return;
|
| 664 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
isGenerating = true;
|
| 666 |
document.getElementById('send-btn').disabled = true;
|
| 667 |
input.value = '';
|
|
@@ -705,11 +822,7 @@
|
|
| 705 |
|
| 706 |
try {
|
| 707 |
if (useStream) {
|
| 708 |
-
const response = await
|
| 709 |
-
model: currentModel,
|
| 710 |
-
stream: true
|
| 711 |
-
});
|
| 712 |
-
|
| 713 |
removeTypingIndicator();
|
| 714 |
|
| 715 |
let fullContent = '';
|
|
@@ -732,22 +845,19 @@
|
|
| 732 |
document.getElementById('messages').appendChild(msgDiv);
|
| 733 |
lucide.createIcons();
|
| 734 |
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
}
|
| 740 |
}
|
| 741 |
|
| 742 |
conv.messages.push({ role: 'assistant', content: fullContent });
|
| 743 |
} else {
|
| 744 |
-
const response = await
|
| 745 |
-
|
| 746 |
-
});
|
| 747 |
-
|
| 748 |
removeTypingIndicator();
|
| 749 |
|
| 750 |
-
const content =
|
| 751 |
conv.messages.push({ role: 'assistant', content });
|
| 752 |
addMessageToUI('assistant', content);
|
| 753 |
}
|
|
@@ -755,10 +865,15 @@
|
|
| 755 |
saveConversations();
|
| 756 |
} catch (error) {
|
| 757 |
removeTypingIndicator();
|
| 758 |
-
|
| 759 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 760 |
} finally {
|
| 761 |
isGenerating = false;
|
|
|
|
| 762 |
document.getElementById('send-btn').disabled = !document.getElementById('user-input').value.trim();
|
| 763 |
}
|
| 764 |
}
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>AI Chat Studio</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<script src="https://unpkg.com/lucide@latest"></script>
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
|
|
| 10 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 11 |
<script>
|
| 12 |
tailwind.config = {
|
|
|
|
| 170 |
<label class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2 block">Modèle IA</label>
|
| 171 |
<div class="relative">
|
| 172 |
<button id="model-dropdown-btn" onclick="toggleModelDropdown()" class="w-full flex items-center justify-between px-3 py-2.5 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-[#12121c] hover:border-brand-400 dark:hover:border-brand-500 transition-all text-sm">
|
| 173 |
+
<span id="selected-model-display" class="truncate">Z.AI / GLM 5.1</span>
|
| 174 |
<i data-lucide="chevron-down" class="w-4 h-4 text-gray-400 flex-shrink-0 ml-2"></i>
|
| 175 |
</button>
|
| 176 |
<div id="model-dropdown" class="hidden absolute top-full left-0 right-0 mt-2 bg-white dark:bg-[#12121c] border border-gray-200 dark:border-gray-700 rounded-xl shadow-xl z-50 max-h-64 overflow-hidden flex flex-col">
|
|
|
|
| 199 |
|
| 200 |
<!-- Chat History -->
|
| 201 |
<div class="flex-1 overflow-y-auto p-3 space-y-1" id="chat-history">
|
|
|
|
| 202 |
</div>
|
| 203 |
|
| 204 |
<!-- Sidebar Footer -->
|
|
|
|
| 300 |
let currentConversationId = null;
|
| 301 |
let isGenerating = false;
|
| 302 |
let currentAbortController = null;
|
| 303 |
+
let apiKey = localStorage.getItem('openrouter_api_key') || '';
|
| 304 |
|
| 305 |
// ==================== MODELS ====================
|
| 306 |
const MODELS = [
|
|
|
|
| 345 |
{ id: 'ibm-granite/granite-4.0-h-micro', name: 'Granite 4.0', provider: 'IBM' },
|
| 346 |
];
|
| 347 |
|
| 348 |
+
// ==================== API KEY ====================
|
| 349 |
+
function saveApiKey(key) {
|
| 350 |
+
apiKey = key.trim();
|
| 351 |
+
localStorage.setItem('openrouter_api_key', apiKey);
|
| 352 |
+
updateApiKeyStatus();
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
function updateApiKeyStatus() {
|
| 356 |
+
const dot = document.getElementById('api-key-dot');
|
| 357 |
+
const label = document.getElementById('api-key-label');
|
| 358 |
+
if (apiKey && apiKey.startsWith('sk-or-')) {
|
| 359 |
+
dot.className = 'w-2 h-2 rounded-full bg-green-400';
|
| 360 |
+
label.textContent = 'Configurée';
|
| 361 |
+
label.className = 'text-green-600 dark:text-green-400';
|
| 362 |
+
} else if (apiKey) {
|
| 363 |
+
dot.className = 'w-2 h-2 rounded-full bg-yellow-400';
|
| 364 |
+
label.textContent = 'Format invalide';
|
| 365 |
+
label.className = 'text-yellow-600 dark:text-yellow-400';
|
| 366 |
+
} else {
|
| 367 |
+
dot.className = 'w-2 h-2 rounded-full bg-red-400';
|
| 368 |
+
label.textContent = 'Non configurée';
|
| 369 |
+
label.className = 'text-gray-500 dark:text-gray-400';
|
| 370 |
+
}
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
function toggleApiKeyVisibility() {
|
| 374 |
+
const input = document.getElementById('api-key-input');
|
| 375 |
+
const eyeIcon = document.getElementById('api-key-eye');
|
| 376 |
+
if (input.type === 'password') {
|
| 377 |
+
input.type = 'text';
|
| 378 |
+
eyeIcon.setAttribute('data-lucide', 'eye-off');
|
| 379 |
+
} else {
|
| 380 |
+
input.type = 'password';
|
| 381 |
+
eyeIcon.setAttribute('data-lucide', 'eye');
|
| 382 |
+
}
|
| 383 |
+
lucide.createIcons();
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
// ==================== INIT ====================
|
| 387 |
function init() {
|
| 388 |
lucide.createIcons();
|
| 389 |
renderChatHistory();
|
| 390 |
renderModelList();
|
| 391 |
updateThemeIcons();
|
| 392 |
+
updateApiKeyStatus();
|
| 393 |
setupInputListener();
|
| 394 |
|
| 395 |
+
// Load saved API key
|
| 396 |
+
if (apiKey) {
|
| 397 |
+
document.getElementById('api-key-input').value = apiKey;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
if (conversations.length > 0) {
|
| 401 |
loadConversation(conversations[0].id);
|
| 402 |
}
|
|
|
|
| 484 |
function selectModel(modelId) {
|
| 485 |
currentModel = modelId;
|
| 486 |
const model = MODELS.find(m => m.id === modelId);
|
| 487 |
+
document.getElementById('selected-model-display').textContent = model ? `${model.provider} / ${model.name}` : modelId;
|
| 488 |
document.getElementById('chat-model-label').textContent = modelId;
|
| 489 |
toggleModelDropdown();
|
| 490 |
renderModelList();
|
|
|
|
| 539 |
renderChatHistory();
|
| 540 |
renderMessages();
|
| 541 |
document.getElementById('user-input').focus();
|
|
|
|
| 542 |
if (window.innerWidth < 1024) toggleSidebar();
|
| 543 |
}
|
| 544 |
|
|
|
|
| 577 |
|
| 578 |
if (!conv || conv.messages.length === 0) {
|
| 579 |
document.getElementById('welcome-screen').style.display = 'flex';
|
|
|
|
| 580 |
container.querySelectorAll('.message-bubble').forEach(el => el.remove());
|
| 581 |
return;
|
| 582 |
}
|
| 583 |
|
| 584 |
document.getElementById('welcome-screen').style.display = 'none';
|
|
|
|
| 585 |
container.querySelectorAll('.message-bubble').forEach(el => el.remove());
|
| 586 |
|
| 587 |
const isDark = document.documentElement.classList.contains('dark');
|
|
|
|
| 696 |
}
|
| 697 |
}
|
| 698 |
|
| 699 |
+
// ==================== OPENROUTER API ====================
|
| 700 |
+
async function callOpenRouter(messages, model, stream) {
|
| 701 |
+
if (!apiKey) {
|
| 702 |
+
throw new Error('Clé API non configurée. Ajoutez votre clé OpenRouter dans la barre latérale.');
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
const url = 'https://openrouter.ai/api/v1/chat/completions';
|
| 706 |
+
const options = {
|
| 707 |
+
method: 'POST',
|
| 708 |
+
headers: {
|
| 709 |
+
'Authorization': `Bearer ${apiKey}`,
|
| 710 |
+
'Content-Type': 'application/json',
|
| 711 |
+
'HTTP-Referer': window.location.href,
|
| 712 |
+
'X-Title': 'AI Chat Studio'
|
| 713 |
+
},
|
| 714 |
+
body: JSON.stringify({
|
| 715 |
+
model: model,
|
| 716 |
+
messages: messages,
|
| 717 |
+
stream: stream
|
| 718 |
+
})
|
| 719 |
+
};
|
| 720 |
+
|
| 721 |
+
if (stream) {
|
| 722 |
+
currentAbortController = new AbortController();
|
| 723 |
+
options.signal = currentAbortController.signal;
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
const response = await fetch(url, options);
|
| 727 |
+
|
| 728 |
+
if (!response.ok) {
|
| 729 |
+
const errorData = await response.json().catch(() => ({}));
|
| 730 |
+
const errorMessage = errorData.error?.message || `Erreur HTTP ${response.status}`;
|
| 731 |
+
throw new Error(errorMessage);
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
return response;
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
function parseSSEStream(response) {
|
| 738 |
+
const reader = response.body.getReader();
|
| 739 |
+
const decoder = new TextDecoder();
|
| 740 |
+
let buffer = '';
|
| 741 |
+
|
| 742 |
+
return {
|
| 743 |
+
async *[Symbol.asyncIterator]() {
|
| 744 |
+
while (true) {
|
| 745 |
+
const { done, value } = await reader.read();
|
| 746 |
+
if (done) break;
|
| 747 |
+
|
| 748 |
+
buffer += decoder.decode(value, { stream: true });
|
| 749 |
+
const lines = buffer.split('\n');
|
| 750 |
+
buffer = lines.pop() || '';
|
| 751 |
+
|
| 752 |
+
for (const line of lines) {
|
| 753 |
+
const trimmed = line.trim();
|
| 754 |
+
if (!trimmed || !trimmed.startsWith('data: ')) continue;
|
| 755 |
+
const data = trimmed.slice(6);
|
| 756 |
+
if (data === '[DONE]') return;
|
| 757 |
+
|
| 758 |
+
try {
|
| 759 |
+
const parsed = JSON.parse(data);
|
| 760 |
+
const content = parsed.choices?.[0]?.delta?.content;
|
| 761 |
+
if (content) yield content;
|
| 762 |
+
} catch (e) {
|
| 763 |
+
// Skip malformed JSON chunks
|
| 764 |
+
}
|
| 765 |
+
}
|
| 766 |
+
}
|
| 767 |
+
}
|
| 768 |
+
};
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
// ==================== SEND MESSAGE ====================
|
| 772 |
async function sendMessage() {
|
| 773 |
const input = document.getElementById('user-input');
|
| 774 |
const prompt = input.value.trim();
|
| 775 |
if (!prompt || isGenerating) return;
|
| 776 |
|
| 777 |
+
if (!apiKey) {
|
| 778 |
+
addMessageToUI('assistant', '⚠️ **Clé API non configurée.** Ajoutez votre clé OpenRouter dans la barre latérale pour commencer.');
|
| 779 |
+
return;
|
| 780 |
+
}
|
| 781 |
+
|
| 782 |
isGenerating = true;
|
| 783 |
document.getElementById('send-btn').disabled = true;
|
| 784 |
input.value = '';
|
|
|
|
| 822 |
|
| 823 |
try {
|
| 824 |
if (useStream) {
|
| 825 |
+
const response = await callOpenRouter(apiMessages, currentModel, true);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 826 |
removeTypingIndicator();
|
| 827 |
|
| 828 |
let fullContent = '';
|
|
|
|
| 845 |
document.getElementById('messages').appendChild(msgDiv);
|
| 846 |
lucide.createIcons();
|
| 847 |
|
| 848 |
+
const stream = parseSSEStream(response);
|
| 849 |
+
for await (const chunk of stream) {
|
| 850 |
+
fullContent += chunk;
|
| 851 |
+
updateStreamingMessage(fullContent);
|
|
|
|
| 852 |
}
|
| 853 |
|
| 854 |
conv.messages.push({ role: 'assistant', content: fullContent });
|
| 855 |
} else {
|
| 856 |
+
const response = await callOpenRouter(apiMessages, currentModel, false);
|
| 857 |
+
const data = await response.json();
|
|
|
|
|
|
|
| 858 |
removeTypingIndicator();
|
| 859 |
|
| 860 |
+
const content = data.choices?.[0]?.message?.content || 'Aucune réponse reçue.';
|
| 861 |
conv.messages.push({ role: 'assistant', content });
|
| 862 |
addMessageToUI('assistant', content);
|
| 863 |
}
|
|
|
|
| 865 |
saveConversations();
|
| 866 |
} catch (error) {
|
| 867 |
removeTypingIndicator();
|
| 868 |
+
if (error.name === 'AbortError') {
|
| 869 |
+
// User cancelled, don't show error
|
| 870 |
+
} else {
|
| 871 |
+
const errorContent = `⚠️ **Erreur :** ${error.message || 'Une erreur est survenue lors de la communication avec l\'API.'}`;
|
| 872 |
+
addMessageToUI('assistant', errorContent);
|
| 873 |
+
}
|
| 874 |
} finally {
|
| 875 |
isGenerating = false;
|
| 876 |
+
currentAbortController = null;
|
| 877 |
document.getElementById('send-btn').disabled = !document.getElementById('user-input').value.trim();
|
| 878 |
}
|
| 879 |
}
|