@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.
- package/CHANGELOG.md +35 -0
- package/dist/assets/{Arrow.dom-CSgnLhN4.js → Arrow.dom-_vGzMMKs.js} +1 -1
- package/dist/assets/basketViewActivator-BZcoCL3V.js +1 -0
- package/dist/assets/{browser-qSKWrKQW.js → browser-Czmf34bo.js} +1 -1
- package/dist/assets/ifc-lite_bg-DyBKoGgk.wasm +0 -0
- package/dist/assets/index-CMQ_Dgkr.css +1 -0
- package/dist/assets/index-D7nEDctQ.js +229 -0
- package/dist/assets/{index-4Y4XaV8N.js → index-DX-Qf5fA.js} +72669 -61673
- package/dist/assets/{native-bridge-CSFDsEkg.js → native-bridge-DAOWftxE.js} +1 -1
- package/dist/assets/{wasm-bridge-Zf90ysEm.js → wasm-bridge-D7jYpn8a.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +21 -20
- package/src/App.tsx +17 -1
- package/src/components/viewer/BasketPresentationDock.tsx +8 -4
- package/src/components/viewer/ChatPanel.tsx +1402 -0
- package/src/components/viewer/CodeEditor.tsx +70 -4
- package/src/components/viewer/CommandPalette.tsx +1 -0
- package/src/components/viewer/HierarchyPanel.tsx +28 -13
- package/src/components/viewer/MainToolbar.tsx +113 -95
- package/src/components/viewer/ScriptPanel.tsx +351 -184
- package/src/components/viewer/UpgradePage.tsx +69 -0
- package/src/components/viewer/Viewport.tsx +23 -0
- package/src/components/viewer/chat/ChatMessage.tsx +144 -0
- package/src/components/viewer/chat/ExecutableCodeBlock.tsx +416 -0
- package/src/components/viewer/chat/ModelSelector.tsx +102 -0
- package/src/components/viewer/chat/renderTextContent.test.ts +23 -0
- package/src/components/viewer/chat/renderTextContent.ts +19 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +10 -3
- package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +126 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +139 -38
- package/src/components/viewer/hierarchy/types.ts +6 -1
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +27 -12
- package/src/hooks/useIfcCache.ts +1 -2
- package/src/hooks/useSandbox.ts +122 -6
- package/src/index.css +10 -0
- package/src/lib/attachments.ts +46 -0
- package/src/lib/llm/ClerkChatSync.tsx +74 -0
- package/src/lib/llm/clerk-auth.ts +62 -0
- package/src/lib/llm/code-extractor.ts +50 -0
- package/src/lib/llm/context-builder.test.ts +18 -0
- package/src/lib/llm/context-builder.ts +305 -0
- package/src/lib/llm/free-models.test.ts +118 -0
- package/src/lib/llm/message-capabilities.test.ts +131 -0
- package/src/lib/llm/message-capabilities.ts +94 -0
- package/src/lib/llm/models.ts +197 -0
- package/src/lib/llm/repair-loop.test.ts +91 -0
- package/src/lib/llm/repair-loop.ts +76 -0
- package/src/lib/llm/script-diagnostics.ts +445 -0
- package/src/lib/llm/script-edit-ops.test.ts +399 -0
- package/src/lib/llm/script-edit-ops.ts +954 -0
- package/src/lib/llm/script-preflight.test.ts +513 -0
- package/src/lib/llm/script-preflight.ts +990 -0
- package/src/lib/llm/script-preservation.test.ts +128 -0
- package/src/lib/llm/script-preservation.ts +152 -0
- package/src/lib/llm/stream-client.test.ts +97 -0
- package/src/lib/llm/stream-client.ts +410 -0
- package/src/lib/llm/system-prompt.test.ts +181 -0
- package/src/lib/llm/system-prompt.ts +665 -0
- package/src/lib/llm/types.ts +150 -0
- package/src/lib/scripts/templates/bim-globals.d.ts +226 -7
- package/src/lib/scripts/templates/create-building.ts +12 -12
- package/src/main.tsx +10 -1
- package/src/sdk/adapters/export-adapter.test.ts +24 -0
- package/src/sdk/adapters/export-adapter.ts +40 -16
- package/src/sdk/adapters/files-adapter.ts +39 -0
- package/src/sdk/adapters/model-compat.ts +1 -1
- package/src/sdk/adapters/mutate-adapter.ts +20 -6
- package/src/sdk/adapters/mutation-view.ts +112 -0
- package/src/sdk/adapters/query-adapter.ts +100 -4
- package/src/sdk/local-backend.ts +4 -0
- package/src/store/index.ts +15 -1
- package/src/store/slices/chatSlice.test.ts +325 -0
- package/src/store/slices/chatSlice.ts +468 -0
- package/src/store/slices/scriptSlice.test.ts +75 -0
- package/src/store/slices/scriptSlice.ts +256 -9
- package/src/vite-env.d.ts +10 -0
- package/vite.config.ts +21 -2
- package/dist/assets/ifc-lite_bg-BOvNXJA_.wasm +0 -0
- package/dist/assets/index-ByrFvN5A.css +0 -1
- package/dist/assets/index-CN7qDq7G.js +0 -216
package/src/hooks/useSandbox.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
+
});
|