95win / index.html
protae5544's picture
Update index.html
bd381fe verified
raw
history blame
31.3 kB
<!DOCTYPE html>
<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 !important;
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="เต็มจอ">&#9633;</button>
<button onclick="window.location.reload()" title="รีเฟรช">&#9632;</button>
<button onclick="window.close()" title="ปิด">&#10006;</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 ? "&#9632;" : "&#9633;";
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,"&lt;").replace(/>/g,"&gt;");
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'));
}
});