Spaces:
Running
Running
<html lang="th"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width,initial-scale=1.0"> | |
<title>AI Chat - Win95 Fullscreen + Stream + File Upload</title> | |
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=IBM+Plex+Mono:400,700&display=swap"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs.min.css"> | |
<style> | |
html, body { height: 100%; margin: 0; } | |
body { | |
font-family: 'IBM Plex Mono', 'Consolas', 'Courier New', monospace; | |
background: #008080; | |
height: 100vh; | |
margin: 0; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
min-height: 100vh; | |
} | |
.win95window { | |
background: #c0c0c0; | |
border: 2px solid #fff; | |
border-bottom: 2px solid #808080; | |
border-right: 2px solid #808080; | |
box-shadow: 4px 8px 0px #0008, 0 0 0 8px #6664; | |
min-width: 320px; | |
max-width: 500px; | |
width: 100%; | |
min-height: 600px; | |
display: flex; | |
flex-direction: column; | |
position: relative; | |
transition: all 0.29s cubic-bezier(.4,.85,.59,1.02); | |
z-index: 1; | |
} | |
.win95window.fullscreen { | |
position: fixed ; | |
left: 0; top: 0; right: 0; bottom: 0; | |
min-width: 100vw; min-height: 100vh; max-width: none; max-height: none; | |
width: 100vw; height: 100vh; | |
border-radius: 0; | |
box-shadow: none; | |
z-index: 9999; | |
} | |
.win95titlebar { | |
background: linear-gradient(90deg, #000080 80%, #1080c0 100%); | |
color: #fff; | |
padding: 7px 10px 7px 10px; | |
font-weight: bold; | |
font-size: 1.05em; | |
letter-spacing: .5px; | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
border-bottom: 2px solid #808080; | |
user-select: none; | |
} | |
.win95titlebar .title { | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
} | |
.win95titlebar .win95controls { | |
display: flex; | |
gap: 2px; | |
} | |
.win95titlebar button { | |
width: 18px; height: 18px; font-size: 1em; | |
border: 2px outset #fff; | |
background: #c0c0c0; | |
color: #000; | |
line-height: 1; | |
cursor: pointer; | |
outline: none; | |
margin-left: 2px; | |
padding: 0; | |
border-radius: 0; | |
} | |
.win95titlebar button:active { | |
border: 2px inset #808080; | |
background: #aaa; | |
} | |
.win95titlebar button:focus { outline: 1px dotted #fff; } | |
.win95content { | |
flex: 1 1 0%; | |
overflow: hidden; | |
display: flex; | |
flex-direction: column; | |
padding: 0; | |
min-height: 0; | |
} | |
.settings95 { | |
background: #e0e0e0; | |
border-bottom: 2px solid #808080; | |
padding: 10px 12px; | |
display: flex; | |
flex-wrap: wrap; | |
align-items: center; | |
font-size: 0.98em; | |
gap: 8px; | |
} | |
.settings95 label { | |
margin-right: 2px; | |
} | |
.settings95 select, | |
.settings95 input[type="text"] { | |
font-family: inherit; | |
font-size: 1em; | |
background: #fff; | |
border: 2px inset #808080; | |
padding: 2px 6px; | |
outline: none; | |
min-width: 96px; | |
max-width: 170px; | |
} | |
.settings95 input[type="text"]#apiKey95 { | |
min-width: 180px; | |
width: 150px; | |
max-width: 99vw; | |
} | |
.settings95 button { | |
font-family: inherit; | |
font-size: 1em; | |
background: #e0e0e0; | |
border: 2px outset #fff; | |
color: #000; | |
padding: 2.5px 14px; | |
margin-left: 2px; | |
cursor: pointer; | |
border-radius: 0; | |
} | |
.settings95 button:active { | |
border: 2px inset #808080; | |
background: #c0c0c0; | |
} | |
.settings95 .api-status { | |
font-size: 0.95em; | |
color: #008000; | |
margin-left: 4px; | |
min-width: 80px; | |
} | |
.chat-container95 { | |
flex: 1 1 0%; | |
overflow-y: auto; | |
background: #fff; | |
padding: 16px 8px; | |
border-top: 2px solid #fff; | |
border-bottom: 2px solid #808080; | |
display: flex; | |
flex-direction: column; | |
gap: 8px; | |
font-size: 1.01em; | |
min-height: 0; | |
} | |
.message95 { | |
max-width: 95%; word-break: break-word; | |
border: 2px solid #fff; | |
border-bottom: 2px solid #808080; | |
border-right: 2px solid #808080; | |
background: #e0e0e0; | |
margin-bottom: 0; | |
padding: 7px 10px; | |
border-radius: 0; | |
box-shadow: 2px 2px 0 #b0b0b0; | |
min-width: 70px; | |
position: relative; | |
} | |
.message95.ai { align-self: flex-start; background: #fffffe; } | |
.message95.user { align-self: flex-end; background: #c0e0ff; } | |
.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; } | |
.code-tools { position: absolute; right: 7px; top: 5px; z-index: 2; display: flex; gap: 3px; } | |
.code-tools button { font-size: 0.95em; padding: 1.5px 7px; background: #e0e0e0; border: 2px outset #fff; border-radius: 0; cursor: pointer; color: #222; } | |
.code-tools button:active { border: 2px inset #808080; background: #c0c0c0; } | |
.code-tools button:disabled { color: #aaa; border-color: #b0b0b0; } | |
.input-area95 { | |
background: #e0e0e0; | |
border-top: 2px solid #fff; | |
padding: 10px 10px 10px 10px; | |
display: flex; | |
gap: 10px; | |
align-items: stretch; | |
flex-wrap: wrap; | |
position: relative; | |
} | |
.input-area95 input[type="text"] { | |
font-family: inherit; | |
font-size: 1em; | |
border: 2px inset #808080; | |
background: #fff; | |
flex: 1 1 0%; | |
padding: 4px 8px; | |
min-width: 0; | |
} | |
.input-area95 button { | |
font-family: inherit; | |
font-size: 1em; | |
background: #e0e0e0; | |
border: 2px outset #fff; | |
color: #000; | |
padding: 3px 18px; | |
cursor: pointer; | |
border-radius: 0; | |
} | |
.input-area95 button:active { | |
border: 2px inset #808080; | |
background: #c0c0c0; | |
} | |
.input-area95 button:disabled { | |
background: #e0e0e0; | |
color: #aaa; | |
border: 2px outset #b0b0b0; | |
cursor: not-allowed; | |
} | |
.input-area95 .file-attach-btn { | |
position: relative; | |
overflow: hidden; | |
padding: 3px 8px; | |
font-size: 1em; | |
min-width: 1.5em; | |
border: 2px outset #fff; | |
background: #e0e0e0; | |
color: #333; | |
border-radius: 0; | |
cursor: pointer; | |
margin-right: 0; | |
margin-left: 0; | |
} | |
.input-area95 .file-attach-btn input[type="file"] { | |
position: absolute; | |
left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; | |
} | |
.input-area95 .file-attach-btn:active { border: 2px inset #808080; background: #c0c0c0; } | |
.input-area95 .file-name { | |
font-size: 0.98em; color: #333; margin-left: 5px; align-self: center; max-width: 110px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; | |
} | |
.dropzone95 { | |
border: 2px dashed #008080; | |
background: #e6ffff; | |
color: #008080; | |
text-align: center; | |
font-size: 1.05em; | |
padding: 24px 5px 18px; | |
border-radius: 8px; | |
margin: 12px 5px; | |
transition: background 0.2s; | |
display: none; | |
z-index: 99; | |
position: absolute; | |
left: 0; right: 0; top: 0; bottom: 0; | |
pointer-events: all; | |
} | |
.error-message95 { | |
color: #b00; | |
background: #fff3f3; | |
border: 1px solid #d99; | |
padding: 7px 10px 7px 30px; | |
margin: 0; | |
font-size: 0.98em; | |
min-height: 20px; | |
background-image: url('data:image/svg+xml;utf8,<svg width="16" height="16" fill="red" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="7" fill="white" stroke="red" stroke-width="1"/><text x="8" y="12" text-anchor="middle" font-size="12" fill="red" font-family="Arial" dy="-2">!</text></svg>'); | |
background-repeat: no-repeat; | |
background-position: 6px 7px; | |
background-size: 16px 16px; | |
} | |
#contextSaveArea95 { | |
padding: 10px 10px 12px; | |
background: #f8f8e0; | |
border-top: 2px solid #fff; | |
border-bottom: 2px solid #808080; | |
display: none; | |
font-size: 0.98em; | |
} | |
#contextSaveArea95 h4 { margin: 0 0 8px 0; color: #444; font-size: 1.08em; } | |
#contextSaveArea95 pre { | |
background: #fff; | |
border: 2px inset #808080; | |
padding: 8px 5px; | |
font-size: 0.95em; | |
max-height: 120px; | |
overflow: auto; | |
margin: 7px 0; | |
white-space: pre-wrap; | |
word-break: break-all; | |
} | |
#contextSaveArea95 label { font-weight: bold; } | |
#contextSaveArea95 input[type="text"] { | |
width: 98%; | |
min-width: 0; | |
border: 2px inset #808080; | |
background: #fff; | |
font-size: 1em; | |
margin-bottom: 3px; | |
padding: 4px 8px; | |
} | |
#contextSaveArea95 button { | |
margin-top: 6px; | |
font-size: 1em; | |
background: #e0e0e0; | |
border: 2px outset #fff; | |
color: #000; | |
padding: 2.5px 14px; | |
cursor: pointer; | |
border-radius: 0; | |
} | |
#contextSaveArea95 button:active { background: #c0c0c0; border: 2px inset #808080; } | |
#contextSaveArea95 button:disabled { background: #e0e0e0; color: #bbb; border: 2px outset #b0b0b0; } | |
@media (max-width: 650px) { | |
.win95window { | |
min-width: 0; | |
max-width: 98vw; | |
box-shadow: 1px 2px 0px #0006, 0 0 0 2px #6664; | |
} | |
.win95content { padding: 0; } | |
.settings95 { | |
flex-direction: column; | |
align-items: flex-start; | |
gap: 4px; | |
padding: 8px 4px; | |
} | |
.input-area95 { | |
flex-direction: column; | |
gap: 8px; | |
padding: 8px 4px; | |
} | |
.chat-container95 { | |
padding: 10px 2px; | |
font-size: 0.98em; | |
} | |
.dropzone95 { | |
font-size: 1em; | |
padding: 20px 2px 14px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="win95window" id="win95window"> | |
<div class="win95titlebar" id="titlebar"> | |
<div class="title"> | |
<span style="display:inline-block;width:18px;height:18px;background:#fff;border:1px solid #808080;margin-right:7px;box-shadow:inset 2px 2px #c0c0c0;"> | |
<span style="display:inline-block;width:9px;height:9px;background:#008080;margin:4px 0 0 4px;vertical-align:middle;"></span> | |
</span> | |
AI Chat - Windows 95 Enhanced | |
</div> | |
<div class="win95controls"> | |
<button id="maximizeBtn" title="เต็มจอ">□</button> | |
<button onclick="window.location.reload()" title="รีเฟรช">■</button> | |
<button onclick="window.close()" title="ปิด">✖</button> | |
</div> | |
</div> | |
<div class="win95content"> | |
<form class="settings95" id="settingsForm95" autocomplete="off" onsubmit="return false;"> | |
<label for="providerSelect95">API:</label> | |
<select id="providerSelect95"> | |
<option value="anthropic">Anthropic (Claude)</option> | |
<option value="xai">xAI (Grok-3)</option> | |
<option value="groq">Groq</option> | |
<option value="openai">OpenAI</option> | |
</select> | |
<label for="modelSelect95">โมเดล:</label> | |
<select id="modelSelect95"></select> | |
<label for="apiKey95">API Key:</label> | |
<input type="text" id="apiKey95" placeholder="กรอก API Key ของคุณ"> | |
<button id="confirmApiKeyBtn95" type="button">บันทึก</button> | |
<span class="api-status" id="apiKeyStatus95"></span> | |
</form> | |
<div id="contextSaveArea95"> | |
<h4>Context ปัจจุบัน</h4> | |
<div id="warningMessage95" style="color:orange;font-weight:bold;"></div> | |
<div>โค้ดล่าสุดที่ตรวจพบ:</div> | |
<pre id="savedCodeDisplay95">ยังไม่พบโค้ด หรือโค้ดยังไม่แสดง</pre> | |
<label for="goalInput95">สรุปเป้าหมาย/คำอธิบาย:</label> | |
<input type="text" id="goalInput95" placeholder="เช่น เพิ่มระบบล็อกอิน"> | |
<button id="confirmSaveBtn95">ยืนยัน context และเริ่มแชทใหม่</button> | |
</div> | |
<div class="chat-container95" id="messagesDiv95"> | |
<div class="message95 ai">สวัสดี! AI Chat สไตล์ Win95 Enhanced พร้อมใช้ 🎉<br>รองรับ Anthropic Claude-4-Sonnet และ xAI Grok-3 ล่าสุด!</div> | |
</div> | |
<form id="chatForm95" class="input-area95" autocomplete="off"> | |
<button type="button" class="file-attach-btn" id="fileAttachBtn" title="แนบไฟล์"> | |
📎<input type="file" id="fileInput" multiple> | |
</button> | |
<span class="file-name" id="fileName"></span> | |
<input type="text" id="userInput95" placeholder="พิมพ์คำถามหรือข้อความ..." autocomplete="off"> | |
<button type="submit">ส่ง</button> | |
<div class="dropzone95" id="dropzone95">วางไฟล์ที่นี่เพื่อแนบ</div> | |
</form> | |
<div id="errorMessage95" class="error-message95"></div> | |
</div> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> | |
<script> | |
// --- ฟีเจอร์ Fullscreen --- | |
const win95window = document.getElementById('win95window'); | |
const maximizeBtn = document.getElementById('maximizeBtn'); | |
let isFullscreen = false; | |
maximizeBtn.onclick = function() { | |
isFullscreen = !isFullscreen; | |
win95window.classList.toggle('fullscreen', isFullscreen); | |
maximizeBtn.innerHTML = isFullscreen ? "■" : "□"; | |
maximizeBtn.title = isFullscreen ? "คืนหน้าต่าง" : "เต็มจอ"; | |
}; | |
// --- Provider/API Setup --- | |
const PROVIDERS = { | |
anthropic: { | |
name: "Anthropic (Claude)", | |
endpoint: "https://api.anthropic.com/v1/messages", | |
apiKeyLabel: "Anthropic API Key", | |
models: [ | |
{ value: "claude-3-5-sonnet-20241022", label: "Claude-3.5 Sonnet" }, | |
{ value: "claude-3-5-haiku-20241022", label: "Claude-3.5 Haiku" }, | |
{ value: "claude-3-opus-20240229", label: "Claude-3 Opus" } | |
], | |
isStream: false, | |
streamEndpoint: "https://api.anthropic.com/v1/messages" | |
}, | |
xai: { | |
name: "xAI (Grok)", | |
endpoint: "https://api.x.ai/v1/chat/completions", | |
apiKeyLabel: "xAI API Key", | |
models: [ | |
{ value: "grok-3-beta", label: "Grok-3 Beta" }, | |
{ value: "grok-3-fast-beta", label: "Grok-3 Fast Beta" }, | |
{ value: "grok-3-mini-beta", label: "Grok-3 Mini Beta (Reasoning)" }, | |
{ value: "grok-3-mini-fast-beta", label: "Grok-3 Mini Fast Beta (Reasoning)" } | |
], | |
isStream: true | |
}, | |
groq: { | |
name: "Groq", | |
endpoint: "https://api.groq.com/openai/v1/chat/completions", | |
apiKeyLabel: "Groq API Key", | |
models: [ | |
{ value: "llama3-70b-8192", label: "Llama-3 70B 8K" }, | |
{ value: "llama3-8b-8192", label: "Llama-3 8B 8K" }, | |
{ value: "mixtral-8x7b-32768", label: "Mixtral 8x7B 32K" }, | |
{ value: "gemma-7b-it", label: "Gemma 7B IT" } | |
], | |
isStream: true | |
}, | |
openai: { | |
name: "OpenAI", | |
endpoint: "https://api.openai.com/v1/chat/completions", | |
apiKeyLabel: "OpenAI API Key", | |
models: [ | |
{ value: "gpt-4o", label: "GPT-4o" }, | |
{ value: "gpt-4-turbo", label: "GPT-4 Turbo" }, | |
{ value: "gpt-3.5-turbo", label: "GPT-3.5 Turbo" } | |
], | |
isStream: true | |
} | |
}; | |
const SYSTEM_PROMPT = "คุณคือผู้ช่วย AI ที่ช่วยเหลือด้านโค้ดและไอเดีย พร้อมอธิบายและยกตัวอย่างในบล็อก markdown (```) อย่างชัดเจน"; | |
const PAYLOAD_CHAR_WARNING_THRESHOLD = 14000; | |
// --- Elements | |
const providerSelect = document.getElementById('providerSelect95'); | |
const modelSelect = document.getElementById('modelSelect95'); | |
const apiKeyInput = document.getElementById('apiKey95'); | |
const confirmApiKeyBtn = document.getElementById('confirmApiKeyBtn95'); | |
const apiKeyStatus = document.getElementById('apiKeyStatus95'); | |
const chatForm = document.getElementById('chatForm95'); | |
const userInput = document.getElementById('userInput95'); | |
const messagesDiv = document.getElementById('messagesDiv95'); | |
const errorDiv = document.getElementById('errorMessage95'); | |
const contextSaveArea = document.getElementById('contextSaveArea95'); | |
const savedCodeDisplay = document.getElementById('savedCodeDisplay95'); | |
const goalInput = document.getElementById('goalInput95'); | |
const confirmSaveBtn = document.getElementById('confirmSaveBtn95'); | |
const warningMessage = document.getElementById('warningMessage95'); | |
const fileAttachBtn = document.getElementById('fileAttachBtn'); | |
const fileInput = document.getElementById('fileInput'); | |
const fileNameSpan = document.getElementById('fileName'); | |
const dropzone = document.getElementById('dropzone95'); | |
// --- State | |
let chatHistory = []; | |
let isAITyping = false; | |
let latestCodeContext = null; | |
let savedCodeContext = null; | |
let savedGoal = null; | |
let attachedFiles = []; | |
// --- highlight.js + code tools | |
function renderWithHighlight(text) { | |
return text.replace(/```(\w+)?\n([\s\S]*?)```/g, function(_, lang, code) { | |
const safeCode = code.replace(/</g,"<").replace(/>/g,">"); | |
const language = lang && hljs.getLanguage(lang) ? lang : 'plaintext'; | |
const codeId = 'code_' + Math.random().toString(36).slice(2); | |
return `<pre><div class="code-tools"> | |
<button type="button" onclick="copyCode('${codeId}')">📋 Copy</button> | |
<button type="button" onclick="downloadCode('${codeId}','code.${lang||'txt'}')">⬇️ Save</button> | |
</div><code id="${codeId}" class="hljs language-${language}">${safeCode}</code></pre>`; | |
}); | |
} | |
window.copyCode = function(id) { | |
const code = document.getElementById(id); | |
if (code) { | |
navigator.clipboard.writeText(code.textContent).then(()=>{ | |
code.parentElement.querySelector('button').textContent='✅'; | |
setTimeout(()=>code.parentElement.querySelector('button').textContent='📋 Copy',1000); | |
}); | |
} | |
}; | |
window.downloadCode = function(id, filename) { | |
const code = document.getElementById(id); | |
if (code) { | |
const blob = new Blob([code.textContent], {type: "text/plain"}); | |
const a = document.createElement('a'); | |
a.href = URL.createObjectURL(blob); | |
a.download = filename; | |
a.click(); | |
setTimeout(()=>URL.revokeObjectURL(a.href), 2000); | |
} | |
}; | |
function setModelOptions(providerKey) { | |
modelSelect.innerHTML = ""; | |
PROVIDERS[providerKey].models.forEach(m => | |
modelSelect.innerHTML += `<option value="${m.value}">${m.label}</option>` | |
); | |
} | |
function setApiKeyLabel(providerKey) { | |
apiKeyInput.placeholder = "กรอก " + PROVIDERS[providerKey].apiKeyLabel; | |
} | |
function loadApiKey(providerKey) { | |
const k = localStorage.getItem("ai95key_" + providerKey) || ""; | |
apiKeyInput.value = k; | |
if (k) { | |
apiKeyStatus.textContent = '✔️ โหลด API Key อัตโนมัติ'; | |
apiKeyStatus.style.color = '#008000'; | |
} else { | |
apiKeyStatus.textContent = ''; | |
} | |
} | |
function saveApiKey(providerKey, key) { | |
localStorage.setItem("ai95key_" + providerKey, key); | |
apiKeyStatus.textContent = "✔️ API Key ถูกบันทึกแล้ว"; | |
apiKeyStatus.style.color = "#008000"; | |
} | |
function getApiKey() { return apiKeyInput.value.trim(); } | |
function getProviderKey() { return providerSelect.value; } | |
function getModelValue() { return modelSelect.value; } | |
function showError(msg) { errorDiv.textContent = msg || ""; errorDiv.style.display = msg ? "block" : "none"; } | |
function addMessage(role, content) { | |
const div = document.createElement('div'); | |
div.className = 'message95 ' + role; | |
div.innerHTML = renderWithHighlight(content.replace(/\n/g, '<br>')); | |
messagesDiv.appendChild(div); | |
div.querySelectorAll('pre code').forEach((block) => hljs.highlightElement(block)); | |
messagesDiv.scrollTop = messagesDiv.scrollHeight; | |
return div; | |
} | |
function showLoading() { chatForm.querySelector('button[type="submit"]').disabled = true; isAITyping = true; } | |
function hideLoading() { chatForm.querySelector('button[type="submit"]').disabled = false; isAITyping = false; } | |
function extractCode(text) { | |
const codeBlockRegex = /```(?:\w+)?\s*\n([\s\S]*?)```/g; | |
let lastCode = null, match; | |
while ((match = codeBlockRegex.exec(text)) !== null) lastCode = match[1].trim(); | |
return lastCode; | |
} | |
function calculatePayloadCharCount(messages) { | |
let count = 0; | |
for (const msg of messages) { | |
if (msg.content && typeof msg.content === 'string') count += msg.content.length; | |
if (msg.content && Array.isArray(msg.content)) { | |
for (const part of msg.content) { | |
if (part.text) count += part.text.length; | |
} | |
} | |
} | |
return count; | |
} | |
function showContextSaveArea(show, code = null, goal = '') { | |
if (show) { | |
savedCodeDisplay.textContent = code !== null ? code : 'ยังไม่พบโค้ดในข้อความ AI ล่าสุด'; | |
if (goal !== undefined) goalInput.value = goal; | |
contextSaveArea.style.display = 'block'; | |
} else { | |
contextSaveArea.style.display = 'none'; | |
warningMessage.textContent = ''; | |
} | |
} | |
function clearChat() { | |
chatHistory = []; | |
messagesDiv.innerHTML = '<div class="message95 ai">สวัสดี! AI Chat สไตล์ Win95 Enhanced พร้อมใช้ 🎉<br>รองรับ Anthropic Claude-4-Sonnet และ xAI Grok-3 ล่าสุด!</div>'; | |
latestCodeContext = null; | |
showContextSaveArea(false, null, goalInput.value); | |
showError(''); | |
} | |
// --- Provider Change | |
providerSelect.addEventListener('change', function() { | |
setModelOptions(getProviderKey()); | |
setApiKeyLabel(getProviderKey()); | |
loadApiKey(getProviderKey()); | |
showError(''); | |
}); | |
setModelOptions(getProviderKey()); | |
setApiKeyLabel(getProviderKey()); | |
loadApiKey(getProviderKey()); | |
// --- API Key Save | |
confirmApiKeyBtn.addEventListener('click', function() { | |
const key = getApiKey(); | |
if (!key) { | |
apiKeyStatus.textContent = ''; | |
showError("กรุณาใส่ API Key ก่อนกดบันทึก"); | |
return; | |
} | |
saveApiKey(getProviderKey(), key); | |
showError(""); | |
}); | |
apiKeyInput.addEventListener('input', ()=>apiKeyStatus.textContent=''); | |
// --- Anthropic API (Messages format | |
// --- API Key Save | |
confirmApiKeyBtn.addEventListener('click', function() { | |
const key = getApiKey(); | |
if (!key) { | |
apiKeyStatus.textContent = ''; | |
showError("กรุณาใส่ API Key ก่อนกดบันทึก"); | |
return; | |
} | |
saveApiKey(getProviderKey(), key); | |
showError(""); | |
}); | |
apiKeyInput.addEventListener('input', ()=>apiKeyStatus.textContent=''); | |
// --- Anthropic API (Messages format) | |
async function callAnthropicAPI(messages) { | |
const response = await fetch(PROVIDERS.anthropic.endpoint, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'x-api-key': getApiKey(), | |
'anthropic-version': '2023-06-01' | |
}, | |
body: JSON.stringify({ | |
model: getModelValue(), | |
max_tokens: 4096, | |
system: SYSTEM_PROMPT, | |
messages: messages | |
}) | |
}); | |
if (!response.ok) { | |
const error = await response.text(); | |
throw new Error(`Anthropic API Error: ${response.status} - ${error}`); | |
} | |
const data = await response.json(); | |
return data.content[0].text; | |
} | |
// --- xAI/OpenAI/Groq API (Chat format with streaming) | |
async function callStreamingAPI(messages) { | |
const provider = PROVIDERS[getProviderKey()]; | |
const response = await fetch(provider.endpoint, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': `Bearer ${getApiKey()}` | |
}, | |
body: JSON.stringify({ | |
model: getModelValue(), | |
messages: [ | |
{ role: 'system', content: SYSTEM_PROMPT }, | |
...messages | |
], | |
stream: true, | |
max_tokens: 4096 | |
}) | |
}); | |
if (!response.ok) { | |
const error = await response.text(); | |
throw new Error(`${provider.name} API Error: ${response.status} - ${error}`); | |
} | |
return response; | |
} | |
// --- File upload handling | |
fileInput.addEventListener('change', handleFileSelect); | |
function handleFileSelect(event) { | |
const files = Array.from(event.target.files); | |
attachedFiles = []; | |
files.forEach(file => { | |
if (file.size > 5 * 1024 * 1024) { // 5MB limit | |
showError(`ไฟล์ ${file.name} มีขนาดใหญ่เกิน 5MB`); | |
return; | |
} | |
const reader = new FileReader(); | |
reader.onload = (e) => { | |
attachedFiles.push({ | |
name: file.name, | |
type: file.type, | |
content: e.target.result | |
}); | |
}; | |
if (file.type.startsWith('image/')) { | |
reader.readAsDataURL(file); | |
} else { | |
reader.readAsText(file); | |
} | |
}); | |
if (files.length > 0) { | |
fileNameSpan.textContent = files.length === 1 ? files[0].name : `${files.length} ไฟล์`; | |
} else { | |
fileNameSpan.textContent = ''; | |
} | |
} | |
// --- Drag and drop | |
document.addEventListener('dragover', (e) => { | |
e.preventDefault(); | |
dropzone.style.display = 'block'; | |
}); | |
document.addEventListener('dragleave', (e) => { | |
if (!e.relatedTarget || !dropzone.contains(e.relatedTarget)) { | |
dropzone.style.display = 'none'; | |
} | |
}); | |
dropzone.addEventListener('drop', (e) => { | |
e.preventDefault(); | |
dropzone.style.display = 'none'; | |
const files = Array.from(e.dataTransfer.files); | |
const mockEvent = { target: { files: files } }; | |
handleFileSelect(mockEvent); | |
}); | |
// --- Context save functionality | |
confirmSaveBtn.addEventListener('click', function() { | |
const code = savedCodeDisplay.textContent; | |
const goal = goalInput.value.trim(); | |
if (code === 'ยังไม่พบโค้ดในข้อความ AI ล่าสุด' || !code) { | |
showError("ไม่พบโค้ดที่จะบันทึก"); | |
return; | |
} | |
savedCodeContext = code; | |
savedGoal = goal; | |
clearChat(); | |
if (savedCodeContext && savedGoal) { | |
addMessage('ai', `Context ถูกบันทึกแล้ว!\n\n**เป้าหมาย:** ${savedGoal}\n\n**โค้ดล่าสุด:**\n\`\`\`\n${savedCodeContext}\n\`\`\`\n\nพร้อมรับคำถามหรือคำสั่งเพิ่มเติม!`); | |
} | |
}); | |
// --- Main chat submission | |
chatForm.addEventListener('submit', async function(e) { | |
e.preventDefault(); | |
const message = userInput.value.trim(); | |
if (!message && attachedFiles.length === 0) return; | |
if (!getApiKey()) { | |
showError("กรุณาใส่ API Key ก่อนส่งข้อความ"); | |
return; | |
} | |
if (isAITyping) return; | |
showError(''); | |
let userMessage = message; | |
// Add file contents to message | |
if (attachedFiles.length > 0) { | |
userMessage += '\n\n--- ไฟล์ที่แนบ ---\n'; | |
attachedFiles.forEach(file => { | |
userMessage += `\n**${file.name}:**\n`; | |
if (file.type.startsWith('image/')) { | |
userMessage += `[รูปภาพ: ${file.type}]\n`; | |
} else { | |
userMessage += `${file.content}\n`; | |
} | |
}); | |
// Clear attachments | |
attachedFiles = []; | |
fileNameSpan.textContent = ''; | |
fileInput.value = ''; | |
} | |
// Add saved context if available | |
if (savedCodeContext && savedGoal) { | |
userMessage = `**Context ที่บันทึกไว้:**\nเป้าหมาย: ${savedGoal}\nโค้ดล่าสุด:\n\`\`\`\n${savedCodeContext}\n\`\`\`\n\n**คำถาม/คำสั่งใหม่:**\n${userMessage}`; | |
savedCodeContext = null; | |
savedGoal = null; | |
} | |
addMessage('user', message); | |
userInput.value = ''; | |
showLoading(); | |
const currentMessage = { role: 'user', content: userMessage }; | |
chatHistory.push(currentMessage); | |
// Check payload size | |
const payloadSize = calculatePayloadCharCount(chatHistory); | |
if (payloadSize > PAYLOAD_CHAR_WARNING_THRESHOLD) { | |
const newCode = extractCode(chatHistory[chatHistory.length - 1]?.content || ''); | |
if (newCode) latestCodeContext = newCode; | |
warningMessage.textContent = `⚠️ ข้อความยาวเกิน ${PAYLOAD_CHAR_WARNING_THRESHOLD} ตัวอักษร (ปัจจุบัน: ${payloadSize})`; | |
showContextSaveArea(true, latestCodeContext); | |
hideLoading(); | |
return; | |
} | |
try { | |
const providerKey = getProviderKey(); | |
let aiResponse = ''; | |
if (providerKey === 'anthropic') { | |
aiResponse = await callAnthropicAPI(chatHistory); | |
const aiDiv = addMessage('ai', aiResponse); | |
// Extract code from AI response | |
const code = extractCode(aiResponse); | |
if (code) latestCodeContext = code; | |
} else { | |
// Streaming response for other providers | |
const response = await callStreamingAPI(chatHistory); | |
const aiDiv = addMessage('ai', ''); | |
const reader = response.body.getReader(); | |
const decoder = new TextDecoder(); | |
let buffer = ''; | |
let fullResponse = ''; | |
while (true) { | |
const { value, done } = await reader.read(); | |
if (done) break; | |
buffer += decoder.decode(value, { stream: true }); | |
const lines = buffer.split('\n'); | |
buffer = lines.pop() || ''; | |
for (const line of lines) { | |
if (line.startsWith('data: ')) { | |
const data = line.slice(6).trim(); | |
if (data === '[DONE]') continue; | |
try { | |
const parsed = JSON.parse(data); | |
const delta = parsed.choices?.[0]?.delta?.content || ''; | |
if (delta) { | |
fullResponse += delta; | |
aiDiv.innerHTML = renderWithHighlight(fullResponse.replace(/\n/g, '<br>')); | |
aiDiv.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block)); | |
messagesDiv.scrollTop = messagesDiv.scrollHeight; | |
} | |
} catch (e) { | |
// Skip malformed JSON | |
} | |
} | |
} | |
} | |
aiResponse = fullResponse; | |
// Extract code from AI response | |
const code = extractCode(aiResponse); | |
if (code) latestCodeContext = code; | |
} | |
chatHistory.push({ role: 'assistant', content: aiResponse }); | |
} catch (error) { | |
console.error('API Error:', error); | |
showError(error.message); | |
chatHistory.pop(); // Remove failed user message | |
} finally { | |
hideLoading(); | |
} | |
}); | |
// --- Initialize | |
window.addEventListener('load', function() { | |
userInput.focus(); | |
// Auto-resize input | |
userInput.addEventListener('input', function() { | |
this.style.height = 'auto'; | |
this.style.height = Math.min(this.scrollHeight, 120) + 'px'; | |
}); | |
}); | |
// --- Keyboard shortcuts | |
document.addEventListener('keydown', function(e) { | |
if (e.key === 'F11') { | |
e.preventDefault(); | |
maximizeBtn.click(); | |
} | |
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { | |
chatForm.dispatchEvent(new Event('submit')); | |
} | |
}); |