Spaces:
Running
Running
Update index.html
Browse files- 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
|
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 -
|
376 |
</div>
|
377 |
<div class="win95controls">
|
378 |
<button id="maximizeBtn" title="เต็มจอ">□</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
|
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-
|
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: "
|
527 |
-
{ value: "
|
528 |
-
{ value: "
|
529 |
-
{ value: "
|
530 |
-
{ value: "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
531 |
],
|
532 |
-
isStream:
|
|
|
533 |
}
|
534 |
};
|
535 |
|
536 |
-
|
537 |
-
const
|
538 |
-
|
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
|
|
|
|
|
|
|
|
|
552 |
const systemPresetSelect = document.getElementById('systemPresetSelect95');
|
553 |
-
const
|
554 |
-
const
|
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
|
561 |
-
const confirmSaveBtn
|
562 |
-
const
|
563 |
-
const
|
564 |
-
const
|
565 |
-
const
|
566 |
-
const
|
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,"<").replace(/>/g,">");
|
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 |
-
|
590 |
-
|
591 |
-
|
592 |
-
|
593 |
-
|
594 |
-
setTimeout(()=>code.parentElement.querySelector('button').textContent='📋 Copy',1000);
|
595 |
-
});
|
596 |
-
}
|
597 |
-
};
|
598 |
|
599 |
-
|
600 |
-
|
601 |
-
|
602 |
-
|
603 |
-
|
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
|
612 |
-
|
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
|
625 |
-
|
626 |
-
|
627 |
-
|
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 |
-
|
634 |
-
|
635 |
-
document.querySelector('label[for="apiKey95"]').style.display = 'inline-block';
|
636 |
}
|
|
|
637 |
}
|
638 |
|
639 |
-
|
640 |
-
|
641 |
-
|
642 |
-
|
643 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
644 |
}
|
|
|
|
|
|
|
|
|
|
|
645 |
|
646 |
-
|
647 |
-
|
648 |
-
|
649 |
-
|
650 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
651 |
} else {
|
652 |
-
|
653 |
}
|
654 |
}
|
655 |
|
656 |
-
|
657 |
-
|
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 |
-
|
667 |
-
|
668 |
-
|
|
|
|
|
|
|
|
|
|
|
669 |
|
670 |
-
|
671 |
-
|
672 |
-
|
673 |
-
|
674 |
-
|
675 |
-
|
676 |
-
|
677 |
-
|
|
|
|
|
678 |
}
|
679 |
-
}
|
680 |
|
681 |
-
|
682 |
-
const
|
683 |
-
|
684 |
-
|
|
|
685 |
|
686 |
-
|
687 |
-
|
|
|
|
|
|
|
688 |
|
689 |
-
|
690 |
-
|
691 |
-
|
692 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
693 |
}
|
694 |
-
return SYSTEM_PRESETS[preset] || SYSTEM_PRESETS.general;
|
695 |
-
}
|
696 |
|
697 |
-
|
|
|
|
|
|
|
698 |
|
699 |
-
|
700 |
-
|
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
|
710 |
-
|
711 |
-
|
712 |
-
|
713 |
-
|
714 |
-
|
715 |
-
while ((match = codeBlockRegex.exec(text)) !== null) lastCode = match[1].trim();
|
716 |
-
return lastCode;
|
717 |
}
|
718 |
|
719 |
-
function
|
720 |
-
|
721 |
-
|
722 |
-
|
723 |
-
|
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
|
733 |
-
|
734 |
-
|
735 |
-
|
736 |
-
contextSaveArea.style.display = 'block';
|
737 |
-
} else {
|
738 |
-
contextSaveArea.style.display = 'none';
|
739 |
-
warningMessage.textContent = '';
|
740 |
-
}
|
741 |
}
|
742 |
|
743 |
-
|
744 |
-
|
745 |
-
|
746 |
-
|
747 |
-
|
748 |
-
|
|
|
|
|
749 |
}
|
750 |
|
751 |
-
// ---
|
752 |
-
|
753 |
-
|
754 |
-
|
755 |
-
|
756 |
-
|
757 |
-
}
|
758 |
-
|
759 |
-
|
760 |
-
|
761 |
-
|
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 |
-
|
774 |
}
|
775 |
});
|
776 |
|
777 |
-
|
778 |
-
|
|
|
779 |
});
|
780 |
|
781 |
-
|
782 |
-
|
783 |
-
|
784 |
-
|
785 |
-
|
786 |
-
|
787 |
-
|
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 |
-
|
800 |
-
|
801 |
-
|
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 |
-
|
819 |
-
const error = await response.text();
|
820 |
-
throw new Error(`Anthropic API Error: ${response.status} - ${error}`);
|
821 |
-
}
|
822 |
|
823 |
-
|
824 |
-
|
|
|
|
|
825 |
}
|
826 |
|
827 |
-
|
828 |
-
|
829 |
-
|
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 |
-
|
845 |
-
|
846 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
847 |
}
|
848 |
-
|
849 |
-
return response;
|
850 |
}
|
851 |
|
852 |
-
|
853 |
-
|
854 |
-
|
855 |
-
|
856 |
-
|
857 |
-
|
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 |
-
|
882 |
-
|
883 |
-
|
884 |
-
|
885 |
-
|
886 |
-
|
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 |
-
|
903 |
-
|
904 |
-
|
|
|
|
|
|
|
|
|
905 |
}
|
906 |
|
907 |
-
|
908 |
-
|
909 |
-
|
910 |
-
|
911 |
-
|
|
|
912 |
|
913 |
-
|
914 |
-
|
915 |
-
|
916 |
-
|
917 |
-
|
918 |
-
|
919 |
-
|
920 |
-
return;
|
921 |
}
|
922 |
-
|
923 |
-
|
924 |
-
|
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 |
-
|
936 |
}
|
937 |
-
})
|
938 |
-
|
939 |
-
|
940 |
-
|
941 |
-
} else {
|
942 |
-
fileNameSpan.textContent = '';
|
943 |
}
|
944 |
-
}
|
945 |
|
946 |
-
//
|
947 |
-
|
948 |
-
e.preventDefault();
|
949 |
-
dropzone.style.display = 'block';
|
950 |
-
});
|
951 |
|
952 |
-
|
953 |
-
|
954 |
-
|
955 |
}
|
956 |
-
});
|
957 |
|
958 |
-
|
959 |
-
|
960 |
-
|
961 |
-
|
962 |
-
const files = Array.from(e.dataTransfer.files);
|
963 |
-
const mockEvent = { target: { files: files } };
|
964 |
-
handleFileSelect(mockEvent);
|
965 |
-
});
|
966 |
|
967 |
-
|
968 |
-
|
969 |
-
|
970 |
-
|
971 |
-
|
972 |
-
|
973 |
-
|
974 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
975 |
}
|
976 |
-
|
977 |
-
|
978 |
-
|
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 |
-
|
988 |
-
|
989 |
-
|
990 |
-
|
991 |
-
|
992 |
-
|
993 |
-
|
994 |
-
|
995 |
-
|
996 |
-
showError("กรุณาใส่ API Key ก่อนส่งข้อความ");
|
997 |
-
return;
|
998 |
}
|
999 |
-
if (isAITyping) return;
|
1000 |
|
1001 |
-
|
1002 |
-
|
1003 |
-
|
1004 |
-
|
1005 |
-
|
1006 |
-
|
1007 |
-
|
1008 |
-
|
1009 |
-
|
1010 |
-
|
1011 |
-
|
1012 |
-
|
1013 |
-
|
|
|
|
|
|
|
1014 |
}
|
1015 |
-
}
|
1016 |
-
|
1017 |
-
|
1018 |
-
|
1019 |
-
|
1020 |
-
|
|
|
|
|
|
|
1021 |
}
|
1022 |
-
|
1023 |
-
|
1024 |
-
|
1025 |
-
|
1026 |
-
|
1027 |
-
|
|
|
|
|
|
|
1028 |
}
|
1029 |
-
|
1030 |
-
|
1031 |
-
|
1032 |
-
|
1033 |
-
|
1034 |
-
|
1035 |
-
|
1036 |
-
|
1037 |
-
|
1038 |
-
|
1039 |
-
|
1040 |
-
|
1041 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1042 |
|
1043 |
-
|
1044 |
-
|
1045 |
-
|
1046 |
-
|
1047 |
-
|
1048 |
-
|
1049 |
-
|
1050 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1051 |
|
1052 |
-
|
1053 |
-
|
1054 |
-
|
1055 |
-
|
1056 |
-
|
1057 |
-
|
1058 |
-
|
1059 |
-
|
1060 |
-
}
|
1061 |
-
|
1062 |
-
|
1063 |
-
|
1064 |
-
|
1065 |
-
|
1066 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1067 |
|
1068 |
-
|
1069 |
-
|
1070 |
-
|
1071 |
-
|
1072 |
-
|
1073 |
-
|
1074 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1075 |
const reader = response.body.getReader();
|
1076 |
-
const decoder = new TextDecoder();
|
1077 |
-
|
1078 |
-
let buffer = '';
|
1079 |
let fullResponse = '';
|
1080 |
-
|
|
|
1081 |
while (true) {
|
1082 |
-
const {
|
1083 |
if (done) break;
|
1084 |
-
|
1085 |
-
|
1086 |
-
|
1087 |
-
|
1088 |
-
|
1089 |
-
for (const line of lines) {
|
1090 |
-
if (providerKey === 'ollama') {
|
1091 |
-
// Ollama format
|
1092 |
try {
|
1093 |
-
const
|
1094 |
-
|
1095 |
-
|
1096 |
-
|
1097 |
-
|
1098 |
-
|
1099 |
-
|
|
|
|
|
|
|
|
|
|
|
1100 |
}
|
1101 |
} catch (e) {
|
1102 |
-
|
1103 |
}
|
1104 |
-
}
|
1105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1106 |
if (line.startsWith('data: ')) {
|
1107 |
-
const data = line.
|
1108 |
-
if (data
|
1109 |
-
|
1110 |
-
|
1111 |
-
|
1112 |
-
|
1113 |
-
|
1114 |
-
|
1115 |
-
|
1116 |
-
|
1117 |
-
|
|
|
1118 |
}
|
1119 |
-
} catch (e) {
|
1120 |
-
// Skip malformed JSON
|
1121 |
}
|
1122 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1123 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1124 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1125 |
}
|
1126 |
|
1127 |
-
|
1128 |
-
|
1129 |
-
|
1130 |
-
const
|
1131 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
1132 |
}
|
1133 |
-
|
1134 |
-
chatHistory.push({ role: 'assistant', content: aiResponse });
|
1135 |
-
|
1136 |
} catch (error) {
|
1137 |
-
|
1138 |
-
|
1139 |
-
chatHistory.pop(); // Remove failed user message
|
1140 |
} finally {
|
1141 |
-
|
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="เต็มจอ">□</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>
|