Faaz commited on
Commit
ea7a00c
·
1 Parent(s): 18d863a

frontend: fix StackBlitz launcher (was opening default Next.js starter)

Browse files

Bug: the previous form-POST to https://stackblitz.com/run was being
silently rejected and StackBlitz fell back to opening their default
'stackblitz-starters/nextjs' template instead of the user's actual code
(verified in user screenshot — file tree shows next.config.js / postcss
/ tailwind.config.ts which we never generate).

Root cause: the bare /run endpoint has known reliability issues with
SameSite cookie / referrer policy in modern browsers; payloads silently
404 and StackBlitz redirects to a default starter.

Fix:
- Load the official @stackblitz/sdk@1.11.0 UMD bundle from unpkg.
- launchInStackBlitz() now prefers sdk.openProject({...}, {newWindow,
openFile}) which uses a maintained iframe handshake. Form-POST is
kept as a fallback if the SDK script fails to load (offline / strict
CSP).
- chooseEntryFile() picks the most likely landing file (app/page.tsx,
src/App.tsx, index.html, ...) so users land on the right tab.

Bonus UX fix for the secondary complaint (model produced HTML when
user asked for Next.js):
- Extracted detectProjectKind() so the same logic powers both the
builder and a new pill on every code block: 'HTML' / 'Next.js' /
'React' / 'Node.js' / 'Snippet', color-coded.
- Users now SEE that the model output HTML even though they asked
for Next.js — set expectations honestly instead of having the
launcher silently mis-template.

Files:
- frontend/index.html: load sdk.umd.js before app.js
- frontend/app.js: new detectProjectKind / projectKindLabel /
chooseEntryFile, refactored launchInStackBlitz with SDK + fallback,
renderMarkdown shows kind pill in code-block header
- frontend/styles.css: .md-lang wrapper + .md-kind pill with
per-kind colors (purple Next, blue React, green Node, orange HTML)

Files changed (3) hide show
  1. frontend/app.js +76 -14
  2. frontend/index.html +6 -0
  3. frontend/styles.css +22 -0
frontend/app.js CHANGED
@@ -277,21 +277,44 @@
277
  return /<!doctype|<html|^\s*import |^\s*export |^\s*function |^\s*const |^\s*class /im.test(code);
278
  }
279
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  // Decide which StackBlitz template + file layout to use based on what
281
  // the model produced. We try to be permissive — anything that looks
282
  // like a React/Next/Node project goes into the WebContainer-backed
283
  // 'node' template; raw HTML uses the static 'html' template.
284
  function buildStackBlitzProject(code, lang) {
 
285
  const l = (lang || '').toLowerCase();
286
- const looksLikeNext = /from ['"]next\/|next\.config|app\/page\.[jt]sx|pages\/index/i.test(code);
287
- const looksLikeReact = /from ['"]react['"]|ReactDOM\.|useState\(|useEffect\(|<\w+\s+\w+={/i.test(code);
288
- const looksLikeNode = /^\s*(?:const|import)\s+\w+\s*=?\s*require\(|process\.env|module\.exports/m.test(code);
289
  const isHtmlDoc = /<!doctype|<html/i.test(code);
290
 
291
  const title = 'MINDI generated project';
292
  const description = 'Generated by MINDI 1.5 Vision-Coder';
293
 
294
- if (looksLikeNext) {
295
  // Minimal Next.js 14 app-router project.
296
  return {
297
  title, description,
@@ -323,7 +346,7 @@
323
  };
324
  }
325
 
326
- if (looksLikeReact || l === 'jsx' || l === 'tsx') {
327
  // Vite + React project (faster boot in WebContainer than CRA).
328
  const ext = (l === 'tsx' || /\:\s*\w+(\[\])?/.test(code)) ? 'tsx' : 'jsx';
329
  return {
@@ -348,7 +371,7 @@
348
  };
349
  }
350
 
351
- if (looksLikeNode || l === 'json') {
352
  return {
353
  title, description,
354
  template: 'node',
@@ -362,7 +385,7 @@
362
  };
363
  }
364
 
365
- if (isHtmlDoc || l === 'html' || l === 'markup') {
366
  return {
367
  title, description,
368
  template: 'html',
@@ -383,13 +406,49 @@
383
  };
384
  }
385
 
386
- // Hand the project off to stackblitz.com via a hidden form POST.
387
- // The new tab opens the cloud IDE with all files pre-loaded and the
388
- // dev server booting. For 'node' templates this means a real Node.js
389
- // runtime in the browser via WebContainers — yes, that includes
390
- // 'npm install' for Next.js / React projects.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  function launchInStackBlitz(code, lang) {
392
  const proj = buildStackBlitzProject(code, lang);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  const form = document.createElement('form');
394
  form.action = 'https://stackblitz.com/run';
395
  form.method = 'POST';
@@ -407,7 +466,6 @@
407
  add('project[title]', proj.title);
408
  add('project[description]', proj.description);
409
  add('project[template]', proj.template);
410
- add('project[settings][compile][trigger]', 'auto');
411
  Object.entries(proj.files).forEach(([path, content]) => {
412
  add(`project[files][${path}]`, content);
413
  });
@@ -502,6 +560,10 @@
502
  const safe = escapeHtml(seg.value);
503
  const dataCode = escapeAttr(seg.value);
504
  const runnable = isCloudRunnable(seg.value, lang);
 
 
 
 
505
  const launchBtns = runnable
506
  ? (
507
  `<button class="md-run" data-code="${dataCode}" data-lang="${escapeAttr(lang)}" type="button" title="Run this code on stackblitz.com (real Node.js / WebContainer sandbox, supports Next.js / React / Node)">\u25B6 StackBlitz</button>` +
@@ -511,7 +573,7 @@
511
  return (
512
  `<pre class="md-code-block">` +
513
  `<div class="md-code-head">` +
514
- `<span>${escapeHtml(lang)}</span>` +
515
  `<div class="md-code-actions">` +
516
  launchBtns +
517
  `<button class="md-copy" data-code="${dataCode}" type="button">Copy</button>` +
 
277
  return /<!doctype|<html|^\s*import |^\s*export |^\s*function |^\s*const |^\s*class /im.test(code);
278
  }
279
 
280
+ // Detect the kind of project the model produced. Returns one of:
281
+ // 'next' | 'react' | 'node' | 'html' | 'snippet'
282
+ // The detection has to be permissive but ordered (Next.js before React
283
+ // before Node) so a Next.js file with `import 'react'` doesn't mis-route.
284
+ function detectProjectKind(code, lang) {
285
+ const l = (lang || '').toLowerCase();
286
+ const looksLikeNext = /from ['"]next\/|next\.config|app\/page\.[jt]sx|pages\/index|getServerSideProps|getStaticProps/i.test(code);
287
+ const looksLikeReact = /from ['"]react['"]|ReactDOM\.|useState\(|useEffect\(|<\w+\s+\w+={/i.test(code);
288
+ const looksLikeNode = /^\s*(?:const|import)\s+\w+\s*=?\s*require\(|process\.env|module\.exports/m.test(code);
289
+ const isHtmlDoc = /<!doctype|<html/i.test(code);
290
+
291
+ if (looksLikeNext) return 'next';
292
+ if (looksLikeReact || l === 'jsx' || l === 'tsx') return 'react';
293
+ if (looksLikeNode || l === 'json') return 'node';
294
+ if (isHtmlDoc || l === 'html' || l === 'markup') return 'html';
295
+ return 'snippet';
296
+ }
297
+
298
+ // Human-friendly label shown on the code-block header pill so users see
299
+ // exactly what we detected (and why a launcher might open as HTML when
300
+ // they asked for Next.js).
301
+ function projectKindLabel(kind) {
302
+ return ({ next: 'Next.js', react: 'React', node: 'Node.js', html: 'HTML', snippet: 'Snippet' })[kind] || kind;
303
+ }
304
+
305
  // Decide which StackBlitz template + file layout to use based on what
306
  // the model produced. We try to be permissive — anything that looks
307
  // like a React/Next/Node project goes into the WebContainer-backed
308
  // 'node' template; raw HTML uses the static 'html' template.
309
  function buildStackBlitzProject(code, lang) {
310
+ const kind = detectProjectKind(code, lang);
311
  const l = (lang || '').toLowerCase();
 
 
 
312
  const isHtmlDoc = /<!doctype|<html/i.test(code);
313
 
314
  const title = 'MINDI generated project';
315
  const description = 'Generated by MINDI 1.5 Vision-Coder';
316
 
317
+ if (kind === 'next') {
318
  // Minimal Next.js 14 app-router project.
319
  return {
320
  title, description,
 
346
  };
347
  }
348
 
349
+ if (kind === 'react') {
350
  // Vite + React project (faster boot in WebContainer than CRA).
351
  const ext = (l === 'tsx' || /\:\s*\w+(\[\])?/.test(code)) ? 'tsx' : 'jsx';
352
  return {
 
371
  };
372
  }
373
 
374
+ if (kind === 'node') {
375
  return {
376
  title, description,
377
  template: 'node',
 
385
  };
386
  }
387
 
388
+ if (kind === 'html') {
389
  return {
390
  title, description,
391
  template: 'html',
 
406
  };
407
  }
408
 
409
+ // Pick the file the user most likely wants to land on when StackBlitz opens.
410
+ function chooseEntryFile(proj) {
411
+ const files = proj.files || {};
412
+ const preferred = [
413
+ 'app/page.tsx', 'app/page.jsx',
414
+ 'src/App.tsx', 'src/App.jsx',
415
+ 'pages/index.tsx', 'pages/index.jsx',
416
+ 'index.html', 'index.js',
417
+ ];
418
+ for (const p of preferred) if (files[p]) return p;
419
+ return Object.keys(files)[0] || 'index.html';
420
+ }
421
+
422
+ // Hand the project off to stackblitz.com. We prefer the official SDK
423
+ // (https://developer.stackblitz.com/platform/api/javascript-sdk) because
424
+ // the bare /run form-POST endpoint silently rejects some payloads and
425
+ // falls back to opening their default Next.js starter — exactly what
426
+ // the user reported. The SDK uses an iframe handshake that's more
427
+ // reliable across browser SameSite / referrer policies.
428
+ // Form POST is kept as a fallback if the SDK script fails to load.
429
  function launchInStackBlitz(code, lang) {
430
  const proj = buildStackBlitzProject(code, lang);
431
+ const sdk = window.StackBlitzSDK;
432
+
433
+ if (sdk && typeof sdk.openProject === 'function') {
434
+ try {
435
+ sdk.openProject(
436
+ {
437
+ title: proj.title,
438
+ description: proj.description,
439
+ template: proj.template,
440
+ files: proj.files,
441
+ },
442
+ { newWindow: true, openFile: chooseEntryFile(proj) }
443
+ );
444
+ return;
445
+ } catch (err) {
446
+ console.warn('StackBlitz SDK launch failed, falling back to form POST:', err);
447
+ }
448
+ }
449
+
450
+ // Fallback: classic form POST. Less reliable but works without the SDK
451
+ // script (useful if it's blocked by an offline / strict-CSP environment).
452
  const form = document.createElement('form');
453
  form.action = 'https://stackblitz.com/run';
454
  form.method = 'POST';
 
466
  add('project[title]', proj.title);
467
  add('project[description]', proj.description);
468
  add('project[template]', proj.template);
 
469
  Object.entries(proj.files).forEach(([path, content]) => {
470
  add(`project[files][${path}]`, content);
471
  });
 
560
  const safe = escapeHtml(seg.value);
561
  const dataCode = escapeAttr(seg.value);
562
  const runnable = isCloudRunnable(seg.value, lang);
563
+ const kind = runnable ? detectProjectKind(seg.value, lang) : null;
564
+ const kindPill = kind
565
+ ? `<span class="md-kind md-kind--${kind}" title="Project kind detected from the generated code. The launchers below open this exact code, even if it doesn't match what you originally asked for.">${projectKindLabel(kind)}</span>`
566
+ : '';
567
  const launchBtns = runnable
568
  ? (
569
  `<button class="md-run" data-code="${dataCode}" data-lang="${escapeAttr(lang)}" type="button" title="Run this code on stackblitz.com (real Node.js / WebContainer sandbox, supports Next.js / React / Node)">\u25B6 StackBlitz</button>` +
 
573
  return (
574
  `<pre class="md-code-block">` +
575
  `<div class="md-code-head">` +
576
+ `<span class="md-lang">${escapeHtml(lang)}${kindPill}</span>` +
577
  `<div class="md-code-actions">` +
578
  launchBtns +
579
  `<button class="md-copy" data-code="${dataCode}" type="button">Copy</button>` +
frontend/index.html CHANGED
@@ -344,6 +344,12 @@
344
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
345
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-sql.min.js"></script>
346
 
 
 
 
 
 
 
347
  <script src="sandbox.js"></script>
348
  <script src="agent.js"></script>
349
  <script src="app.js"></script>
 
344
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
345
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-sql.min.js"></script>
346
 
347
+ <!-- StackBlitz SDK — drives the "Run in StackBlitz" launcher.
348
+ The form-POST /run endpoint is unreliable (gets silently rejected
349
+ and falls back to the default Next.js starter); the SDK's hidden
350
+ iframe handshake is the maintained, robust path. -->
351
+ <script src="https://unpkg.com/@stackblitz/sdk@1.11.0/bundles/sdk.umd.js"></script>
352
+
353
  <script src="sandbox.js"></script>
354
  <script src="agent.js"></script>
355
  <script src="app.js"></script>
frontend/styles.css CHANGED
@@ -686,6 +686,28 @@ body.sidebar-open .scrim { opacity: 1; pointer-events: auto; }
686
  border-bottom: 1px solid var(--border);
687
  }
688
  .md-code-head span:first-child { color: var(--c-code); font-weight: 500; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
  .md-code-actions {
690
  display: flex;
691
  gap: 6px;
 
686
  border-bottom: 1px solid var(--border);
687
  }
688
  .md-code-head span:first-child { color: var(--c-code); font-weight: 500; }
689
+ .md-lang {
690
+ display: inline-flex;
691
+ align-items: center;
692
+ gap: 8px;
693
+ }
694
+ .md-kind {
695
+ font-family: var(--mono);
696
+ font-size: 10px;
697
+ font-weight: 600;
698
+ letter-spacing: .04em;
699
+ text-transform: none;
700
+ padding: 2px 7px;
701
+ border-radius: 999px;
702
+ border: 1px solid currentColor;
703
+ background: rgba(255, 255, 255, .03);
704
+ cursor: help;
705
+ }
706
+ .md-kind--next { color: #f8fafc; }
707
+ .md-kind--react { color: #60a5fa; }
708
+ .md-kind--node { color: #34d399; }
709
+ .md-kind--html { color: #fb923c; }
710
+ .md-kind--snippet { color: var(--text-mute); }
711
  .md-code-actions {
712
  display: flex;
713
  gap: 6px;