kimhyunwoo commited on
Commit
4daea0d
Β·
verified Β·
1 Parent(s): ce2c049

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +449 -219
index.html CHANGED
@@ -3,316 +3,541 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>AI μ½”λ”© λ„μš°λ―Έ (단일 파일)</title>
7
  <!-- Monaco Editor λ‘œλ” CSS -->
8
  <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">
 
 
 
 
9
  <style>
10
- /* 이전 style.css λ‚΄μš© 전체λ₯Ό 여기에 λΆ™μ—¬λ„£μŠ΅λ‹ˆλ‹€. */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  body {
12
- font-family: sans-serif;
13
  margin: 0;
14
- background-color: #f4f4f4;
15
- color: #333;
 
 
 
 
16
  padding: 20px;
 
17
  }
18
 
19
- .container {
20
- max-width: 1200px;
21
- margin: auto;
22
- background-color: #fff;
23
- padding: 20px;
24
- border-radius: 8px;
25
- box-shadow: 0 0 10px rgba(0,0,0,0.1);
 
 
 
26
  }
27
 
28
- h1, h2 {
29
- color: #333;
30
- text-align: center;
31
- margin-bottom: 20px;
 
 
 
 
32
  }
33
 
34
- .main-layout {
35
  display: flex;
36
- flex-wrap: wrap; /* μž‘μ€ ν™”λ©΄μ—μ„œ μ€„λ°”κΏˆλ˜λ„λ‘ */
37
- gap: 20px;
38
- margin-bottom: 20px;
39
  }
40
 
41
- .editor-pane, .control-pane {
42
  flex: 1;
43
- min-width: 300px; /* μ΅œμ†Œ λ„ˆλΉ„ μ§€μ • */
44
- padding: 15px;
45
- border: 1px solid #ddd;
46
- border-radius: 5px;
47
- background-color: #f9f9f9;
48
  }
49
 
50
- .editor-pane h2, .control-pane h2 {
51
- margin-top: 0;
52
- text-align: left;
 
 
53
  }
54
 
55
- textarea {
56
- width: calc(100% - 22px); /* padding and border */
57
- min-height: 80px;
58
- padding: 10px;
59
- margin-bottom: 10px;
60
- border: 1px solid #ccc;
61
- border-radius: 4px;
62
- font-size: 14px;
63
- box-sizing: border-box; /* λ„ˆλΉ„ 계산에 padding, border 포함 */
64
  }
65
 
66
- button {
67
- display: block;
68
- width: 100%;
69
- padding: 10px;
70
- background-color: #007bff;
 
 
 
 
 
71
  color: white;
72
- border: none;
73
- border-radius: 4px;
74
- cursor: pointer;
 
75
  font-size: 16px;
76
- margin-bottom: 10px;
77
- box-sizing: border-box;
78
  }
 
 
 
 
 
 
 
 
 
 
79
 
80
- button:hover {
81
- background-color: #0056b3;
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
83
 
84
- .editor-actions {
85
- margin-top: 10px;
86
  display: flex;
87
- gap: 10px;
88
  align-items: center;
 
 
89
  }
90
 
91
- .editor-actions button {
92
- width: auto;
93
  flex-grow: 1;
 
 
 
 
 
 
 
 
 
 
 
 
94
  }
 
95
 
96
- #languageSelect {
97
- padding: 10px;
98
- border-radius: 4px;
99
- border: 1px solid #ccc;
100
- height: 40px; /* λ²„νŠΌκ³Ό 높이 맞좀 */
101
- box-sizing: border-box;
 
 
 
 
 
 
 
102
  }
 
 
103
 
104
- .preview-pane {
105
- margin-top: 20px;
106
- padding: 15px;
107
- border: 1px solid #ddd;
108
- border-radius: 5px;
109
- background-color: #f9f9f9;
110
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  #html-preview {
 
113
  width: 100%;
114
- height: 400px;
115
- border: 1px solid #ccc;
116
- background-color: #fff;
117
  }
118
- /* λ‘œλ”© μŠ€ν”Όλ„ˆ μŠ€νƒ€μΌ */
 
119
  .loader {
120
- border: 5px solid #f3f3f3; /* Light grey */
121
- border-top: 5px solid #3498db; /* Blue */
122
- border-radius: 50%;
123
- width: 30px;
124
- height: 30px;
125
- animation: spin 1s linear infinite;
126
- margin: 10px auto;
127
- display: none; /* κΈ°λ³Έ μˆ¨κΉ€ */
128
  }
 
129
 
130
- @keyframes spin {
131
- 0% { transform: rotate(0deg); }
132
- 100% { transform: rotate(360deg); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  }
 
 
 
 
 
 
 
 
 
134
  </style>
135
  </head>
136
  <body>
137
- <div class="container">
138
- <h1>AI μ½”λ”© λ„μš°λ―Έ (단일 파일)</h1>
139
-
140
- <div class="main-layout">
141
- <div class="editor-pane">
142
- <h2>μ½”λ“œ 에디터</h2>
143
- <div id="editor-container" style="width:100%;height:400px;border:1px solid #ccc;"></div>
144
- <div class="editor-actions">
145
- <button id="getCompletionBtn">μ½”λ“œ μžλ™μ™„μ„± (FIM)</button>
146
- <select id="languageSelect">
147
- <option value="python">Python</option>
148
- <option value="javascript" selected>JavaScript</option>
149
- <option value="html">HTML</option>
150
- <option value="css">CSS</option>
151
- <option value="java">Java</option>
152
- <option value="csharp">C#</option>
153
- </select>
 
 
 
 
154
  </div>
155
- </div>
156
-
157
- <div class="control-pane">
158
- <h2>HTML 생성 (λŒ€ν™”ν˜•)</h2>
159
- <textarea id="html-prompt" placeholder="예: '빨간색 배경의 ν™˜μ˜ λ©”μ‹œμ§€ νŽ˜μ΄μ§€ λ§Œλ“€μ–΄μ€˜'"></textarea>
160
- <button id="generateHtmlBtn">HTML 생성 및 미리보기</button>
161
- <div id="htmlLoader" class="loader"></div>
162
 
163
- <h2>일반 μ½”λ“œ 생성 (λͺ…λ Ήν˜•)</h2>
164
- <textarea id="code-prompt" placeholder="예: 'μ£Όμ–΄μ§„ 리슀트λ₯Ό μ •λ ¬ν•˜λŠ” 파이썬 ν•¨μˆ˜ λ§Œλ“€μ–΄μ€˜'"></textarea>
165
- <button id="generateCodeBtn">μ½”λ“œ 생성</button>
166
- <div id="codeLoader" class="loader"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  </div>
168
  </div>
169
- <div id="completionLoader" class="loader" style="text-align: center; width: 100%;"></div>
170
-
171
-
172
- <div class="preview-pane">
173
- <h2>HTML 미리보기</h2>
174
- <iframe id="html-preview" sandbox="allow-scripts allow-same-origin"></iframe>
175
- </div>
176
-
177
  </div>
178
 
179
- <!-- Monaco Editor λ‘œλ” JS -->
180
  <script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
181
  <script>
182
- // 이전 script.js λ‚΄μš© 전체λ₯Ό 여기에 λΆ™μ—¬λ„£μŠ΅λ‹ˆλ‹€.
183
  let editor;
184
-
185
- // λ‘œλ” μš”μ†Œ κ°€μ Έμ˜€κΈ°
186
- const htmlLoader = document.getElementById('htmlLoader');
187
- const codeLoader = document.getElementById('codeLoader');
 
 
 
188
  const completionLoader = document.getElementById('completionLoader');
189
 
190
- function showLoader(loaderElement) {
191
- if (loaderElement) loaderElement.style.display = 'block';
192
- }
193
-
194
- function hideLoader(loaderElement) {
195
- if (loaderElement) loaderElement.style.display = 'none';
196
- }
197
-
198
 
 
199
  require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' }});
200
  require(['vs/editor/editor.main'], function() {
201
  editor = monaco.editor.create(document.getElementById('editor-container'), {
202
- value: [
203
- '// 여기에 μ½”λ“œλ₯Ό μž…λ ₯ν•˜κ±°λ‚˜ AIμ—κ²Œ 생성을 μš”μ²­ν•˜μ„Έμš”.',
204
- '// HTML 생성 μ‹œ 이곳과 미리보기 창에 κ²°κ³Όκ°€ ν‘œμ‹œλ©λ‹ˆλ‹€.'
205
- ].join('\n'),
206
- language: 'javascript', // κΈ°λ³Έ μ–Έμ–΄
207
- theme: 'vs-dark',
208
  automaticLayout: true,
209
- wordWrap: 'on', // μžλ™ μ€„λ°”κΏˆ
210
- minimap: { enabled: false } // λ―Έλ‹ˆλ§΅ λΉ„ν™œμ„±ν™” (선택 사항)
 
 
 
 
 
 
211
  });
212
- });
213
 
214
- const htmlPromptEl = document.getElementById('html-prompt');
215
- const codePromptEl = document.getElementById('code-prompt');
216
- const generateHtmlBtn = document.getElementById('generateHtmlBtn');
217
- const generateCodeBtn = document.getElementById('generateCodeBtn');
218
- const getCompletionBtn = document.getElementById('getCompletionBtn');
219
- const languageSelect = document.getElementById('languageSelect');
220
- const htmlPreviewEl = document.getElementById('html-preview');
221
 
222
- languageSelect.addEventListener('change', (event) => {
223
- if (editor) {
224
- monaco.editor.setModelLanguage(editor.getModel(), event.target.value);
225
- }
226
  });
227
 
228
- generateHtmlBtn.addEventListener('click', async () => {
229
- const prompt = htmlPromptEl.value.trim();
230
- if (!prompt) {
231
- alert('HTML 생성 μš”μ²­ λ‚΄μš©μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”.');
232
- return;
233
- }
234
- showLoader(htmlLoader);
235
- generateHtmlBtn.disabled = true;
236
-
237
- try {
238
- const response = await fetch('/api/generate-html', {
239
- method: 'POST',
240
- headers: { 'Content-Type': 'application/json' },
241
- body: JSON.stringify({ prompt })
242
- });
243
- if (!response.ok) {
244
- const errorData = await response.json();
245
- throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
 
 
 
 
 
 
246
  }
247
- const data = await response.json();
248
- if (editor) {
249
- editor.setValue(data.html);
250
- monaco.editor.setModelLanguage(editor.getModel(), 'html');
251
- }
252
- htmlPreviewEl.srcdoc = data.html;
253
- } catch (error) {
254
- console.error('HTML 생성 였λ₯˜:', error);
255
- alert(`HTML 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error.message}`);
256
- if (editor) editor.setValue(`// HTML 생성 였λ₯˜: ${error.message}`);
257
- htmlPreviewEl.srcdoc = `<h3>였λ₯˜ λ°œμƒ</h3><p>${error.message}</p>`;
258
- } finally {
259
- hideLoader(htmlLoader);
260
- generateHtmlBtn.disabled = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  }
 
 
 
 
 
 
 
 
 
 
262
  });
263
 
264
- generateCodeBtn.addEventListener('click', async () => {
265
- const prompt = codePromptEl.value.trim();
266
- const language = languageSelect.value;
267
- if (!prompt) {
268
- alert('μ½”λ“œ 생성 μš”μ²­ λ‚΄μš©μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”.');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  return;
270
  }
271
- showLoader(codeLoader);
272
- generateCodeBtn.disabled = true;
273
 
274
  try {
275
- const response = await fetch('/api/generate-code', {
276
  method: 'POST',
277
  headers: { 'Content-Type': 'application/json' },
278
- body: JSON.stringify({ prompt, language })
279
  });
 
280
  if (!response.ok) {
281
  const errorData = await response.json();
282
  throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
283
  }
284
  const data = await response.json();
285
- if (editor) {
286
- editor.setValue(data.code);
287
- monaco.editor.setModelLanguage(editor.getModel(), language);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  }
 
289
  } catch (error) {
290
- console.error('μ½”λ“œ 생성 였λ₯˜:', error);
291
- alert(`μ½”λ“œ 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error.message}`);
292
- if (editor) editor.setValue(`// μ½”λ“œ 생성 였λ₯˜: ${error.message}`);
293
  } finally {
294
- hideLoader(codeLoader);
295
- generateCodeBtn.disabled = false;
296
  }
297
- });
298
 
299
- getCompletionBtn.addEventListener('click', async () => {
 
300
  if (!editor) return;
301
 
302
  const model = editor.getModel();
303
  const position = editor.getPosition();
304
  const fullCode = model.getValue();
305
- // FIM APIλŠ” 빈 prefix/suffix도 ν—ˆμš©ν•˜λ―€λ‘œ, μ»€μ„œ μœ„μΉ˜ κΈ°μ€€μœΌλ‘œ λ‚˜λˆ”
306
  const offset = model.getOffsetAt(position);
307
  const prefix = fullCode.substring(0, offset);
308
  const suffix = fullCode.substring(offset);
309
- const language = languageSelect.value; // ν˜„μž¬ μ„ νƒλœ 에디터 μ–Έμ–΄
310
 
311
- // FIM은 빈 λ‚΄μš©μ—μ„œλ„ μž‘λ™ν•  수 μžˆμœΌλ‚˜, μ‚¬μš©μžκ°€ ν˜Όλž€μŠ€λŸ¬μšΈ 수 μžˆμœΌλ―€λ‘œ μ΅œμ†Œν•œμ˜ μž…λ ₯ μœ λ„ (선택사항)
312
- // if (prefix.trim() === '' && suffix.trim() === '') {
313
- // alert('μžλ™μ™„μ„±μ„ μœ„ν•΄ μ½”λ“œ 일뢀λ₯Ό μž…λ ₯ν•˜κ±°λ‚˜ μ»€μ„œλ₯Ό νŠΉμ • μœ„μΉ˜μ— λ‘μ„Έμš”.');
314
- // return;
315
- // }
316
  showLoader(completionLoader);
317
  getCompletionBtn.disabled = true;
318
 
@@ -320,7 +545,7 @@
320
  const response = await fetch('/api/complete-code', {
321
  method: 'POST',
322
  headers: { 'Content-Type': 'application/json' },
323
- body: JSON.stringify({ prefix, suffix, language }) // languageλŠ” FIM APIμ—μ„œ 직접 μ‚¬μš© μ•ˆλ  수 있음
324
  });
325
  if (!response.ok) {
326
  const errorData = await response.json();
@@ -328,22 +553,27 @@
328
  }
329
  const data = await response.json();
330
 
331
- // ν˜„μž¬ μ»€μ„œ μœ„μΉ˜μ— μ œμ•ˆλœ μ½”λ“œλ₯Ό μ‚½μž…
332
- editor.executeEdits("api-completion", [{
333
  range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column),
334
  text: data.completion
335
  }]);
336
- // μ‚½μž… ν›„ μ»€μ„œλ₯Ό μ œμ•ˆλœ ν…μŠ€νŠΈμ˜ 끝으둜 이동 (선택 사항)
337
- // editor.setPosition(editor.getModel().getFullModelRange().getEndPosition());
338
 
339
- } catch (error) {
340
- console.error('μ½”λ“œ μžλ™μ™„μ„± 였λ₯˜:', error);
341
- alert(`μ½”λ“œ μžλ™μ™„μ„± 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error.message}`);
 
 
 
342
  } finally {
343
  hideLoader(completionLoader);
344
  getCompletionBtn.disabled = false;
345
  }
346
- });
 
 
 
 
347
  </script>
348
  </body>
349
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI μ½”λ”© νŒŒνŠΈλ„ˆ</title>
7
  <!-- Monaco Editor λ‘œλ” CSS -->
8
  <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">
9
+ <!-- Google Fonts (macOS λŠλ‚Œ 폰트) -->
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
13
  <style>
14
+ :root {
15
+ --bg-color: #f0f2f5; /* 밝은 νšŒμƒ‰ λ°°κ²½ */
16
+ --app-bg-color: #ffffff; /* μ•± λ°°κ²½ */
17
+ --sidebar-bg-color: #e8e8e8; /* μ‚¬μ΄λ“œλ°” λ°°κ²½ (더 밝게) */
18
+ --text-color: #1c1e21; /* κΈ°λ³Έ ν…μŠ€νŠΈ */
19
+ --text-secondary-color: #606770; /* 보쑰 ν…μŠ€νŠΈ */
20
+ --accent-color: #007aff; /* macOS 블루 */
21
+ --border-color: #d1d1d7;
22
+ --editor-bg: #282c34; /* μ–΄λ‘μš΄ 에디터 λ°°κ²½ */
23
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
24
+ --border-radius: 12px;
25
+ --box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
26
+ --box-shadow-light: 0 1px 3px rgba(0,0,0,0.05);
27
+ }
28
+
29
+ /* μŠ€ν¬λ‘€λ°” μŠ€νƒ€μΌ (선택 사항) */
30
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
31
+ ::-webkit-scrollbar-track { background: transparent; }
32
+ ::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; }
33
+ ::-webkit-scrollbar-thumb:hover { background: #a1a1a1; }
34
+
35
  body {
36
+ font-family: var(--font-family);
37
  margin: 0;
38
+ background-color: var(--bg-color);
39
+ color: var(--text-color);
40
+ display: flex;
41
+ justify-content: center;
42
+ align-items: center;
43
+ min-height: 100vh;
44
  padding: 20px;
45
+ box-sizing: border-box;
46
  }
47
 
48
+ .app-container {
49
+ display: flex;
50
+ width: 95%;
51
+ max-width: 1400px;
52
+ height: 90vh;
53
+ max-height: 800px;
54
+ background-color: var(--app-bg-color);
55
+ border-radius: var(--border-radius);
56
+ box-shadow: var(--box-shadow);
57
+ overflow: hidden;
58
  }
59
 
60
+ /* μ‚¬μ΄λ“œλ°” (선택 사항 - μ—¬κΈ°μ„œλŠ” μ±„νŒ…, 에디터, 미리보기λ₯Ό μ£Ό μ˜μ—­μœΌλ‘œ) */
61
+ /* .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; } */
62
+
63
+ .main-content {
64
+ flex-grow: 1;
65
+ display: flex;
66
+ flex-direction: column;
67
+ overflow: hidden;
68
  }
69
 
70
+ .chat-editor-area {
71
  display: flex;
72
+ flex-grow: 1;
73
+ overflow: hidden; /* λ‚΄λΆ€ 슀크둀 관리 */
 
74
  }
75
 
76
+ .chat-panel {
77
  flex: 1;
78
+ display: flex;
79
+ flex-direction: column;
80
+ padding: 20px;
81
+ border-right: 1px solid var(--border-color);
82
+ overflow-y: auto; /* μ±„νŒ… 슀크둀 */
83
  }
84
 
85
+ .chat-messages {
86
+ flex-grow: 1;
87
+ margin-bottom: 15px;
88
+ overflow-y: auto;
89
+ padding-right: 10px; /* μŠ€ν¬λ‘€λ°” 곡간 */
90
  }
91
 
92
+ .message {
93
+ display: flex;
94
+ margin-bottom: 15px;
95
+ max-width: 90%;
 
 
 
 
 
96
  }
97
 
98
+ .message.user {
99
+ margin-left: auto; /* μ‚¬μš©μž λ©”μ‹œμ§€λŠ” 였λ₯Έμͺ½ μ •λ ¬ */
100
+ flex-direction: row-reverse;
101
+ }
102
+
103
+ .avatar {
104
+ width: 36px;
105
+ height: 36px;
106
+ border-radius: 50%;
107
+ background-color: var(--accent-color);
108
  color: white;
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ font-weight: 500;
113
  font-size: 16px;
114
+ margin-right: 10px;
115
+ flex-shrink: 0;
116
  }
117
+ .message.user .avatar { margin-left: 10px; margin-right: 0; background-color: #6c757d; }
118
+
119
+ .message-content {
120
+ padding: 10px 15px;
121
+ border-radius: 18px;
122
+ background-color: #e4e6eb; /* AI λ©”μ‹œμ§€ λ°°κ²½ */
123
+ color: var(--text-color);
124
+ word-wrap: break-word; /* κΈ΄ 단어 μ€„λ°”κΏˆ */
125
+ }
126
+ .message.user .message-content { background-color: var(--accent-color); color: white; }
127
 
128
+ .message-content pre { /* μ½”λ“œ 블둝 μŠ€νƒ€μΌ */
129
+ background-color: var(--editor-bg) !important; /* Monaco ν…Œλ§ˆμ™€ μœ μ‚¬ν•˜κ²Œ */
130
+ color: #abb2bf !important; /* Monaco κΈ°λ³Έ ν…μŠ€νŠΈ 색상 */
131
+ padding: 10px;
132
+ border-radius: 8px;
133
+ overflow-x: auto;
134
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
135
+ font-size: 0.9em;
136
+ white-space: pre-wrap; /* μžλ™ μ€„λ°”κΏˆ */
137
+ word-break: break-all; /* κΈ΄ μ½”λ“œ μ€„λ°”κΏˆ */
138
+ }
139
+ /* Monaco 에디터가 λ Œλ”λ§ν•œ μ½”λ“œ 블둝에 λŒ€ν•œ μŠ€νƒ€μΌ (ν•„μš” μ‹œ) */
140
+ .monaco-editor-code-block {
141
+ border-radius: 8px !important;
142
+ box-shadow: var(--box-shadow-light);
143
  }
144
 
145
+
146
+ .chat-input-area {
147
  display: flex;
 
148
  align-items: center;
149
+ padding-top: 15px;
150
+ border-top: 1px solid var(--border-color);
151
  }
152
 
153
+ .chat-input-area textarea {
 
154
  flex-grow: 1;
155
+ padding: 10px 15px;
156
+ border: 1px solid var(--border-color);
157
+ border-radius: 18px;
158
+ resize: none;
159
+ font-family: var(--font-family);
160
+ font-size: 15px;
161
+ min-height: 22px; /* 1쀄 높이 */
162
+ max-height: 100px; /* μ΅œλŒ€ 높이 */
163
+ line-height: 1.4;
164
+ margin-right: 10px;
165
+ box-sizing: border-box;
166
+ overflow-y: auto; /* λ‚΄μš© λ§Žμ•„μ§€λ©΄ 슀크둀 */
167
  }
168
+ .chat-input-area textarea:focus { border-color: var(--accent-color); outline: none; box-shadow: 0 0 0 2px rgba(0,122,255,0.2); }
169
 
170
+ .chat-input-area button {
171
+ background-color: var(--accent-color);
172
+ color: white;
173
+ border: none;
174
+ border-radius: 50%; /* μ›ν˜• λ²„νŠΌ */
175
+ width: 40px;
176
+ height: 40px;
177
+ font-size: 20px;
178
+ cursor: pointer;
179
+ display: flex;
180
+ align-items: center;
181
+ justify-content: center;
182
+ transition: background-color 0.2s;
183
  }
184
+ .chat-input-area button:hover { background-color: #0056b3; }
185
+ .chat-input-area button:disabled { background-color: #b0c4de; cursor: not-allowed; }
186
 
187
+ .editor-preview-area {
188
+ flex: 2; /* 에디터와 미리보기 μ˜μ—­μ„ 더 λ„“κ²Œ */
189
+ display: flex;
190
+ flex-direction: column;
191
+ overflow: hidden;
 
192
  }
193
+
194
+ .editor-wrapper {
195
+ flex-basis: 60%; /* 에디터가 μ°¨μ§€ν•  κΈ°λ³Έ 높이 λΉ„μœ¨ */
196
+ min-height: 200px; /* μ΅œμ†Œ 높이 */
197
+ padding: 10px; /* 에디터 μ£Όλ³€ μ—¬λ°± */
198
+ background-color: #f9f9f9; /* 에디터 μ˜μ—­ λ°°κ²½ */
199
+ display: flex;
200
+ flex-direction: column;
201
+ }
202
+ #editor-container {
203
+ width: 100% !important;
204
+ height: 100% !important; /* λΆ€λͺ¨ 높이에 맞�� */
205
+ border: 1px solid var(--border-color);
206
+ border-radius: var(--border-radius);
207
+ overflow: hidden; /* Monaco μ—λ””ν„°μ˜ 자체 슀크둀 μ‚¬μš© */
208
+ }
209
+ .editor-controls {
210
+ display: flex;
211
+ justify-content: space-between;
212
+ align-items: center;
213
+ padding: 8px 0;
214
+ }
215
+ .editor-controls select, .editor-controls button {
216
+ padding: 6px 12px;
217
+ border-radius: 6px;
218
+ border: 1px solid var(--border-color);
219
+ background-color: white;
220
+ font-family: var(--font-family);
221
+ font-size: 13px;
222
+ cursor: pointer;
223
+ }
224
+ .editor-controls button { background-color: #e9ecef; color: var(--text-color); }
225
+ .editor-controls button:hover { background-color: #d1d5db; }
226
 
227
+
228
+ .preview-wrapper {
229
+ flex-basis: 40%; /* 미리보기가 μ°¨μ§€ν•  κΈ°λ³Έ 높이 λΉ„μœ¨ */
230
+ min-height: 150px;
231
+ border-top: 1px solid var(--border-color);
232
+ padding: 10px;
233
+ background-color: #f9f9f9; /* 미리보기 μ˜μ—­ λ°°κ²½ */
234
+ display: flex; /* λ‚΄λΆ€ iframe 크기 쑰절 μœ„ν•΄ */
235
+ flex-direction: column;
236
+ }
237
+ .preview-wrapper h3 {
238
+ margin: 0 0 8px 0;
239
+ font-size: 14px;
240
+ font-weight: 500;
241
+ color: var(--text-secondary-color);
242
+ }
243
  #html-preview {
244
+ flex-grow: 1; /* 남은 곡간 λͺ¨λ‘ μ°¨μ§€ */
245
  width: 100%;
246
+ border: 1px solid var(--border-color);
247
+ border-radius: var(--border-radius);
248
+ background-color: white;
249
  }
250
+
251
+ /* λ‘œλ”© μŠ€ν”Όλ„ˆ */
252
  .loader {
253
+ display: flex;
254
+ align-items: center;
255
+ font-size: 13px;
256
+ color: var(--text-secondary-color);
257
+ margin-left: 10px; /* μž…λ ₯μ°½ μ˜†μ— ν‘œμ‹œλ  λ•Œ */
 
 
 
258
  }
259
+ .loader.inline-editor { margin-top: 5px; justify-content: center; }
260
 
261
+ .spinner {
262
+ width: 16px;
263
+ height: 16px;
264
+ border: 2px solid var(--accent-color);
265
+ border-top-color: transparent;
266
+ border-radius: 50%;
267
+ animation: spin 0.8s linear infinite;
268
+ margin-right: 8px;
269
+ }
270
+ @keyframes spin { to { transform: rotate(360deg); } }
271
+
272
+ /* λ°˜μ‘ν˜• μ‘°μ • */
273
+ @media (max-width: 1024px) {
274
+ .app-container { flex-direction: column; height: auto; max-height: none; }
275
+ .chat-editor-area { flex-direction: column; }
276
+ .chat-panel { border-right: none; border-bottom: 1px solid var(--border-color); max-height: 50vh; }
277
+ .editor-preview-area { flex: 1; } /* 남은 곡간 λͺ¨λ‘ μ°¨μ§€ */
278
  }
279
+ @media (max-width: 768px) {
280
+ body { padding: 0; }
281
+ .app-container { width: 100%; height: 100vh; border-radius: 0; }
282
+ .chat-panel { padding: 15px; }
283
+ .editor-wrapper, .preview-wrapper { padding: 8px; }
284
+ .chat-input-area textarea { font-size: 14px; }
285
+ .chat-input-area button { width: 36px; height: 36px; font-size: 18px; }
286
+ }
287
+
288
  </style>
289
  </head>
290
  <body>
291
+ <div class="app-container">
292
+ <!-- <div class="sidebar">Sidebar Content (Optional)</div> -->
293
+ <div class="main-content">
294
+ <div class="chat-editor-area">
295
+ <div class="chat-panel">
296
+ <div class="chat-messages" id="chatMessages">
297
+ <!-- AI μ‹œμž‘ λ©”μ‹œμ§€ -->
298
+ <div class="message ai">
299
+ <div class="avatar">AI</div>
300
+ <div class="message-content">
301
+ μ•ˆλ…•ν•˜μ„Έμš”! 무엇을 λ„μ™€λ“œλ¦΄κΉŒμš”? HTML 생성, μ½”λ“œ μž‘μ„±, λ˜λŠ” κΈ°μ‘΄ μ½”λ“œ μžλ™ 완성을 μš”μ²­ν•˜μ‹€ 수 μžˆμŠ΅λ‹ˆλ‹€.
302
+ </div>
303
+ </div>
304
+ </div>
305
+ <div class="chat-input-area">
306
+ <textarea id="chatInput" placeholder="여기에 λ©”μ‹œμ§€λ₯Ό μž…λ ₯ν•˜μ„Έμš”..." rows="1"></textarea>
307
+ <button id="sendButton" title="전솑">
308
+ <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>
309
+ </button>
310
+ <div class="loader" id="chatLoader" style="display: none;"><div class="spinner"></div>처리 쀑...</div>
311
+ </div>
312
  </div>
 
 
 
 
 
 
 
313
 
314
+ <div class="editor-preview-area">
315
+ <div class="editor-wrapper">
316
+ <div class="editor-controls">
317
+ <select id="languageSelect">
318
+ <option value="html">HTML</option>
319
+ <option value="css">CSS</option>
320
+ <option value="javascript" selected>JavaScript</option>
321
+ <option value="python">Python</option>
322
+ <option value="java">Java</option>
323
+ <option value="csharp">C#</option>
324
+ </select>
325
+ <button id="getCompletionBtn">AI μ½”λ“œ μ±„μš°κΈ° (FIM)</button>
326
+ </div>
327
+ <div id="editor-container"></div>
328
+ <div class="loader inline-editor" id="completionLoader" style="display: none;"><div class="spinner"></div>AIκ°€ μ½”λ“œλ₯Ό μ±„μš°λŠ” 쀑...</div>
329
+ </div>
330
+ <div class="preview-wrapper">
331
+ <h3>HTML 미리보기</h3>
332
+ <iframe id="html-preview" sandbox="allow-scripts allow-same-origin"></iframe>
333
+ </div>
334
+ </div>
335
  </div>
336
  </div>
 
 
 
 
 
 
 
 
337
  </div>
338
 
 
339
  <script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
340
  <script>
 
341
  let editor;
342
+ const chatMessages = document.getElementById('chatMessages');
343
+ const chatInput = document.getElementById('chatInput');
344
+ const sendButton = document.getElementById('sendButton');
345
+ const languageSelect = document.getElementById('languageSelect');
346
+ const getCompletionBtn = document.getElementById('getCompletionBtn');
347
+ const htmlPreviewEl = document.getElementById('html-preview');
348
+ const chatLoader = document.getElementById('chatLoader');
349
  const completionLoader = document.getElementById('completionLoader');
350
 
351
+ // --- λ‘œλ” ν‘œμ‹œ/μˆ¨κΉ€ ν•¨μˆ˜ ---
352
+ function showLoader(loaderElement) { if (loaderElement) loaderElement.style.display = 'flex'; }
353
+ function hideLoader(loaderElement) { if (loaderElement) loaderElement.style.display = 'none'; }
 
 
 
 
 
354
 
355
+ // --- Monaco Editor μ΄ˆκΈ°ν™” ---
356
  require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' }});
357
  require(['vs/editor/editor.main'], function() {
358
  editor = monaco.editor.create(document.getElementById('editor-container'), {
359
+ value: '// μ½”λ“œλ₯Ό μž‘μ„±ν•˜κ±°λ‚˜ AIμ—κ²Œ μš”μ²­ν•˜μ„Έμš”.\n// μ±„νŒ…μ°½μ— "μ‚¬κ°ν˜• λ²„νŠΌ λ§Œλ“€μ–΄μ€˜" 같이 μž…λ ₯ν•΄λ³΄μ„Έμš”.',
360
+ language: languageSelect.value,
361
+ theme: 'vs-dark', // μ–΄λ‘μš΄ ν…Œλ§ˆ
 
 
 
362
  automaticLayout: true,
363
+ wordWrap: 'on',
364
+ minimap: { enabled: true, scale: 1 },
365
+ fontSize: 14,
366
+ lineNumbers: 'on',
367
+ roundedSelection: false,
368
+ scrollBeyondLastLine: false,
369
+ readOnly: false, // μ‚¬μš©μžκ°€ 직접 νŽΈμ§‘ κ°€λŠ₯ν•˜λ„λ‘
370
+ padding: { top: 10, bottom: 10 }
371
  });
 
372
 
373
+ // μ–Έμ–΄ 선택 μ‹œ 에디터 μ–Έμ–΄ λ³€κ²½
374
+ languageSelect.addEventListener('change', () => {
375
+ monaco.editor.setModelLanguage(editor.getModel(), languageSelect.value);
376
+ });
 
 
 
377
 
378
+ // FIM λ²„νŠΌ 클릭 이벀트
379
+ getCompletionBtn.addEventListener('click', handleFIMRequest);
 
 
380
  });
381
 
382
+ // --- μ±„νŒ… λ©”μ‹œμ§€ 처리 ---
383
+ function addMessage(text, sender, isCode = false, lang = 'plaintext') {
384
+ const messageDiv = document.createElement('div');
385
+ messageDiv.classList.add('message', sender);
386
+
387
+ const avatarDiv = document.createElement('div');
388
+ avatarDiv.classList.add('avatar');
389
+ avatarDiv.textContent = sender === 'user' ? 'λ‚˜' : 'AI';
390
+ messageDiv.appendChild(avatarDiv);
391
+
392
+ const contentDiv = document.createElement('div');
393
+ contentDiv.classList.add('message-content');
394
+
395
+ if (isCode) {
396
+ // μ½”λ“œλ₯Ό Monaco Editorλ₯Ό μ‚¬μš©ν•΄ λ Œλ”λ§ (읽기 μ „μš©)
397
+ const pre = document.createElement('pre');
398
+ // κ°„λ‹¨νžˆ ν…μŠ€νŠΈλ‘œ λ„£κ±°λ‚˜, μƒˆλ‘œμš΄ Monaco μΈμŠ€ν„΄μŠ€λ₯Ό λ§Œλ“€ 수 있음.
399
+ // μ—¬κΈ°μ„œλŠ” κ°„λ‹¨νžˆ pre νƒœκ·Έλ‘œ μ²˜λ¦¬ν•˜κ³ , CSS둜 μŠ€νƒ€μΌλ§.
400
+ // μ‹€μ œλ‘œλŠ” AI μ‘λ‹΅μ—μ„œ ```html ... ``` 같은 λ§ˆν¬λ‹€μš΄ μ½”λ“œλΈ”λ‘ νŒŒμ‹± ν•„μš”
401
+ let codeContent = text;
402
+ const codeBlockMatch = text.match(/```(\w*)\n([\s\S]*?)```/);
403
+ if (codeBlockMatch) {
404
+ lang = codeBlockMatch[1] || lang;
405
+ codeContent = codeBlockMatch[2];
406
  }
407
+ pre.textContent = codeContent; // μ‹€μ œλ‘œλŠ” highlight.js λ“±μœΌλ‘œ ν•˜μ΄λΌμ΄νŒ… 적용
408
+ contentDiv.appendChild(pre);
409
+
410
+ // μ½”λ“œ 에디터에 μ½”λ“œ μ‚½μž… λ²„νŠΌ μΆ”κ°€ (선택 사항)
411
+ const insertButton = document.createElement('button');
412
+ insertButton.textContent = '에디터에 μ‚½μž…';
413
+ insertButton.style.marginTop = '8px';
414
+ insertButton.style.padding = '5px 10px';
415
+ insertButton.style.fontSize = '12px';
416
+ insertButton.style.backgroundColor = '#f0f0f0';
417
+ insertButton.style.color = '#333';
418
+ insertButton.style.border = '1px solid #ccc';
419
+ insertButton.style.borderRadius = '4px';
420
+ insertButton.style.cursor = 'pointer';
421
+ insertButton.onclick = () => {
422
+ if (editor) {
423
+ editor.setValue(codeContent);
424
+ monaco.editor.setModelLanguage(editor.getModel(), lang);
425
+ if (lang.toLowerCase() === 'html') {
426
+ htmlPreviewEl.srcdoc = codeContent;
427
+ }
428
+ }
429
+ };
430
+ contentDiv.appendChild(insertButton);
431
+
432
+ } else {
433
+ contentDiv.textContent = text;
434
  }
435
+
436
+ messageDiv.appendChild(contentDiv);
437
+ chatMessages.appendChild(messageDiv);
438
+ chatMessages.scrollTop = chatMessages.scrollHeight; // 항상 μ΅œμ‹  λ©”μ‹œμ§€ 보이도둝 슀크둀
439
+ }
440
+
441
+ // --- μ±„νŒ… μž…λ ₯ 처리 ---
442
+ chatInput.addEventListener('input', () => { // textarea 높이 μžλ™ 쑰절
443
+ chatInput.style.height = 'auto';
444
+ chatInput.style.height = (chatInput.scrollHeight) + 'px';
445
  });
446
 
447
+ chatInput.addEventListener('keypress', (e) => {
448
+ if (e.key === 'Enter' && !e.shiftKey) { // Shift+EnterλŠ” μ€„λ°”κΏˆ
449
+ e.preventDefault();
450
+ handleSendMessage();
451
+ }
452
+ });
453
+ sendButton.addEventListener('click', handleSendMessage);
454
+
455
+ async function handleSendMessage() {
456
+ const messageText = chatInput.value.trim();
457
+ if (!messageText) return;
458
+
459
+ addMessage(messageText, 'user');
460
+ chatInput.value = '';
461
+ chatInput.style.height = 'auto'; // 높이 μ΄ˆκΈ°ν™”
462
+ sendButton.disabled = true;
463
+ showLoader(chatLoader);
464
+
465
+ // AI 응닡 처리 (κ°„λ‹¨ν•œ κ·œμΉ™ 기반으둜 μ–΄λ–€ API ν˜ΈμΆœν• μ§€ κ²°μ •)
466
+ let endpoint = '/api/generate-code'; // 기본은 μ½”λ“œ 생성
467
+ let payload = { prompt: messageText, language: languageSelect.value };
468
+ let responseIsCode = true;
469
+ let responseLang = languageSelect.value;
470
+
471
+ if (messageText.toLowerCase().includes('html λ§Œλ“€μ–΄μ€˜') || messageText.toLowerCase().includes('μ›ΉνŽ˜μ΄μ§€ λ§Œλ“€μ–΄μ€˜') || messageText.toLowerCase().includes('html둜')) {
472
+ endpoint = '/api/generate-html';
473
+ payload = { prompt: messageText };
474
+ responseLang = 'html';
475
+ } else if (messageText.toLowerCase().includes('λ‹€μŒ μ½”λ“œ μ™„μ„±ν•΄μ€˜') || messageText.toLowerCase().includes('μ½”λ“œ μ±„μ›Œμ€˜')) {
476
+ // 이 κ²½μš°λŠ” FIM λ²„νŠΌμ„ μ‚¬μš©ν•˜λ„λ‘ μœ λ„ν•˜κ±°λ‚˜, 에디터 λ‚΄μš©μ„ 가져와야 함.
477
+ // μ—¬κΈ°μ„œλŠ” 일반 μ½”λ“œ μƒμ„±μœΌλ‘œ 처리.
478
+ addMessage("AI: μ½”λ“œ μžλ™ μ™„μ„±(FIM)은 에디터 μœ„μ˜ 'AI μ½”λ“œ μ±„μš°κΈ°' λ²„νŠΌμ„ μ‚¬μš©ν•΄μ£Όμ„Έμš”.", 'ai');
479
+ hideLoader(chatLoader);
480
+ sendButton.disabled = false;
481
  return;
482
  }
483
+
 
484
 
485
  try {
486
+ const response = await fetch(endpoint, {
487
  method: 'POST',
488
  headers: { 'Content-Type': 'application/json' },
489
+ body: JSON.stringify(payload)
490
  });
491
+
492
  if (!response.ok) {
493
  const errorData = await response.json();
494
  throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
495
  }
496
  const data = await response.json();
497
+
498
+ let aiResponseText = '';
499
+ if (endpoint === '/api/generate-html') {
500
+ aiResponseText = data.html;
501
+ addMessage("AI: μš”μ²­ν•˜μ‹  HTML μ½”λ“œμž…λ‹ˆλ‹€.", 'ai');
502
+ addMessage(aiResponseText, 'ai', true, 'html');
503
+ if (editor && confirm("μƒμ„±λœ HTML을 에디터에 ν‘œμ‹œν•˜κ³  미리보기λ₯Ό μ—…λ°μ΄νŠΈν• κΉŒμš”?")) {
504
+ editor.setValue(aiResponseText);
505
+ monaco.editor.setModelLanguage(editor.getModel(), 'html');
506
+ htmlPreviewEl.srcdoc = aiResponseText;
507
+ } else {
508
+ htmlPreviewEl.srcdoc = aiResponseText; // λ―Έλ¦¬λ³΄κΈ°λŠ” 항상 μ—…λ°μ΄νŠΈ
509
+ }
510
+ } else if (endpoint === '/api/generate-code') {
511
+ aiResponseText = data.code;
512
+ addMessage(`AI: μš”μ²­ν•˜μ‹  ${responseLang} μ½”λ“œμž…λ‹ˆλ‹€.`, 'ai');
513
+ addMessage(aiResponseText, 'ai', true, responseLang);
514
+ if (editor && confirm(`μƒμ„±λœ ${responseLang} μ½”λ“œλ₯Ό 에디터에 ν‘œμ‹œν• κΉŒμš”?`)) {
515
+ editor.setValue(aiResponseText);
516
+ monaco.editor.setModelLanguage(editor.getModel(), responseLang);
517
+ }
518
  }
519
+
520
  } catch (error) {
521
+ console.error('API Error:', error);
522
+ addMessage(`AI: μ£„μ†‘ν•©λ‹ˆλ‹€. μš”μ²­ 처리 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ${error.message}`, 'ai');
 
523
  } finally {
524
+ hideLoader(chatLoader);
525
+ sendButton.disabled = false;
526
  }
527
+ }
528
 
529
+ // --- FIM μš”μ²­ 처리 ---
530
+ async function handleFIMRequest() {
531
  if (!editor) return;
532
 
533
  const model = editor.getModel();
534
  const position = editor.getPosition();
535
  const fullCode = model.getValue();
 
536
  const offset = model.getOffsetAt(position);
537
  const prefix = fullCode.substring(0, offset);
538
  const suffix = fullCode.substring(offset);
539
+ const language = languageSelect.value;
540
 
 
 
 
 
 
541
  showLoader(completionLoader);
542
  getCompletionBtn.disabled = true;
543
 
 
545
  const response = await fetch('/api/complete-code', {
546
  method: 'POST',
547
  headers: { 'Content-Type': 'application/json' },
548
+ body: JSON.stringify({ prefix, suffix, language })
549
  });
550
  if (!response.ok) {
551
  const errorData = await response.json();
 
553
  }
554
  const data = await response.json();
555
 
556
+ editor.executeEdits("api-fim-completion", [{
 
557
  range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column),
558
  text: data.completion
559
  }]);
560
+ addMessage(`AI: μ½”λ“œλ₯Ό μ±„μ›Œ λ„£μ—ˆμŠ΅λ‹ˆλ‹€. (에디터 확인)`, 'ai');
 
561
 
562
+
563
+ } catch (error)
564
+ {
565
+ console.error('FIM Error:', error);
566
+ addMessage(`AI: μ½”λ“œ μžλ™ μ™„μ„± 쀑 였λ₯˜: ${error.message}`, 'ai');
567
+ alert(`μ½”λ“œ μžλ™ μ™„μ„± 쀑 였λ₯˜: ${error.message}`);
568
  } finally {
569
  hideLoader(completionLoader);
570
  getCompletionBtn.disabled = false;
571
  }
572
+ }
573
+
574
+ // 초기 λ©”μ‹œμ§€ 이후 첫 μž…λ ₯ μ‹œ μ•ˆλ‚΄ (선택)
575
+ // chatInput.addEventListener('focus', () => { ... }, { once: true });
576
+
577
  </script>
578
  </body>
579
  </html>