@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
|
@@ -0,0 +1,197 @@
|
|
|
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
|
+
* LLM model registry.
|
|
7
|
+
* Model IDs are sourced from environment variables only.
|
|
8
|
+
* Pro model cost metadata comes from cost-bucket env vars:
|
|
9
|
+
* - *_PRO_MODELS_LOW => $
|
|
10
|
+
* - *_PRO_MODELS_MEDIUM => $$
|
|
11
|
+
* - *_PRO_MODELS_HIGH => $$$
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { LLMModel } from './types.js';
|
|
15
|
+
|
|
16
|
+
function readEnv(key: string): string | undefined {
|
|
17
|
+
const importMetaEnv = (import.meta as unknown as { env?: Record<string, unknown> }).env;
|
|
18
|
+
const viteVal = importMetaEnv?.[key];
|
|
19
|
+
const nodeVal = typeof process !== 'undefined' ? process.env[key] : undefined;
|
|
20
|
+
const val = typeof viteVal === 'string' ? viteVal : nodeVal;
|
|
21
|
+
if (typeof val !== 'string') return undefined;
|
|
22
|
+
const trimmed = val.trim();
|
|
23
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseCsvEnv(key: string): string[] {
|
|
27
|
+
const raw = readEnv(key);
|
|
28
|
+
if (!raw) return [];
|
|
29
|
+
return raw
|
|
30
|
+
.split(',')
|
|
31
|
+
.map((part) => part.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseCsvFromFirstDefined(keys: string[]): string[] {
|
|
36
|
+
for (const key of keys) {
|
|
37
|
+
const values = parseCsvEnv(key);
|
|
38
|
+
if (values.length > 0) return values;
|
|
39
|
+
}
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function uniqueInOrder(ids: string[]): string[] {
|
|
44
|
+
const out: string[] = [];
|
|
45
|
+
const seen = new Set<string>();
|
|
46
|
+
for (const id of ids) {
|
|
47
|
+
if (!seen.has(id)) {
|
|
48
|
+
seen.add(id);
|
|
49
|
+
out.push(id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function titleCaseProvider(rawProvider: string): string {
|
|
56
|
+
const overrides: Record<string, string> = {
|
|
57
|
+
openai: 'OpenAI',
|
|
58
|
+
anthropic: 'Anthropic',
|
|
59
|
+
google: 'Google',
|
|
60
|
+
meta: 'Meta',
|
|
61
|
+
'meta-llama': 'Meta',
|
|
62
|
+
xai: 'xAI',
|
|
63
|
+
'x-ai': 'xAI',
|
|
64
|
+
mistralai: 'Mistral',
|
|
65
|
+
qwen: 'Alibaba',
|
|
66
|
+
deepseek: 'DeepSeek',
|
|
67
|
+
minimax: 'MiniMax',
|
|
68
|
+
'z-ai': 'Zhipu',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const normalized = rawProvider.toLowerCase();
|
|
72
|
+
if (overrides[normalized]) return overrides[normalized];
|
|
73
|
+
return rawProvider
|
|
74
|
+
.split(/[-_]/)
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
77
|
+
.join(' ');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function humanizeModelSlug(slug: string): string {
|
|
81
|
+
const withoutTier = slug.split(':')[0] ?? slug;
|
|
82
|
+
return withoutTier
|
|
83
|
+
.replace(/[._-]+/g, ' ')
|
|
84
|
+
.split(' ')
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
.map((word) => {
|
|
87
|
+
if (/^[0-9.]+$/.test(word)) return word;
|
|
88
|
+
const upper = word.toUpperCase();
|
|
89
|
+
if (upper === 'GPT' || upper === 'OSS' || upper === 'R1') return upper;
|
|
90
|
+
if (word.length <= 2) return upper;
|
|
91
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
92
|
+
})
|
|
93
|
+
.join(' ');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildModel(id: string, tier: 'free' | 'pro', cost?: LLMModel['cost']): LLMModel {
|
|
97
|
+
const [providerRaw, modelRaw = id] = id.split('/');
|
|
98
|
+
return {
|
|
99
|
+
id,
|
|
100
|
+
tier,
|
|
101
|
+
name: humanizeModelSlug(modelRaw),
|
|
102
|
+
provider: titleCaseProvider(providerRaw ?? 'Unknown'),
|
|
103
|
+
contextWindow: 128_000,
|
|
104
|
+
supportsImages: false,
|
|
105
|
+
supportsFileAttachments: true,
|
|
106
|
+
cost: tier === 'pro' ? cost : undefined,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const freeModelIds = uniqueInOrder(parseCsvFromFirstDefined(['VITE_LLM_FREE_MODELS', 'LLM_FREE_MODELS']));
|
|
111
|
+
const proLowCostIds = uniqueInOrder(parseCsvFromFirstDefined(['VITE_LLM_PRO_MODELS_LOW', 'LLM_PRO_MODELS_LOW']));
|
|
112
|
+
const proMediumCostIds = uniqueInOrder(parseCsvFromFirstDefined(['VITE_LLM_PRO_MODELS_MEDIUM', 'LLM_PRO_MODELS_MEDIUM']));
|
|
113
|
+
const proHighCostIds = uniqueInOrder(parseCsvFromFirstDefined(['VITE_LLM_PRO_MODELS_HIGH', 'LLM_PRO_MODELS_HIGH']));
|
|
114
|
+
|
|
115
|
+
// Backward-compatible fallback for older env shape with one pro list.
|
|
116
|
+
const legacyProIds = uniqueInOrder(parseCsvFromFirstDefined(['VITE_LLM_PRO_MODELS', 'LLM_PRO_MODELS']));
|
|
117
|
+
const useLegacyProList = proLowCostIds.length === 0 && proMediumCostIds.length === 0 && proHighCostIds.length === 0;
|
|
118
|
+
|
|
119
|
+
const rawFreeModels: LLMModel[] = freeModelIds.map((id) => buildModel(id, 'free'));
|
|
120
|
+
|
|
121
|
+
const proCostBuckets: Array<{ ids: string[]; cost: LLMModel['cost'] }> = [
|
|
122
|
+
{ ids: proLowCostIds, cost: '$' },
|
|
123
|
+
{ ids: useLegacyProList ? legacyProIds : proMediumCostIds, cost: '$$' },
|
|
124
|
+
{ ids: proHighCostIds, cost: '$$$' },
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const seenProModelIds = new Set<string>();
|
|
128
|
+
const rawProModels: LLMModel[] = proCostBuckets.flatMap(({ ids, cost }) =>
|
|
129
|
+
ids.flatMap((id) => {
|
|
130
|
+
if (seenProModelIds.has(id)) return [];
|
|
131
|
+
seenProModelIds.add(id);
|
|
132
|
+
return [buildModel(id, 'pro', cost)];
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const imageCapableModelIds = new Set(
|
|
137
|
+
uniqueInOrder(parseCsvFromFirstDefined(['VITE_LLM_IMAGE_MODELS', 'LLM_IMAGE_MODELS'])),
|
|
138
|
+
);
|
|
139
|
+
const fileCapableModelIds = new Set(
|
|
140
|
+
uniqueInOrder(parseCsvFromFirstDefined(['VITE_LLM_FILE_ATTACHMENT_MODELS', 'LLM_FILE_ATTACHMENT_MODELS'])),
|
|
141
|
+
);
|
|
142
|
+
const hasImageOverrideList = imageCapableModelIds.size > 0;
|
|
143
|
+
const hasFileOverrideList = fileCapableModelIds.size > 0;
|
|
144
|
+
|
|
145
|
+
function applyCapabilities(model: LLMModel): LLMModel {
|
|
146
|
+
const supportsImages = hasImageOverrideList ? imageCapableModelIds.has(model.id) : model.supportsImages;
|
|
147
|
+
const supportsFileAttachments = hasFileOverrideList
|
|
148
|
+
? fileCapableModelIds.has(model.id)
|
|
149
|
+
: model.supportsFileAttachments;
|
|
150
|
+
return {
|
|
151
|
+
...model,
|
|
152
|
+
supportsImages,
|
|
153
|
+
supportsFileAttachments,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const FREE_MODELS: LLMModel[] = rawFreeModels.map(applyCapabilities);
|
|
158
|
+
export const PRO_MODELS: LLMModel[] = rawProModels.map(applyCapabilities);
|
|
159
|
+
export const ALL_MODELS = [...FREE_MODELS, ...PRO_MODELS];
|
|
160
|
+
|
|
161
|
+
const FALLBACK_MODEL: LLMModel = {
|
|
162
|
+
id: 'llm-model-missing',
|
|
163
|
+
name: 'No model configured',
|
|
164
|
+
provider: 'Unknown',
|
|
165
|
+
tier: 'free',
|
|
166
|
+
contextWindow: 128_000,
|
|
167
|
+
supportsImages: false,
|
|
168
|
+
supportsFileAttachments: true,
|
|
169
|
+
notes: 'Set VITE_LLM_FREE_MODELS and VITE_LLM_PRO_MODELS_LOW/MEDIUM/HIGH in environment.',
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const DEFAULT_FREE_MODEL = FREE_MODELS[0] ?? PRO_MODELS[0] ?? FALLBACK_MODEL;
|
|
173
|
+
export const DEFAULT_PRO_MODEL = PRO_MODELS[0] ?? DEFAULT_FREE_MODEL;
|
|
174
|
+
|
|
175
|
+
export function getModelById(id: string): LLMModel | undefined {
|
|
176
|
+
return ALL_MODELS.find((m) => m.id === id);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Check whether a model ID requires a pro subscription */
|
|
180
|
+
export function requiresPro(modelId: string): boolean {
|
|
181
|
+
const model = getModelById(modelId);
|
|
182
|
+
return model?.tier === 'pro';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function getDefaultModelForEntitlement(hasPro: boolean): LLMModel {
|
|
186
|
+
return hasPro ? DEFAULT_PRO_MODEL : DEFAULT_FREE_MODEL;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function coerceModelForEntitlement(modelId: string | null | undefined, hasPro: boolean): string {
|
|
190
|
+
if (modelId) {
|
|
191
|
+
const model = getModelById(modelId);
|
|
192
|
+
if (model && (!requiresPro(modelId) || hasPro)) {
|
|
193
|
+
return modelId;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return getDefaultModelForEntitlement(hasPro).id;
|
|
197
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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 type { ChatMessage } from './types.js';
|
|
8
|
+
import { buildRepairAttemptKey, buildRepairSessionKey, getEscalatedRepairScope, pruneMessagesForRepair } from './repair-loop.js';
|
|
9
|
+
import { createPreflightDiagnostic } from './script-diagnostics.js';
|
|
10
|
+
|
|
11
|
+
test('pruneMessagesForRepair removes prior repair prompt and patch reply context', () => {
|
|
12
|
+
const messages: ChatMessage[] = [
|
|
13
|
+
{ id: '1', role: 'user', content: 'Create a house', createdAt: 1 },
|
|
14
|
+
{ id: '2', role: 'assistant', content: '```js\nconst h = 1;\n```', createdAt: 2 },
|
|
15
|
+
{ id: '3', role: 'user', content: 'The script needs a root-cause repair.\n\nFailure type: preflight', createdAt: 3 },
|
|
16
|
+
{ id: '4', role: 'assistant', content: '```ifc-script-edits\n{"scriptEdits":[]}\n```', createdAt: 4 },
|
|
17
|
+
{ id: '5', role: 'user', content: 'Add a roof', createdAt: 5 },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const pruned = pruneMessagesForRepair(messages);
|
|
21
|
+
assert.deepEqual(pruned.map((message) => message.id), ['1', '2', '5']);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('buildRepairAttemptKey stays stable for same targeted failure', () => {
|
|
25
|
+
const diagnostics = [
|
|
26
|
+
createPreflightDiagnostic(
|
|
27
|
+
'wall_hosted_opening_pattern',
|
|
28
|
+
'Suspicious door call.',
|
|
29
|
+
'error',
|
|
30
|
+
{
|
|
31
|
+
methodName: 'addIfcDoor',
|
|
32
|
+
range: { from: 120, to: 180 },
|
|
33
|
+
snippet: 'bim.create.addIfcDoor(h, ground, { Name: "Front Door" });',
|
|
34
|
+
},
|
|
35
|
+
),
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const first = buildRepairAttemptKey({
|
|
39
|
+
reason: 'preflight',
|
|
40
|
+
diagnostics,
|
|
41
|
+
currentCode: 'const h = bim.create.project({ Name: "House" });',
|
|
42
|
+
});
|
|
43
|
+
const second = buildRepairAttemptKey({
|
|
44
|
+
reason: 'preflight',
|
|
45
|
+
diagnostics,
|
|
46
|
+
currentCode: 'const h = bim.create.project({ Name: "House" });',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
assert.equal(first, second);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('pruneMessagesForRepair also removes malformed assistant repair replies', () => {
|
|
53
|
+
const messages: ChatMessage[] = [
|
|
54
|
+
{ id: '1', role: 'user', content: 'Create a tower', createdAt: 1 },
|
|
55
|
+
{ id: '2', role: 'assistant', content: '```js\nconst h = 1;\n```', createdAt: 2 },
|
|
56
|
+
{ id: '3', role: 'user', content: 'The script needs a root-cause repair.\n\nFailure type: patch-apply', createdAt: 3 },
|
|
57
|
+
{ id: '4', role: 'assistant', content: 'Here is a corrected snippet:\n```js\nconst width = 30;\n```', createdAt: 4 },
|
|
58
|
+
{ id: '5', role: 'user', content: 'Try again', createdAt: 5 },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const pruned = pruneMessagesForRepair(messages);
|
|
62
|
+
assert.deepEqual(pruned.map((message) => message.id), ['1', '2', '5']);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('buildRepairSessionKey tracks the root cause across script revisions', () => {
|
|
66
|
+
const diagnostics = [
|
|
67
|
+
createPreflightDiagnostic(
|
|
68
|
+
'detached_snippet_scope',
|
|
69
|
+
'Detached snippet risk.',
|
|
70
|
+
'error',
|
|
71
|
+
{ symbol: 'width' },
|
|
72
|
+
),
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const first = buildRepairSessionKey({
|
|
76
|
+
diagnostics,
|
|
77
|
+
currentCode: 'const width = 30;',
|
|
78
|
+
});
|
|
79
|
+
const second = buildRepairSessionKey({
|
|
80
|
+
diagnostics,
|
|
81
|
+
currentCode: 'const width = 30;\nconst depth = 40;',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
assert.equal(first, second);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('getEscalatedRepairScope widens repair sessions once', () => {
|
|
88
|
+
assert.equal(getEscalatedRepairScope('local'), 'block');
|
|
89
|
+
assert.equal(getEscalatedRepairScope('block'), 'structural');
|
|
90
|
+
assert.equal(getEscalatedRepairScope('structural'), null);
|
|
91
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
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 } from './types.js';
|
|
6
|
+
import { getPrimaryRootCause, type ScriptDiagnostic } from './script-diagnostics.js';
|
|
7
|
+
|
|
8
|
+
const REPAIR_PROMPT_PREFIX = 'The script needs a root-cause repair.';
|
|
9
|
+
|
|
10
|
+
export function isRepairPromptMessage(content: string): boolean {
|
|
11
|
+
return content.startsWith(REPAIR_PROMPT_PREFIX);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function pruneMessagesForRepair(messages: ChatMessage[]): ChatMessage[] {
|
|
15
|
+
const result: ChatMessage[] = [];
|
|
16
|
+
let skippingRepairReply = false;
|
|
17
|
+
|
|
18
|
+
for (const message of messages) {
|
|
19
|
+
if (message.role === 'user' && isRepairPromptMessage(message.content)) {
|
|
20
|
+
skippingRepairReply = true;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (skippingRepairReply && message.role === 'assistant') {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (message.role === 'user') skippingRepairReply = false;
|
|
29
|
+
result.push(message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildRepairAttemptKey(params: {
|
|
36
|
+
reason: string;
|
|
37
|
+
currentCode: string;
|
|
38
|
+
diagnostics?: ScriptDiagnostic[];
|
|
39
|
+
}): string {
|
|
40
|
+
const primary = getPrimaryRootCause(params.diagnostics ?? []);
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
params.reason,
|
|
44
|
+
primary?.rootCauseKey ?? '',
|
|
45
|
+
primary?.repairScope ?? '',
|
|
46
|
+
hashText(params.currentCode),
|
|
47
|
+
].join('|');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildRepairSessionKey(params: {
|
|
51
|
+
currentCode: string;
|
|
52
|
+
diagnostics?: ScriptDiagnostic[];
|
|
53
|
+
}): string {
|
|
54
|
+
const primary = getPrimaryRootCause(params.diagnostics ?? []);
|
|
55
|
+
return primary?.rootCauseKey ?? hashText(params.currentCode);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getEscalatedRepairScope(scope: ScriptDiagnostic['repairScope']): ScriptDiagnostic['repairScope'] | null {
|
|
59
|
+
switch (scope) {
|
|
60
|
+
case 'local':
|
|
61
|
+
return 'block';
|
|
62
|
+
case 'block':
|
|
63
|
+
return 'structural';
|
|
64
|
+
default:
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function hashText(text: string): string {
|
|
70
|
+
let hash = 2166136261;
|
|
71
|
+
for (let i = 0; i < text.length; i++) {
|
|
72
|
+
hash ^= text.charCodeAt(i);
|
|
73
|
+
hash = Math.imul(hash, 16777619);
|
|
74
|
+
}
|
|
75
|
+
return (hash >>> 0).toString(16);
|
|
76
|
+
}
|