interactive-renderer.tsx 2.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. 'use client';
  2. import { useMemo } from 'react';
  3. import type { InteractiveContent } from '@/lib/types/stage';
  4. interface InteractiveRendererProps {
  5. readonly content: InteractiveContent;
  6. readonly mode: 'autonomous' | 'playback';
  7. readonly sceneId: string;
  8. }
  9. export function InteractiveRenderer({ content, mode: _mode, sceneId }: InteractiveRendererProps) {
  10. const patchedHtml = useMemo(
  11. () => (content.html ? patchHtmlForIframe(content.html) : undefined),
  12. [content.html],
  13. );
  14. return (
  15. <div className="w-full h-full relative">
  16. <iframe
  17. srcDoc={patchedHtml}
  18. src={patchedHtml ? undefined : content.url}
  19. className="absolute inset-0 w-full h-full border-0"
  20. title={`Interactive Scene ${sceneId}`}
  21. sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
  22. />
  23. </div>
  24. );
  25. }
  26. /**
  27. * Patch embedded HTML to display correctly inside an iframe.
  28. *
  29. * Fixes:
  30. * - min-h-screen / h-screen → use 100% of iframe viewport
  31. * - Ensure html/body fill the iframe with no overflow issues
  32. * - Canvas elements use container sizing instead of viewport
  33. */
  34. function patchHtmlForIframe(html: string): string {
  35. const iframeCss = `<style data-iframe-patch>
  36. html, body {
  37. width: 100%;
  38. height: 100%;
  39. margin: 0;
  40. padding: 0;
  41. overflow-x: hidden;
  42. overflow-y: auto;
  43. }
  44. /* Fix min-h-screen: in iframes 100vh is the iframe height, which is correct,
  45. but ensure body actually fills it */
  46. body { min-height: 100vh; }
  47. </style>`;
  48. // Insert right after <head> or at the start of the document
  49. const headIdx = html.indexOf('<head>');
  50. if (headIdx !== -1) {
  51. const insertPos = headIdx + 6; // after <head>
  52. return html.substring(0, insertPos) + '\n' + iframeCss + html.substring(insertPos);
  53. }
  54. const headWithAttrs = html.indexOf('<head ');
  55. if (headWithAttrs !== -1) {
  56. const closeAngle = html.indexOf('>', headWithAttrs);
  57. if (closeAngle !== -1) {
  58. const insertPos = closeAngle + 1;
  59. return html.substring(0, insertPos) + '\n' + iframeCss + html.substring(insertPos);
  60. }
  61. }
  62. // Fallback: prepend
  63. return iframeCss + html;
  64. }