@ifc-lite/viewer 1.23.0 → 1.25.0
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/.turbo/turbo-build.log +34 -31
- package/CHANGELOG.md +96 -0
- package/dist/assets/{basketViewActivator-Dn_bHUl2.js → basketViewActivator-CU8_toGq.js} +7 -7
- package/dist/assets/{bcf-B9SFl84i.js → bcf-DXGDhw56.js} +23 -23
- package/dist/assets/{deflate-yMpdCIqk.js → deflate-Bb1_H2Yf.js} +1 -1
- package/dist/assets/{exporters-D-BvrNIg.js → exporters-DZhLN0ux.js} +1861 -1658
- package/dist/assets/geometry-controller.worker-DQOSYqtw.js +7 -0
- package/dist/assets/geometry.worker-B62e03Ao.js +1 -0
- package/dist/assets/{geotiff-D1tvcDCb.js → geotiff-y0ZxbRJd.js} +10 -10
- package/dist/assets/{ids-DZLs0snJ.js → ids-DruUNtfD.js} +4 -4
- package/dist/assets/ifc-lite-Ch2T9pP9.js +7 -0
- package/dist/assets/{ifc-lite_bg-DyHX37GQ.wasm → ifc-lite_bg-D7O1WHgP.wasm} +0 -0
- package/dist/assets/{ifc-lite_bg-BIryVCXQ.wasm → ifc-lite_bg-iH_07wf8.wasm} +0 -0
- package/dist/assets/index-Bws3UAkj.css +1 -0
- package/dist/assets/{index-CXSBhkcJ.js → index-Dr88ZlSY.js} +64100 -47030
- package/dist/assets/{jpeg-DUMcZp24.js → jpeg-B3_loqFe.js} +1 -1
- package/dist/assets/lens-PYsLu_MA.js +1 -0
- package/dist/assets/{lerc-IN4uWojP.js → lerc-nkwS8ZUe.js} +1 -1
- package/dist/assets/{lzw-Cnw0hH-m.js → lzw-D3cW5Wpg.js} +1 -1
- package/dist/assets/{native-bridge-BVf2uzoH.js → native-bridge-BcYJooq8.js} +2 -2
- package/dist/assets/{packbits-BskJCwk0.js → packbits-DDN4xzB5.js} +1 -1
- package/dist/assets/{parser.worker-BdtkkaGf.js → parser.worker-BW1IMUed.js} +3 -3
- package/dist/assets/raw-CoIXstQ-.js +1 -0
- package/dist/assets/{sandbox-VLI_y7cl.js → sandbox-DETNEyQb.js} +498 -470
- package/dist/assets/{server-client-BLcKaWQB.js → server-client-CmzJOeS7.js} +1 -1
- package/dist/assets/{wasm-bridge-BAfZh7YT.js → wasm-bridge-CT7mK9W0.js} +1 -1
- package/dist/assets/{webimage-Db2xzze3.js → webimage-CBjgg4up.js} +1 -1
- package/dist/assets/{workerHelpers--sAYm9yN.js → workerHelpers-IEQDo8r3.js} +1 -1
- package/dist/assets/{zstd-BDToOQyD.js → zstd-C8oQ6qdS.js} +1 -1
- package/dist/index.html +8 -8
- package/package.json +11 -9
- package/src/App.tsx +5 -2
- package/src/components/extensions/AuditLogPanel.tsx +259 -0
- package/src/components/extensions/BundlePreview.tsx +102 -0
- package/src/components/extensions/CapabilityReview.tsx +333 -0
- package/src/components/extensions/ExtensionDockHost.tsx +192 -0
- package/src/components/extensions/ExtensionToolbarSlot.tsx +106 -0
- package/src/components/extensions/ExtensionsPanel.tsx +481 -0
- package/src/components/extensions/FlavorDialog.tsx +398 -0
- package/src/components/extensions/FlavorImportPreview.tsx +79 -0
- package/src/components/extensions/FlavorIndicator.tsx +81 -0
- package/src/components/extensions/FlavorListView.tsx +318 -0
- package/src/components/extensions/FlavorMergeDialog.tsx +326 -0
- package/src/components/extensions/HelpHint.tsx +182 -0
- package/src/components/extensions/IdeasPanel.tsx +344 -0
- package/src/components/extensions/PlanCard.tsx +227 -0
- package/src/components/extensions/PrivacyPanel.tsx +312 -0
- package/src/components/extensions/PromoteToolDialog.tsx +313 -0
- package/src/components/extensions/RepairQueuePanel.tsx +222 -0
- package/src/components/extensions/icon-registry.ts +92 -0
- package/src/components/extensions/toast-helpers.ts +49 -0
- package/src/components/extensions/widget/WidgetErrorBoundary.tsx +62 -0
- package/src/components/extensions/widget/WidgetRenderer.tsx +428 -0
- package/src/components/viewer/ChatPanel.tsx +251 -3
- package/src/components/viewer/CommandPalette.tsx +74 -4
- package/src/components/viewer/Drawing2DCanvas.tsx +178 -1
- package/src/components/viewer/EntityContextMenu.tsx +70 -0
- package/src/components/viewer/ExportDialog.tsx +9 -1
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +21 -6
- package/src/components/viewer/LensPanel.tsx +50 -0
- package/src/components/viewer/MainToolbar.tsx +170 -87
- package/src/components/viewer/ScriptPanel.tsx +105 -1
- package/src/components/viewer/Section2DPanel.tsx +58 -2
- package/src/components/viewer/StatusBar.tsx +18 -0
- package/src/components/viewer/ViewerLayout.tsx +53 -4
- package/src/components/viewer/Viewport.tsx +72 -0
- package/src/hooks/useActionLogger.test.ts +161 -0
- package/src/hooks/useActionLogger.ts +141 -0
- package/src/hooks/useForkExtension.ts +51 -0
- package/src/hooks/useIfcFederation.ts +7 -1
- package/src/hooks/useInstalledExtensions.ts +43 -0
- package/src/hooks/usePrivacyDisclosure.ts +48 -0
- package/src/hooks/useRunExtensionTests.ts +67 -0
- package/src/hooks/useSlotContributions.ts +38 -0
- package/src/hooks/useSymbolicAnnotations.test.ts +124 -0
- package/src/hooks/useSymbolicAnnotations.ts +776 -0
- package/src/lib/desktop-product.ts +7 -1
- package/src/lib/lens/adapter.ts +14 -0
- package/src/lib/llm/prompt-cache.ts +77 -0
- package/src/lib/llm/stream-client.ts +20 -2
- package/src/lib/llm/stream-direct.ts +11 -1
- package/src/lib/llm/system-prompt.ts +42 -0
- package/src/lib/safe-mode.ts +30 -0
- package/src/sdk/ExtensionHostProvider.tsx +103 -0
- package/src/services/extensions/flavor-service.ts +183 -0
- package/src/services/extensions/host-commands.ts +112 -0
- package/src/services/extensions/host-installer.ts +289 -0
- package/src/services/extensions/host.ts +514 -0
- package/src/services/extensions/idb-flavor-storage.test.ts +140 -0
- package/src/services/extensions/idb-flavor-storage.ts +241 -0
- package/src/services/extensions/idb-log-storage.test.ts +110 -0
- package/src/services/extensions/idb-log-storage.ts +171 -0
- package/src/services/extensions/idb-storage.ts +228 -0
- package/src/services/extensions/runtime-errors.ts +26 -0
- package/src/services/extensions/sandbox-factory.ts +217 -0
- package/src/store/constants.ts +48 -6
- package/src/store/index.ts +6 -1
- package/src/store/slices/drawing2DSlice.ts +8 -0
- package/src/store/slices/extensionsSlice.ts +90 -0
- package/src/store/slices/lensSlice.ts +28 -0
- package/src/store/slices/visibilitySlice.test.ts +6 -0
- package/src/store/slices/visibilitySlice.ts +17 -8
- package/src/store/types.ts +2 -0
- package/dist/assets/geometry-controller.worker-Cm5pvyR6.js +0 -7
- package/dist/assets/geometry.worker-ClNvXIrj.js +0 -1
- package/dist/assets/ifc-lite-BDg0iIbj.js +0 -7
- package/dist/assets/index-DS_xJQfP.css +0 -1
- package/dist/assets/lens-CpjUdqpw.js +0 -1
- package/dist/assets/raw-DzTtEZIY.js +0 -1
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
* `PlanCard` — render an AuthoringPlan with edit affordances.
|
|
7
|
+
*
|
|
8
|
+
* The plan-before-code UX: the AI proposes a structured plan; the user
|
|
9
|
+
* trims contributions, prunes capabilities, edits the summary, then
|
|
10
|
+
* approves. After approval, the chat panel routes through the actual
|
|
11
|
+
* bundle synthesis (a follow-up that consumes this card's `onApprove`).
|
|
12
|
+
*
|
|
13
|
+
* Spec: docs/architecture/ai-customization/04-ai-authoring.md §4.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useMemo, useState } from 'react';
|
|
17
|
+
import { Check, ChevronRight, Edit3, ShieldAlert, Sparkles, X } from 'lucide-react';
|
|
18
|
+
import {
|
|
19
|
+
computeRisks,
|
|
20
|
+
overallTier,
|
|
21
|
+
parseCapability,
|
|
22
|
+
type AuthoringPlan,
|
|
23
|
+
type CapabilityRisk,
|
|
24
|
+
type RiskTier,
|
|
25
|
+
} from '@ifc-lite/extensions';
|
|
26
|
+
import { Button } from '@/components/ui/button';
|
|
27
|
+
import { Input } from '@/components/ui/input';
|
|
28
|
+
import { Label } from '@/components/ui/label';
|
|
29
|
+
import { cn } from '@/lib/utils';
|
|
30
|
+
|
|
31
|
+
interface PlanCardProps {
|
|
32
|
+
/** The plan to show. Editable copy is stored in component state. */
|
|
33
|
+
plan: AuthoringPlan;
|
|
34
|
+
/** Called when the user confirms the (possibly edited) plan. */
|
|
35
|
+
onApprove(plan: AuthoringPlan): void;
|
|
36
|
+
/** Called when the user cancels / dismisses. */
|
|
37
|
+
onCancel(): void;
|
|
38
|
+
/** Hide the inline edit fields. Useful for read-only review of past plans. */
|
|
39
|
+
readOnly?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function PlanCard({ plan, onApprove, onCancel, readOnly }: PlanCardProps) {
|
|
43
|
+
const [draft, setDraft] = useState<AuthoringPlan>(plan);
|
|
44
|
+
const risks = useMemo<CapabilityRisk[]>(() => {
|
|
45
|
+
const parsed = draft.capabilities
|
|
46
|
+
.map((raw) => parseCapability(raw))
|
|
47
|
+
.filter((r): r is { ok: true; value: ReturnType<typeof parseCapability> extends { ok: true; value: infer V } ? V : never } => r.ok)
|
|
48
|
+
.map((r) => r.value);
|
|
49
|
+
return computeRisks(parsed);
|
|
50
|
+
}, [draft.capabilities]);
|
|
51
|
+
const overall = overallTier(risks);
|
|
52
|
+
|
|
53
|
+
const toggleCapability = (raw: string) => {
|
|
54
|
+
setDraft((p) => ({
|
|
55
|
+
...p,
|
|
56
|
+
capabilities: p.capabilities.includes(raw)
|
|
57
|
+
? p.capabilities.filter((c) => c !== raw)
|
|
58
|
+
: [...p.capabilities, raw],
|
|
59
|
+
}));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const removeContribution = (idx: number) => {
|
|
63
|
+
setDraft((p) => ({
|
|
64
|
+
...p,
|
|
65
|
+
contributions: p.contributions.filter((_, i) => i !== idx),
|
|
66
|
+
}));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className={cn(
|
|
71
|
+
'rounded-lg border bg-card p-4 space-y-3',
|
|
72
|
+
overall === 'red' && 'border-destructive/50',
|
|
73
|
+
overall === 'yellow' && 'border-amber-500/40',
|
|
74
|
+
)}>
|
|
75
|
+
<div className="flex items-start gap-2">
|
|
76
|
+
<div className={cn(
|
|
77
|
+
'mt-1 p-1.5 rounded-md',
|
|
78
|
+
overall === 'red' ? 'bg-destructive/20 text-destructive' : overall === 'yellow' ? 'bg-amber-500/20 text-amber-600 dark:text-amber-400' : 'bg-primary/20 text-primary',
|
|
79
|
+
)}>
|
|
80
|
+
{overall === 'red' ? <ShieldAlert className="h-4 w-4" /> : <Sparkles className="h-4 w-4" />}
|
|
81
|
+
</div>
|
|
82
|
+
<div className="flex-1 min-w-0">
|
|
83
|
+
<div className="text-xs uppercase tracking-wide font-semibold text-muted-foreground">
|
|
84
|
+
Authoring plan
|
|
85
|
+
</div>
|
|
86
|
+
{readOnly ? (
|
|
87
|
+
<div className="text-sm font-medium">{draft.summary}</div>
|
|
88
|
+
) : (
|
|
89
|
+
<Input
|
|
90
|
+
value={draft.summary}
|
|
91
|
+
onChange={(e) => setDraft((p) => ({ ...p, summary: e.target.value }))}
|
|
92
|
+
className="mt-1 text-sm font-medium"
|
|
93
|
+
aria-label="Plan summary"
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<p className="text-xs text-muted-foreground leading-relaxed">{draft.rationale}</p>
|
|
100
|
+
|
|
101
|
+
<div className="space-y-1.5">
|
|
102
|
+
<Label className="text-[11px] uppercase tracking-wide">Contributions</Label>
|
|
103
|
+
{draft.contributions.length === 0 ? (
|
|
104
|
+
<div className="text-xs text-muted-foreground italic">No contributions.</div>
|
|
105
|
+
) : (
|
|
106
|
+
<ul className="space-y-1">
|
|
107
|
+
{draft.contributions.map((c, i) => (
|
|
108
|
+
<li key={i} className="flex items-center gap-2 text-xs rounded-md border bg-muted/30 px-2 py-1.5">
|
|
109
|
+
<ChevronRight className="h-3 w-3 text-muted-foreground shrink-0" />
|
|
110
|
+
<span className="font-mono uppercase text-[10px] text-muted-foreground shrink-0">{c.kind}</span>
|
|
111
|
+
<span className="flex-1 min-w-0 truncate">{c.label}</span>
|
|
112
|
+
{c.slot && (
|
|
113
|
+
<code className="text-[10px] text-muted-foreground font-mono">{c.slot}</code>
|
|
114
|
+
)}
|
|
115
|
+
{!readOnly && (
|
|
116
|
+
<Button
|
|
117
|
+
size="icon-xs"
|
|
118
|
+
variant="ghost"
|
|
119
|
+
onClick={() => removeContribution(i)}
|
|
120
|
+
aria-label={`Remove contribution ${i}`}
|
|
121
|
+
>
|
|
122
|
+
<X className="h-3 w-3" />
|
|
123
|
+
</Button>
|
|
124
|
+
)}
|
|
125
|
+
</li>
|
|
126
|
+
))}
|
|
127
|
+
</ul>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div className="space-y-1.5">
|
|
132
|
+
<Label className="text-[11px] uppercase tracking-wide">Capabilities</Label>
|
|
133
|
+
{draft.capabilities.length === 0 ? (
|
|
134
|
+
<div className="text-xs text-muted-foreground italic">No capabilities requested.</div>
|
|
135
|
+
) : (
|
|
136
|
+
<ul className="space-y-1">
|
|
137
|
+
{risks.map((risk) => (
|
|
138
|
+
<li
|
|
139
|
+
key={risk.capability.raw}
|
|
140
|
+
className="flex items-start gap-2 text-xs rounded-md border bg-muted/30 px-2 py-1.5"
|
|
141
|
+
>
|
|
142
|
+
{!readOnly && (
|
|
143
|
+
<input
|
|
144
|
+
type="checkbox"
|
|
145
|
+
className="mt-0.5"
|
|
146
|
+
checked={draft.capabilities.includes(risk.capability.raw)}
|
|
147
|
+
onChange={() => toggleCapability(risk.capability.raw)}
|
|
148
|
+
aria-label={`Toggle capability ${risk.capability.raw}`}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
<div className="flex-1 min-w-0">
|
|
152
|
+
<div className="flex items-center gap-2">
|
|
153
|
+
<code className="font-mono text-[10px]">{risk.capability.raw}</code>
|
|
154
|
+
<RiskBadge tier={risk.tier} />
|
|
155
|
+
</div>
|
|
156
|
+
<div className="mt-0.5 text-[11px] text-muted-foreground">
|
|
157
|
+
{risk.description}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</li>
|
|
161
|
+
))}
|
|
162
|
+
</ul>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div className="space-y-1.5">
|
|
167
|
+
<Label className="text-[11px] uppercase tracking-wide">Triggers</Label>
|
|
168
|
+
<div className="flex flex-wrap gap-1">
|
|
169
|
+
{draft.triggers.map((t) => (
|
|
170
|
+
<code key={t} className="text-[10px] font-mono rounded bg-muted px-1.5 py-0.5">{t}</code>
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{draft.tests.length > 0 && (
|
|
176
|
+
<div className="space-y-1.5">
|
|
177
|
+
<Label className="text-[11px] uppercase tracking-wide">Tests</Label>
|
|
178
|
+
<ul className="space-y-1">
|
|
179
|
+
{draft.tests.map((t, i) => (
|
|
180
|
+
<li key={i} className="text-xs rounded-md border bg-muted/30 px-2 py-1.5">
|
|
181
|
+
<div className="font-medium">{t.name}</div>
|
|
182
|
+
<div className="text-[11px] text-muted-foreground">
|
|
183
|
+
Fixture: <code className="font-mono">{t.fixture}</code>
|
|
184
|
+
</div>
|
|
185
|
+
<div className="text-[11px] text-muted-foreground italic mt-0.5">
|
|
186
|
+
{t.assertionSummary}
|
|
187
|
+
</div>
|
|
188
|
+
</li>
|
|
189
|
+
))}
|
|
190
|
+
</ul>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{draft.notes && (
|
|
195
|
+
<div className="text-[11px] text-muted-foreground italic border-l-2 border-muted pl-2">
|
|
196
|
+
{draft.notes}
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
{!readOnly && (
|
|
201
|
+
<div className="flex items-center justify-end gap-2 pt-1">
|
|
202
|
+
<Button variant="ghost" size="sm" onClick={onCancel}>
|
|
203
|
+
<X className="mr-1 h-3.5 w-3.5" />
|
|
204
|
+
Cancel
|
|
205
|
+
</Button>
|
|
206
|
+
<Button size="sm" onClick={() => onApprove(draft)}>
|
|
207
|
+
<Check className="mr-1 h-3.5 w-3.5" />
|
|
208
|
+
Author it
|
|
209
|
+
</Button>
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function RiskBadge({ tier }: { tier: RiskTier }) {
|
|
217
|
+
return (
|
|
218
|
+
<span className={cn(
|
|
219
|
+
'inline-flex items-center rounded-full px-1.5 py-0.5 text-[9px] font-medium uppercase tracking-wide',
|
|
220
|
+
tier === 'red' && 'bg-destructive/20 text-destructive',
|
|
221
|
+
tier === 'yellow' && 'bg-amber-500/20 text-amber-600 dark:text-amber-400',
|
|
222
|
+
tier === 'green' && 'bg-emerald-500/20 text-emerald-700 dark:text-emerald-400',
|
|
223
|
+
)}>
|
|
224
|
+
{tier}
|
|
225
|
+
</span>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
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
|
+
* `PrivacyPanel` — local privacy controls.
|
|
7
|
+
*
|
|
8
|
+
* Surfaces the no-content rule from RFC §06 §7 in prose, plus three
|
|
9
|
+
* actions the user can take any time:
|
|
10
|
+
*
|
|
11
|
+
* - Export the action log as a JSON file (data they can audit).
|
|
12
|
+
* - Clear the action log.
|
|
13
|
+
* - Edit the prompt overlay (their personal notes the assistant
|
|
14
|
+
* sees alongside the system prompt).
|
|
15
|
+
*
|
|
16
|
+
* Everything here is local. Nothing here triggers a network call.
|
|
17
|
+
*
|
|
18
|
+
* Spec: docs/architecture/ai-customization/06-self-improvement.md §7.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useEffect, useRef, useState } from 'react';
|
|
22
|
+
import { Brain, Download, Eraser, ScrollText, Save, Shield, X } from 'lucide-react';
|
|
23
|
+
import {
|
|
24
|
+
clampOverlay,
|
|
25
|
+
extractMemoryProposals,
|
|
26
|
+
mergeIntoOverlay,
|
|
27
|
+
type Flavor,
|
|
28
|
+
type MemoryProposal,
|
|
29
|
+
type TranscriptTurn,
|
|
30
|
+
} from '@ifc-lite/extensions';
|
|
31
|
+
import { useViewerStore } from '@/store';
|
|
32
|
+
import { Button } from '@/components/ui/button';
|
|
33
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
34
|
+
import { useExtensionHost } from '@/sdk/ExtensionHostProvider';
|
|
35
|
+
import { toast } from '@/components/ui/toast';
|
|
36
|
+
import { HelpHint } from './HelpHint';
|
|
37
|
+
|
|
38
|
+
interface PrivacyPanelProps {
|
|
39
|
+
onClose?: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function PrivacyPanel({ onClose }: PrivacyPanelProps) {
|
|
43
|
+
const host = useExtensionHost();
|
|
44
|
+
const [logSize, setLogSize] = useState({ events: 0, bytes: 0 });
|
|
45
|
+
const [activeFlavor, setActiveFlavor] = useState<Flavor | undefined>();
|
|
46
|
+
const [overlayDraft, setOverlayDraft] = useState('');
|
|
47
|
+
const [dirty, setDirty] = useState(false);
|
|
48
|
+
const [busy, setBusy] = useState(false);
|
|
49
|
+
const [proposals, setProposals] = useState<MemoryProposal[]>([]);
|
|
50
|
+
const chatMessages = useViewerStore((s) => s.chatMessages);
|
|
51
|
+
// `refresh` is captured once by the long-lived `flavors.onChange`
|
|
52
|
+
// listener, so it must read `dirty` through a ref — a closed-over
|
|
53
|
+
// `dirty` would freeze at `false` and clobber the user's edits when
|
|
54
|
+
// a later flavor change fires.
|
|
55
|
+
const dirtyRef = useRef(dirty);
|
|
56
|
+
dirtyRef.current = dirty;
|
|
57
|
+
|
|
58
|
+
const refresh = async () => {
|
|
59
|
+
try {
|
|
60
|
+
setLogSize({ events: host.actionLog.size(), bytes: host.actionLog.byteSize() });
|
|
61
|
+
const flavor = await host.flavors.getActive();
|
|
62
|
+
setActiveFlavor(flavor);
|
|
63
|
+
if (flavor && !dirtyRef.current) {
|
|
64
|
+
setOverlayDraft(flavor.promptOverlay?.content ?? '');
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.warn('[PrivacyPanel] refresh failed:', err);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
void refresh();
|
|
73
|
+
const offFlavor = host.flavors.onChange(() => void refresh());
|
|
74
|
+
const offLog = host.actionLog.subscribe(() => {
|
|
75
|
+
setLogSize({ events: host.actionLog.size(), bytes: host.actionLog.byteSize() });
|
|
76
|
+
});
|
|
77
|
+
return () => {
|
|
78
|
+
offFlavor();
|
|
79
|
+
offLog();
|
|
80
|
+
};
|
|
81
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
+
}, [host]);
|
|
83
|
+
|
|
84
|
+
const handleExportLog = () => {
|
|
85
|
+
const json = host.actionLog.exportJson();
|
|
86
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
87
|
+
const url = URL.createObjectURL(blob);
|
|
88
|
+
const a = document.createElement('a');
|
|
89
|
+
a.href = url;
|
|
90
|
+
a.download = `ifclite-action-log-${new Date().toISOString().slice(0, 10)}.json`;
|
|
91
|
+
a.click();
|
|
92
|
+
URL.revokeObjectURL(url);
|
|
93
|
+
toast.success('Action log exported.');
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleClearLog = () => {
|
|
97
|
+
if (!confirm('Clear the local action log? Suggestions reset until you build up new patterns.')) return;
|
|
98
|
+
host.actionLog.clear();
|
|
99
|
+
// Wipe the IDB mirror too — otherwise reload would resurrect the
|
|
100
|
+
// events the user just asked to forget.
|
|
101
|
+
void host.clearPersistedActionLog().catch((err) => {
|
|
102
|
+
console.warn('[PrivacyPanel] clear persisted action log failed:', err);
|
|
103
|
+
});
|
|
104
|
+
setLogSize({ events: 0, bytes: 0 });
|
|
105
|
+
toast.success('Action log cleared.');
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const handleExtractMemory = () => {
|
|
109
|
+
const transcript: TranscriptTurn[] = chatMessages.map((m) => ({
|
|
110
|
+
role: m.role === 'system' ? 'system' : (m.role as 'user' | 'assistant'),
|
|
111
|
+
content: m.content,
|
|
112
|
+
}));
|
|
113
|
+
const next = extractMemoryProposals(transcript);
|
|
114
|
+
setProposals(next);
|
|
115
|
+
if (next.length === 0) {
|
|
116
|
+
toast.info('No stable preferences detected in this session yet.');
|
|
117
|
+
} else {
|
|
118
|
+
toast.success(`Found ${next.length} candidate preference${next.length === 1 ? '' : 's'}.`);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleAcceptProposals = () => {
|
|
123
|
+
const next = mergeIntoOverlay(overlayDraft, proposals);
|
|
124
|
+
setOverlayDraft(next);
|
|
125
|
+
setDirty(true);
|
|
126
|
+
setProposals([]);
|
|
127
|
+
toast.success(`Added ${proposals.length} preference${proposals.length === 1 ? '' : 's'} to the overlay. Save to keep them.`);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const handleSaveOverlay = async () => {
|
|
131
|
+
if (!activeFlavor) {
|
|
132
|
+
toast.error('No active flavor — switch to one before editing its overlay.');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
setBusy(true);
|
|
136
|
+
try {
|
|
137
|
+
const clamped = clampOverlay(overlayDraft, { maxTokens: 4000 });
|
|
138
|
+
await host.flavors.put(
|
|
139
|
+
{ ...activeFlavor, promptOverlay: clamped.overlay },
|
|
140
|
+
'overlay edit',
|
|
141
|
+
);
|
|
142
|
+
setOverlayDraft(clamped.overlay.content);
|
|
143
|
+
setDirty(false);
|
|
144
|
+
if (clamped.truncated) {
|
|
145
|
+
toast.info(`Overlay clamped to ~${clamped.estimatedTokens} tokens.`);
|
|
146
|
+
} else {
|
|
147
|
+
toast.success(`Overlay saved (${clamped.estimatedTokens} tokens).`);
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
toast.error(`Save failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
151
|
+
} finally {
|
|
152
|
+
setBusy(false);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className="flex flex-col h-full">
|
|
158
|
+
<div className="flex items-center justify-between border-b px-4 py-3">
|
|
159
|
+
<div className="flex items-center gap-2">
|
|
160
|
+
<Shield className="h-4 w-4" />
|
|
161
|
+
<h2 className="text-sm font-semibold">Privacy</h2>
|
|
162
|
+
<HelpHint label="Privacy">
|
|
163
|
+
<p>
|
|
164
|
+
IFClite keeps a <strong>content-free action log</strong>{' '}
|
|
165
|
+
of intents you perform (model loads, lens applies,
|
|
166
|
+
exports) — used by the pattern miner to suggest one-click
|
|
167
|
+
tools. The log never records model content, chat content,
|
|
168
|
+
file names, or API keys.
|
|
169
|
+
</p>
|
|
170
|
+
<p>
|
|
171
|
+
The <strong>prompt overlay</strong> on the active flavor
|
|
172
|
+
is appended to every chat system prompt — use it for
|
|
173
|
+
stable preferences. <strong>Extract from chat</strong>{' '}
|
|
174
|
+
scans the current session for explicit preferences and
|
|
175
|
+
proposes them.
|
|
176
|
+
</p>
|
|
177
|
+
</HelpHint>
|
|
178
|
+
</div>
|
|
179
|
+
{onClose && (
|
|
180
|
+
<Button size="icon" variant="ghost" onClick={onClose} aria-label="Close">
|
|
181
|
+
<X className="h-3.5 w-3.5" />
|
|
182
|
+
</Button>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<ScrollArea className="flex-1">
|
|
187
|
+
<div className="px-4 py-3 space-y-4 text-xs">
|
|
188
|
+
<section className="space-y-1.5">
|
|
189
|
+
<h3 className="text-[11px] uppercase tracking-wide font-semibold text-muted-foreground">
|
|
190
|
+
What we store locally
|
|
191
|
+
</h3>
|
|
192
|
+
<p className="text-muted-foreground leading-relaxed">
|
|
193
|
+
ifc-lite keeps a content-free <strong>action log</strong> of the
|
|
194
|
+
high-level intents you perform (model loads, lens applies,
|
|
195
|
+
exports). We use it to mine recurring patterns and surface
|
|
196
|
+
one-click tool suggestions. The log never records model
|
|
197
|
+
content, chat content, file names, or API keys.
|
|
198
|
+
</p>
|
|
199
|
+
<p className="text-muted-foreground leading-relaxed">
|
|
200
|
+
Suggestions, the audit log, the prompt overlay, and your
|
|
201
|
+
flavor library are all stored in your browser's IndexedDB —
|
|
202
|
+
nothing here is sent off device unless you explicitly export.
|
|
203
|
+
</p>
|
|
204
|
+
</section>
|
|
205
|
+
|
|
206
|
+
<section className="space-y-1.5">
|
|
207
|
+
<h3 className="text-[11px] uppercase tracking-wide font-semibold text-muted-foreground">
|
|
208
|
+
Action log
|
|
209
|
+
</h3>
|
|
210
|
+
<div className="rounded border bg-muted/30 px-3 py-2">
|
|
211
|
+
<div>
|
|
212
|
+
{logSize.events} events · {(logSize.bytes / 1024).toFixed(1)} KiB
|
|
213
|
+
</div>
|
|
214
|
+
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
215
|
+
<Button size="sm" variant="outline" onClick={handleExportLog} disabled={logSize.events === 0}>
|
|
216
|
+
<Download className="mr-1 h-3.5 w-3.5" />
|
|
217
|
+
Export JSON
|
|
218
|
+
</Button>
|
|
219
|
+
<Button size="sm" variant="outline" onClick={handleClearLog} disabled={logSize.events === 0}>
|
|
220
|
+
<Eraser className="mr-1 h-3.5 w-3.5" />
|
|
221
|
+
Clear
|
|
222
|
+
</Button>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</section>
|
|
226
|
+
|
|
227
|
+
<section className="space-y-1.5">
|
|
228
|
+
<h3 className="text-[11px] uppercase tracking-wide font-semibold text-muted-foreground">
|
|
229
|
+
Prompt overlay
|
|
230
|
+
</h3>
|
|
231
|
+
<p className="text-muted-foreground">
|
|
232
|
+
Notes appended to the AI assistant's system prompt for the
|
|
233
|
+
active flavor. Use it for stable preferences ("write CSV
|
|
234
|
+
exports with semicolons", "default to red color for IfcWall").
|
|
235
|
+
Capped at ~4000 tokens.
|
|
236
|
+
</p>
|
|
237
|
+
{!activeFlavor ? (
|
|
238
|
+
<div className="rounded border bg-muted/30 px-3 py-2 text-muted-foreground italic">
|
|
239
|
+
No active flavor. Activate or import one to attach overlay
|
|
240
|
+
notes to it.
|
|
241
|
+
</div>
|
|
242
|
+
) : (
|
|
243
|
+
<>
|
|
244
|
+
<div className="text-[10px] text-muted-foreground flex items-center gap-1">
|
|
245
|
+
<ScrollText className="h-3 w-3" />
|
|
246
|
+
Editing overlay for{' '}
|
|
247
|
+
<span className="font-medium text-foreground">{activeFlavor.name}</span>
|
|
248
|
+
</div>
|
|
249
|
+
<textarea
|
|
250
|
+
className="w-full min-h-[160px] rounded border bg-background p-2 font-mono text-[11px] leading-relaxed"
|
|
251
|
+
value={overlayDraft}
|
|
252
|
+
onChange={(e) => {
|
|
253
|
+
setOverlayDraft(e.target.value);
|
|
254
|
+
setDirty(true);
|
|
255
|
+
}}
|
|
256
|
+
placeholder="e.g. Always export CSV with semicolon separators. Default lens for IfcWall: by-fire-rating."
|
|
257
|
+
/>
|
|
258
|
+
<div className="flex items-center justify-between gap-2 flex-wrap">
|
|
259
|
+
<span className="text-[10px] text-muted-foreground">
|
|
260
|
+
{Math.ceil(overlayDraft.length / 4)} approx tokens
|
|
261
|
+
</span>
|
|
262
|
+
<div className="flex items-center gap-1">
|
|
263
|
+
<Button size="sm" variant="outline" onClick={handleExtractMemory} disabled={chatMessages.length === 0}>
|
|
264
|
+
<Brain className="mr-1 h-3.5 w-3.5" />
|
|
265
|
+
Extract from chat
|
|
266
|
+
</Button>
|
|
267
|
+
<Button size="sm" onClick={() => void handleSaveOverlay()} disabled={busy || !dirty}>
|
|
268
|
+
<Save className="mr-1 h-3.5 w-3.5" />
|
|
269
|
+
Save overlay
|
|
270
|
+
</Button>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
{proposals.length > 0 && (
|
|
275
|
+
<div className="rounded border bg-muted/30 px-3 py-2 space-y-2">
|
|
276
|
+
<div className="text-[11px] font-medium">
|
|
277
|
+
{proposals.length} candidate preference{proposals.length === 1 ? '' : 's'}
|
|
278
|
+
</div>
|
|
279
|
+
<div className="text-[10px] text-amber-700 dark:text-amber-400 italic">
|
|
280
|
+
Rule-based scan — review each line before saving.
|
|
281
|
+
The extractor uses a heuristic blocklist; it is not
|
|
282
|
+
a guarantee that no content slips through.
|
|
283
|
+
</div>
|
|
284
|
+
<ul className="space-y-1 text-[11px]">
|
|
285
|
+
{proposals.map((p, i) => (
|
|
286
|
+
<li key={i} className="flex items-start gap-2">
|
|
287
|
+
<span className="text-muted-foreground">·</span>
|
|
288
|
+
<span className="flex-1">{p.phrasing}</span>
|
|
289
|
+
<span className="text-[10px] text-muted-foreground">
|
|
290
|
+
{Math.round(p.confidence * 100)}%
|
|
291
|
+
</span>
|
|
292
|
+
</li>
|
|
293
|
+
))}
|
|
294
|
+
</ul>
|
|
295
|
+
<div className="flex items-center justify-end gap-1">
|
|
296
|
+
<Button size="sm" variant="ghost" onClick={() => setProposals([])}>
|
|
297
|
+
Discard
|
|
298
|
+
</Button>
|
|
299
|
+
<Button size="sm" onClick={handleAcceptProposals}>
|
|
300
|
+
Add to overlay
|
|
301
|
+
</Button>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
</>
|
|
306
|
+
)}
|
|
307
|
+
</section>
|
|
308
|
+
</div>
|
|
309
|
+
</ScrollArea>
|
|
310
|
+
</div>
|
|
311
|
+
);
|
|
312
|
+
}
|