alexrs commited on
Commit
3f166c3
Β·
1 Parent(s): 4536743

Fix preview and UI behavior

Browse files
Files changed (2) hide show
  1. app.py +2 -2
  2. index.html +261 -31
app.py CHANGED
@@ -347,8 +347,8 @@ def _targeted_prompt(
347
  "If the user asks a coding question or wants reasoning that does not require running code, "
348
  "answer directly without a fenced block. If they ask to generate, revise, or fix runnable "
349
  "web code, return one ```html fenced block only. The code is rendered inside a sandboxed "
350
- "iframe that spans the full width of the preview panel and is about 680px tall, so design "
351
- "the page to fill that iframe responsively: html/body at margin:0 and 100% width/height, "
352
  "avoid fixed widths larger than the iframe, and resize any <canvas> to its container "
353
  "(including on window resize) so the whole app is visible without horizontal scrolling."
354
  f"{context_block}\n\n"
 
347
  "If the user asks a coding question or wants reasoning that does not require running code, "
348
  "answer directly without a fenced block. If they ask to generate, revise, or fix runnable "
349
  "web code, return one ```html fenced block only. The code is rendered inside a sandboxed "
350
+ "iframe that fills the preview panel, so design the page to fill that iframe responsively: "
351
+ "html/body at margin:0 and 100% width/height, "
352
  "avoid fixed widths larger than the iframe, and resize any <canvas> to its container "
353
  "(including on window resize) so the whole app is visible without horizontal scrolling."
354
  f"{context_block}\n\n"
index.html CHANGED
@@ -102,6 +102,24 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
102
  }
103
  #config-warning.visible { display: block; }
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  /* ═══════════════════════════════════════════════════════
106
  HEADER
107
  ═══════════════════════════════════════════════════════ */
@@ -359,15 +377,21 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
359
  border-radius: var(--radius);
360
  background: rgba(255,179,0,0.03);
361
  }
362
- .think-block summary {
 
 
 
 
363
  padding: 6px 10px;
364
  cursor: pointer;
365
  font-size: 12px;
 
 
366
  color: var(--gray-dim);
367
  user-select: none;
368
  transition: color var(--transition);
369
  }
370
- .think-block summary:hover { color: var(--amber); }
371
  .think-block .think-content {
372
  padding: 6px 12px 10px;
373
  font-size: 12px;
@@ -375,6 +399,9 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
375
  line-height: 1.55;
376
  border-top: 1px solid rgba(255,179,0,0.1);
377
  }
 
 
 
378
 
379
  /* Streaming cursor */
380
  .streaming-cursor::after {
@@ -557,6 +584,7 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
557
  width: 45%;
558
  min-width: 340px;
559
  max-width: 55%;
 
560
  background: var(--bg-panel);
561
  }
562
 
@@ -590,22 +618,27 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
590
 
591
  #output-content {
592
  flex: 1;
593
- overflow: auto;
 
594
  position: relative;
595
  }
596
 
597
  /* Tab panes */
598
- .tab-pane { display: none; height: 100%; }
599
  .tab-pane.active { display: flex; flex-direction: column; }
600
 
601
  /* Preview tab */
602
  #pane-preview {
603
- align-items: center;
604
- justify-content: center;
605
  position: relative;
 
 
606
  }
607
 
608
  .preview-placeholder {
 
 
609
  text-align: center;
610
  color: var(--gray-dim);
611
  padding: 40px 20px;
@@ -631,9 +664,11 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
631
 
632
  #preview-iframe {
633
  display: none;
 
 
634
  width: 100%;
635
- flex: 1;
636
- min-height: 680px;
637
  border: none;
638
  background: #fff;
639
  }
@@ -897,6 +932,13 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
897
  </div>
898
  </header>
899
 
 
 
 
 
 
 
 
900
  <!-- Main Layout -->
901
  <div id="main">
902
  <!-- Terminal Panel -->
@@ -1008,7 +1050,12 @@ const state = {
1008
  activeTab: 'preview',
1009
  lastExecution: null,
1010
  lastCode: '',
1011
- lastCodeLang: ''
 
 
 
 
 
1012
  };
1013
 
1014
  // ═══════════════════════════════════════════════════════
@@ -1020,12 +1067,14 @@ document.addEventListener('DOMContentLoaded', () => {
1020
 
1021
  if (CONFIG.model_url) {
1022
  document.getElementById('model-pill').href = CONFIG.model_url;
 
1023
  }
1024
  if (CONFIG.model_id) {
1025
  document.getElementById('model-pill-text').textContent = CONFIG.model_id;
1026
  }
1027
  if (CONFIG.opencode_url) {
1028
  document.getElementById('opencode-pill').href = CONFIG.opencode_url;
 
1029
  }
1030
 
1031
  // Config warning
@@ -1048,6 +1097,12 @@ document.addEventListener('DOMContentLoaded', () => {
1048
  handleSend();
1049
  }
1050
  });
 
 
 
 
 
 
1051
  });
1052
 
1053
  function autoResize() {
@@ -1072,6 +1127,8 @@ function renderExamples() {
1072
  chip.textContent = ex.label;
1073
  chip.title = ex.prompt;
1074
  chip.addEventListener('click', () => {
 
 
1075
  if (ex.target) setTarget(ex.target);
1076
  sendMessage(ex.prompt);
1077
  });
@@ -1119,6 +1176,7 @@ function addAssistantMessage() {
1119
  div.id = 'current-assistant-msg';
1120
  div.innerHTML = `<span class="msg-prefix">north&gt;</span><span class="msg-content streaming-cursor"></span>`;
1121
  container.appendChild(div);
 
1122
  scrollToBottom();
1123
  return div;
1124
  }
@@ -1127,7 +1185,12 @@ function updateAssistantMessage(content, isStreaming) {
1127
  const div = document.getElementById('current-assistant-msg');
1128
  if (!div) return;
1129
  const contentEl = div.querySelector('.msg-content');
 
 
1130
  contentEl.innerHTML = parseMarkdown(content);
 
 
 
1131
  if (isStreaming) {
1132
  contentEl.classList.add('streaming-cursor');
1133
  } else {
@@ -1158,13 +1221,18 @@ function scrollToBottom() {
1158
  function parseMarkdown(text) {
1159
  if (!text) return '';
1160
 
1161
- // Handle think blocks
 
1162
  text = text.replace(/<think>([\s\S]*?)<\/think>/g, (_, content) => {
1163
- return `<details class="think-block"><summary>πŸ’­ Reasoning (click to expand)</summary><div class="think-content">${escapeHtml(content.trim())}</div></details>`;
 
 
1164
  });
1165
  // Handle unclosed think blocks (during streaming)
1166
  text = text.replace(/<think>([\s\S]*)$/g, (_, content) => {
1167
- return `<details class="think-block"><summary>πŸ’­ Reasoning (thinking…)</summary><div class="think-content">${escapeHtml(content.trim())}</div></details>`;
 
 
1168
  });
1169
 
1170
  // Extract code blocks first to prevent inner processing
@@ -1172,20 +1240,12 @@ function parseMarkdown(text) {
1172
  text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
1173
  const idx = codeBlocks.length;
1174
  codeBlocks.push({ lang: lang || 'text', code: code.trimEnd() });
1175
- return `\x00CODEBLOCK_${idx}\x00`;
1176
  });
1177
 
1178
  // Escape HTML in remaining text
1179
  text = escapeHtml(text);
1180
 
1181
- // Restore code blocks with formatting
1182
- text = text.replace(/\x00CODEBLOCK_(\d+)\x00/g, (_, idx) => {
1183
- const block = codeBlocks[parseInt(idx)];
1184
- const escapedCode = escapeHtml(block.code);
1185
- const id = `code-${Date.now()}-${idx}`;
1186
- return `<div class="code-block-wrap"><div class="code-block-header"><span class="code-lang">${escapeHtml(block.lang)}</span><button class="btn-copy" onclick="copyBlock(this, '${id}')">πŸ“‹ Copy</button></div><pre><code id="${id}">${escapedCode}</code></pre></div>`;
1187
- });
1188
-
1189
  // Inline formatting
1190
  text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1191
  text = text.replace(/(?<!\*)\*([^*]+?)\*(?!\*)/g, '<em>$1</em>');
@@ -1211,6 +1271,15 @@ function parseMarkdown(text) {
1211
  return match;
1212
  });
1213
 
 
 
 
 
 
 
 
 
 
1214
  // Line breaks (preserve paragraph structure)
1215
  text = text.replace(/\n\n/g, '</p><p>');
1216
  text = text.replace(/\n/g, '<br>');
@@ -1218,12 +1287,60 @@ function parseMarkdown(text) {
1218
 
1219
  // Clean up empty paragraphs
1220
  text = text.replace(/<p>\s*<\/p>/g, '');
1221
- text = text.replace(/<p>(<(?:div|ul|ol|h[1-3]|details))/g, '$1');
1222
- text = text.replace(/(<\/(?:div|ul|ol|h[1-3]|details)>)<\/p>/g, '$1');
1223
 
1224
  return text;
1225
  }
1226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1227
  function escapeHtml(text) {
1228
  const div = document.createElement('div');
1229
  div.textContent = text;
@@ -1291,7 +1408,8 @@ function renderStatus(text, statusState) {
1291
  // ═══════════════════════════════════════════════════════
1292
  // OUTPUT PANEL
1293
  // ═══════════════════════════════════════════════════════
1294
- function switchTab(tab) {
 
1295
  state.activeTab = tab;
1296
  document.querySelectorAll('.output-tab').forEach((btn) => {
1297
  btn.classList.toggle('active', btn.dataset.tab === tab);
@@ -1299,6 +1417,9 @@ function switchTab(tab) {
1299
  document.querySelectorAll('.tab-pane').forEach((pane) => {
1300
  pane.classList.toggle('active', pane.id === `pane-${tab}`);
1301
  });
 
 
 
1302
  }
1303
 
1304
  function renderExecution(execution) {
@@ -1332,7 +1453,7 @@ function renderExecution(execution) {
1332
  // Preview
1333
  const placeholder = document.getElementById('preview-placeholder');
1334
  const img = document.getElementById('preview-image');
1335
- const iframe = document.getElementById('preview-iframe');
1336
  const fsBtn = document.getElementById('btn-fullscreen');
1337
 
1338
  if (execution.image_url) {
@@ -1347,11 +1468,15 @@ function renderExecution(execution) {
1347
  } else if (execution.target === 'web' && execution.code) {
1348
  placeholder.style.display = 'none';
1349
  img.style.display = 'none';
1350
- iframe.srcdoc = execution.code;
1351
  iframe.style.display = 'block';
1352
  fsBtn.style.display = 'block';
 
 
 
1353
  if (state.activeTab !== 'console' && state.activeTab !== 'code') {
1354
- switchTab('preview');
 
 
1355
  }
1356
  } else {
1357
  // No visual preview, but maybe there's stdout
@@ -1364,10 +1489,11 @@ function renderExecution(execution) {
1364
  }
1365
 
1366
  function resetOutput() {
 
1367
  document.getElementById('preview-placeholder').style.display = '';
1368
  document.getElementById('preview-image').style.display = 'none';
1369
- document.getElementById('preview-iframe').style.display = 'none';
1370
- document.getElementById('preview-iframe').srcdoc = '';
1371
  document.getElementById('btn-fullscreen').style.display = 'none';
1372
  document.getElementById('console-stdout').textContent = 'No output.';
1373
  document.getElementById('console-stderr').textContent = 'No errors.';
@@ -1377,11 +1503,100 @@ function resetOutput() {
1377
  state.lastExecution = null;
1378
  state.lastCode = '';
1379
  state.lastCodeLang = '';
 
 
 
1380
  }
1381
 
1382
  // ═══════════════════════════════════════════════════════
1383
  // FULLSCREEN
1384
  // ═══════════════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1385
  function openFullscreen() {
1386
  const overlay = document.getElementById('fullscreen-overlay');
1387
  const iframe = document.getElementById('fullscreen-iframe');
@@ -1396,6 +1611,14 @@ function closeFullscreen() {
1396
  document.getElementById('fullscreen-iframe').srcdoc = '';
1397
  }
1398
 
 
 
 
 
 
 
 
 
1399
  // ═══════════════════════════════════════════════════════
1400
  // SEND / RECEIVE
1401
  // ═══════════════════════════════════════════════════════
@@ -1575,12 +1798,13 @@ function stopGeneration() {
1575
  onGenerationEnd();
1576
  }
1577
 
1578
- function newChat() {
1579
  state.history = [];
1580
  state.executionContext = {};
1581
  state.lastExecution = null;
1582
  state.lastCode = '';
1583
  state.lastCodeLang = '';
 
1584
 
1585
  if (state.currentEventSource) {
1586
  state.currentEventSource.close();
@@ -1593,7 +1817,13 @@ function newChat() {
1593
  resetOutput();
1594
  switchTab('preview');
1595
  renderStatus('Idle', 'idle');
1596
- addSystemMessage(`Session reset. Welcome back to ${CONFIG.app_title || 'North Mini Code 1.0'}.`);
 
 
 
 
 
 
1597
  }
1598
  </script>
1599
  </body>
 
102
  }
103
  #config-warning.visible { display: block; }
104
 
105
+ #playground-banner {
106
+ background: linear-gradient(90deg, rgba(0,212,255,0.08), rgba(57,255,20,0.05));
107
+ border-bottom: 1px solid var(--border);
108
+ color: var(--gray-mid);
109
+ font-size: 12px;
110
+ padding: 7px 18px;
111
+ text-align: center;
112
+ flex-shrink: 0;
113
+ }
114
+ #playground-banner strong {
115
+ color: var(--gray-light);
116
+ font-weight: 600;
117
+ }
118
+ #playground-banner a {
119
+ color: var(--cyan);
120
+ font-weight: 600;
121
+ }
122
+
123
  /* ═══════════════════════════════════════════════════════
124
  HEADER
125
  ═══════════════════════════════════════════════════════ */
 
377
  border-radius: var(--radius);
378
  background: rgba(255,179,0,0.03);
379
  }
380
+ .think-summary {
381
+ display: block;
382
+ width: 100%;
383
+ background: transparent;
384
+ border: none;
385
  padding: 6px 10px;
386
  cursor: pointer;
387
  font-size: 12px;
388
+ font-family: var(--font-mono);
389
+ text-align: left;
390
  color: var(--gray-dim);
391
  user-select: none;
392
  transition: color var(--transition);
393
  }
394
+ .think-summary:hover { color: var(--amber); }
395
  .think-block .think-content {
396
  padding: 6px 12px 10px;
397
  font-size: 12px;
 
399
  line-height: 1.55;
400
  border-top: 1px solid rgba(255,179,0,0.1);
401
  }
402
+ .think-block:not(.open) .think-content {
403
+ display: none;
404
+ }
405
 
406
  /* Streaming cursor */
407
  .streaming-cursor::after {
 
584
  width: 45%;
585
  min-width: 340px;
586
  max-width: 55%;
587
+ min-height: 0;
588
  background: var(--bg-panel);
589
  }
590
 
 
618
 
619
  #output-content {
620
  flex: 1;
621
+ min-height: 0;
622
+ overflow: hidden;
623
  position: relative;
624
  }
625
 
626
  /* Tab panes */
627
+ .tab-pane { display: none; height: 100%; min-height: 0; }
628
  .tab-pane.active { display: flex; flex-direction: column; }
629
 
630
  /* Preview tab */
631
  #pane-preview {
632
+ align-items: stretch;
633
+ justify-content: stretch;
634
  position: relative;
635
+ min-height: 0;
636
+ overflow: hidden;
637
  }
638
 
639
  .preview-placeholder {
640
+ align-self: center;
641
+ margin: auto;
642
  text-align: center;
643
  color: var(--gray-dim);
644
  padding: 40px 20px;
 
664
 
665
  #preview-iframe {
666
  display: none;
667
+ position: absolute;
668
+ inset: 0;
669
  width: 100%;
670
+ height: 100%;
671
+ min-height: 0;
672
  border: none;
673
  background: #fff;
674
  }
 
932
  </div>
933
  </header>
934
 
935
+ <div id="playground-banner">
936
+ <a id="banner-model-link" href="https://huggingface.co/CohereLabs/North-Mini-Code-1.0" target="_blank" rel="noopener"><strong>North-Mini-Code-1.0</strong></a>
937
+ is built for agentic coding and works best in your terminal with
938
+ <a id="opencode-banner-link" href="#" target="_blank" rel="noopener">OpenCode β†—</a>.
939
+ This Space is a browser playground for trying the model.
940
+ </div>
941
+
942
  <!-- Main Layout -->
943
  <div id="main">
944
  <!-- Terminal Panel -->
 
1050
  activeTab: 'preview',
1051
  lastExecution: null,
1052
  lastCode: '',
1053
+ lastCodeLang: '',
1054
+ pendingWebPreviewCode: '',
1055
+ loadedWebPreviewCode: '',
1056
+ scheduledWebPreviewCode: '',
1057
+ reasoningExpanded: false,
1058
+ lastReasoningPressAt: 0
1059
  };
1060
 
1061
  // ═══════════════════════════════════════════════════════
 
1067
 
1068
  if (CONFIG.model_url) {
1069
  document.getElementById('model-pill').href = CONFIG.model_url;
1070
+ document.getElementById('banner-model-link').href = CONFIG.model_url;
1071
  }
1072
  if (CONFIG.model_id) {
1073
  document.getElementById('model-pill-text').textContent = CONFIG.model_id;
1074
  }
1075
  if (CONFIG.opencode_url) {
1076
  document.getElementById('opencode-pill').href = CONFIG.opencode_url;
1077
+ document.getElementById('opencode-banner-link').href = CONFIG.opencode_url;
1078
  }
1079
 
1080
  // Config warning
 
1097
  handleSend();
1098
  }
1099
  });
1100
+
1101
+ document.addEventListener('pointerdown', handleReasoningPress, true);
1102
+ document.addEventListener('mousedown', handleReasoningPress, true);
1103
+ document.addEventListener('keydown', handleReasoningKeydown, true);
1104
+ document.addEventListener('keydown', handleFullscreenKeydown);
1105
+ observePreviewSize();
1106
  });
1107
 
1108
  function autoResize() {
 
1127
  chip.textContent = ex.label;
1128
  chip.title = ex.prompt;
1129
  chip.addEventListener('click', () => {
1130
+ if (state.isGenerating) return;
1131
+ resetConversation();
1132
  if (ex.target) setTarget(ex.target);
1133
  sendMessage(ex.prompt);
1134
  });
 
1176
  div.id = 'current-assistant-msg';
1177
  div.innerHTML = `<span class="msg-prefix">north&gt;</span><span class="msg-content streaming-cursor"></span>`;
1178
  container.appendChild(div);
1179
+ state.reasoningExpanded = false;
1180
  scrollToBottom();
1181
  return div;
1182
  }
 
1185
  const div = document.getElementById('current-assistant-msg');
1186
  if (!div) return;
1187
  const contentEl = div.querySelector('.msg-content');
1188
+ const keepReasoningExpanded = state.reasoningExpanded || Boolean(contentEl.querySelector('.think-block.open'));
1189
+ state.reasoningExpanded = keepReasoningExpanded;
1190
  contentEl.innerHTML = parseMarkdown(content);
1191
+ contentEl.querySelectorAll('.think-block').forEach((block) => {
1192
+ setReasoningBlockOpen(block, keepReasoningExpanded);
1193
+ });
1194
  if (isStreaming) {
1195
  contentEl.classList.add('streaming-cursor');
1196
  } else {
 
1221
  function parseMarkdown(text) {
1222
  if (!text) return '';
1223
 
1224
+ // Extract reasoning blocks before escaping so they render as collapsed details.
1225
+ const thinkBlocks = [];
1226
  text = text.replace(/<think>([\s\S]*?)<\/think>/g, (_, content) => {
1227
+ const idx = thinkBlocks.length;
1228
+ thinkBlocks.push(renderThinkBlock(content, 'πŸ’­ Reasoning (click to expand)'));
1229
+ return `@@THINKBLOCK_${idx}@@`;
1230
  });
1231
  // Handle unclosed think blocks (during streaming)
1232
  text = text.replace(/<think>([\s\S]*)$/g, (_, content) => {
1233
+ const idx = thinkBlocks.length;
1234
+ thinkBlocks.push(renderThinkBlock(content, 'πŸ’­ Reasoning (thinking…)'));
1235
+ return `@@THINKBLOCK_${idx}@@`;
1236
  });
1237
 
1238
  // Extract code blocks first to prevent inner processing
 
1240
  text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
1241
  const idx = codeBlocks.length;
1242
  codeBlocks.push({ lang: lang || 'text', code: code.trimEnd() });
1243
+ return `@@CODEBLOCK_${idx}@@`;
1244
  });
1245
 
1246
  // Escape HTML in remaining text
1247
  text = escapeHtml(text);
1248
 
 
 
 
 
 
 
 
 
1249
  // Inline formatting
1250
  text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1251
  text = text.replace(/(?<!\*)\*([^*]+?)\*(?!\*)/g, '<em>$1</em>');
 
1271
  return match;
1272
  });
1273
 
1274
+ // Restore block-level placeholders after inline parsing so their contents remain literal.
1275
+ text = text.replace(/@@CODEBLOCK_(\d+)@@/g, (_, idx) => {
1276
+ const block = codeBlocks[parseInt(idx)];
1277
+ const escapedCode = escapeHtml(block.code);
1278
+ const id = `code-${Date.now()}-${idx}`;
1279
+ return `<div class="code-block-wrap"><div class="code-block-header"><span class="code-lang">${escapeHtml(block.lang)}</span><button class="btn-copy" onclick="copyBlock(this, '${id}')">πŸ“‹ Copy</button></div><pre><code id="${id}">${escapedCode}</code></pre></div>`;
1280
+ });
1281
+ text = text.replace(/@@THINKBLOCK_(\d+)@@/g, (_, idx) => thinkBlocks[parseInt(idx)]);
1282
+
1283
  // Line breaks (preserve paragraph structure)
1284
  text = text.replace(/\n\n/g, '</p><p>');
1285
  text = text.replace(/\n/g, '<br>');
 
1287
 
1288
  // Clean up empty paragraphs
1289
  text = text.replace(/<p>\s*<\/p>/g, '');
1290
+ text = text.replace(/<p>(<(?:div|ul|ol|h[1-3]))/g, '$1');
1291
+ text = text.replace(/(<\/(?:div|ul|ol|h[1-3])>)<\/p>/g, '$1');
1292
 
1293
  return text;
1294
  }
1295
 
1296
+ function renderThinkBlock(content, summary) {
1297
+ const escapedContent = escapeHtml(content.trim()).replace(/\n/g, '<br>');
1298
+ const openClass = state.reasoningExpanded ? ' open' : '';
1299
+ const expanded = state.reasoningExpanded ? 'true' : 'false';
1300
+ return `<div class="think-block${openClass}"><button type="button" class="think-summary" aria-expanded="${expanded}">${summary}</button><div class="think-content">${escapedContent}</div></div>`;
1301
+ }
1302
+
1303
+ function handleReasoningPress(event) {
1304
+ updateReasoningFromEvent(event);
1305
+ }
1306
+
1307
+ function handleReasoningKeydown(event) {
1308
+ if (event.key !== 'Enter' && event.key !== ' ') return;
1309
+ updateReasoningFromEvent(event);
1310
+ }
1311
+
1312
+ function updateReasoningFromEvent(event) {
1313
+ if (event.type === 'mousedown' && Date.now() - state.lastReasoningPressAt < 500) {
1314
+ return;
1315
+ }
1316
+ const target = event.target;
1317
+ if (!target || !target.closest) return;
1318
+ const button = target.closest('.think-summary');
1319
+ if (!button) return;
1320
+ const block = button.closest('.think-block');
1321
+ if (!block) return;
1322
+ event.preventDefault();
1323
+ event.stopPropagation();
1324
+ if (event.stopImmediatePropagation) {
1325
+ event.stopImmediatePropagation();
1326
+ }
1327
+ state.lastReasoningPressAt = Date.now();
1328
+ const nextOpen = !block.classList.contains('open');
1329
+ state.reasoningExpanded = nextOpen;
1330
+ const scope = block.closest('.msg-content') || document;
1331
+ scope.querySelectorAll('.think-block').forEach((trace) => {
1332
+ setReasoningBlockOpen(trace, nextOpen);
1333
+ });
1334
+ }
1335
+
1336
+ function setReasoningBlockOpen(block, open) {
1337
+ block.classList.toggle('open', open);
1338
+ const button = block.querySelector('.think-summary');
1339
+ if (button) {
1340
+ button.setAttribute('aria-expanded', open ? 'true' : 'false');
1341
+ }
1342
+ }
1343
+
1344
  function escapeHtml(text) {
1345
  const div = document.createElement('div');
1346
  div.textContent = text;
 
1408
  // ═══════════════════════════════════════════════════════
1409
  // OUTPUT PANEL
1410
  // ═══════════════════════════════════════════════════════
1411
+ function switchTab(tab, { forcePreviewReload = false } = {}) {
1412
+ const wasPreview = state.activeTab === 'preview';
1413
  state.activeTab = tab;
1414
  document.querySelectorAll('.output-tab').forEach((btn) => {
1415
  btn.classList.toggle('active', btn.dataset.tab === tab);
 
1417
  document.querySelectorAll('.tab-pane').forEach((pane) => {
1418
  pane.classList.toggle('active', pane.id === `pane-${tab}`);
1419
  });
1420
+ if (tab === 'preview') {
1421
+ ensureWebPreviewLoaded({ forceReload: forcePreviewReload || !wasPreview });
1422
+ }
1423
  }
1424
 
1425
  function renderExecution(execution) {
 
1453
  // Preview
1454
  const placeholder = document.getElementById('preview-placeholder');
1455
  const img = document.getElementById('preview-image');
1456
+ const iframe = getPreviewIframe();
1457
  const fsBtn = document.getElementById('btn-fullscreen');
1458
 
1459
  if (execution.image_url) {
 
1468
  } else if (execution.target === 'web' && execution.code) {
1469
  placeholder.style.display = 'none';
1470
  img.style.display = 'none';
 
1471
  iframe.style.display = 'block';
1472
  fsBtn.style.display = 'block';
1473
+ state.pendingWebPreviewCode = execution.code;
1474
+ state.loadedWebPreviewCode = '';
1475
+ state.scheduledWebPreviewCode = '';
1476
  if (state.activeTab !== 'console' && state.activeTab !== 'code') {
1477
+ switchTab('preview', { forcePreviewReload: true });
1478
+ } else {
1479
+ iframe.srcdoc = '';
1480
  }
1481
  } else {
1482
  // No visual preview, but maybe there's stdout
 
1489
  }
1490
 
1491
  function resetOutput() {
1492
+ const iframe = getPreviewIframe();
1493
  document.getElementById('preview-placeholder').style.display = '';
1494
  document.getElementById('preview-image').style.display = 'none';
1495
+ iframe.style.display = 'none';
1496
+ iframe.srcdoc = '';
1497
  document.getElementById('btn-fullscreen').style.display = 'none';
1498
  document.getElementById('console-stdout').textContent = 'No output.';
1499
  document.getElementById('console-stderr').textContent = 'No errors.';
 
1503
  state.lastExecution = null;
1504
  state.lastCode = '';
1505
  state.lastCodeLang = '';
1506
+ state.pendingWebPreviewCode = '';
1507
+ state.loadedWebPreviewCode = '';
1508
+ state.scheduledWebPreviewCode = '';
1509
  }
1510
 
1511
  // ═══════════════════════════════════════════════════════
1512
  // FULLSCREEN
1513
  // ═══════════════════════════════════════════════════════
1514
+ function getPreviewIframe() {
1515
+ return document.getElementById('preview-iframe');
1516
+ }
1517
+
1518
+ function recreatePreviewIframe() {
1519
+ const oldFrame = getPreviewIframe();
1520
+ const freshFrame = document.createElement('iframe');
1521
+ freshFrame.id = 'preview-iframe';
1522
+ freshFrame.setAttribute('sandbox', 'allow-scripts');
1523
+ freshFrame.style.display = oldFrame.style.display || 'block';
1524
+ oldFrame.replaceWith(freshFrame);
1525
+ return freshFrame;
1526
+ }
1527
+
1528
+ function ensureWebPreviewLoaded({ forceReload = false } = {}) {
1529
+ const iframe = getPreviewIframe();
1530
+ if (!state.pendingWebPreviewCode || state.activeTab !== 'preview' || iframe.style.display === 'none') {
1531
+ return;
1532
+ }
1533
+ if (!forceReload && state.loadedWebPreviewCode === state.pendingWebPreviewCode) {
1534
+ schedulePreviewResize(iframe);
1535
+ return;
1536
+ }
1537
+ if (!forceReload && state.scheduledWebPreviewCode === state.pendingWebPreviewCode) {
1538
+ return;
1539
+ }
1540
+
1541
+ state.scheduledWebPreviewCode = state.pendingWebPreviewCode;
1542
+ iframe.srcdoc = '';
1543
+ const loadWhenLaidOut = () => {
1544
+ if (state.activeTab !== 'preview' || !state.pendingWebPreviewCode) {
1545
+ state.scheduledWebPreviewCode = '';
1546
+ return;
1547
+ }
1548
+ if (!forceReload && state.loadedWebPreviewCode === state.pendingWebPreviewCode) return;
1549
+ const visibleFrame = getPreviewIframe();
1550
+ const rect = visibleFrame.getBoundingClientRect();
1551
+ if (rect.width < 10 || rect.height < 10) {
1552
+ state.scheduledWebPreviewCode = '';
1553
+ setTimeout(() => ensureWebPreviewLoaded({ forceReload }), 50);
1554
+ return;
1555
+ }
1556
+ const freshFrame = recreatePreviewIframe();
1557
+ freshFrame.srcdoc = state.pendingWebPreviewCode;
1558
+ state.loadedWebPreviewCode = state.pendingWebPreviewCode;
1559
+ state.scheduledWebPreviewCode = '';
1560
+ schedulePreviewResize(freshFrame);
1561
+ };
1562
+ requestAnimationFrame(() => requestAnimationFrame(loadWhenLaidOut));
1563
+ setTimeout(loadWhenLaidOut, 75);
1564
+ }
1565
+
1566
+ function schedulePreviewResize(iframe) {
1567
+ const dispatchResize = () => {
1568
+ try {
1569
+ iframe.contentWindow?.dispatchEvent(new Event('resize'));
1570
+ } catch (_err) {
1571
+ // Sandboxed srcdoc frames may reject parent-origin access; visible-load timing is the main fix.
1572
+ }
1573
+ };
1574
+ requestAnimationFrame(() => {
1575
+ requestAnimationFrame(dispatchResize);
1576
+ });
1577
+ setTimeout(dispatchResize, 100);
1578
+ setTimeout(dispatchResize, 350);
1579
+ }
1580
+
1581
+ function observePreviewSize() {
1582
+ const previewPane = document.getElementById('pane-preview');
1583
+ if (!previewPane) return;
1584
+
1585
+ window.addEventListener('resize', () => {
1586
+ if (state.activeTab === 'preview' && state.loadedWebPreviewCode) {
1587
+ schedulePreviewResize(getPreviewIframe());
1588
+ }
1589
+ });
1590
+
1591
+ if (typeof ResizeObserver === 'undefined') return;
1592
+ const observer = new ResizeObserver(() => {
1593
+ if (state.activeTab === 'preview' && state.loadedWebPreviewCode) {
1594
+ schedulePreviewResize(getPreviewIframe());
1595
+ }
1596
+ });
1597
+ observer.observe(previewPane);
1598
+ }
1599
+
1600
  function openFullscreen() {
1601
  const overlay = document.getElementById('fullscreen-overlay');
1602
  const iframe = document.getElementById('fullscreen-iframe');
 
1611
  document.getElementById('fullscreen-iframe').srcdoc = '';
1612
  }
1613
 
1614
+ function handleFullscreenKeydown(event) {
1615
+ if (event.key !== 'Escape') return;
1616
+ const overlay = document.getElementById('fullscreen-overlay');
1617
+ if (!overlay.classList.contains('active')) return;
1618
+ event.preventDefault();
1619
+ closeFullscreen();
1620
+ }
1621
+
1622
  // ═══════════════════════════════════════════════════════
1623
  // SEND / RECEIVE
1624
  // ═══════════════════════════════════════════════════════
 
1798
  onGenerationEnd();
1799
  }
1800
 
1801
+ function resetConversation(announcement) {
1802
  state.history = [];
1803
  state.executionContext = {};
1804
  state.lastExecution = null;
1805
  state.lastCode = '';
1806
  state.lastCodeLang = '';
1807
+ state.reasoningExpanded = false;
1808
 
1809
  if (state.currentEventSource) {
1810
  state.currentEventSource.close();
 
1817
  resetOutput();
1818
  switchTab('preview');
1819
  renderStatus('Idle', 'idle');
1820
+ if (announcement) {
1821
+ addSystemMessage(announcement);
1822
+ }
1823
+ }
1824
+
1825
+ function newChat() {
1826
+ resetConversation(`Session reset. Welcome back to ${CONFIG.app_title || 'North Mini Code 1.0'}.`);
1827
  }
1828
  </script>
1829
  </body>