secutorpro commited on
Commit
6ca4187
·
verified ·
1 Parent(s): b2a71dc

🐳 18/04 - 22:02 - corrige tout

Browse files
Files changed (1) hide show
  1. 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>Puter AI Chat Studio</title>
7
- <script src="https:=======cdn.tailwindcss.com"></script>
8
-
9
- <script src="https:=======unpkg.com/lucide@latest"></script>
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">z-ai/glm-5.1</span>
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 puter.ai.chat(apiMessages, {
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
- for await (const part of response) {
736
- if (part?.text) {
737
- fullContent += part.text;
738
- updateStreamingMessage(fullContent);
739
- }
740
  }
741
 
742
  conv.messages.push({ role: 'assistant', content: fullContent });
743
  } else {
744
- const response = await puter.ai.chat(apiMessages, {
745
- model: currentModel
746
- });
747
-
748
  removeTypingIndicator();
749
 
750
- const content = typeof response === 'string' ? response : (response?.message?.content || response?.toString() || 'Aucune réponse');
751
  conv.messages.push({ role: 'assistant', content });
752
  addMessageToUI('assistant', content);
753
  }
@@ -755,10 +865,15 @@
755
  saveConversations();
756
  } catch (error) {
757
  removeTypingIndicator();
758
- const errorContent = `⚠️ Erreur : ${error.message || 'Une erreur est survenue'}`;
759
- addMessageToUI('assistant', errorContent);
 
 
 
 
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
  }