code2 / index.html
kimhyunwoo's picture
Update index.html
4daea0d verified
raw
history blame
25.6 kB
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI μ½”λ”© νŒŒνŠΈλ„ˆ</title>
<!-- Monaco Editor λ‘œλ” CSS -->
<link rel="stylesheet" data-name="vs/editor/editor.main" href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/editor/editor.main.min.css">
<!-- Google Fonts (macOS λŠλ‚Œ 폰트) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #f0f2f5; /* 밝은 νšŒμƒ‰ λ°°κ²½ */
--app-bg-color: #ffffff; /* μ•± λ°°κ²½ */
--sidebar-bg-color: #e8e8e8; /* μ‚¬μ΄λ“œλ°” λ°°κ²½ (더 밝게) */
--text-color: #1c1e21; /* κΈ°λ³Έ ν…μŠ€νŠΈ */
--text-secondary-color: #606770; /* 보쑰 ν…μŠ€νŠΈ */
--accent-color: #007aff; /* macOS 블루 */
--border-color: #d1d1d7;
--editor-bg: #282c34; /* μ–΄λ‘μš΄ 에디터 λ°°κ²½ */
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--border-radius: 12px;
--box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
--box-shadow-light: 0 1px 3px rgba(0,0,0,0.05);
}
/* μŠ€ν¬λ‘€λ°” μŠ€νƒ€μΌ (선택 사항) */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #a1a1a1; }
body {
font-family: var(--font-family);
margin: 0;
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
box-sizing: border-box;
}
.app-container {
display: flex;
width: 95%;
max-width: 1400px;
height: 90vh;
max-height: 800px;
background-color: var(--app-bg-color);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
overflow: hidden;
}
/* μ‚¬μ΄λ“œλ°” (선택 사항 - μ—¬κΈ°μ„œλŠ” μ±„νŒ…, 에디터, 미리보기λ₯Ό μ£Ό μ˜μ—­μœΌλ‘œ) */
/* .sidebar { flex: 0 0 280px; background-color: var(--sidebar-bg-color); padding: 20px; border-right: 1px solid var(--border-color); display: flex; flex-direction: column; } */
.main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-editor-area {
display: flex;
flex-grow: 1;
overflow: hidden; /* λ‚΄λΆ€ 슀크둀 관리 */
}
.chat-panel {
flex: 1;
display: flex;
flex-direction: column;
padding: 20px;
border-right: 1px solid var(--border-color);
overflow-y: auto; /* μ±„νŒ… 슀크둀 */
}
.chat-messages {
flex-grow: 1;
margin-bottom: 15px;
overflow-y: auto;
padding-right: 10px; /* μŠ€ν¬λ‘€λ°” 곡간 */
}
.message {
display: flex;
margin-bottom: 15px;
max-width: 90%;
}
.message.user {
margin-left: auto; /* μ‚¬μš©μž λ©”μ‹œμ§€λŠ” 였λ₯Έμͺ½ μ •λ ¬ */
flex-direction: row-reverse;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: var(--accent-color);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
font-size: 16px;
margin-right: 10px;
flex-shrink: 0;
}
.message.user .avatar { margin-left: 10px; margin-right: 0; background-color: #6c757d; }
.message-content {
padding: 10px 15px;
border-radius: 18px;
background-color: #e4e6eb; /* AI λ©”μ‹œμ§€ λ°°κ²½ */
color: var(--text-color);
word-wrap: break-word; /* κΈ΄ 단어 μ€„λ°”κΏˆ */
}
.message.user .message-content { background-color: var(--accent-color); color: white; }
.message-content pre { /* μ½”λ“œ 블둝 μŠ€νƒ€μΌ */
background-color: var(--editor-bg) !important; /* Monaco ν…Œλ§ˆμ™€ μœ μ‚¬ν•˜κ²Œ */
color: #abb2bf !important; /* Monaco κΈ°λ³Έ ν…μŠ€νŠΈ 색상 */
padding: 10px;
border-radius: 8px;
overflow-x: auto;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 0.9em;
white-space: pre-wrap; /* μžλ™ μ€„λ°”κΏˆ */
word-break: break-all; /* κΈ΄ μ½”λ“œ μ€„λ°”κΏˆ */
}
/* Monaco 에디터가 λ Œλ”λ§ν•œ μ½”λ“œ 블둝에 λŒ€ν•œ μŠ€νƒ€μΌ (ν•„μš” μ‹œ) */
.monaco-editor-code-block {
border-radius: 8px !important;
box-shadow: var(--box-shadow-light);
}
.chat-input-area {
display: flex;
align-items: center;
padding-top: 15px;
border-top: 1px solid var(--border-color);
}
.chat-input-area textarea {
flex-grow: 1;
padding: 10px 15px;
border: 1px solid var(--border-color);
border-radius: 18px;
resize: none;
font-family: var(--font-family);
font-size: 15px;
min-height: 22px; /* 1쀄 높이 */
max-height: 100px; /* μ΅œλŒ€ 높이 */
line-height: 1.4;
margin-right: 10px;
box-sizing: border-box;
overflow-y: auto; /* λ‚΄μš© λ§Žμ•„μ§€λ©΄ 슀크둀 */
}
.chat-input-area textarea:focus { border-color: var(--accent-color); outline: none; box-shadow: 0 0 0 2px rgba(0,122,255,0.2); }
.chat-input-area button {
background-color: var(--accent-color);
color: white;
border: none;
border-radius: 50%; /* μ›ν˜• λ²„νŠΌ */
width: 40px;
height: 40px;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.chat-input-area button:hover { background-color: #0056b3; }
.chat-input-area button:disabled { background-color: #b0c4de; cursor: not-allowed; }
.editor-preview-area {
flex: 2; /* 에디터와 미리보기 μ˜μ—­μ„ 더 λ„“κ²Œ */
display: flex;
flex-direction: column;
overflow: hidden;
}
.editor-wrapper {
flex-basis: 60%; /* 에디터가 μ°¨μ§€ν•  κΈ°λ³Έ 높이 λΉ„μœ¨ */
min-height: 200px; /* μ΅œμ†Œ 높이 */
padding: 10px; /* 에디터 μ£Όλ³€ μ—¬λ°± */
background-color: #f9f9f9; /* 에디터 μ˜μ—­ λ°°κ²½ */
display: flex;
flex-direction: column;
}
#editor-container {
width: 100% !important;
height: 100% !important; /* λΆ€λͺ¨ 높이에 맞좀 */
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
overflow: hidden; /* Monaco μ—λ””ν„°μ˜ 자체 슀크둀 μ‚¬μš© */
}
.editor-controls {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.editor-controls select, .editor-controls button {
padding: 6px 12px;
border-radius: 6px;
border: 1px solid var(--border-color);
background-color: white;
font-family: var(--font-family);
font-size: 13px;
cursor: pointer;
}
.editor-controls button { background-color: #e9ecef; color: var(--text-color); }
.editor-controls button:hover { background-color: #d1d5db; }
.preview-wrapper {
flex-basis: 40%; /* 미리보기가 μ°¨μ§€ν•  κΈ°λ³Έ 높이 λΉ„μœ¨ */
min-height: 150px;
border-top: 1px solid var(--border-color);
padding: 10px;
background-color: #f9f9f9; /* 미리보기 μ˜μ—­ λ°°κ²½ */
display: flex; /* λ‚΄λΆ€ iframe 크기 쑰절 μœ„ν•΄ */
flex-direction: column;
}
.preview-wrapper h3 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 500;
color: var(--text-secondary-color);
}
#html-preview {
flex-grow: 1; /* 남은 곡간 λͺ¨λ‘ μ°¨μ§€ */
width: 100%;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background-color: white;
}
/* λ‘œλ”© μŠ€ν”Όλ„ˆ */
.loader {
display: flex;
align-items: center;
font-size: 13px;
color: var(--text-secondary-color);
margin-left: 10px; /* μž…λ ₯μ°½ μ˜†μ— ν‘œμ‹œλ  λ•Œ */
}
.loader.inline-editor { margin-top: 5px; justify-content: center; }
.spinner {
width: 16px;
height: 16px;
border: 2px solid var(--accent-color);
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* λ°˜μ‘ν˜• μ‘°μ • */
@media (max-width: 1024px) {
.app-container { flex-direction: column; height: auto; max-height: none; }
.chat-editor-area { flex-direction: column; }
.chat-panel { border-right: none; border-bottom: 1px solid var(--border-color); max-height: 50vh; }
.editor-preview-area { flex: 1; } /* 남은 곡간 λͺ¨λ‘ μ°¨μ§€ */
}
@media (max-width: 768px) {
body { padding: 0; }
.app-container { width: 100%; height: 100vh; border-radius: 0; }
.chat-panel { padding: 15px; }
.editor-wrapper, .preview-wrapper { padding: 8px; }
.chat-input-area textarea { font-size: 14px; }
.chat-input-area button { width: 36px; height: 36px; font-size: 18px; }
}
</style>
</head>
<body>
<div class="app-container">
<!-- <div class="sidebar">Sidebar Content (Optional)</div> -->
<div class="main-content">
<div class="chat-editor-area">
<div class="chat-panel">
<div class="chat-messages" id="chatMessages">
<!-- AI μ‹œμž‘ λ©”μ‹œμ§€ -->
<div class="message ai">
<div class="avatar">AI</div>
<div class="message-content">
μ•ˆλ…•ν•˜μ„Έμš”! 무엇을 λ„μ™€λ“œλ¦΄κΉŒμš”? HTML 생성, μ½”λ“œ μž‘μ„±, λ˜λŠ” κΈ°μ‘΄ μ½”λ“œ μžλ™ 완성을 μš”μ²­ν•˜μ‹€ 수 μžˆμŠ΅λ‹ˆλ‹€.
</div>
</div>
</div>
<div class="chat-input-area">
<textarea id="chatInput" placeholder="여기에 λ©”μ‹œμ§€λ₯Ό μž…λ ₯ν•˜μ„Έμš”..." rows="1"></textarea>
<button id="sendButton" title="전솑">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
</button>
<div class="loader" id="chatLoader" style="display: none;"><div class="spinner"></div>처리 쀑...</div>
</div>
</div>
<div class="editor-preview-area">
<div class="editor-wrapper">
<div class="editor-controls">
<select id="languageSelect">
<option value="html">HTML</option>
<option value="css">CSS</option>
<option value="javascript" selected>JavaScript</option>
<option value="python">Python</option>
<option value="java">Java</option>
<option value="csharp">C#</option>
</select>
<button id="getCompletionBtn">AI μ½”λ“œ μ±„μš°κΈ° (FIM)</button>
</div>
<div id="editor-container"></div>
<div class="loader inline-editor" id="completionLoader" style="display: none;"><div class="spinner"></div>AIκ°€ μ½”λ“œλ₯Ό μ±„μš°λŠ” 쀑...</div>
</div>
<div class="preview-wrapper">
<h3>HTML 미리보기</h3>
<iframe id="html-preview" sandbox="allow-scripts allow-same-origin"></iframe>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
<script>
let editor;
const chatMessages = document.getElementById('chatMessages');
const chatInput = document.getElementById('chatInput');
const sendButton = document.getElementById('sendButton');
const languageSelect = document.getElementById('languageSelect');
const getCompletionBtn = document.getElementById('getCompletionBtn');
const htmlPreviewEl = document.getElementById('html-preview');
const chatLoader = document.getElementById('chatLoader');
const completionLoader = document.getElementById('completionLoader');
// --- λ‘œλ” ν‘œμ‹œ/μˆ¨κΉ€ ν•¨μˆ˜ ---
function showLoader(loaderElement) { if (loaderElement) loaderElement.style.display = 'flex'; }
function hideLoader(loaderElement) { if (loaderElement) loaderElement.style.display = 'none'; }
// --- Monaco Editor μ΄ˆκΈ°ν™” ---
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' }});
require(['vs/editor/editor.main'], function() {
editor = monaco.editor.create(document.getElementById('editor-container'), {
value: '// μ½”λ“œλ₯Ό μž‘μ„±ν•˜κ±°λ‚˜ AIμ—κ²Œ μš”μ²­ν•˜μ„Έμš”.\n// μ±„νŒ…μ°½μ— "μ‚¬κ°ν˜• λ²„νŠΌ λ§Œλ“€μ–΄μ€˜" 같이 μž…λ ₯ν•΄λ³΄μ„Έμš”.',
language: languageSelect.value,
theme: 'vs-dark', // μ–΄λ‘μš΄ ν…Œλ§ˆ
automaticLayout: true,
wordWrap: 'on',
minimap: { enabled: true, scale: 1 },
fontSize: 14,
lineNumbers: 'on',
roundedSelection: false,
scrollBeyondLastLine: false,
readOnly: false, // μ‚¬μš©μžκ°€ 직접 νŽΈμ§‘ κ°€λŠ₯ν•˜λ„λ‘
padding: { top: 10, bottom: 10 }
});
// μ–Έμ–΄ 선택 μ‹œ 에디터 μ–Έμ–΄ λ³€κ²½
languageSelect.addEventListener('change', () => {
monaco.editor.setModelLanguage(editor.getModel(), languageSelect.value);
});
// FIM λ²„νŠΌ 클릭 이벀트
getCompletionBtn.addEventListener('click', handleFIMRequest);
});
// --- μ±„νŒ… λ©”μ‹œμ§€ 처리 ---
function addMessage(text, sender, isCode = false, lang = 'plaintext') {
const messageDiv = document.createElement('div');
messageDiv.classList.add('message', sender);
const avatarDiv = document.createElement('div');
avatarDiv.classList.add('avatar');
avatarDiv.textContent = sender === 'user' ? 'λ‚˜' : 'AI';
messageDiv.appendChild(avatarDiv);
const contentDiv = document.createElement('div');
contentDiv.classList.add('message-content');
if (isCode) {
// μ½”λ“œλ₯Ό Monaco Editorλ₯Ό μ‚¬μš©ν•΄ λ Œλ”λ§ (읽기 μ „μš©)
const pre = document.createElement('pre');
// κ°„λ‹¨νžˆ ν…μŠ€νŠΈλ‘œ λ„£κ±°λ‚˜, μƒˆλ‘œμš΄ Monaco μΈμŠ€ν„΄μŠ€λ₯Ό λ§Œλ“€ 수 있음.
// μ—¬κΈ°μ„œλŠ” κ°„λ‹¨νžˆ pre νƒœκ·Έλ‘œ μ²˜λ¦¬ν•˜κ³ , CSS둜 μŠ€νƒ€μΌλ§.
// μ‹€μ œλ‘œλŠ” AI μ‘λ‹΅μ—μ„œ ```html ... ``` 같은 λ§ˆν¬λ‹€μš΄ μ½”λ“œλΈ”λ‘ νŒŒμ‹± ν•„μš”
let codeContent = text;
const codeBlockMatch = text.match(/```(\w*)\n([\s\S]*?)```/);
if (codeBlockMatch) {
lang = codeBlockMatch[1] || lang;
codeContent = codeBlockMatch[2];
}
pre.textContent = codeContent; // μ‹€μ œλ‘œλŠ” highlight.js λ“±μœΌλ‘œ ν•˜μ΄λΌμ΄νŒ… 적용
contentDiv.appendChild(pre);
// μ½”λ“œ 에디터에 μ½”λ“œ μ‚½μž… λ²„νŠΌ μΆ”κ°€ (선택 사항)
const insertButton = document.createElement('button');
insertButton.textContent = '에디터에 μ‚½μž…';
insertButton.style.marginTop = '8px';
insertButton.style.padding = '5px 10px';
insertButton.style.fontSize = '12px';
insertButton.style.backgroundColor = '#f0f0f0';
insertButton.style.color = '#333';
insertButton.style.border = '1px solid #ccc';
insertButton.style.borderRadius = '4px';
insertButton.style.cursor = 'pointer';
insertButton.onclick = () => {
if (editor) {
editor.setValue(codeContent);
monaco.editor.setModelLanguage(editor.getModel(), lang);
if (lang.toLowerCase() === 'html') {
htmlPreviewEl.srcdoc = codeContent;
}
}
};
contentDiv.appendChild(insertButton);
} else {
contentDiv.textContent = text;
}
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight; // 항상 μ΅œμ‹  λ©”μ‹œμ§€ 보이도둝 슀크둀
}
// --- μ±„νŒ… μž…λ ₯ 처리 ---
chatInput.addEventListener('input', () => { // textarea 높이 μžλ™ 쑰절
chatInput.style.height = 'auto';
chatInput.style.height = (chatInput.scrollHeight) + 'px';
});
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { // Shift+EnterλŠ” μ€„λ°”κΏˆ
e.preventDefault();
handleSendMessage();
}
});
sendButton.addEventListener('click', handleSendMessage);
async function handleSendMessage() {
const messageText = chatInput.value.trim();
if (!messageText) return;
addMessage(messageText, 'user');
chatInput.value = '';
chatInput.style.height = 'auto'; // 높이 μ΄ˆκΈ°ν™”
sendButton.disabled = true;
showLoader(chatLoader);
// AI 응닡 처리 (κ°„λ‹¨ν•œ κ·œμΉ™ 기반으둜 μ–΄λ–€ API ν˜ΈμΆœν• μ§€ κ²°μ •)
let endpoint = '/api/generate-code'; // 기본은 μ½”λ“œ 생성
let payload = { prompt: messageText, language: languageSelect.value };
let responseIsCode = true;
let responseLang = languageSelect.value;
if (messageText.toLowerCase().includes('html λ§Œλ“€μ–΄μ€˜') || messageText.toLowerCase().includes('μ›ΉνŽ˜μ΄μ§€ λ§Œλ“€μ–΄μ€˜') || messageText.toLowerCase().includes('html둜')) {
endpoint = '/api/generate-html';
payload = { prompt: messageText };
responseLang = 'html';
} else if (messageText.toLowerCase().includes('λ‹€μŒ μ½”λ“œ μ™„μ„±ν•΄μ€˜') || messageText.toLowerCase().includes('μ½”λ“œ μ±„μ›Œμ€˜')) {
// 이 κ²½μš°λŠ” FIM λ²„νŠΌμ„ μ‚¬μš©ν•˜λ„λ‘ μœ λ„ν•˜κ±°λ‚˜, 에디터 λ‚΄μš©μ„ 가져와야 함.
// μ—¬κΈ°μ„œλŠ” 일반 μ½”λ“œ μƒμ„±μœΌλ‘œ 처리.
addMessage("AI: μ½”λ“œ μžλ™ μ™„μ„±(FIM)은 에디터 μœ„μ˜ 'AI μ½”λ“œ μ±„μš°κΈ°' λ²„νŠΌμ„ μ‚¬μš©ν•΄μ£Όμ„Έμš”.", 'ai');
hideLoader(chatLoader);
sendButton.disabled = false;
return;
}
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
let aiResponseText = '';
if (endpoint === '/api/generate-html') {
aiResponseText = data.html;
addMessage("AI: μš”μ²­ν•˜μ‹  HTML μ½”λ“œμž…λ‹ˆλ‹€.", 'ai');
addMessage(aiResponseText, 'ai', true, 'html');
if (editor && confirm("μƒμ„±λœ HTML을 에디터에 ν‘œμ‹œν•˜κ³  미리보기λ₯Ό μ—…λ°μ΄νŠΈν• κΉŒμš”?")) {
editor.setValue(aiResponseText);
monaco.editor.setModelLanguage(editor.getModel(), 'html');
htmlPreviewEl.srcdoc = aiResponseText;
} else {
htmlPreviewEl.srcdoc = aiResponseText; // λ―Έλ¦¬λ³΄κΈ°λŠ” 항상 μ—…λ°μ΄νŠΈ
}
} else if (endpoint === '/api/generate-code') {
aiResponseText = data.code;
addMessage(`AI: μš”μ²­ν•˜μ‹  ${responseLang} μ½”λ“œμž…λ‹ˆλ‹€.`, 'ai');
addMessage(aiResponseText, 'ai', true, responseLang);
if (editor && confirm(`μƒμ„±λœ ${responseLang} μ½”λ“œλ₯Ό 에디터에 ν‘œμ‹œν• κΉŒμš”?`)) {
editor.setValue(aiResponseText);
monaco.editor.setModelLanguage(editor.getModel(), responseLang);
}
}
} catch (error) {
console.error('API Error:', error);
addMessage(`AI: μ£„μ†‘ν•©λ‹ˆλ‹€. μš”μ²­ 처리 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error.message}`, 'ai');
} finally {
hideLoader(chatLoader);
sendButton.disabled = false;
}
}
// --- FIM μš”μ²­ 처리 ---
async function handleFIMRequest() {
if (!editor) return;
const model = editor.getModel();
const position = editor.getPosition();
const fullCode = model.getValue();
const offset = model.getOffsetAt(position);
const prefix = fullCode.substring(0, offset);
const suffix = fullCode.substring(offset);
const language = languageSelect.value;
showLoader(completionLoader);
getCompletionBtn.disabled = true;
try {
const response = await fetch('/api/complete-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prefix, suffix, language })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
editor.executeEdits("api-fim-completion", [{
range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column),
text: data.completion
}]);
addMessage(`AI: μ½”λ“œλ₯Ό μ±„μ›Œ λ„£μ—ˆμŠ΅λ‹ˆλ‹€. (에디터 확인)`, 'ai');
} catch (error)
{
console.error('FIM Error:', error);
addMessage(`AI: μ½”λ“œ μžλ™ μ™„μ„± 쀑 였λ₯˜: ${error.message}`, 'ai');
alert(`μ½”λ“œ μžλ™ μ™„μ„± 쀑 였λ₯˜: ${error.message}`);
} finally {
hideLoader(completionLoader);
getCompletionBtn.disabled = false;
}
}
// 초기 λ©”μ‹œμ§€ 이후 첫 μž…λ ₯ μ‹œ μ•ˆλ‚΄ (선택)
// chatInput.addEventListener('focus', () => { ... }, { once: true });
</script>
</body>
</html>