@ifc-lite/viewer 1.14.2 → 1.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/assets/{Arrow.dom-CSgnLhN4.js → Arrow.dom-_vGzMMKs.js} +1 -1
  3. package/dist/assets/basketViewActivator-BZcoCL3V.js +1 -0
  4. package/dist/assets/{browser-qSKWrKQW.js → browser-Czmf34bo.js} +1 -1
  5. package/dist/assets/ifc-lite_bg-DyBKoGgk.wasm +0 -0
  6. package/dist/assets/index-CMQ_Dgkr.css +1 -0
  7. package/dist/assets/index-D7nEDctQ.js +229 -0
  8. package/dist/assets/{index-4Y4XaV8N.js → index-DX-Qf5fA.js} +72669 -61673
  9. package/dist/assets/{native-bridge-CSFDsEkg.js → native-bridge-DAOWftxE.js} +1 -1
  10. package/dist/assets/{wasm-bridge-Zf90ysEm.js → wasm-bridge-D7jYpn8a.js} +1 -1
  11. package/dist/index.html +2 -2
  12. package/package.json +21 -20
  13. package/src/App.tsx +17 -1
  14. package/src/components/viewer/BasketPresentationDock.tsx +8 -4
  15. package/src/components/viewer/ChatPanel.tsx +1402 -0
  16. package/src/components/viewer/CodeEditor.tsx +70 -4
  17. package/src/components/viewer/CommandPalette.tsx +1 -0
  18. package/src/components/viewer/HierarchyPanel.tsx +28 -13
  19. package/src/components/viewer/MainToolbar.tsx +113 -95
  20. package/src/components/viewer/ScriptPanel.tsx +351 -184
  21. package/src/components/viewer/UpgradePage.tsx +69 -0
  22. package/src/components/viewer/Viewport.tsx +23 -0
  23. package/src/components/viewer/chat/ChatMessage.tsx +144 -0
  24. package/src/components/viewer/chat/ExecutableCodeBlock.tsx +416 -0
  25. package/src/components/viewer/chat/ModelSelector.tsx +102 -0
  26. package/src/components/viewer/chat/renderTextContent.test.ts +23 -0
  27. package/src/components/viewer/chat/renderTextContent.ts +19 -0
  28. package/src/components/viewer/hierarchy/HierarchyNode.tsx +10 -3
  29. package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +126 -0
  30. package/src/components/viewer/hierarchy/treeDataBuilder.ts +139 -38
  31. package/src/components/viewer/hierarchy/types.ts +6 -1
  32. package/src/components/viewer/hierarchy/useHierarchyTree.ts +27 -12
  33. package/src/hooks/useIfcCache.ts +1 -2
  34. package/src/hooks/useSandbox.ts +122 -6
  35. package/src/index.css +10 -0
  36. package/src/lib/attachments.ts +46 -0
  37. package/src/lib/llm/ClerkChatSync.tsx +74 -0
  38. package/src/lib/llm/clerk-auth.ts +62 -0
  39. package/src/lib/llm/code-extractor.ts +50 -0
  40. package/src/lib/llm/context-builder.test.ts +18 -0
  41. package/src/lib/llm/context-builder.ts +305 -0
  42. package/src/lib/llm/free-models.test.ts +118 -0
  43. package/src/lib/llm/message-capabilities.test.ts +131 -0
  44. package/src/lib/llm/message-capabilities.ts +94 -0
  45. package/src/lib/llm/models.ts +197 -0
  46. package/src/lib/llm/repair-loop.test.ts +91 -0
  47. package/src/lib/llm/repair-loop.ts +76 -0
  48. package/src/lib/llm/script-diagnostics.ts +445 -0
  49. package/src/lib/llm/script-edit-ops.test.ts +399 -0
  50. package/src/lib/llm/script-edit-ops.ts +954 -0
  51. package/src/lib/llm/script-preflight.test.ts +513 -0
  52. package/src/lib/llm/script-preflight.ts +990 -0
  53. package/src/lib/llm/script-preservation.test.ts +128 -0
  54. package/src/lib/llm/script-preservation.ts +152 -0
  55. package/src/lib/llm/stream-client.test.ts +97 -0
  56. package/src/lib/llm/stream-client.ts +410 -0
  57. package/src/lib/llm/system-prompt.test.ts +181 -0
  58. package/src/lib/llm/system-prompt.ts +665 -0
  59. package/src/lib/llm/types.ts +150 -0
  60. package/src/lib/scripts/templates/bim-globals.d.ts +226 -7
  61. package/src/lib/scripts/templates/create-building.ts +12 -12
  62. package/src/main.tsx +10 -1
  63. package/src/sdk/adapters/export-adapter.test.ts +24 -0
  64. package/src/sdk/adapters/export-adapter.ts +40 -16
  65. package/src/sdk/adapters/files-adapter.ts +39 -0
  66. package/src/sdk/adapters/model-compat.ts +1 -1
  67. package/src/sdk/adapters/mutate-adapter.ts +20 -6
  68. package/src/sdk/adapters/mutation-view.ts +112 -0
  69. package/src/sdk/adapters/query-adapter.ts +100 -4
  70. package/src/sdk/local-backend.ts +4 -0
  71. package/src/store/index.ts +15 -1
  72. package/src/store/slices/chatSlice.test.ts +325 -0
  73. package/src/store/slices/chatSlice.ts +468 -0
  74. package/src/store/slices/scriptSlice.test.ts +75 -0
  75. package/src/store/slices/scriptSlice.ts +256 -9
  76. package/src/vite-env.d.ts +10 -0
  77. package/vite.config.ts +21 -2
  78. package/dist/assets/ifc-lite_bg-BOvNXJA_.wasm +0 -0
  79. package/dist/assets/index-ByrFvN5A.css +0 -1
  80. package/dist/assets/index-CN7qDq7G.js +0 -216
@@ -14,6 +14,12 @@ import { useCallback, useEffect, useRef } from 'react';
14
14
  import { useBim } from '../sdk/BimProvider.js';
15
15
  import { useViewerStore } from '../store/index.js';
16
16
  import type { Sandbox, ScriptResult, SandboxConfig } from '@ifc-lite/sandbox';
17
+ import { validateScriptPreflightDetailed } from '../lib/llm/script-preflight.js';
18
+ import {
19
+ createRuntimeDiagnostic,
20
+ formatDiagnosticsForDisplay,
21
+ type RuntimeScriptDiagnostic,
22
+ } from '../lib/llm/script-diagnostics.js';
17
23
 
18
24
  /** Type guard for ScriptError shape (has logs + durationMs) */
19
25
  function isScriptError(err: unknown): err is { message: string; logs: Array<{ level: string; args: unknown[]; timestamp: number }>; durationMs: number } {
@@ -27,6 +33,101 @@ function isScriptError(err: unknown): err is { message: string; logs: Array<{ le
27
33
  );
28
34
  }
29
35
 
36
+ function augmentScriptError(message: string, code?: string): { message: string; diagnostics: RuntimeScriptDiagnostic[] } {
37
+ const lower = message.toLowerCase();
38
+ const source = code ?? '';
39
+ const missingIdent = /['"]([A-Za-z_]\w*)['"] is not defined/i.exec(message)?.[1];
40
+ const looksDetachedCreateSnippet = /\bbim\.create\.[A-Za-z]+\(\s*h\s*,/.test(source)
41
+ && !/\b(?:const|let|var)\s+h\b/.test(source)
42
+ && !/bim\.create\.project\(/.test(source);
43
+ const looksWorldPlacementScript = /\bbim\.create\.(addIfcCurtainWall|addIfcMember|addIfcPlate)\(/.test(source)
44
+ && /\baddIfcBuildingStorey\(/.test(source)
45
+ && /\bconst\s+elevation\b|\bz\s*=/.test(source);
46
+
47
+ if (lower.includes(`can't access property "location", placement is undefined`)) {
48
+ const diagnostic = createRuntimeDiagnostic(
49
+ 'generic_placement_contract',
50
+ 'Likely cause: a generic `bim.create.addElement(...)` payload is using `Position` or missing `Placement.Location`. Use `Placement: { Location: [x, y, z] }` and `Depth`.',
51
+ 'error',
52
+ { methodName: 'addElement', symbol: 'Placement.Location', fixHint: 'Use `Placement: { Location: [...] }` and include `Depth`.' },
53
+ );
54
+ return { message: `${message}\n${diagnostic.message}`, diagnostics: [diagnostic] };
55
+ }
56
+ if (lower.includes('invalid creator handle')) {
57
+ const diagnostic = createRuntimeDiagnostic(
58
+ 'generic_placement_contract',
59
+ 'Likely cause: the script finalized or invalidated the active creator handle before later create calls completed. Move `bim.create.toIfc(h)` to the end and do not reuse a finalized handle.',
60
+ 'error',
61
+ {
62
+ symbol: 'h',
63
+ failureKind: 'creator_lifecycle',
64
+ rootCauseKey: 'creator_lifecycle_violation',
65
+ repairScope: 'structural',
66
+ fixHint: 'Finalize the model only once, after all create calls are done.',
67
+ },
68
+ );
69
+ return { message: `${message}\n${diagnostic.message}`, diagnostics: [diagnostic] };
70
+ }
71
+ if (lower.includes(`can't access property "tostring", v is undefined`)) {
72
+ if (/\bbim\.create\.addIfcPlate\(/.test(source) && /\bHeight\s*:/.test(source) && !/\bDepth\s*:/.test(source)) {
73
+ const diagnostic = createRuntimeDiagnostic(
74
+ 'plate_contract_mismatch',
75
+ 'Likely cause: `bim.create.addIfcPlate(...)` was given slab-style keys. Re-check the plate contract and use `Position`, `Width`, `Depth`, and `Thickness` instead of `Height`.',
76
+ 'error',
77
+ { methodName: 'addIfcPlate', symbol: 'Height', fixHint: 'Use `Position`, `Width`, `Depth`, and `Thickness` for plates.' },
78
+ );
79
+ return { message: `${message}\n${diagnostic.message}`, diagnostics: [diagnostic] };
80
+ }
81
+ if (looksWorldPlacementScript) {
82
+ const diagnostic = createRuntimeDiagnostic(
83
+ 'world_placement_elevation',
84
+ 'Likely cause: a repeated world-placement method (such as `addIfcCurtainWall(...)`, `addIfcMember(...)`, or `addIfcPlate(...)`) is missing the current level elevation in its Z coordinates. These methods do not inherit storey-relative Z automatically.',
85
+ 'error',
86
+ {
87
+ failureKind: 'world_placement',
88
+ repairScope: 'block',
89
+ fixHint: 'Include the current level/storey elevation in `Start`, `End`, or `Position` Z coordinates.',
90
+ },
91
+ );
92
+ return { message: `${message}\n${diagnostic.message}`, diagnostics: [diagnostic] };
93
+ }
94
+ return {
95
+ message: `${message}\nLikely cause: a required numeric geometry field is missing or undefined (commonly \`Elevation\`, \`Width\`, \`Depth\`, \`Height\`, or \`Thickness\`). Re-check the exact required keys for the create method you called.`,
96
+ diagnostics: [],
97
+ };
98
+ }
99
+ if (lower.includes(`'position' is not defined`) || lower.includes(`"position" is not defined`)) {
100
+ return {
101
+ message: `${message}\nLikely cause: the script contains a malformed BIM object literal or transpilation fallback corrupted a plain JS key like \`Position: [...]\`. Re-send the exact object with explicit key-value pairs.`,
102
+ diagnostics: [],
103
+ };
104
+ }
105
+ if (missingIdent && ['h', 'storey', 'width', 'depth', 'i', 'z'].includes(missingIdent) && looksDetachedCreateSnippet) {
106
+ const diagnostic = createRuntimeDiagnostic(
107
+ 'detached_snippet_scope',
108
+ `Likely cause: the fix replaced the full script with a detached fragment that still depends on outer variables like \`${missingIdent}\`. Preserve the surrounding project/storey/loop context and patch the existing script in place.`,
109
+ 'error',
110
+ {
111
+ symbol: missingIdent,
112
+ failureKind: 'detached_snippet',
113
+ repairScope: 'structural',
114
+ fixHint: 'Patch the existing script instead of returning a smaller fragment.',
115
+ },
116
+ );
117
+ return { message: `${message}\n${diagnostic.message}`, diagnostics: [diagnostic] };
118
+ }
119
+ if (lower.includes('rotated') && lower.includes('window') && lower.includes('wall')) {
120
+ const diagnostic = createRuntimeDiagnostic(
121
+ 'wall_hosted_opening_alignment',
122
+ 'Likely cause: a standalone `bim.create.addIfcWindow(...)` was used where a wall-hosted insert was needed. Use `bim.create.addIfcWallWindow(...)` or wall `Openings` for wall-aligned placement.',
123
+ 'error',
124
+ { methodName: 'addIfcWindow', fixHint: 'Use `addIfcWallWindow(...)` or wall `Openings` for wall-aligned placement.' },
125
+ );
126
+ return { message: `${message}\n${diagnostic.message}`, diagnostics: [diagnostic] };
127
+ }
128
+ return { message, diagnostics: [] };
129
+ }
130
+
30
131
  /**
31
132
  * Hook that provides a sandbox execution interface.
32
133
  *
@@ -41,18 +142,30 @@ export function useSandbox(config?: SandboxConfig) {
41
142
  const setExecutionState = useViewerStore((s) => s.setScriptExecutionState);
42
143
  const setResult = useViewerStore((s) => s.setScriptResult);
43
144
  const setError = useViewerStore((s) => s.setScriptError);
145
+ const setDiagnostics = useViewerStore((s) => s.setScriptDiagnostics);
44
146
 
45
147
  /** Execute a script in an isolated sandbox context */
46
148
  const execute = useCallback(async (code: string): Promise<ScriptResult | null> => {
47
149
  setExecutionState('running');
48
150
  setError(null);
151
+ setDiagnostics([]);
152
+
153
+ const preflightDiagnostics = validateScriptPreflightDetailed(code);
154
+ if (preflightDiagnostics.length > 0) {
155
+ const preflightErrors = formatDiagnosticsForDisplay(preflightDiagnostics);
156
+ setError(
157
+ `Preflight validation failed:\n${preflightErrors.map((e) => `- ${e}`).join('\n')}`,
158
+ preflightDiagnostics,
159
+ );
160
+ return null;
161
+ }
49
162
 
50
163
  let sandbox: Sandbox | null = null;
51
164
  try {
52
165
  // Create a fresh sandbox for every execution — full isolation
53
166
  const { createSandbox } = await import('@ifc-lite/sandbox');
54
167
  sandbox = await createSandbox(bim, {
55
- permissions: { model: true, query: true, viewer: true, mutate: true, lens: true, export: true, ...config?.permissions },
168
+ permissions: { model: true, query: true, viewer: true, mutate: true, lens: true, export: true, files: true, ...config?.permissions },
56
169
  limits: { timeoutMs: 30_000, ...config?.limits },
57
170
  });
58
171
  activeSandboxRef.current = sandbox;
@@ -65,10 +178,11 @@ export function useSandbox(config?: SandboxConfig) {
65
178
  });
66
179
  return result;
67
180
  } catch (err: unknown) {
68
- const message = err instanceof Error ? err.message : String(err);
69
- setError(message);
181
+ const runtime = augmentScriptError(err instanceof Error ? err.message : String(err), code);
70
182
 
71
- // If the error is a ScriptError with captured logs, preserve them
183
+ // If the error is a ScriptError with captured logs, preserve them.
184
+ // Important: setError must run AFTER setResult, because setResult clears
185
+ // scriptLastError in the store.
72
186
  if (isScriptError(err)) {
73
187
  setResult({
74
188
  value: undefined,
@@ -76,6 +190,7 @@ export function useSandbox(config?: SandboxConfig) {
76
190
  durationMs: err.durationMs,
77
191
  });
78
192
  }
193
+ setError(runtime.message, runtime.diagnostics);
79
194
  return null;
80
195
  } finally {
81
196
  // Always dispose the sandbox after execution
@@ -86,7 +201,7 @@ export function useSandbox(config?: SandboxConfig) {
86
201
  activeSandboxRef.current = null;
87
202
  }
88
203
  }
89
- }, [bim, config?.permissions, config?.limits, setExecutionState, setResult, setError]);
204
+ }, [bim, config?.permissions, config?.limits, setDiagnostics, setExecutionState, setResult, setError]);
90
205
 
91
206
  /** Reset clears any active sandbox (no-op if none running) */
92
207
  const reset = useCallback(() => {
@@ -97,7 +212,8 @@ export function useSandbox(config?: SandboxConfig) {
97
212
  setExecutionState('idle');
98
213
  setResult(null);
99
214
  setError(null);
100
- }, [setExecutionState, setResult, setError]);
215
+ setDiagnostics([]);
216
+ }, [setDiagnostics, setExecutionState, setResult, setError]);
101
217
 
102
218
  // Cleanup on unmount
103
219
  useEffect(() => {
package/src/index.css CHANGED
@@ -528,3 +528,13 @@ body {
528
528
  border-radius: 0.25rem;
529
529
  animation: skeleton-pulse 2s ease-in-out infinite;
530
530
  }
531
+
532
+ /* Clerk overlays (billing/sidebar/modal) should always stack above app overlays. */
533
+ .cl-portalRoot,
534
+ .cl-modalBackdrop,
535
+ .cl-modalContent,
536
+ .cl-drawerRoot,
537
+ .cl-drawerBackdrop,
538
+ .cl-drawerContent {
539
+ z-index: 100000 !important;
540
+ }
@@ -0,0 +1,46 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ import type { ChatMessage, FileAttachment } from './llm/types.js';
6
+
7
+ function isScriptReadableAttachment(attachment: FileAttachment): boolean {
8
+ if (attachment.isImage) return false;
9
+ return Boolean(
10
+ attachment.textContent
11
+ || attachment.csvData
12
+ || attachment.csvColumns,
13
+ );
14
+ }
15
+
16
+ /**
17
+ * Collect non-image file attachments from chat history and the current composer.
18
+ *
19
+ * Latest attachment wins when the same filename appears multiple times.
20
+ */
21
+ export function collectActiveFileAttachments(
22
+ messages: ChatMessage[],
23
+ pendingAttachments: FileAttachment[] = [],
24
+ ): FileAttachment[] {
25
+ const latestByName = new Map<string, FileAttachment>();
26
+
27
+ const remember = (attachment: FileAttachment) => {
28
+ if (!isScriptReadableAttachment(attachment)) return;
29
+ if (latestByName.has(attachment.name)) {
30
+ latestByName.delete(attachment.name);
31
+ }
32
+ latestByName.set(attachment.name, attachment);
33
+ };
34
+
35
+ for (const message of messages) {
36
+ for (const attachment of message.attachments ?? []) {
37
+ remember(attachment);
38
+ }
39
+ }
40
+
41
+ for (const attachment of pendingAttachments) {
42
+ remember(attachment);
43
+ }
44
+
45
+ return Array.from(latestByName.values());
46
+ }
@@ -0,0 +1,74 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ import { useEffect } from 'react';
6
+ import { useAuth } from '@clerk/clerk-react';
7
+ import { useViewerStore } from '@/store';
8
+
9
+ /**
10
+ * Sync Clerk session state into chat store for authenticated LLM requests.
11
+ * Keeps chat UX decoupled from auth/billing details.
12
+ */
13
+ export function ClerkChatSync() {
14
+ const { isLoaded, isSignedIn, userId, getToken, has } = useAuth();
15
+ const setChatAuthToken = useViewerStore((s) => s.setChatAuthToken);
16
+ const switchChatUserContext = useViewerStore((s) => s.switchChatUserContext);
17
+ const currentChatUserId = useViewerStore((s) => s.chatStorageUserId);
18
+ const currentChatHasPro = useViewerStore((s) => s.chatHasPro);
19
+
20
+ useEffect(() => {
21
+ if (!isLoaded) return;
22
+
23
+ if (!isSignedIn) {
24
+ switchChatUserContext(null, false, {
25
+ clearPersistedCurrent: currentChatUserId !== null,
26
+ restoreMessages: false,
27
+ });
28
+ setChatAuthToken(null);
29
+ return;
30
+ }
31
+
32
+ let cancelled = false;
33
+
34
+ const syncAuth = async () => {
35
+ try {
36
+ const token = await getToken({ skipCache: true });
37
+ const proPlan = has?.({ plan: 'pro' }) ?? false;
38
+ const proFeature = has?.({ feature: 'pro_models' }) ?? false;
39
+ const nextHasPro = proPlan || proFeature;
40
+ if (!cancelled) {
41
+ // Avoid resetting chat usage/messages on routine token refreshes for
42
+ // the same signed-in user. Only switch context when identity or
43
+ // entitlement actually changes.
44
+ if (currentChatUserId !== (userId ?? null) || currentChatHasPro !== nextHasPro) {
45
+ switchChatUserContext(userId ?? null, nextHasPro, {
46
+ clearPersistedCurrent: currentChatUserId !== null && currentChatUserId !== userId,
47
+ restoreMessages: true,
48
+ });
49
+ }
50
+ if (token) {
51
+ setChatAuthToken(token);
52
+ }
53
+ }
54
+ } catch {
55
+ if (!cancelled) {
56
+ // Preserve the current signed-in chat context on transient token
57
+ // refresh failures. Explicit sign-out is handled above.
58
+ }
59
+ }
60
+ };
61
+
62
+ void syncAuth();
63
+ // Keep short-lived JWTs fresh so chat/usage polling doesn't reuse expired tokens.
64
+ const timer = window.setInterval(() => {
65
+ void syncAuth();
66
+ }, 15_000);
67
+ return () => {
68
+ cancelled = true;
69
+ window.clearInterval(timer);
70
+ };
71
+ }, [currentChatHasPro, currentChatUserId, getToken, has, isLoaded, isSignedIn, setChatAuthToken, switchChatUserContext, userId]);
72
+
73
+ return null;
74
+ }
@@ -0,0 +1,62 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Auth helpers for the LLM chat integration.
7
+ *
8
+ * Setup:
9
+ * 1. Install: pnpm add @clerk/clerk-react
10
+ * 2. Set VITE_CLERK_PUBLISHABLE_KEY in .env
11
+ * 3. Create plans in dashboard:
12
+ * - Free plan (slug: 'free', features: ['llm_chat', 'free_models'])
13
+ * - Pro plan (slug: 'pro', $8/month, features: ['llm_chat', 'free_models', 'pro_models'])
14
+ * 4. Wrap app with <ClerkProvider>
15
+ *
16
+ * Usage in components:
17
+ *
18
+ * ```tsx
19
+ * import { useAuth, useUser, Protect } from '@clerk/clerk-react';
20
+ *
21
+ * const { has } = useAuth();
22
+ * const hasPro = has?.({ feature: 'pro_models' }) ?? false;
23
+ *
24
+ * const { getToken } = useAuth();
25
+ * const token = await getToken();
26
+ * ```
27
+ */
28
+
29
+ /**
30
+ * Subscription tiers and their features.
31
+ */
32
+ export const SUBSCRIPTION_PLANS = {
33
+ free: {
34
+ slug: 'free',
35
+ name: 'Free',
36
+ features: ['llm_chat', 'free_models'],
37
+ description: 'AI chat with free models',
38
+ },
39
+ pro: {
40
+ slug: 'pro',
41
+ name: 'Pro',
42
+ features: ['llm_chat', 'free_models', 'pro_models'],
43
+ description: 'All models with monthly credits ($8/month)',
44
+ },
45
+ } as const;
46
+
47
+ /**
48
+ * Feature flags that map to plan features.
49
+ */
50
+ export const FEATURES = {
51
+ LLM_CHAT: 'llm_chat',
52
+ FREE_MODELS: 'free_models',
53
+ PRO_MODELS: 'pro_models',
54
+ } as const;
55
+
56
+ /**
57
+ * Check if auth is configured (publishable key present).
58
+ * When not configured, the chat works in anonymous free-tier mode.
59
+ */
60
+ export function isClerkConfigured(): boolean {
61
+ return Boolean(import.meta.env.VITE_CLERK_PUBLISHABLE_KEY);
62
+ }
@@ -0,0 +1,50 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Extract executable code blocks from LLM markdown responses.
7
+ */
8
+
9
+ import type { CodeBlock } from './types.js';
10
+
11
+ /**
12
+ * Parse fenced code blocks from a markdown string.
13
+ * Supports ```js, ```javascript, ```typescript, ```ts, and bare ``` blocks.
14
+ */
15
+ export function extractCodeBlocks(markdown: string): CodeBlock[] {
16
+ const blocks: CodeBlock[] = [];
17
+ // Match ```lang\n...code...\n```
18
+ const regex = /```(\w*)\n([\s\S]*?)```/g;
19
+ let match: RegExpExecArray | null;
20
+ let index = 0;
21
+
22
+ while ((match = regex.exec(markdown)) !== null) {
23
+ const language = match[1] || 'js';
24
+ const code = match[2].trim();
25
+
26
+ // Only extract JS/TS code blocks (skip html, css, json, etc. unless they look like scripts)
27
+ const isExecutable = ['js', 'javascript', 'ts', 'typescript', ''].includes(language.toLowerCase());
28
+ // Also include unlabeled blocks that reference `bim.`
29
+ const referencesBim = code.includes('bim.');
30
+
31
+ if (isExecutable || referencesBim) {
32
+ blocks.push({ index, language, code });
33
+ index++;
34
+ }
35
+ }
36
+
37
+ return blocks;
38
+ }
39
+
40
+ /**
41
+ * Inject CSV data into a script as a `const DATA = [...]` declaration.
42
+ * Prepends the data array before the LLM-generated script body.
43
+ */
44
+ export function injectCsvData(
45
+ script: string,
46
+ data: Record<string, string>[],
47
+ ): string {
48
+ const dataDeclaration = `const DATA = ${JSON.stringify(data)};\n\n`;
49
+ return dataDeclaration + script;
50
+ }
@@ -0,0 +1,18 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ import test from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { parseCSV } from './context-builder.js';
8
+
9
+ test('parseCSV preserves embedded newlines inside quoted fields', () => {
10
+ const csv = 'Name,Notes\n"Lobby","Line 1\nLine 2"\n"Office","Single line"';
11
+
12
+ const parsed = parseCSV(csv);
13
+
14
+ assert.deepEqual(parsed.columns, ['Name', 'Notes']);
15
+ assert.equal(parsed.rows.length, 2);
16
+ assert.equal(parsed.rows[0]?.Notes, 'Line 1\nLine 2');
17
+ assert.equal(parsed.rows[1]?.Notes, 'Single line');
18
+ });