protae5544 commited on
Commit
bd1b6f2
·
verified ·
1 Parent(s): 54d02a3

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +780 -547
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
6
- <title>AI Chat - Win95 Fullscreen + Stream + File Upload</title>
7
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=IBM+Plex+Mono:400,700&display=swap">
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs.min.css">
9
  <style>
@@ -135,6 +135,7 @@
135
  border: 2px inset #808080;
136
  background: #c0c0c0;
137
  }
 
138
  .settings95 .api-status {
139
  font-size: 0.95em;
140
  color: #008000;
@@ -193,6 +194,7 @@
193
  }
194
  .message95.ai { align-self: flex-start; background: #fffffe; }
195
  .message95.user { align-self: flex-end; background: #c0e0ff; }
 
196
  .message95 pre { background: #fff; border: 2px inset #808080; padding: 9px 5px 15px 5px; position: relative; white-space: pre-wrap; word-break: break-all; margin-bottom: 10px; margin-top: 7px; overflow-x: auto; }
197
  .code-tools { position: absolute; right: 7px; top: 5px; z-index: 2; display: flex; gap: 3px; }
198
  .code-tools button { font-size: 0.95em; padding: 1.5px 7px; background: #e0e0e0; border: 2px outset #fff; border-radius: 0; cursor: pointer; color: #222; }
@@ -330,6 +332,31 @@
330
  }
331
  #contextSaveArea95 button:active { background: #c0c0c0; border: 2px inset #808080; }
332
  #contextSaveArea95 button:disabled { background: #e0e0e0; color: #bbb; border: 2px outset #b0b0b0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  @media (max-width: 650px) {
334
  .win95window {
335
  min-width: 0;
@@ -349,6 +376,12 @@
349
  gap: 4px;
350
  padding: 6px 4px;
351
  }
 
 
 
 
 
 
352
  .input-area95 {
353
  flex-direction: column;
354
  gap: 8px;
@@ -372,7 +405,7 @@
372
  <span style="display:inline-block;width:18px;height:18px;background:#fff;border:1px solid #808080;margin-right:7px;box-shadow:inset 2px 2px #c0c0c0;">
373
  <span style="display:inline-block;width:9px;height:9px;background:#008080;margin:4px 0 0 4px;vertical-align:middle;"></span>
374
  </span>
375
- AI Chat - Windows 95 Enhanced
376
  </div>
377
  <div class="win95controls">
378
  <button id="maximizeBtn" title="เต็มจอ">&#9633;</button>
@@ -390,6 +423,7 @@
390
  <option value="openai">OpenAI</option>
391
  <option value="ollama">Ollama (Local)</option>
392
  <option value="huggingface">HuggingFace</option>
 
393
  </select>
394
  <label for="modelSelect95">โมเดล:</label>
395
  <select id="modelSelect95"></select>
@@ -399,12 +433,29 @@
399
  <span class="api-status" id="apiKeyStatus95"></span>
400
  </form>
401
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  <div class="system-preset-area">
403
  <label for="systemPresetSelect95">ระบบ:</label>
404
  <select id="systemPresetSelect95">
405
  <option value="general">แชทธรรมดา</option>
406
  <option value="code-full">ส่งโค้ดเต็ม</option>
407
  <option value="code-function">ส่งเฉพาะฟังก์ชัน</option>
 
408
  <option value="custom">กำหนดเอง</option>
409
  </select>
410
  <input type="text" id="customSystemPrompt95" placeholder="ใส่ System Prompt ของคุณ" style="display:none; flex: 1; min-width: 200px;">
@@ -420,11 +471,11 @@
420
  <button id="confirmSaveBtn95">ยืนยัน context และเริ่มแชทใหม่</button>
421
  </div>
422
  <div class="chat-container95" id="messagesDiv95">
423
- <div class="message95 ai">สวัสดี! AI Chat สไตล์ Win95 Enhanced พร้อมใช้ 🎉<br>รองรับหลาย AI Provider และมีระบบ preset ใหม่!</div>
424
  </div>
425
  <form id="chatForm95" class="input-area95" autocomplete="off">
426
  <button type="button" class="file-attach-btn" id="fileAttachBtn" title="แนบไฟล์">
427
- 📎<input type="file" id="fileInput" multiple>
428
  </button>
429
  <span class="file-name" id="fileName"></span>
430
  <input type="text" id="userInput95" placeholder="พิมพ์คำถามหรือข้อความ..." autocomplete="off">
@@ -455,13 +506,13 @@ const PROVIDERS = {
455
  apiKeyLabel: "Anthropic API Key",
456
  models: [
457
  { value: "claude-3-5-sonnet-20241022", label: "Claude-3.5 Sonnet" },
458
- { value: "claude-3-5-haiku-20241022", label: "Claude-3.5 Haiku" },
459
  { value: "claude-3-opus-20240229", label: "Claude-3 Opus" },
460
- { value: "claude-3-sonnet-20240229", label: "Claude-3 Sonnet" },
461
- { value: "claude-3-haiku-20240307", label: "Claude-3 Haiku" }
462
  ],
463
  isStream: false,
464
- streamEndpoint: "https://api.anthropic.com/v1/messages"
 
465
  },
466
  xai: {
467
  name: "xAI (Grok)",
@@ -475,7 +526,8 @@ const PROVIDERS = {
475
  { value: "grok-2-1212", label: "Grok-2" },
476
  { value: "grok-2-mini", label: "Grok-2 Mini" }
477
  ],
478
- isStream: true
 
479
  },
480
  groq: {
481
  name: "Groq",
@@ -489,7 +541,8 @@ const PROVIDERS = {
489
  { value: "gemma2-9b-it", label: "Gemma-2 9B IT" },
490
  { value: "gemma-7b-it", label: "Gemma 7B IT" }
491
  ],
492
- isStream: true
 
493
  },
494
  openai: {
495
  name: "OpenAI",
@@ -503,665 +556,845 @@ const PROVIDERS = {
503
  { value: "o1-preview", label: "o1-preview" },
504
  { value: "o1-mini", label: "o1-mini" }
505
  ],
506
- isStream: true
 
507
  },
508
  ollama: {
509
  name: "Ollama (Local)",
510
  endpoint: "http://localhost:11434/api/chat",
511
  apiKeyLabel: "ไม่ต้องใส่ API Key",
512
  models: [
 
513
  { value: "llama3.2", label: "Llama 3.2" },
514
  { value: "llama3.1", label: "Llama 3.1" },
515
  { value: "codellama", label: "Code Llama" },
516
  { value: "mistral", label: "Mistral" },
517
  { value: "gemma2", label: "Gemma 2" }
518
  ],
519
- isStream: true
 
520
  },
521
  huggingface: {
522
  name: "HuggingFace",
523
- endpoint: "https://api-inference.huggingface.co/models",
524
  apiKeyLabel: "HuggingFace API Key",
525
  models: [
526
- { value: "microsoft/DialoGPT-medium", label: "DialoGPT Medium" },
527
- { value: "microsoft/DialoGPT-large", label: "DialoGPT Large" },
528
- { value: "facebook/blenderbot-400M-distill", label: "BlenderBot 400M" },
529
- { value: "google/flan-t5-large", label: "Flan-T5 Large" },
530
- { value: "bigscience/bloom-560m", label: "BLOOM 560M" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531
  ],
532
- isStream: false
 
533
  }
534
  };
535
 
536
- // --- System Presets ---
537
- const SYSTEM_PRESETS = {
538
- general: "คุณคือผู้ช่วย AI ที่ช่วยเหลือตอบคำถามและแชทธรรมดาได้ทุกเรื่อง",
539
- "code-full": "คุณคือผู้ช่วยโปรแกรมเมอร์ เมื่อผู้ใช้ถามเกี่ยวกับโค้ด ให้ส่งโค้ดเต็มทั้งไฟล์กลับมาเสมอ ห้ามตัดย่อหรือใช้ ... ต้องแสดงโค้ดครบทุกบรรทัด",
540
- "code-function": "คุณคือผู้ช่วยโปรแกรมเมอร์ เมื่อผู้ใช้ถามเกี่ยวกับโค้ด ให้ส่งเฉพาะฟังก์ชันหรือส่วนที่เกี่ยวข้องกับคำถาม ไม่ต้องส่งโค้ดทั้งไฟล์",
541
- custom: "" // จะใช้ค่าจาก input
542
- };
543
-
544
- const PAYLOAD_CHAR_WARNING_THRESHOLD = 44000;
545
-
546
- // --- Elements
547
- const providerSelect = document.getElementById('providerSelect95');
548
- const modelSelect = document.getElementById('modelSelect95');
549
- const apiKeyInput = document.getElementById('apiKey95');
550
  const confirmApiKeyBtn = document.getElementById('confirmApiKeyBtn95');
551
- const apiKeyStatus = document.getElementById('apiKeyStatus95');
 
 
 
 
552
  const systemPresetSelect = document.getElementById('systemPresetSelect95');
553
- const customSystemPrompt = document.getElementById('customSystemPrompt95');
554
- const chatForm = document.getElementById('chatForm95');
555
- const userInput = document.getElementById('userInput95');
556
- const messagesDiv = document.getElementById('messagesDiv95');
557
- const errorDiv = document.getElementById('errorMessage95');
558
- const contextSaveArea = document.getElementById('contextSaveArea95');
559
  const savedCodeDisplay = document.getElementById('savedCodeDisplay95');
560
- const goalInput = document.getElementById('goalInput95');
561
- const confirmSaveBtn = document.getElementById('confirmSaveBtn95');
562
- const warningMessage = document.getElementById('warningMessage95');
563
- const fileAttachBtn = document.getElementById('fileAttachBtn');
564
- const fileInput = document.getElementById('fileInput');
565
- const fileNameSpan = document.getElementById('fileName');
566
- const dropzone = document.getElementById('dropzone95');
567
-
568
- // --- State
569
- let chatHistory = [];
570
- let isAITyping = false;
571
- let latestCodeContext = null;
572
- let savedCodeContext = null;
573
- let savedGoal = null;
574
- let attachedFiles = [];
575
-
576
- // --- highlight.js + code tools
577
- function renderWithHighlight(text) {
578
- return text.replace(/```(\w+)?\n([\s\S]*?)```/g, function(_, lang, code) {
579
- const safeCode = code.replace(/</g,"&lt;").replace(/>/g,"&gt;");
580
- const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext';
581
- const codeId = 'code_' + Math.random().toString(36).slice(2);
582
- return `<pre><div class="code-tools">
583
- <button type="button" onclick="copyCode('${codeId}')">📋 Copy</button>
584
- <button type="button" onclick="downloadCode('${codeId}','code.${lang||'txt'}')">⬇️ Save</button>
585
- </div><code id="${codeId}" class="hljs language-${language}">${safeCode}</code></pre>`;
586
- });
587
- }
588
 
589
- window.copyCode = function(id) {
590
- const code = document.getElementById(id);
591
- if (code) {
592
- navigator.clipboard.writeText(code.textContent).then(()=>{
593
- code.parentElement.querySelector('button').textContent='';
594
- setTimeout(()=>code.parentElement.querySelector('button').textContent='📋 Copy',1000);
595
- });
596
- }
597
- };
598
 
599
- window.downloadCode = function(id, filename) {
600
- const code = document.getElementById(id);
601
- if (code) {
602
- const blob = new Blob([code.textContent], {type: "text/plain"});
603
- const a = document.createElement('a');
604
- a.href = URL.createObjectURL(blob);
605
- a.download = filename;
606
- a.click();
607
- setTimeout(()=>URL.revokeObjectURL(a.href), 2000);
608
- }
609
- };
610
 
611
- function setModelOptions(providerKey) {
612
- modelSelect.innerHTML = "";
613
- PROVIDERS[providerKey].models.forEach(m =>
614
- modelSelect.innerHTML += `<option value="${m.value}">${m.label}</option>`
615
- );
616
-
617
- // บันทึกและโหลดโมเดลที่เลือกไว้
618
- const savedModel = localStorage.getItem("ai95model_" + providerKey);
619
- if (savedModel && PROVIDERS[providerKey].models.find(m => m.value === savedModel)) {
620
- modelSelect.value = savedModel;
621
- }
622
  }
623
 
624
- function setApiKeyLabel(providerKey) {
625
- apiKeyInput.placeholder = "กรอก " + PROVIDERS[providerKey].apiKeyLabel;
626
-
627
- // ซ่อน API Key input สำหรับ Ollama
628
- if (providerKey === 'ollama') {
629
- apiKeyInput.style.display = 'none';
630
- confirmApiKeyBtn.style.display = 'none';
631
- document.querySelector('label[for="apiKey95"]').style.display = 'none';
632
  } else {
633
- apiKeyInput.style.display = 'inline-block';
634
- confirmApiKeyBtn.style.display = 'inline-block';
635
- document.querySelector('label[for="apiKey95"]').style.display = 'inline-block';
636
  }
 
637
  }
638
 
639
- function loadApiKey(providerKey) {
640
- if (providerKey === 'ollama') {
641
- apiKeyStatus.textContent = '✔️ Local Mode';
642
- apiKeyStatus.style.color = '#008000';
643
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
644
  }
 
 
 
 
 
645
 
646
- const k = localStorage.getItem("ai95key_" + providerKey) || "";
647
- apiKeyInput.value = k;
648
- if (k) {
649
- apiKeyStatus.textContent = '✔️ โหลด API Key อัตโนมัติ';
650
- apiKeyStatus.style.color = '#008000';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
  } else {
652
- apiKeyStatus.textContent = '';
653
  }
654
  }
655
 
656
- function saveApiKey(providerKey, key) {
657
- localStorage.setItem("ai95key_" + providerKey, key);
658
- apiKeyStatus.textContent = "✔️ API Key ถูกบันทึกแล้ว";
659
- apiKeyStatus.style.color = "#008000";
660
- }
661
-
662
- function saveModel(providerKey, model) {
663
- localStorage.setItem("ai95model_" + providerKey, model);
664
- }
665
 
666
- function saveSystemPreset(preset) {
667
- localStorage.setItem("ai95systemPreset", preset);
668
- }
 
 
 
 
 
669
 
670
- function loadSystemPreset() {
671
- const saved = localStorage.getItem("ai95systemPreset");
672
- if (saved) {
673
- systemPresetSelect.value = saved;
674
- if (saved === 'custom') {
675
- customSystemPrompt.style.display = 'inline-block';
676
- customSystemPrompt.value = localStorage.getItem("ai95customPrompt") || "";
677
- }
 
 
678
  }
679
- }
680
 
681
- function getApiKey() {
682
- const providerKey = getProviderKey();
683
- return providerKey === 'ollama' ? 'local' : apiKeyInput.value.trim();
684
- }
 
685
 
686
- function getProviderKey() { return providerSelect.value; }
687
- function getModelValue() { return modelSelect.value; }
 
 
 
688
 
689
- function getCurrentSystemPrompt() {
690
- const preset = systemPresetSelect.value;
691
- if (preset === 'custom') {
692
- return customSystemPrompt.value.trim() || SYSTEM_PRESETS.general;
 
 
 
 
 
 
 
 
 
 
693
  }
694
- return SYSTEM_PRESETS[preset] || SYSTEM_PRESETS.general;
695
- }
696
 
697
- function showError(msg) { errorDiv.textContent = msg || ""; errorDiv.style.display = msg ? "block" : "none"; }
 
 
 
698
 
699
- function addMessage(role, content) {
700
- const div = document.createElement('div');
701
- div.className = 'message95 ' + role;
702
- div.innerHTML = renderWithHighlight(content.replace(/\n/g, '<br>'));
703
- messagesDiv.appendChild(div);
704
- div.querySelectorAll('pre code').forEach((block) => hljs.highlightElement(block));
705
  messagesDiv.scrollTop = messagesDiv.scrollHeight;
706
- return div;
707
  }
708
 
709
- function showLoading() { chatForm.querySelector('button[type="submit"]').disabled = true; isAITyping = true; }
710
- function hideLoading() { chatForm.querySelector('button[type="submit"]').disabled = false; isAITyping = false; }
711
-
712
- function extractCode(text) {
713
- const codeBlockRegex = /```(?:\w+)?\s*\n([\s\S]*?)```/g;
714
- let lastCode = null, match;
715
- while ((match = codeBlockRegex.exec(text)) !== null) lastCode = match[1].trim();
716
- return lastCode;
717
  }
718
 
719
- function calculatePayloadCharCount(messages) {
720
- let count = 0;
721
- for (const msg of messages) {
722
- if (msg.content && typeof msg.content === 'string') count += msg.content.length;
723
- if (msg.content && Array.isArray(msg.content)) {
724
- for (const part of msg.content) {
725
- if (part.text) count += part.text.length;
726
- }
727
- }
728
- }
729
- return count;
730
  }
731
 
732
- function showContextSaveArea(show, code = null, goal = '') {
733
- if (show) {
734
- savedCodeDisplay.textContent = code !== null ? code : 'ยังไม่พบโค้ดในข้อความ AI ล่าสุด';
735
- if (goal !== undefined) goalInput.value = goal;
736
- contextSaveArea.style.display = 'block';
737
- } else {
738
- contextSaveArea.style.display = 'none';
739
- warningMessage.textContent = '';
740
- }
741
  }
742
 
743
- function clearChat() {
744
- chatHistory = [];
745
- messagesDiv.innerHTML = '<div class="message95 ai">สวัสดี! AI Chat สไตล์ Win95 Enhanced พร้อมใช้ 🎉<br>รองรับหลาย AI Provider และมีระบบ preset ใหม่!</div>';
746
- latestCodeContext = null;
747
- showContextSaveArea(false, null, goalInput.value);
748
- showError('');
 
 
749
  }
750
 
751
- // --- Provider Change
752
- providerSelect.addEventListener('change', function() {
753
- setModelOptions(getProviderKey());
754
- setApiKeyLabel(getProviderKey());
755
- loadApiKey(getProviderKey());
756
- showError('');
757
- });
758
-
759
- // --- Model Change
760
- modelSelect.addEventListener('change', function() {
761
- saveModel(getProviderKey(), getModelValue());
762
- });
763
-
764
- // --- System Preset Change
765
- systemPresetSelect.addEventListener('change', function() {
766
- const preset = systemPresetSelect.value;
767
- saveSystemPreset(preset);
768
-
769
- if (preset === 'custom') {
770
- customSystemPrompt.style.display = 'inline-block';
771
- customSystemPrompt.focus();
772
  } else {
773
- customSystemPrompt.style.display = 'none';
774
  }
775
  });
776
 
777
- customSystemPrompt.addEventListener('input', function() {
778
- localStorage.setItem("ai95customPrompt", this.value);
 
779
  });
780
 
781
- // --- Initialize
782
- setModelOptions(getProviderKey());
783
- setApiKeyLabel(getProviderKey());
784
- loadApiKey(getProviderKey());
785
- loadSystemPreset();
786
-
787
- // --- API Key Save
788
- confirmApiKeyBtn.addEventListener('click', function() {
789
- const key = getApiKey();
790
- if (!key) {
791
- apiKeyStatus.textContent = '';
792
- showError("กรุณาใส่ API Key ก่อนกดบันทึก");
793
- return;
794
- }
795
- saveApiKey(getProviderKey(), key);
796
- showError("");
797
  });
798
 
799
- apiKeyInput.addEventListener('input', ()=>apiKeyStatus.textContent='');
800
-
801
- // --- Anthropic API (Messages format)
802
- async function callAnthropicAPI(messages) {
803
- const response = await fetch(PROVIDERS.anthropic.endpoint, {
804
- method: 'POST',
805
- headers: {
806
- 'Content-Type': 'application/json',
807
- 'x-api-key': getApiKey(),
808
- 'anthropic-version': '2023-06-01'
809
- },
810
- body: JSON.stringify({
811
- model: getModelValue(),
812
- max_tokens: 4096,
813
- system: getCurrentSystemPrompt(),
814
- messages: messages
815
- })
816
- });
817
 
818
- if (!response.ok) {
819
- const error = await response.text();
820
- throw new Error(`Anthropic API Error: ${response.status} - ${error}`);
821
- }
822
 
823
- const data = await response.json();
824
- return data.content[0].text;
 
 
825
  }
826
 
827
- // --- Ollama API
828
- async function callOllamaAPI(messages) {
829
- const response = await fetch(PROVIDERS.ollama.endpoint, {
830
- method: 'POST',
831
- headers: {
832
- 'Content-Type': 'application/json'
833
- },
834
- body: JSON.stringify({
835
- model: getModelValue(),
836
- messages: [
837
- { role: 'system', content: getCurrentSystemPrompt() },
838
- ...messages
839
- ],
840
- stream: true
841
- })
842
- });
843
 
844
- if (!response.ok) {
845
- const error = await response.text();
846
- throw new Error(`Ollama API Error: ${response.status} - ${error}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
847
  }
848
-
849
- return response;
850
  }
851
 
852
- // --- HuggingFace API
853
- async function callHuggingFaceAPI(messages) {
854
- const modelName = getModelValue();
855
- const lastMessage = messages[messages.length - 1];
856
-
857
- const response = await fetch(`${PROVIDERS.huggingface.endpoint}/${modelName}`, {
858
- method: 'POST',
859
- headers: {
860
- 'Content-Type': 'application/json',
861
- 'Authorization': `Bearer ${getApiKey()}`
862
- },
863
- body: JSON.stringify({
864
- inputs: lastMessage.content,
865
- parameters: {
866
- max_length: 2048,
867
- temperature: 0.7
868
- }
869
- })
870
  });
871
-
872
- if (!response.ok) {
873
- const error = await response.text();
874
- throw new Error(`HuggingFace API Error: ${response.status} - ${error}`);
875
- }
876
-
877
- const data = await response.json();
878
- return data[0]?.generated_text || "ไม่สามารถสร้างคำตอบได้";
879
  }
880
 
881
- // --- Streaming API (xAI/OpenAI/Groq)
882
- async function callStreamingAPI(messages) {
883
- const provider = PROVIDERS[getProviderKey()];
884
-
885
- const response = await fetch(provider.endpoint, {
886
- method: 'POST',
887
- headers: {
888
- 'Content-Type': 'application/json',
889
- 'Authorization': `Bearer ${getApiKey()}`
890
- },
891
- body: JSON.stringify({
892
- model: getModelValue(),
893
- messages: [
894
- { role: 'system', content: getCurrentSystemPrompt() },
895
- ...messages
896
- ],
897
- stream: true,
898
- max_tokens: 4096
899
- })
900
  });
 
901
 
902
- if (!response.ok) {
903
- const error = await response.text();
904
- throw new Error(`${provider.name} API Error: ${response.status} - ${error}`);
 
 
 
 
905
  }
906
 
907
- return response;
908
- }
909
-
910
- // --- File upload handling
911
- fileInput.addEventListener('change', handleFileSelect);
 
912
 
913
- function handleFileSelect(event) {
914
- const files = Array.from(event.target.files);
915
- attachedFiles = [];
916
-
917
- files.forEach(file => {
918
- if (file.size > 5 * 1024 * 1024) { // 5MB limit
919
- showError(`ไฟล์ ${file.name} มีขนาดใหญ่เกิน 5MB`);
920
- return;
921
  }
922
-
923
- const reader = new FileReader();
924
- reader.onload = (e) => {
925
- attachedFiles.push({
926
- name: file.name,
927
- type: file.type,
928
- content: e.target.result
929
- });
930
- };
931
-
932
- if (file.type.startsWith('image/')) {
933
- reader.readAsDataURL(file);
934
  } else {
935
- reader.readAsText(file);
936
  }
937
- });
938
-
939
- if (files.length > 0) {
940
- fileNameSpan.textContent = files.length === 1 ? files[0].name : `${files.length} ไฟล์`;
941
- } else {
942
- fileNameSpan.textContent = '';
943
  }
944
- }
945
 
946
- // --- Drag and drop
947
- document.addEventListener('dragover', (e) => {
948
- e.preventDefault();
949
- dropzone.style.display = 'block';
950
- });
951
 
952
- document.addEventListener('dragleave', (e) => {
953
- if (!e.relatedTarget || !dropzone.contains(e.relatedTarget)) {
954
- dropzone.style.display = 'none';
955
  }
956
- });
957
 
958
- dropzone.addEventListener('drop', (e) => {
959
- e.preventDefault();
960
- dropzone.style.display = 'none';
961
-
962
- const files = Array.from(e.dataTransfer.files);
963
- const mockEvent = { target: { files: files } };
964
- handleFileSelect(mockEvent);
965
- });
966
 
967
- // --- Context save functionality
968
- confirmSaveBtn.addEventListener('click', function() {
969
- const code = savedCodeDisplay.textContent;
970
- const goal = goalInput.value.trim();
971
-
972
- if (code === 'ยังไม่พบโค้ดในข้อความ AI ล่าสุด' || !code) {
973
- showError("ไม่พบโค้ดที่จะบันทึก");
974
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
975
  }
976
-
977
- savedCodeContext = code;
978
- savedGoal = goal;
979
-
980
- clearChat();
981
-
982
- if (savedCodeContext && savedGoal) {
983
- addMessage('ai', `Context ถูกบันทึกแล้ว!\n\n**เป้าหมาย:** ${savedGoal}\n\n**โค้ดล่าสุด:**\n\`\`\`\n${savedCodeContext}\n\`\`\`\n\nพร้อมรับคำถามหรือคำสั่งเพิ่มเติม!`);
984
  }
985
- });
986
 
987
- // --- Main chat submission
988
- chatForm.addEventListener('submit', async function(e) {
989
- e.preventDefault();
990
-
991
- const message = userInput.value.trim();
992
- if (!message && attachedFiles.length === 0) return;
993
-
994
- const providerKey = getProviderKey();
995
- if (providerKey !== 'ollama' && !getApiKey()) {
996
- showError("กรุณาใส่ API Key ก่อนส่งข้อความ");
997
- return;
998
  }
999
- if (isAITyping) return;
1000
 
1001
- showError('');
1002
-
1003
- let userMessage = message;
1004
-
1005
- // Add file contents to message
1006
- if (attachedFiles.length > 0) {
1007
- userMessage += '\n\n--- ไฟล์ที่แนบ ---\n';
1008
- attachedFiles.forEach(file => {
1009
- userMessage += `\n**${file.name}:**\n`;
1010
- if (file.type.startsWith('image/')) {
1011
- userMessage += `[รูปภาพ: ${file.type}]\n`;
1012
- } else {
1013
- userMessage += `${file.content}\n`;
 
 
 
1014
  }
1015
- });
1016
-
1017
- // Clear attachments
1018
- attachedFiles = [];
1019
- fileNameSpan.textContent = '';
1020
- fileInput.value = '';
 
 
 
1021
  }
1022
-
1023
- // Add saved context if available
1024
- if (savedCodeContext && savedGoal) {
1025
- userMessage = `**Context ที่บันทึกไว้:**\nเป้าหมาย: ${savedGoal}\nโค้ดล่าสุด:\n\`\`\`\n${savedCodeContext}\n\`\`\`\n\n**คำถาม/คำสั่งใหม่:**\n${userMessage}`;
1026
- savedCodeContext = null;
1027
- savedGoal = null;
 
 
 
1028
  }
1029
-
1030
- addMessage('user', message);
1031
- userInput.value = '';
1032
- showLoading();
1033
-
1034
- const currentMessage = { role: 'user', content: userMessage };
1035
- chatHistory.push(currentMessage);
1036
-
1037
- // Check payload size
1038
- const payloadSize = calculatePayloadCharCount(chatHistory);
1039
- if (payloadSize > PAYLOAD_CHAR_WARNING_THRESHOLD) {
1040
- const newCode = extractCode(chatHistory[chatHistory.length - 1]?.content || '');
1041
- if (newCode) latestCodeContext = newCode;
 
 
 
 
 
 
 
 
1042
 
1043
- warningMessage.textContent = `⚠️ ข้อความยาวเกิน ${PAYLOAD_CHAR_WARNING_THRESHOLD} ตัวอักษร (ปัจจุบัน: ${payloadSize})`;
1044
- showContextSaveArea(true, latestCodeContext);
1045
- hideLoading();
1046
- return;
1047
- }
1048
-
1049
- try {
1050
- let aiResponse = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1051
 
1052
- if (providerKey === 'anthropic') {
1053
- aiResponse = await callAnthropicAPI(chatHistory);
1054
- const aiDiv = addMessage('ai', aiResponse);
1055
-
1056
- // Extract code from AI response
1057
- const code = extractCode(aiResponse);
1058
- if (code) latestCodeContext = code;
1059
-
1060
- } else if (providerKey === 'huggingface') {
1061
- aiResponse = await callHuggingFaceAPI(chatHistory);
1062
- const aiDiv = addMessage('ai', aiResponse);
1063
-
1064
- // Extract code from AI response
1065
- const code = extractCode(aiResponse);
1066
- if (code) latestCodeContext = code;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1067
 
1068
- } else {
1069
- // Streaming response for other providers
1070
- const response = providerKey === 'ollama' ?
1071
- await callOllamaAPI(chatHistory) :
1072
- await callStreamingAPI(chatHistory);
1073
-
1074
- const aiDiv = addMessage('ai', '');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1075
  const reader = response.body.getReader();
1076
- const decoder = new TextDecoder();
1077
-
1078
- let buffer = '';
1079
  let fullResponse = '';
1080
-
 
1081
  while (true) {
1082
- const { value, done } = await reader.read();
1083
  if (done) break;
1084
-
1085
- buffer += decoder.decode(value, { stream: true });
1086
- const lines = buffer.split('\n');
1087
- buffer = lines.pop() || '';
1088
-
1089
- for (const line of lines) {
1090
- if (providerKey === 'ollama') {
1091
- // Ollama format
1092
  try {
1093
- const parsed = JSON.parse(line);
1094
- const delta = parsed.message?.content || '';
1095
- if (delta) {
1096
- fullResponse += delta;
1097
- aiDiv.innerHTML = renderWithHighlight(fullResponse.replace(/\n/g, '<br>'));
1098
- aiDiv.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
1099
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
 
 
 
 
 
1100
  }
1101
  } catch (e) {
1102
- // Skip malformed JSON
1103
  }
1104
- } else {
1105
- // OpenAI-compatible format
 
 
 
 
 
 
 
 
 
 
 
 
1106
  if (line.startsWith('data: ')) {
1107
- const data = line.slice(6).trim();
1108
- if (data === '[DONE]') continue;
1109
-
1110
- try {
1111
- const parsed = JSON.parse(data);
1112
- const delta = parsed.choices?.[0]?.delta?.content || '';
1113
- if (delta) {
1114
- fullResponse += delta;
1115
- aiDiv.innerHTML = renderWithHighlight(fullResponse.replace(/\n/g, '<br>'));
1116
- aiDiv.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
1117
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
 
1118
  }
1119
- } catch (e) {
1120
- // Skip malformed JSON
1121
  }
1122
  }
 
 
 
 
 
 
 
 
 
 
 
 
1123
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1124
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1125
  }
1126
 
1127
- aiResponse = fullResponse;
1128
-
1129
- // Extract code from AI response
1130
- const code = extractCode(aiResponse);
1131
- if (code) latestCodeContext = code;
 
 
 
 
 
 
1132
  }
1133
-
1134
- chatHistory.push({ role: 'assistant', content: aiResponse });
1135
-
1136
  } catch (error) {
1137
- console.error('API Error:', error);
1138
- showError(error.message);
1139
- chatHistory.pop(); // Remove failed user message
1140
  } finally {
1141
- hideLoading();
1142
- }
1143
- });
1144
-
1145
- // --- Initialize
1146
- window.addEventListener('load', function() {
1147
- userInput.focus();
1148
-
1149
- // Auto-resize input
1150
- userInput.addEventListener('input', function() {
1151
- this.style.height = 'auto';
1152
- this.style.height = Math.min(this.scrollHeight, 120) + 'px';
1153
- });
1154
- });
1155
-
1156
- // --- Keyboard shortcuts
1157
- document.addEventListener('keydown', function(e) {
1158
- if (e.key === 'F11') {
1159
- e.preventDefault();
1160
- maximizeBtn.click();
1161
- }
1162
-
1163
- if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
1164
- chatForm.dispatchEvent(new Event('submit'));
1165
  }
1166
  });
1167
  </script>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
6
+ <title>AI Chat - Win95 Enhanced Multi-Modal</title>
7
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=IBM+Plex+Mono:400,700&display=swap">
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs.min.css">
9
  <style>
 
135
  border: 2px inset #808080;
136
  background: #c0c0c0;
137
  }
138
+ .settings95 button:focus { outline: 1px dotted #fff; }
139
  .settings95 .api-status {
140
  font-size: 0.95em;
141
  color: #008000;
 
194
  }
195
  .message95.ai { align-self: flex-start; background: #fffffe; }
196
  .message95.user { align-self: flex-end; background: #c0e0ff; }
197
+ .message95 .image-preview { max-width: 200px; margin: 8px 0; border: 2px inset #808080; }
198
  .message95 pre { background: #fff; border: 2px inset #808080; padding: 9px 5px 15px 5px; position: relative; white-space: pre-wrap; word-break: break-all; margin-bottom: 10px; margin-top: 7px; overflow-x: auto; }
199
  .code-tools { position: absolute; right: 7px; top: 5px; z-index: 2; display: flex; gap: 3px; }
200
  .code-tools button { font-size: 0.95em; padding: 1.5px 7px; background: #e0e0e0; border: 2px outset #fff; border-radius: 0; cursor: pointer; color: #222; }
 
332
  }
333
  #contextSaveArea95 button:active { background: #c0c0c0; border: 2px inset #808080; }
334
  #contextSaveArea95 button:disabled { background: #e0e0e0; color: #bbb; border: 2px outset #b0b0b0; }
335
+ .model-pair-selection {
336
+ background: #f8f8f8;
337
+ border-bottom: 2px solid #808080;
338
+ padding: 8px 12px;
339
+ display: flex;
340
+ flex-wrap: wrap;
341
+ align-items: center;
342
+ font-size: 0.95em;
343
+ gap: 8px;
344
+ }
345
+ .model-pair-selection label {
346
+ margin-right: 4px;
347
+ font-weight: bold;
348
+ color: #444;
349
+ }
350
+ .model-pair-selection select {
351
+ font-family: inherit;
352
+ font-size: 1em;
353
+ background: #fff;
354
+ border: 2px inset #808080;
355
+ padding: 2px 6px;
356
+ outline: none;
357
+ min-width: 120px;
358
+ max-width: 180px;
359
+ }
360
  @media (max-width: 650px) {
361
  .win95window {
362
  min-width: 0;
 
376
  gap: 4px;
377
  padding: 6px 4px;
378
  }
379
+ .model-pair-selection {
380
+ flex-direction: column;
381
+ align-items: flex-start;
382
+ gap: 4px;
383
+ padding: 6px 4px;
384
+ }
385
  .input-area95 {
386
  flex-direction: column;
387
  gap: 8px;
 
405
  <span style="display:inline-block;width:18px;height:18px;background:#fff;border:1px solid #808080;margin-right:7px;box-shadow:inset 2px 2px #c0c0c0;">
406
  <span style="display:inline-block;width:9px;height:9px;background:#008080;margin:4px 0 0 4px;vertical-align:middle;"></span>
407
  </span>
408
+ AI Chat - Win95 Multi-Modal Enhanced
409
  </div>
410
  <div class="win95controls">
411
  <button id="maximizeBtn" title="เต็มจอ">&#9633;</button>
 
423
  <option value="openai">OpenAI</option>
424
  <option value="ollama">Ollama (Local)</option>
425
  <option value="huggingface">HuggingFace</option>
426
+ <option value="google">Google (Gemini)</option>
427
  </select>
428
  <label for="modelSelect95">โมเดล:</label>
429
  <select id="modelSelect95"></select>
 
433
  <span class="api-status" id="apiKeyStatus95"></span>
434
  </form>
435
 
436
+ <div class="model-pair-selection" id="multiModalSelection" style="display:none;">
437
+ <label for="visionModelSelect95">Vision Model:</label>
438
+ <select id="visionModelSelect95">
439
+ <option value="Qwen/Qwen2.5-Coder-32B-Instruct">Qwen2.5-Coder 32B</option>
440
+ <option value="Qwen/Qwen2-VL-72B-Instruct">Qwen2-VL 72B</option>
441
+ <option value="microsoft/Florence-2-large">Florence-2 Large</option>
442
+ <option value="llava-hf/llava-1.5-7b-hf">LLaVA 1.5 7B (HF)</option>
443
+ </select>
444
+ <label for="ocrModelSelect95">OCR Model:</label>
445
+ <select id="ocrModelSelect95">
446
+ <option value="scb10x/typhoon-v1.5x-72b-instruct">Typhoon OCR 7B</option>
447
+ <option value="microsoft/trocr-base-printed">TrOCR Base</option>
448
+ <option value="PaddlePaddle/PaddleOCR">PaddleOCR</option>
449
+ </select>
450
+ </div>
451
+
452
  <div class="system-preset-area">
453
  <label for="systemPresetSelect95">ระบบ:</label>
454
  <select id="systemPresetSelect95">
455
  <option value="general">แชทธรรมดา</option>
456
  <option value="code-full">ส่งโค้ดเต็ม</option>
457
  <option value="code-function">ส่งเฉพาะฟังก์ชัน</option>
458
+ <option value="multimodal">Multi-Modal Analysis</option>
459
  <option value="custom">กำหนดเอง</option>
460
  </select>
461
  <input type="text" id="customSystemPrompt95" placeholder="ใส่ System Prompt ของคุณ" style="display:none; flex: 1; min-width: 200px;">
 
471
  <button id="confirmSaveBtn95">ยืนยัน context และเริ่มแชทใหม่</button>
472
  </div>
473
  <div class="chat-container95" id="messagesDiv95">
474
+ <div class="message95 ai">สวัสดี! AI Chat Multi-Modal Enhanced พร้อมใช้ 🎉<br>รองรับการวิเคราะห์รูปภาพและ OCR ด้วย Qwen2.5-Coder + Typhoon OCR!</div>
475
  </div>
476
  <form id="chatForm95" class="input-area95" autocomplete="off">
477
  <button type="button" class="file-attach-btn" id="fileAttachBtn" title="แนบไฟล์">
478
+ 📎<input type="file" id="fileInput" multiple accept="image/*,text/*,.pdf,.docx,.xlsx">
479
  </button>
480
  <span class="file-name" id="fileName"></span>
481
  <input type="text" id="userInput95" placeholder="พิมพ์คำถามหรือข้อความ..." autocomplete="off">
 
506
  apiKeyLabel: "Anthropic API Key",
507
  models: [
508
  { value: "claude-3-5-sonnet-20241022", label: "Claude-3.5 Sonnet" },
509
+ { value: "claude-3-haiku-20240307", label: "Claude-3 Haiku" },
510
  { value: "claude-3-opus-20240229", label: "Claude-3 Opus" },
511
+ { value: "claude-3-sonnet-20240229", label: "Claude-3 Sonnet" }
 
512
  ],
513
  isStream: false,
514
+ streamEndpoint: "https://api.anthropic.com/v1/messages",
515
+ supportsMultiModal: true
516
  },
517
  xai: {
518
  name: "xAI (Grok)",
 
526
  { value: "grok-2-1212", label: "Grok-2" },
527
  { value: "grok-2-mini", label: "Grok-2 Mini" }
528
  ],
529
+ isStream: true,
530
+ supportsMultiModal: true
531
  },
532
  groq: {
533
  name: "Groq",
 
541
  { value: "gemma2-9b-it", label: "Gemma-2 9B IT" },
542
  { value: "gemma-7b-it", label: "Gemma 7B IT" }
543
  ],
544
+ isStream: true,
545
+ supportsMultiModal: false
546
  },
547
  openai: {
548
  name: "OpenAI",
 
556
  { value: "o1-preview", label: "o1-preview" },
557
  { value: "o1-mini", label: "o1-mini" }
558
  ],
559
+ isStream: true,
560
+ supportsMultiModal: true
561
  },
562
  ollama: {
563
  name: "Ollama (Local)",
564
  endpoint: "http://localhost:11434/api/chat",
565
  apiKeyLabel: "ไม่ต้องใส่ API Key",
566
  models: [
567
+ { value: "llama3.2-vision", label: "Llama 3.2 Vision" },
568
  { value: "llama3.2", label: "Llama 3.2" },
569
  { value: "llama3.1", label: "Llama 3.1" },
570
  { value: "codellama", label: "Code Llama" },
571
  { value: "mistral", label: "Mistral" },
572
  { value: "gemma2", label: "Gemma 2" }
573
  ],
574
+ isStream: true,
575
+ supportsMultiModal: true
576
  },
577
  huggingface: {
578
  name: "HuggingFace",
579
+ endpoint: "https://api-inference.huggingface.co/models/", // Add trailing slash
580
  apiKeyLabel: "HuggingFace API Key",
581
  models: [
582
+ { value: "Qwen/Qwen2.5-Coder-32B-Instruct", label: "Qwen2.5-Coder 32B (Text/Code)" },
583
+ { value: "Qwen/Qwen2-VL-72B-Instruct", label: "Qwen2-VL 72B (Vision)" },
584
+ { value: "scb10x/typhoon-v1.5x-72b-instruct", label: "Typhoon 72B (Text/Code)" },
585
+ { value: "microsoft/Florence-2-large", label: "Florence-2 Large (Vision)" },
586
+ { value: "llava-hf/llava-1.5-7b-hf", label: "LLaVA 1.5 7B (Vision)" },
587
+ { value: "microsoft/trocr-base-printed", label: "TrOCR Base (OCR)" },
588
+ { value: "google/t5-base-qa", label: "T5 Base QA (Text)" } // Example, more models can be added
589
+ ],
590
+ isStream: false, // HuggingFace inference API usually doesn't stream by default
591
+ supportsMultiModal: true,
592
+ specificModelEndpoints: { // For multi-modal HF models, we might need specific endpoints
593
+ "Qwen/Qwen2-VL-72B-Instruct": "https://api-inference.huggingface.co/models/Qwen/Qwen2-VL-72B-Instruct",
594
+ "microsoft/Florence-2-large": "https://api-inference.huggingface.co/models/microsoft/Florence-2-large",
595
+ "llava-hf/llava-1.5-7b-hf": "https://api-inference.huggingface.co/models/llava-hf/llava-1.5-7b-hf",
596
+ "microsoft/trocr-base-printed": "https://api-inference.huggingface.co/models/microsoft/trocr-base-printed",
597
+ "PaddlePaddle/PaddleOCR": "https://api-inference.huggingface.co/models/PaddlePaddle/PaddleOCR", // Placeholder, usually requires dedicated setup or API
598
+ }
599
+ },
600
+ google: {
601
+ name: "Google (Gemini)",
602
+ endpoint: "https://generativelanguage.googleapis.com/v1beta/models/",
603
+ apiKeyLabel: "Google API Key",
604
+ models: [
605
+ { value: "gemini-pro", label: "Gemini Pro" },
606
+ { value: "gemini-pro-vision", label: "Gemini Pro Vision" },
607
+ { value: "gemini-1.5-pro-latest", label: "Gemini 1.5 Pro" },
608
+ { value: "gemini-1.5-flash-latest", label: "Gemini 1.5 Flash" }
609
  ],
610
+ isStream: true,
611
+ supportsMultiModal: true
612
  }
613
  };
614
 
615
+ const providerSelect = document.getElementById('providerSelect95');
616
+ const modelSelect = document.getElementById('modelSelect95');
617
+ const apiKeyInput = document.getElementById('apiKey95');
 
 
 
 
 
 
 
 
 
 
 
618
  const confirmApiKeyBtn = document.getElementById('confirmApiKeyBtn95');
619
+ const apiKeyStatus = document.getElementById('apiKeyStatus95');
620
+ const messagesDiv = document.getElementById('messagesDiv95');
621
+ const userInput = document.getElementById('userInput95');
622
+ const chatForm = document.getElementById('chatForm95');
623
+ const errorMessageDiv = document.getElementById('errorMessage95');
624
  const systemPresetSelect = document.getElementById('systemPresetSelect95');
625
+ const customSystemPromptInput = document.getElementById('customSystemPrompt95');
626
+ const contextSaveArea = document.getElementById('contextSaveArea95');
 
 
 
 
627
  const savedCodeDisplay = document.getElementById('savedCodeDisplay95');
628
+ const goalInput = document.getElementById('goalInput95');
629
+ const confirmSaveBtn = document.getElementById('confirmSaveBtn95');
630
+ const fileInput = document.getElementById('fileInput');
631
+ const fileNameSpan = document.getElementById('fileName');
632
+ const dropzone = document.getElementById('dropzone95');
633
+ const multiModalSelection = document.getElementById('multiModalSelection');
634
+ const visionModelSelect = document.getElementById('visionModelSelect95');
635
+ const ocrModelSelect = document.getElementById('ocrModelSelect95');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
 
637
+ let currentApiKey = '';
638
+ let conversationHistory = [];
639
+ let attachedFiles = []; // Stores file objects
640
+ let lastDetectedCode = '';
641
+ let savedGoal = '';
 
 
 
 
642
 
643
+ // --- Save/Load API Key from Local Storage ---
644
+ function saveApiKey(provider, key) {
645
+ localStorage.setItem(`apiKey_${provider}`, key);
646
+ updateApiKeyStatus(true);
647
+ }
 
 
 
 
 
 
648
 
649
+ function loadApiKey(provider) {
650
+ return localStorage.getItem(`apiKey_${provider}`) || '';
 
 
 
 
 
 
 
 
 
651
  }
652
 
653
+ function updateApiKeyStatus(isSuccess) {
654
+ if (isSuccess) {
655
+ apiKeyStatus.textContent = "บันทึกแล้ว!";
656
+ apiKeyStatus.style.color = '#008000';
 
 
 
 
657
  } else {
658
+ apiKeyStatus.textContent = "ไม่ได้บันทึก";
659
+ apiKeyStatus.style.color = '#b00';
 
660
  }
661
+ setTimeout(() => apiKeyStatus.textContent = '', 3000);
662
  }
663
 
664
+ confirmApiKeyBtn.addEventListener('click', () => {
665
+ const provider = providerSelect.value;
666
+ currentApiKey = apiKeyInput.value.trim();
667
+ saveApiKey(provider, currentApiKey);
668
+ });
669
+
670
+ // --- Initialize Models and API Key on Load ---
671
+ function populateModels() {
672
+ const selectedProvider = providerSelect.value;
673
+ const providerInfo = PROVIDERS[selectedProvider];
674
+ modelSelect.innerHTML = ''; // Clear existing options
675
+
676
+ if (providerInfo && providerInfo.models) {
677
+ providerInfo.models.forEach(model => {
678
+ const option = document.createElement('option');
679
+ option.value = model.value;
680
+ option.textContent = model.label;
681
+ modelSelect.appendChild(option);
682
+ });
683
  }
684
+
685
+ // Update API Key input based on selected provider
686
+ apiKeyInput.value = loadApiKey(selectedProvider);
687
+ currentApiKey = apiKeyInput.value.trim();
688
+ apiKeyInput.placeholder = PROVIDERS[selectedProvider].apiKeyLabel;
689
 
690
+ // Show/hide multi-modal model selection
691
+ if (providerInfo.supportsMultiModal) {
692
+ multiModalSelection.style.display = 'flex';
693
+ // Populate vision and OCR models specific to HuggingFace if selected
694
+ if (selectedProvider === 'huggingface') {
695
+ visionModelSelect.innerHTML = '';
696
+ ocrModelSelect.innerHTML = '';
697
+ PROVIDERS.huggingface.models.forEach(model => {
698
+ if (model.label.includes('(Vision)')) {
699
+ const option = document.createElement('option');
700
+ option.value = model.value;
701
+ option.textContent = model.label;
702
+ visionModelSelect.appendChild(option);
703
+ } else if (model.label.includes('(OCR)')) {
704
+ const option = document.createElement('option');
705
+ option.value = model.value;
706
+ option.textContent = model.label;
707
+ ocrModelSelect.appendChild(option);
708
+ }
709
+ });
710
+ // Add a "None" option for OCR if no OCR model is needed for a specific task
711
+ const noneOcrOption = document.createElement('option');
712
+ noneOcrOption.value = 'none';
713
+ noneOcrOption.textContent = 'None (ไม่ใช้ OCR)';
714
+ ocrModelSelect.appendChild(noneOcrOption);
715
+ } else {
716
+ // For other providers, ensure they have default options or are handled
717
+ // For simplicity, we can default to specific models or clear them if not relevant
718
+ // This part might need further refinement based on actual multi-modal APIs
719
+ visionModelSelect.innerHTML = `
720
+ <option value="default_vision">Default Vision Model</option>
721
+ `;
722
+ ocrModelSelect.innerHTML = `
723
+ <option value="default_ocr">Default OCR Model</option>
724
+ <option value="none">None (ไม่ใช้ OCR)</option>
725
+ `;
726
+ }
727
  } else {
728
+ multiModalSelection.style.display = 'none';
729
  }
730
  }
731
 
732
+ providerSelect.addEventListener('change', populateModels);
733
+ document.addEventListener('DOMContentLoaded', populateModels);
 
 
 
 
 
 
 
734
 
735
+ // --- System Preset and Custom Prompt ---
736
+ systemPresetSelect.addEventListener('change', (event) => {
737
+ if (event.target.value === 'custom') {
738
+ customSystemPromptInput.style.display = 'block';
739
+ } else {
740
+ customSystemPromptInput.style.display = 'none';
741
+ }
742
+ });
743
 
744
+ // --- Chat Message Display ---
745
+ function addMessage(text, sender, imageUrl = null, codeLanguage = null) {
746
+ const messageDiv = document.createElement('div');
747
+ messageDiv.classList.add('message95', sender);
748
+
749
+ if (imageUrl) {
750
+ const img = document.createElement('img');
751
+ img.src = imageUrl;
752
+ img.classList.add('image-preview');
753
+ messageDiv.appendChild(img);
754
  }
 
755
 
756
+ // Check for code blocks and apply highlighting
757
+ const codeBlockRegex = /```(\w+)?\n([\s\S]+?)\n```/g;
758
+ let lastIndex = 0;
759
+ let match;
760
+ let contentHtml = '';
761
 
762
+ while ((match = codeBlockRegex.exec(text)) !== null) {
763
+ // Add text before the code block
764
+ if (match.index > lastIndex) {
765
+ contentHtml += formatText(text.substring(lastIndex, match.index));
766
+ }
767
 
768
+ const language = match[1] || 'plaintext';
769
+ const code = match[2];
770
+
771
+ // Store the last detected code if it's the latest message and from AI
772
+ if (sender === 'ai') {
773
+ lastDetectedCode = code;
774
+ savedCodeDisplay.textContent = code;
775
+ }
776
+
777
+ contentHtml += `<pre><div class="code-tools">
778
+ <button onclick="copyCode(this)">คัดลอก</button>
779
+ <button onclick="pasteCodeToInput(this)" title="วางโค้ดลงในช่องพิมพ์">ส่งต่อ</button>
780
+ </div><code class="language-${language}">${hljs.highlight(code, {language: language, ignoreIllegals: true}).value}</code></pre>`;
781
+ lastIndex = codeBlockRegex.lastIndex;
782
  }
 
 
783
 
784
+ // Add any remaining text after the last code block
785
+ if (lastIndex < text.length) {
786
+ contentHtml += formatText(text.substring(lastIndex));
787
+ }
788
 
789
+ messageDiv.innerHTML += contentHtml;
790
+ messagesDiv.appendChild(messageDiv);
 
 
 
 
791
  messagesDiv.scrollTop = messagesDiv.scrollHeight;
 
792
  }
793
 
794
+ function formatText(text) {
795
+ // Basic formatting for bold, italic, and links outside code blocks
796
+ let formattedText = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); // Bold
797
+ formattedText = formattedText.replace(/\*(.*?)\*/g, '<em>$1</em>'); // Italic
798
+ formattedText = formattedText.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank">$1</a>'); // Links
799
+ return formattedText;
 
 
800
  }
801
 
802
+ function copyCode(button) {
803
+ const codeElement = button.closest('pre').querySelector('code');
804
+ navigator.clipboard.writeText(codeElement.textContent);
805
+ button.textContent = 'คัดลอกแล้ว!';
806
+ setTimeout(() => button.textContent = 'คัดลอก', 2000);
 
 
 
 
 
 
807
  }
808
 
809
+ function pasteCodeToInput(button) {
810
+ const codeElement = button.closest('pre').querySelector('code');
811
+ userInput.value = codeElement.textContent;
812
+ userInput.focus();
 
 
 
 
 
813
  }
814
 
815
+ // --- Error Handling ---
816
+ function displayError(message) {
817
+ errorMessageDiv.textContent = `ข้อผิดพลาด: ${message}`;
818
+ errorMessageDiv.style.display = 'block';
819
+ setTimeout(() => {
820
+ errorMessageDiv.style.display = 'none';
821
+ errorMessageDiv.textContent = '';
822
+ }, 5000);
823
  }
824
 
825
+ // --- Context Saving Feature ---
826
+ confirmSaveBtn.addEventListener('click', () => {
827
+ const goal = goalInput.value.trim();
828
+ if (lastDetectedCode || goal) {
829
+ savedGoal = goal;
830
+ conversationHistory = [
831
+ { role: 'system', content: `ผู้ใช้กำลังทำงานกับโค้ดต่อไปนี้:\n\`\`\`\n${lastDetectedCode}\n\`\`\`\nเป้าหมายคือ: ${savedGoal || 'ไม่มีเป้าหมายเฉพาะเจาะจง'}` }
832
+ ];
833
+ addMessage("Context ถูกบันทึกและเริ่มต้นการแชทใหม่แล้ว!", "ai");
834
+ contextSaveArea.style.display = 'none';
835
+ savedCodeDisplay.textContent = '';
836
+ goalInput.value = '';
837
+ lastDetectedCode = '';
 
 
 
 
 
 
 
 
838
  } else {
839
+ displayError("ไม่มีโค้ดหรือเป้าหมายให้บันทึก");
840
  }
841
  });
842
 
843
+ // --- Drag and Drop for Files ---
844
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
845
+ chatForm.addEventListener(eventName, preventDefaults, false);
846
  });
847
 
848
+ function preventDefaults(e) {
849
+ e.preventDefault();
850
+ e.stopPropagation();
851
+ }
852
+
853
+ ['dragenter', 'dragover'].forEach(eventName => {
854
+ chatForm.addEventListener(eventName, () => dropzone.style.display = 'flex', false);
 
 
 
 
 
 
 
 
 
855
  });
856
 
857
+ ['dragleave', 'drop'].forEach(eventName => {
858
+ chatForm.addEventListener(eventName, () => dropzone.style.display = 'none', false);
859
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
860
 
861
+ dropzone.addEventListener('drop', handleDrop, false);
 
 
 
862
 
863
+ function handleDrop(e) {
864
+ const dt = e.dataTransfer;
865
+ const files = dt.files;
866
+ handleFiles(files);
867
  }
868
 
869
+ fileInput.addEventListener('change', (e) => {
870
+ handleFiles(e.target.files);
871
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
872
 
873
+ function handleFiles(files) {
874
+ attachedFiles = [];
875
+ fileNameSpan.textContent = '';
876
+ if (files.length > 0) {
877
+ for (const file of files) {
878
+ if (file.type.startsWith('image/')) {
879
+ attachedFiles.push({ type: 'image', file: file });
880
+ } else if (file.type.startsWith('text/') || file.name.endsWith('.txt') || file.name.endsWith('.md') || file.name.endsWith('.html') || file.name.endsWith('.css') || file.name.endsWith('.js')) {
881
+ attachedFiles.push({ type: 'text', file: file });
882
+ } else if (file.type === 'application/pdf' || file.name.endsWith('.pdf')) {
883
+ attachedFiles.push({ type: 'pdf', file: file }); // Handle PDF
884
+ } else if (file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || file.name.endsWith('.docx')) {
885
+ attachedFiles.push({ type: 'docx', file: file }); // Handle DOCX
886
+ } else if (file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || file.name.endsWith('.xlsx')) {
887
+ attachedFiles.push({ type: 'xlsx', file: file }); // Handle XLSX
888
+ } else {
889
+ displayError(`ไม่รองรับไฟล์ประเภท: ${file.type}.`);
890
+ return;
891
+ }
892
+ }
893
+ fileNameSpan.textContent = attachedFiles.map(f => f.file.name).join(', ');
894
  }
 
 
895
  }
896
 
897
+ async function readFileAsBase64(file) {
898
+ return new Promise((resolve, reject) => {
899
+ const reader = new FileReader();
900
+ reader.onload = () => resolve(reader.result.split(',')[1]); // Get base64 string
901
+ reader.onerror = error => reject(error);
902
+ reader.readAsDataURL(file);
 
 
 
 
 
 
 
 
 
 
 
 
903
  });
 
 
 
 
 
 
 
 
904
  }
905
 
906
+ async function readFileAsText(file) {
907
+ return new Promise((resolve, reject) => {
908
+ const reader = new FileReader();
909
+ reader.onload = () => resolve(reader.result);
910
+ reader.onerror = error => reject(error);
911
+ reader.readAsText(file);
 
 
 
 
 
 
 
 
 
 
 
 
 
912
  });
913
+ }
914
 
915
+ // --- Main Chat Logic ---
916
+ chatForm.addEventListener('submit', async (e) => {
917
+ e.preventDefault();
918
+ const userMessage = userInput.value.trim();
919
+ if (!userMessage && attachedFiles.length === 0) {
920
+ displayError("กรุณาพิมพ์ข้อความหรือแนบไฟล์");
921
+ return;
922
  }
923
 
924
+ const selectedProvider = providerSelect.value;
925
+ const selectedModel = modelSelect.value;
926
+ const providerInfo = PROVIDERS[selectedProvider];
927
+ const apiKey = currentApiKey;
928
+ const systemPreset = systemPresetSelect.value;
929
+ let systemPrompt = '';
930
 
931
+ if (systemPreset === 'custom') {
932
+ systemPrompt = customSystemPromptInput.value.trim();
933
+ } else if (systemPreset === 'code-full') {
934
+ if (lastDetectedCode) {
935
+ systemPrompt = `คุณคือผู้ช่วยพัฒนาเว็บที่มีความสามารถสูงในการเขียนโค้ดและสร้าง artifact ได้อย่างสมบูรณ์แบบตามคำสั่งของผู้ใช้ คุณจะทำงานร่วมกับผู้ใช้เพื่อแก้ไขและปรับปรุง artifact ที่มีอยู่ โค้ดทั้งหมดจะต้องถูกเขียนใน code block เดียว เพื่อเป็นไฟล์โค้ดที่สมบูรณ์และพร้อมใช้งาน โดยไม่มีการแยกโค้ด HTML และ JavaScript ในการตอบกลับของคุณ ให้เอาต์พุตเฉพาะโค้ด HTML โดยไม่มีข้อความอธิบายใดๆ เพิ่มเติม เมื่อใดก็ตามที่ได้รับคำสั่ง คุณจะตรวจสอบการรันโค้ดอีกครั้งเพื่อให้แน่ใจว่าไม่มีข้อผิดพลาดในการเอาต์พุต ผู้ใช้ได้ให้โค้ด HTML/CSS/JS ล่าสุดแก่คุณแล้ว และต้องการให้คุณช่วยเหลือในการพัฒนาต่อ. โค้ดปัจจุบันคือ: \n\`\`\`html\n${lastDetectedCode}\n\`\`\`\nเป้าหมายของโค้ดนี้คือ: ${savedGoal || 'ไม่ระบุ'}.`;
936
+ } else {
937
+ systemPrompt = `คุณคือผู้ช่วยพัฒนาเว็บที่มีความสามารถสูงในการเขียนโค้ดและสร้าง artifact ได้อย่างสมบูรณ์แบบตามคำสั่งของผู้ใช้ คุณจะทำงานร่วมกับผู้ใช้เพื่อแก้ไขและปรับปรุง artifact ที่มีอยู่ โค้ดทั้งหมดจะต้องถูกเขียนใน code block เดียว เพื่อเป็นไฟล์โค้ดที่สมบูรณ์และพร้อมใช้งาน โดยไม่มีการแยกโค้ด HTML และ JavaScript ในการตอบกลับของคุณ ให้เอาต์พุตเฉพาะโค้ด HTML โดยไม่มีข้อความอธิบายใดๆ เพิ่มเติม เมื่อใดก็ตามที่ได้รับคำสั่ง คุณจะตรวจสอบการรันโค้ดอีกครั้งเพื่อให้แน่ใจว่าไม่มีข้อผิดพลาดในการเอาต์พุต`;
 
938
  }
939
+ } else if (systemPreset === 'code-function') {
940
+ if (lastDetectedCode) {
941
+ systemPrompt = `คุณคือผู้ช่วยพัฒนาเว็บที่มีความสามารถสูงในการเขียนโค้ดและสร้าง artifact ���ด้อย่างสมบูรณ์แบบตามคำสั่งของผู้ใช้ คุณจะทำงานร่วมกับผู้ใช้เพื่อแก้ไขและปรับปรุง artifact ที่มีอยู่ สำหรับการตอบกลับที่เกี่ยวข้องกับโค้ด ให้คุณตอบกลับเฉพาะโค้ดที่เกี่ยวข้องกับฟังก์ชันหรือส่วนที่แก้ไขเท่านั้น หากคุณให้ไฟล์โค้ดที่สมบูรณ์ คุณจะต้องตอบกลับเฉพาะโค้ด HTML เท่านั้น โดยไม่มีข้อความอธิบายใดๆ เพิ่มเติม เมื่อใดก็ตามที่ได้รับคำสั่ง คุณจะตรวจสอบการรันโค้ดอีกครั้งเพื่อให้แน่ใจว่าไม่มีข้อผิดพลาดในการเอาต์พุต ผู้ใช้ได้ให้โค้ด HTML/CSS/JS ล่าสุดแก่คุณแล้ว และต้องการให้คุณช่วยเหลือในการพัฒนาต่อ. โค้ดปัจจุบันคือ: \n\`\`\`html\n${lastDetectedCode}\n\`\`\`\nเป้าหมายของโค้ดนี้คือ: ${savedGoal || 'ไม่ระบุ'}.`;
 
 
 
 
 
 
 
 
 
942
  } else {
943
+ systemPrompt = `คุณคือผู้ช่วยพัฒนาเว็บที่มีความสามารถสูงในการเขียนโค้ดและสร้าง artifact ได้อย่างสมบูรณ์แบบตามคำสั่งของผู้ใช้ คุณจะทำงานร่วมกับผู้ใช้เพื่อแก้ไขและปรับปรุง artifact ที่มีอยู่ สำหรับการตอบกลับที่เกี่ยวข้องกับโค้ด ให้คุณตอบกลับเฉพาะโค้ดที่เกี่ยวข้องกับฟังก์ชันหรือส่วนที่แก้ไขเท่านั้น หากคุณให้ไฟล์โค้ดที่สมบูรณ์ คุณจะต้องตอบกลับเฉพาะโค้ด HTML เท่านั้น โดยไม่มีข้อความอธิบายใดๆ เพิ่มเติม เมื่อใดก็ตามที่ได้รับคำสั่ง คุณจะตรวจสอบการรันโค้ดอีกครั้งเพื่อให้แน่ใจว่าไม่มีข้อผิดพลาดในการเอาต์พุต`;
944
  }
945
+ } else if (systemPreset === 'multimodal') {
946
+ systemPrompt = `คุณคือผู้ช่วย Multi-Modal AI ที่เชี่ยวชาญในการวิเคราะห์รูปภาพ, ข้อความ, และเอกสาร. คุณสามารถตอบคำถามเกี่ยวกับเนื้อหาในไฟล์ที่แนบมา, สรุปข้อมูล, หรือดึงข้อความจากรูปภาพโดยใช้ OCR. เมื่อมีการร้องขอวิเคราะห์รูปภาพหรือ OCR ให้ใช้ความสามารถเหล่านั้นเพื่อตอบคำถาม. หากมีข้อมูลจากไฟล์แนบหลายประเภท ให้พยายามผสานรวมข้อมูลเหล่านั้นเพื่อการตอบสนองที่ครอบคลุม.`;
947
+ } else { // general
948
+ systemPrompt = `คุณคือ AI ผู้ช่วยที่สุภาพและให้ข้อมูลที่เป็นประโยชน์`;
 
 
949
  }
 
950
 
951
+ // Clear previous error
952
+ errorMessageDiv.style.display = 'none';
 
 
 
953
 
954
+ if (!apiKey && selectedProvider !== 'ollama') {
955
+ displayError("กรุณาใส่ API Key ของคุณ");
956
+ return;
957
  }
 
958
 
959
+ let userContent = [];
960
+ if (userMessage) {
961
+ userContent.push({ type: 'text', text: userMessage });
962
+ }
 
 
 
 
963
 
964
+ for (const fileObj of attachedFiles) {
965
+ const file = fileObj.file;
966
+ if (fileObj.type === 'image') {
967
+ try {
968
+ const base64 = await readFileAsBase64(file);
969
+ userContent.push({ type: 'image_url', image_url: { url: `data:${file.type};base64,${base64}` } });
970
+ addMessage(`แนบรูปภาพ: ${file.name}`, 'user', `data:${file.type};base64,${base64}`);
971
+ } catch (error) {
972
+ displayError(`ไม่สามารถอ่านไฟล์รูปภาพได้: ${error.message}`);
973
+ return;
974
+ }
975
+ } else if (fileObj.type === 'text') {
976
+ try {
977
+ const textContent = await readFileAsText(file);
978
+ userContent.push({ type: 'text', text: `เนื้อหาจากไฟล์ ${file.name}:\n\`\`\`\n${textContent}\n\`\`\`` });
979
+ addMessage(`แนบไฟล์ข้อความ: ${file.name}`, 'user');
980
+ } catch (error) {
981
+ displayError(`ไม่สามารถอ่านไฟล์ข้อความได้: ${error.message}`);
982
+ return;
983
+ }
984
+ } else if (fileObj.type === 'pdf' || fileObj.type === 'docx' || fileObj.type === 'xlsx') {
985
+ // For PDF, DOCX, XLSX, we need to inform the model that we have these files
986
+ // Actual content extraction would require a backend or a dedicated library
987
+ userContent.push({ type: 'text', text: `(มีไฟล์ ${fileObj.type.toUpperCase()} แนบมา: ${file.name}. โปรดพิจารณาข้อมูลจากไฟล์นี้ในการตอบกลับ หากต้องการวิเคราะห์เนื้อหา ให้แจ้งฉันเพื่อประมวลผล)` });
988
+ addMessage(`แนบไฟล์ ${fileObj.type.toUpperCase()}: ${file.name}`, 'user');
989
+ }
990
  }
991
+
992
+ if (userContent.length === 0) {
993
+ displayError("กรุณาพิมพ์ข้อความหรือแนบไฟล์");
994
+ return;
 
 
 
 
995
  }
 
996
 
997
+ addMessage(userMessage, 'user');
998
+ userInput.value = '';
999
+ fileNameSpan.textContent = '';
1000
+ attachedFiles = []; // Clear attached files after sending
1001
+
1002
+ // Prepare message for API
1003
+ let messages = [];
1004
+ if (systemPrompt) {
1005
+ messages.push({ role: 'system', content: systemPrompt });
 
 
1006
  }
 
1007
 
1008
+ // Add previous conversation history
1009
+ messages = messages.concat(conversationHistory);
1010
+
1011
+ // Add current user message
1012
+ if (selectedProvider === 'anthropic' || selectedProvider === 'openai' || selectedProvider === 'xai' || selectedProvider === 'google') {
1013
+ messages.push({ role: 'user', content: userContent });
1014
+ } else if (selectedProvider === 'ollama') {
1015
+ // Ollama's chat endpoint expects content as string for text models
1016
+ // For vision models, it can handle image_data
1017
+ let contentForOllama = [];
1018
+ for (const part of userContent) {
1019
+ if (part.type === 'text') {
1020
+ contentForOllama.push(part.text);
1021
+ } else if (part.type === 'image_url') {
1022
+ const base64 = part.image_url.url.split(',')[1];
1023
+ contentForOllama.push({ type: 'image', image_data: base64 });
1024
  }
1025
+ }
1026
+ messages.push({ role: 'user', content: contentForOllama.join('\n') }); // Concatenate text parts
1027
+ } else if (selectedProvider === 'huggingface') {
1028
+ // HuggingFace has various model types and endpoints.
1029
+ // This is a simplified approach. For actual multi-modal, we'd need separate calls.
1030
+ // For general text models, it expects a single string input.
1031
+ // For vision models, it expects image bytes.
1032
+ // We will handle multi-modal specifically below.
1033
+ messages.push({ role: 'user', content: userContent.filter(p => p.type === 'text').map(p => p.text).join('\n') });
1034
  }
1035
+
1036
+ let requestBody = {};
1037
+ let url = providerInfo.endpoint;
1038
+ let headers = {
1039
+ 'Content-Type': 'application/json',
1040
+ };
1041
+
1042
+ if (selectedProvider !== 'ollama') { // Ollama doesn't need API key in header
1043
+ headers['Authorization'] = `Bearer ${apiKey}`;
1044
  }
1045
+
1046
+ // Logic for Multi-Modal HuggingFace calls
1047
+ if (selectedProvider === 'huggingface' && (attachedFiles.some(f => f.type === 'image') || systemPreset === 'multimodal')) {
1048
+ const visionModel = visionModelSelect.value;
1049
+ const ocrModel = ocrModelSelect.value;
1050
+ let imageFile = attachedFiles.find(f => f.type === 'image')?.file;
1051
+
1052
+ if (!imageFile && systemPreset === 'multimodal') {
1053
+ displayError("โปรดแนบรูปภาพสำหรับการวิเคราะห์ Multi-Modal.");
1054
+ return;
1055
+ }
1056
+
1057
+ let imageBase64 = null;
1058
+ if (imageFile) {
1059
+ try {
1060
+ imageBase64 = await readFileAsBase64(imageFile);
1061
+ } catch (error) {
1062
+ displayError(`ไม่สามารถอ่านไฟล์รูปภาพได้: ${error.message}`);
1063
+ return;
1064
+ }
1065
+ }
1066
 
1067
+ let visionAnalysis = '';
1068
+ let ocrText = '';
1069
+
1070
+ // Step 1: Call Vision Model (if applicable)
1071
+ if (visionModel && visionModel !== 'none' && imageBase64) {
1072
+ const visionEndpoint = PROVIDERS.huggingface.specificModelEndpoints[visionModel] || `${providerInfo.endpoint}${visionModel}`;
1073
+ try {
1074
+ const visionResponse = await fetch(visionEndpoint, {
1075
+ method: 'POST',
1076
+ headers: {
1077
+ 'Authorization': `Bearer ${apiKey}`,
1078
+ 'Content-Type': 'application/json'
1079
+ },
1080
+ body: JSON.stringify({
1081
+ inputs: `data:image/jpeg;base64,${imageBase64}`,
1082
+ parameters: { return_full_text: false } // Adjust parameters as needed by model
1083
+ })
1084
+ });
1085
+ if (!visionResponse.ok) {
1086
+ const errorData = await visionResponse.json();
1087
+ throw new Error(`Vision API error: ${visionResponse.statusText} - ${JSON.stringify(errorData)}`);
1088
+ }
1089
+ const visionData = await visionResponse.json();
1090
+ // Parse visionData based on model output format (this is generic)
1091
+ visionAnalysis = `\n[ข้อมูลจาก Vision Model (${visionModel})]:\n${JSON.stringify(visionData, null, 2)}\n`;
1092
+ } catch (error) {
1093
+ displayError(`ข้อผิดพลาดในการวิเคราะห์รูปภาพด้วย ${visionModel}: ${error.message}`);
1094
+ return;
1095
+ }
1096
+ }
1097
+
1098
+ // Step 2: Call OCR Model (if applicable)
1099
+ if (ocrModel && ocrModel !== 'none' && imageBase64) {
1100
+ const ocrEndpoint = PROVIDERS.huggingface.specificModelEndpoints[ocrModel] || `${providerInfo.endpoint}${ocrModel}`;
1101
+ try {
1102
+ const ocrResponse = await fetch(ocrEndpoint, {
1103
+ method: 'POST',
1104
+ headers: {
1105
+ 'Authorization': `Bearer ${apiKey}`,
1106
+ 'Content-Type': 'application/json'
1107
+ },
1108
+ body: JSON.stringify({
1109
+ inputs: `data:image/jpeg;base64,${imageBase64}`
1110
+ })
1111
+ });
1112
+ if (!ocrResponse.ok) {
1113
+ const errorData = await ocrResponse.json();
1114
+ throw new Error(`OCR API error: ${ocrResponse.statusText} - ${JSON.stringify(errorData)}`);
1115
+ }
1116
+ const ocrData = await ocrResponse.json();
1117
+ // Parse OCR data based on model output format (this is generic)
1118
+ ocrText = `\n[ข้อมูลจาก OCR Model (${ocrModel})]:\n${ocrData.extracted_text || JSON.stringify(ocrData, null, 2)}\n`;
1119
+ } catch (error) {
1120
+ displayError(`ข้อผิดพลาดในการทำ OCR ด้วย ${ocrModel}: ${error.message}`);
1121
+ return;
1122
+ }
1123
+ }
1124
 
1125
+ // Combine vision/OCR results with user's text message
1126
+ let combinedPrompt = userMessage;
1127
+ if (visionAnalysis) combinedPrompt += visionAnalysis;
1128
+ if (ocrText) combinedPrompt += ocrText;
1129
+
1130
+ // Now send the combined prompt to a text-based model if needed, or just display results
1131
+ // For HuggingFace, if a primary text model is chosen, we'd send to it.
1132
+ // Assuming the main model select should be a text model for combined analysis
1133
+ url = `${providerInfo.endpoint}${selectedModel}`; // Use the primary selected model for text processing
1134
+ requestBody = {
1135
+ inputs: combinedPrompt,
1136
+ parameters: {
1137
+ max_new_tokens: 1000,
1138
+ temperature: 0.7,
1139
+ top_p: 0.9,
1140
+ },
1141
+ options: {
1142
+ use_cache: false // Prevent caching for dynamic inputs
1143
+ }
1144
+ };
1145
+ headers['Content-Type'] = 'application/json'; // Ensure correct content type for JSON body
1146
+
1147
+ } else if (selectedProvider === 'huggingface') {
1148
+ // Normal text-only HuggingFace request
1149
+ url = `${providerInfo.endpoint}${selectedModel}`;
1150
+ requestBody = {
1151
+ inputs: messages[messages.length - 1].content.filter(p => p.type === 'text').map(p => p.text).join('\n'),
1152
+ parameters: {
1153
+ max_new_tokens: 1000,
1154
+ temperature: 0.7,
1155
+ top_p: 0.9,
1156
+ },
1157
+ options: {
1158
+ use_cache: false
1159
+ }
1160
+ };
1161
+ headers['Content-Type'] = 'application/json';
1162
+
1163
+ } else if (selectedProvider === 'google') {
1164
+ url += `${selectedModel}:generateContent?key=${apiKey}`;
1165
+ let contents = [];
1166
+ let currentContent = {
1167
+ role: "user",
1168
+ parts: []
1169
+ };
1170
 
1171
+ for (const part of userContent) {
1172
+ if (part.type === 'text') {
1173
+ currentContent.parts.push({ text: part.text });
1174
+ } else if (part.type === 'image_url') {
1175
+ currentContent.parts.push({
1176
+ inline_data: {
1177
+ mime_type: part.image_url.url.split(';')[0].split(':')[1],
1178
+ data: part.image_url.url.split(',')[1]
1179
+ }
1180
+ });
1181
+ }
1182
+ }
1183
+ contents.push(currentContent);
1184
+
1185
+ // Convert history to Gemini format
1186
+ for (let i = 0; i < conversationHistory.length; i++) {
1187
+ const item = conversationHistory[i];
1188
+ if (item.role === 'system') {
1189
+ // Gemini doesn't have a direct 'system' role in chat, often absorbed into initial user prompt or ignored.
1190
+ // For simplicity, we can prepend to the first user message or ignore.
1191
+ // For now, we'll try to add it as a user message that guides the AI.
1192
+ if (i === 0 && item.content) { // Only apply if it's the very first system prompt
1193
+ contents.unshift({
1194
+ role: "user",
1195
+ parts: [{ text: item.content }]
1196
+ });
1197
+ contents.unshift({
1198
+ role: "model",
1199
+ parts: [{ text: "เข้าใจแล้ว." }]
1200
+ });
1201
+ }
1202
+ } else if (item.role === 'user') {
1203
+ contents.push({ role: "user", parts: [{ text: item.content.text || item.content }] });
1204
+ } else if (item.role === 'assistant') {
1205
+ contents.push({ role: "model", parts: [{ text: item.content }] });
1206
+ }
1207
+ }
1208
+
1209
+ requestBody = {
1210
+ contents: contents,
1211
+ generationConfig: {
1212
+ maxOutputTokens: 2000,
1213
+ temperature: 0.7,
1214
+ topP: 0.9
1215
+ }
1216
+ };
1217
+ headers['Content-Type'] = 'application/json';
1218
+
1219
+
1220
+ } else if (selectedProvider === 'ollama') {
1221
+ requestBody = {
1222
+ model: selectedModel,
1223
+ messages: messages,
1224
+ stream: providerInfo.isStream
1225
+ };
1226
+ } else if (selectedProvider === 'anthropic') {
1227
+ requestBody = {
1228
+ model: selectedModel,
1229
+ messages: messages.map(msg => ({ // Anthropic expects 'user' and 'assistant' roles, 'system' handled by system_prompt
1230
+ role: msg.role === 'system' ? 'user' : msg.role, // Temporarily map system to user for Anthropic's initial context
1231
+ content: msg.content
1232
+ })),
1233
+ max_tokens: 4096,
1234
+ stream: providerInfo.isStream
1235
+ };
1236
+ if (systemPrompt) {
1237
+ requestBody.system = systemPrompt; // Anthropic's dedicated system_prompt field
1238
+ }
1239
+ } else { // OpenAI, Groq, xAI
1240
+ requestBody = {
1241
+ model: selectedModel,
1242
+ messages: messages.map(msg => {
1243
+ if (Array.isArray(msg.content) && selectedProvider === 'openai') {
1244
+ // OpenAI can take array of content parts
1245
+ return { role: msg.role, content: msg.content };
1246
+ } else if (msg.role === 'system') {
1247
+ return { role: 'system', content: msg.content };
1248
+ } else {
1249
+ // For other providers or if content is string, just use text
1250
+ return { role: msg.role, content: typeof msg.content === 'string' ? msg.content : (msg.content.text || msg.content.map(p => p.text).join('\n')) };
1251
+ }
1252
+ }),
1253
+ stream: providerInfo.isStream
1254
+ };
1255
+ }
1256
+
1257
+ try {
1258
+ const response = await fetch(url, {
1259
+ method: 'POST',
1260
+ headers: headers,
1261
+ body: JSON.stringify(requestBody)
1262
+ });
1263
+
1264
+ if (!response.ok) {
1265
+ const errorData = await response.json();
1266
+ throw new Error(`API Error: ${response.status} - ${JSON.stringify(errorData)}`);
1267
+ }
1268
+
1269
+ if (providerInfo.isStream && selectedProvider !== 'google') { // Handle streaming for OpenAI, Groq, xAI, Ollama
1270
  const reader = response.body.getReader();
1271
+ const decoder = new TextDecoder('utf-8');
 
 
1272
  let fullResponse = '';
1273
+ let assistantMessageDiv = null;
1274
+
1275
  while (true) {
1276
+ const { done, value } = await reader.read();
1277
  if (done) break;
1278
+
1279
+ const chunk = decoder.decode(value);
1280
+ if (selectedProvider === 'ollama') {
1281
+ // Ollama can send multiple JSON objects in one chunk
1282
+ chunk.split('\n').filter(Boolean).forEach(line => {
 
 
 
1283
  try {
1284
+ const data = JSON.parse(line);
1285
+ if (data.done === false && data.message && data.message.content) {
1286
+ fullResponse += data.message.content;
1287
+ if (!assistantMessageDiv) {
1288
+ assistantMessageDiv = document.createElement('div');
1289
+ assistantMessageDiv.classList.add('message95', 'ai');
1290
+ messagesDiv.appendChild(assistantMessageDiv);
1291
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1292
+ }
1293
+ assistantMessageDiv.innerHTML = formatText(fullResponse); // Update HTML content
1294
+ } else if (data.done === true) {
1295
+ // Done, finalize the message
1296
  }
1297
  } catch (e) {
1298
+ console.error("Error parsing Ollama stream chunk:", e, line);
1299
  }
1300
+ });
1301
+ } else if (selectedProvider === 'huggingface') {
1302
+ // HuggingFace typically does not stream, so this block might not be hit for HF models
1303
+ // If a streaming HF endpoint is used, this logic would need to be adapted
1304
+ fullResponse += chunk;
1305
+ if (!assistantMessageDiv) {
1306
+ assistantMessageDiv = document.createElement('div');
1307
+ assistantMessageDiv.classList.add('message95', 'ai');
1308
+ messagesDiv.appendChild(assistantMessageDiv);
1309
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1310
+ }
1311
+ assistantMessageDiv.innerHTML = formatText(fullResponse); // Update HTML content
1312
+ } else { // OpenAI, Groq, xAI
1313
+ chunk.split('\n').filter(Boolean).forEach(line => {
1314
  if (line.startsWith('data: ')) {
1315
+ const data = JSON.parse(line.substring(6));
1316
+ if (data.choices && data.choices.length > 0) {
1317
+ const delta = data.choices[0].delta;
1318
+ if (delta && delta.content) {
1319
+ fullResponse += delta.content;
1320
+ if (!assistantMessageDiv) {
1321
+ assistantMessageDiv = document.createElement('div');
1322
+ assistantMessageDiv.classList.add('message95', 'ai');
1323
+ messagesDiv.appendChild(assistantMessageDiv);
1324
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1325
+ }
1326
+ assistantMessageDiv.innerHTML = formatText(fullResponse); // Update HTML content
1327
  }
 
 
1328
  }
1329
  }
1330
+ });
1331
+ }
1332
+ }
1333
+ if (assistantMessageDiv) {
1334
+ hljs.highlightAll(); // Re-highlight all code blocks after stream finishes
1335
+ conversationHistory.push({ role: 'assistant', content: fullResponse });
1336
+ const codeMatch = fullResponse.match(/```(\w+)?\n([\s\S]+?)\n```/);
1337
+ if (codeMatch) {
1338
+ lastDetectedCode = codeMatch[2];
1339
+ savedCodeDisplay.textContent = lastDetectedCode;
1340
+ contextSaveArea.style.display = 'block';
1341
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1342
  }
1343
+ }
1344
+ } else if (selectedProvider === 'google') {
1345
+ const result = await response.json();
1346
+ let fullResponse = '';
1347
+ if (result.candidates && result.candidates[0] && result.candidates[0].content && result.candidates[0].content.parts) {
1348
+ fullResponse = result.candidates[0].content.parts.map(part => part.text).join('');
1349
+ }
1350
+ addMessage(fullResponse, 'ai');
1351
+ conversationHistory.push({ role: 'assistant', content: fullResponse });
1352
+
1353
+ const codeMatch = fullResponse.match(/```(\w+)?\n([\s\S]+?)\n```/);
1354
+ if (codeMatch) {
1355
+ lastDetectedCode = codeMatch[2];
1356
+ savedCodeDisplay.textContent = lastDetectedCode;
1357
+ contextSaveArea.style.display = 'block';
1358
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1359
  }
1360
+ hljs.highlightAll();
1361
+
1362
+ } else { // Handle non-streaming responses (e.g., HuggingFace Inference API, Anthropic)
1363
+ const data = await response.json();
1364
+ let assistantResponseText = '';
1365
+ if (selectedProvider === 'anthropic') {
1366
+ assistantResponseText = data.content[0].text;
1367
+ } else if (selectedProvider === 'huggingface') {
1368
+ // HuggingFace response varies greatly by model
1369
+ // This is a generic handling for text generation models
1370
+ if (Array.isArray(data) && data[0] && data[0].generated_text) {
1371
+ assistantResponseText = data[0].generated_text;
1372
+ } else {
1373
+ assistantResponseText = JSON.stringify(data, null, 2); // Fallback for unexpected formats
1374
+ }
1375
+ } else { // Fallback for other non-streaming or new providers
1376
+ assistantResponseText = data.choices[0].message.content;
1377
  }
1378
 
1379
+ addMessage(assistantResponseText, 'ai');
1380
+ conversationHistory.push({ role: 'assistant', content: assistantResponseText });
1381
+
1382
+ const codeMatch = assistantResponseText.match(/```(\w+)?\n([\s\S]+?)\n```/);
1383
+ if (codeMatch) {
1384
+ lastDetectedCode = codeMatch[2];
1385
+ savedCodeDisplay.textContent = lastDetectedCode;
1386
+ contextSaveArea.style.display = 'block';
1387
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
1388
+ }
1389
+ hljs.highlightAll();
1390
  }
1391
+
 
 
1392
  } catch (error) {
1393
+ displayError(error.message);
1394
+ console.error("Fetch error:", error);
 
1395
  } finally {
1396
+ chatForm.querySelector('button[type="submit"]').disabled = false;
1397
+ userInput.focus();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1398
  }
1399
  });
1400
  </script>