@genesislcap/ai-assistant 15.23.3 → 15.24.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/dist/ai-assistant.api.json +419 -50
- package/dist/ai-assistant.d.ts +217 -683
- package/dist/chat-driver.cjs.map +1 -1
- package/dist/chat-driver.mjs.map +1 -1
- package/dist/custom-elements.json +310 -609
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +16 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/index.d.ts +1 -0
- package/dist/dts/components/settings-modal/index.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.d.ts +215 -0
- package/dist/dts/components/settings-modal/settings-modal.d.ts.map +1 -0
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
- package/dist/dts/main/attachment-controller.d.ts +66 -0
- package/dist/dts/main/attachment-controller.d.ts.map +1 -0
- package/dist/dts/main/budget-controller.d.ts +368 -0
- package/dist/dts/main/budget-controller.d.ts.map +1 -0
- package/dist/dts/main/composer-menu-controller.d.ts +113 -0
- package/dist/dts/main/composer-menu-controller.d.ts.map +1 -0
- package/dist/dts/main/composer-resize-controller.d.ts +80 -0
- package/dist/dts/main/composer-resize-controller.d.ts.map +1 -0
- package/dist/dts/main/context-gate-controller.d.ts +197 -0
- package/dist/dts/main/context-gate-controller.d.ts.map +1 -0
- package/dist/dts/main/cost-ledger-controller.d.ts +228 -0
- package/dist/dts/main/cost-ledger-controller.d.ts.map +1 -0
- package/dist/dts/main/main.d.ts +201 -722
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.types.d.ts +25 -0
- package/dist/dts/main/main.types.d.ts.map +1 -1
- package/dist/dts/main/meta-snapshot.d.ts +50 -0
- package/dist/dts/main/meta-snapshot.d.ts.map +1 -0
- package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +18 -0
- package/dist/esm/components/settings-modal/index.js +1 -0
- package/dist/esm/components/settings-modal/settings-modal.js +436 -0
- package/dist/esm/components/settings-modal/settings-modal.template.js +15 -4
- package/dist/esm/main/attachment-controller.js +254 -0
- package/dist/esm/main/budget-controller.js +745 -0
- package/dist/esm/main/composer-menu-controller.js +234 -0
- package/dist/esm/main/composer-resize-controller.js +119 -0
- package/dist/esm/main/context-gate-controller.js +289 -0
- package/dist/esm/main/cost-ledger-controller.js +498 -0
- package/dist/esm/main/main.js +501 -2268
- package/dist/esm/main/meta-snapshot.js +108 -0
- package/dist/esm/state/persistence/session-persister.js +5 -4
- package/docs/migration-GENC-1580.md +209 -0
- package/package.json +16 -16
|
@@ -100,15 +100,26 @@ const platformContextUsageTemplate = (progressTag) => html `
|
|
|
100
100
|
<div class="settings-context-usage" part="context-usage">
|
|
101
101
|
<div class="settings-context-header">
|
|
102
102
|
<span class="settings-context-label">Context window</span>
|
|
103
|
+
<!--
|
|
104
|
+
Read through the pre-formatted \`settingsContextUsage\` rather than
|
|
105
|
+
\`contextTokens\`/\`contextLimit\` directly. These bindings used to carry
|
|
106
|
+
non-null assertions, and they re-evaluate in the SAME notify batch as the
|
|
107
|
+
\`when\` above that unmounts them — so a session Clear with this tab open
|
|
108
|
+
threw \`Cannot read properties of undefined (reading 'toLocaleString')\`
|
|
109
|
+
twice per render. Optional access on a shape that can be absent cannot.
|
|
110
|
+
The \`?? ''\` fallbacks matter for the same reason: these bindings provably
|
|
111
|
+
DO evaluate while the figures are absent, so without them a lost race
|
|
112
|
+
with the guard would flash the literal text "undefined".
|
|
113
|
+
-->
|
|
103
114
|
<span class="settings-context-tokens">
|
|
104
|
-
${(x) => x.
|
|
105
|
-
${(x) => x.
|
|
106
|
-
(${(x) =>
|
|
115
|
+
${(x) => { var _a, _b; return (_b = (_a = x.settingsContextUsage) === null || _a === void 0 ? void 0 : _a.used) !== null && _b !== void 0 ? _b : ''; }} /
|
|
116
|
+
${(x) => { var _a, _b; return (_b = (_a = x.settingsContextUsage) === null || _a === void 0 ? void 0 : _a.limit) !== null && _b !== void 0 ? _b : ''; }} tokens
|
|
117
|
+
(${(x) => { var _a, _b; return (_b = (_a = x.settingsContextUsage) === null || _a === void 0 ? void 0 : _a.percent) !== null && _b !== void 0 ? _b : ''; }}%)
|
|
107
118
|
</span>
|
|
108
119
|
</div>
|
|
109
120
|
<${progressTag}
|
|
110
121
|
part="context-progress"
|
|
111
|
-
value="${(x) =>
|
|
122
|
+
value="${(x) => { var _a, _b; return (_b = (_a = x.settingsContextUsage) === null || _a === void 0 ? void 0 : _a.barValue) !== null && _b !== void 0 ? _b : 0; }}"
|
|
112
123
|
></${progressTag}>
|
|
113
124
|
</div>
|
|
114
125
|
`)}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { __awaiter, __decorate } from "tslib";
|
|
2
|
+
import { observable } from '@genesislcap/web-core';
|
|
3
|
+
import { logger } from '../utils/logger';
|
|
4
|
+
/**
|
|
5
|
+
* The image types BOTH vendors accept, and therefore the only ones sent as image blocks.
|
|
6
|
+
*
|
|
7
|
+
* A host's `acceptedFiles` may well say `image/*`, which covers HEIC, BMP, TIFF, AVIF and SVG —
|
|
8
|
+
* none of which the APIs take. Sending one fails the whole turn the user is waiting on, so
|
|
9
|
+
* anything outside this set falls through to the text branch instead: harmless for a binary, and
|
|
10
|
+
* genuinely useful for SVG, which is text a model can read.
|
|
11
|
+
*
|
|
12
|
+
* `image/jpg` is absent on purpose — `normalizeImageMime` rewrites it to `image/jpeg` at the
|
|
13
|
+
* transport, but a file arriving with that spelling is still a real JPEG, so it is accepted here.
|
|
14
|
+
*/
|
|
15
|
+
const VENDOR_IMAGE_MIME_TYPES = new Set([
|
|
16
|
+
'image/jpeg',
|
|
17
|
+
'image/jpg',
|
|
18
|
+
'image/png',
|
|
19
|
+
'image/gif',
|
|
20
|
+
'image/webp',
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Ceiling on ONE image attachment, under Anthropic's ~5MB-per-image limit with room for the ~33%
|
|
24
|
+
* base64 overhead. Refused here, with the size named, rather than at the vendor: a 400 arrives
|
|
25
|
+
* only after the user has waited out the turn.
|
|
26
|
+
*
|
|
27
|
+
* A VENDOR guard, and only that — it does not protect the session's storage quota, and the
|
|
28
|
+
* arithmetic says so: one file at this ceiling is ~4.7MB of base64, which alone can exceed a 5MB
|
|
29
|
+
* `localStorage` budget before the transcript is counted. Attachments riding into persisted
|
|
30
|
+
* storage is a real and separate problem; it is not this constant's to solve.
|
|
31
|
+
*/
|
|
32
|
+
const MAX_IMAGE_ATTACHMENT_BYTES = 3500000;
|
|
33
|
+
/**
|
|
34
|
+
* Ceiling on ALL attachments carried by one message, across every type and every successive pick.
|
|
35
|
+
*
|
|
36
|
+
* The per-file check above is blind to the total: the picker APPENDS, so ten files each just under
|
|
37
|
+
* the per-image limit are ten passes and one ~34MB request. Sized against the vendors' request
|
|
38
|
+
* ceilings — comfortably inside the smaller of them with room for the transcript itself — so a
|
|
39
|
+
* turn that would be rejected wholesale is refused here, where the user can still act on it,
|
|
40
|
+
* naming the file that crossed the line.
|
|
41
|
+
*
|
|
42
|
+
* Like its sibling, this does NOT make attachments safe to persist — 8MB of attachments is still
|
|
43
|
+
* ~10.7MB of base64. What a persisted session should keep of its images is GENC-1531; note the
|
|
44
|
+
* transports also REPLAY history attachments on every later call, so it is a recurring cost as
|
|
45
|
+
* well as a storage one.
|
|
46
|
+
*/
|
|
47
|
+
const MAX_ATTACHMENTS_TOTAL_BYTES = 8000000;
|
|
48
|
+
/** Base64 carries 3 bytes in every 4 characters, so decoded size is 3/4 of the encoded length. */
|
|
49
|
+
const BASE64_DECODED_RATIO = 0.75;
|
|
50
|
+
/** Bytes per megabyte, for the size the refusal message quotes back to the user. */
|
|
51
|
+
const BYTES_PER_MB = 1000000;
|
|
52
|
+
/** A rough byte size for an already-accepted attachment, for budgeting against the total cap. */
|
|
53
|
+
function attachmentBytes(a) {
|
|
54
|
+
var _a, _b, _c, _d;
|
|
55
|
+
return a.kind === 'image'
|
|
56
|
+
? Math.ceil(((_b = (_a = a.data) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) * BASE64_DECODED_RATIO)
|
|
57
|
+
: ((_d = (_c = a.content) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The composer's file attachments: what is pending on the next message, what was
|
|
61
|
+
* rejected and why, and the reading and validation that gets a picked `File` into
|
|
62
|
+
* a {@link ChatAttachment}.
|
|
63
|
+
*
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
export class AttachmentController {
|
|
67
|
+
constructor(
|
|
68
|
+
/** Typed `HTMLElement` — the hidden file input is all this needs from the DOM. */
|
|
69
|
+
host, deps) {
|
|
70
|
+
this.host = host;
|
|
71
|
+
this.deps = deps;
|
|
72
|
+
/** Attachments pending on the next message. */
|
|
73
|
+
this.attachments = [];
|
|
74
|
+
/** Rejection messages for files the last pick refused. */
|
|
75
|
+
this.errors = [];
|
|
76
|
+
}
|
|
77
|
+
/** Open the OS file picker via the hidden input the template renders. */
|
|
78
|
+
triggerFileInput() {
|
|
79
|
+
var _a, _b;
|
|
80
|
+
(_b = (_a = this.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.file-input')) === null || _b === void 0 ? void 0 : _b.click();
|
|
81
|
+
}
|
|
82
|
+
/** `@change` on the hidden input. Fire-and-forget; the read is async. */
|
|
83
|
+
handleFileSelect(e) {
|
|
84
|
+
void this.loadSelected(e);
|
|
85
|
+
}
|
|
86
|
+
remove(attachment) {
|
|
87
|
+
this.attachments = this.attachments.filter((a) => a !== attachment);
|
|
88
|
+
}
|
|
89
|
+
dismissError(message) {
|
|
90
|
+
const idx = this.errors.indexOf(message);
|
|
91
|
+
if (idx !== -1) {
|
|
92
|
+
this.errors = [...this.errors.slice(0, idx), ...this.errors.slice(idx + 1)];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Hand the pending attachments to a send and clear both lists in one step.
|
|
97
|
+
*
|
|
98
|
+
* Read-and-clear as one method rather than two statements: the previous inline
|
|
99
|
+
* form copied the array and then blanked the fields separately, leaving a state
|
|
100
|
+
* the element could be observed in between the two.
|
|
101
|
+
*/
|
|
102
|
+
take() {
|
|
103
|
+
const pending = this.attachments.length ? [...this.attachments] : undefined;
|
|
104
|
+
this.attachments = [];
|
|
105
|
+
this.errors = [];
|
|
106
|
+
return pending;
|
|
107
|
+
}
|
|
108
|
+
/** Drop everything pending, without handing it anywhere. */
|
|
109
|
+
clear() {
|
|
110
|
+
this.attachments = [];
|
|
111
|
+
this.errors = [];
|
|
112
|
+
}
|
|
113
|
+
/** Total bytes already committed, for budgeting a further pick against the cap. */
|
|
114
|
+
pendingBytes() {
|
|
115
|
+
return this.attachments.reduce((n, a) => n + attachmentBytes(a), 0);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Read and validate a batch of files without committing them — for
|
|
119
|
+
* `submitMessage({ files })`, which must be able to refuse the whole message
|
|
120
|
+
* rather than half-attach it.
|
|
121
|
+
*/
|
|
122
|
+
process(files_1) {
|
|
123
|
+
return __awaiter(this, arguments, void 0, function* (files, existingBytes = 0) {
|
|
124
|
+
// Budget the batch BEFORE reading any of it. `file.size` needs no I/O, so the verdict is
|
|
125
|
+
// deterministic and in the user's own pick order; deciding inside the parallel map below would
|
|
126
|
+
// hand the last slot to whichever read happened to finish first. `existingBytes` is what the
|
|
127
|
+
// caller has already committed to — the picker appends to a running list, so its files have to
|
|
128
|
+
// count against what is already attached rather than starting from zero each time.
|
|
129
|
+
let running = existingBytes;
|
|
130
|
+
const overBudget = new Map();
|
|
131
|
+
for (const file of files) {
|
|
132
|
+
if (running + file.size > MAX_ATTACHMENTS_TOTAL_BYTES) {
|
|
133
|
+
overBudget.set(file, `"${file.name}" would take this message past the ` +
|
|
134
|
+
`${MAX_ATTACHMENTS_TOTAL_BYTES / BYTES_PER_MB}MB attachment limit ` +
|
|
135
|
+
`(${(running / BYTES_PER_MB).toFixed(1)}MB already attached).`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
running += file.size;
|
|
139
|
+
}
|
|
140
|
+
const results = yield Promise.all(files.map((file) => __awaiter(this, void 0, void 0, function* () {
|
|
141
|
+
if (!this.isAccepted(file)) {
|
|
142
|
+
return { ok: false, message: `"${file.name}" is not an accepted file type.` };
|
|
143
|
+
}
|
|
144
|
+
const tooMuch = overBudget.get(file);
|
|
145
|
+
if (tooMuch)
|
|
146
|
+
return { ok: false, message: tooMuch };
|
|
147
|
+
try {
|
|
148
|
+
// An image file becomes a real image attachment — the transports render those as
|
|
149
|
+
// image blocks the model SEES. Reading it as text (the only path that existed)
|
|
150
|
+
// produced mojibake the model "read" as prose: an accepted .png was worse than a
|
|
151
|
+
// rejected one. Text stays the default for everything else.
|
|
152
|
+
//
|
|
153
|
+
// Only the types the vendors actually accept take this branch. `image/*` covers far
|
|
154
|
+
// more than that, and sending one an API rejects fails the whole turn the user is
|
|
155
|
+
// waiting on — where the text branch merely degrades. It also keeps SVG working:
|
|
156
|
+
// SVG *is* text, and reading it as such gives the model something it can genuinely
|
|
157
|
+
// read, which a base64 image block of it would not.
|
|
158
|
+
if (VENDOR_IMAGE_MIME_TYPES.has(file.type.toLowerCase())) {
|
|
159
|
+
if (file.size > MAX_IMAGE_ATTACHMENT_BYTES) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
message: `"${file.name}" is ${Math.ceil(file.size / BYTES_PER_MB)}MB; images are limited to ` +
|
|
163
|
+
`${MAX_IMAGE_ATTACHMENT_BYTES / BYTES_PER_MB}MB.`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const data = yield this.readAsBase64(file);
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
attachment: { kind: 'image', name: file.name, mimeType: file.type, data },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const content = yield this.readAsText(file);
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
attachment: { name: file.name, content, mimeType: file.type || 'text/plain' },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
catch (readError) {
|
|
179
|
+
logger.error('Failed to read file:', readError);
|
|
180
|
+
this.deps.logMeta('file.read-failed', {
|
|
181
|
+
file: file.name,
|
|
182
|
+
message: readError instanceof Error ? readError.message : String(readError),
|
|
183
|
+
});
|
|
184
|
+
return { ok: false, message: `Failed to read "${file.name}".` };
|
|
185
|
+
}
|
|
186
|
+
})));
|
|
187
|
+
return {
|
|
188
|
+
attachments: results
|
|
189
|
+
.filter((r) => r.ok)
|
|
190
|
+
.map((r) => r.attachment),
|
|
191
|
+
errors: results
|
|
192
|
+
.filter((r) => !r.ok)
|
|
193
|
+
.map((r) => r.message),
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
isAccepted(file) {
|
|
198
|
+
var _a, _b;
|
|
199
|
+
const accepted = this.deps.acceptedFiles();
|
|
200
|
+
if (!accepted)
|
|
201
|
+
return false;
|
|
202
|
+
const acceptedList = accepted.split(',').map((s) => s.trim().toLowerCase());
|
|
203
|
+
const ext = `.${(_b = (_a = file.name.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : ''}`;
|
|
204
|
+
return acceptedList.some((a) => a === ext ||
|
|
205
|
+
a === file.type ||
|
|
206
|
+
(a.endsWith('/*') && file.type.startsWith(a.replace('/*', '/'))));
|
|
207
|
+
}
|
|
208
|
+
// Blob's own promise APIs, not FileReader: UTF-8 only (a `charset` parameter on the blob's
|
|
209
|
+
// type is ignored, where FileReader would honour it), and they exist in the node test runtime
|
|
210
|
+
// too — FileReader does not, so neither reader was testable before.
|
|
211
|
+
readAsText(file) {
|
|
212
|
+
return file.text();
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The base64 payload of a file, without any `data:` prefix — the raw form
|
|
216
|
+
* `ChatImageAttachment.data` specifies (transports add their own framing).
|
|
217
|
+
*/
|
|
218
|
+
readAsBase64(file) {
|
|
219
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
220
|
+
const bytes = new Uint8Array(yield file.arrayBuffer());
|
|
221
|
+
let binary = '';
|
|
222
|
+
const CHUNK = 0x8000;
|
|
223
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
224
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
225
|
+
}
|
|
226
|
+
return btoa(binary);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/** The picker path: read the picked files and APPEND whatever passed. */
|
|
230
|
+
loadSelected(e) {
|
|
231
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
232
|
+
var _a;
|
|
233
|
+
const input = e.target;
|
|
234
|
+
const files = Array.from((_a = input.files) !== null && _a !== void 0 ? _a : []);
|
|
235
|
+
input.value = '';
|
|
236
|
+
const { attachments, errors } = yield this.process(files, this.pendingBytes());
|
|
237
|
+
if (attachments.length) {
|
|
238
|
+
this.attachments = [...this.attachments, ...attachments];
|
|
239
|
+
this.deps.logMeta('attachment.added', {
|
|
240
|
+
count: attachments.length,
|
|
241
|
+
names: attachments.map((a) => a.name),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (errors.length)
|
|
245
|
+
this.errors = [...this.errors, ...errors];
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
__decorate([
|
|
250
|
+
observable
|
|
251
|
+
], AttachmentController.prototype, "attachments", void 0);
|
|
252
|
+
__decorate([
|
|
253
|
+
observable
|
|
254
|
+
], AttachmentController.prototype, "errors", void 0);
|