@oar-sh/oar 0.9.23 → 0.9.25
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/.github/extensions/web-relay/skills/reasoning-stream.mjs +3 -1
- package/CHANGELOG.md +307 -0
- package/README.md +12 -11
- package/bin/oar.js +54 -4
- package/package.json +11 -7
- package/server/agent-sdk-loader.mjs +116 -0
- package/server/agent-sdks/claude/entry.mjs +4 -0
- package/server/agent-sdks/claude/package-lock.json +1438 -0
- package/server/agent-sdks/claude/package.json +13 -0
- package/server/agent-sdks/cursor/entry-sqlite.mjs +2 -0
- package/server/agent-sdks/cursor/entry.mjs +3 -0
- package/server/agent-sdks/cursor/package-lock.json +171 -0
- package/server/agent-sdks/cursor/package.json +11 -0
- package/server/agent-sdks/koffi/entry.cjs +2 -0
- package/server/agent-sdks/koffi/package-lock.json +368 -0
- package/server/agent-sdks/koffi/package.json +10 -0
- package/server/claude-worker/claude-preview-tool.mjs +5 -2
- package/server/claude-worker/claude-remote-relay-tool.mjs +20 -2
- package/server/claude-worker/claude-sdk-adapter.mjs +4 -1
- package/server/claude-worker/sdk-message-normalizer.mjs +29 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +13 -8
- package/server/cursor-worker/cursor-sdk-adapter.mjs +5 -3
- package/server/cursor-worker/sdk-message-normalizer.mjs +13 -0
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +3 -0
- package/server/migrations/0039-media-index.mjs +167 -0
- package/server/migrations/0040-image-session-defaults.mjs +93 -0
- package/server/public/app/agent-sdk-ui.js +349 -0
- package/server/public/app/api-client.js +149 -8
- package/server/public/app/ask-user-view.js +2 -1
- package/server/public/app/attachments-view.js +66 -9
- package/server/public/app/bootstrap.js +289 -94
- package/server/public/app/chat-actions-menu-state.mjs +1 -0
- package/server/public/app/context-media-tab.js +143 -0
- package/server/public/app/conversation-view.js +90 -5
- package/server/public/app/image-composer-ui.js +518 -0
- package/server/public/app/image-turn-placeholder.mjs +233 -0
- package/server/public/app/journal-view.js +12 -2
- package/server/public/app/media-gallery-model.mjs +202 -0
- package/server/public/app/media-gallery-ui.js +524 -0
- package/server/public/app/media-view.js +79 -0
- package/server/public/app/model-refresh-ui.mjs +413 -0
- package/server/public/app/no-conversation-state.mjs +5 -2
- package/server/public/app/openai-image-composer.mjs +492 -0
- package/server/public/app/openai-image-cost-ui.js +426 -0
- package/server/public/app/openai-image-spend.mjs +443 -0
- package/server/public/app/plan-usage-view.mjs +38 -6
- package/server/public/app/provider-readiness.mjs +67 -4
- package/server/public/app/relay-error-ctas.mjs +5 -0
- package/server/public/app/session-history-ui.js +2 -0
- package/server/public/app/settings-modal.js +171 -0
- package/server/public/app/socket-handlers.js +47 -0
- package/server/public/app/tab-strip-fade.mjs +1 -1
- package/server/public/app/viewer-gallery.mjs +23 -1
- package/server/public/index.html +478 -9
- package/server/relay-tools.md +1 -0
- package/server/repositories/image-conversation-repository.mjs +69 -0
- package/server/repositories/media-repository.mjs +305 -0
- package/server/routes/agent-sdk-routes.mjs +73 -0
- package/server/routes/media-routes.mjs +361 -0
- package/server/routes/messages-routes.mjs +1467 -343
- package/server/routes/sessions-routes.mjs +596 -123
- package/server/server-runtime.mjs +414 -81
- package/server/services/about-service.mjs +26 -6
- package/server/services/agent-sdk-launch-gate.mjs +124 -0
- package/server/services/agent-sdk-paths.mjs +307 -0
- package/server/services/agent-sdk-store.mjs +1211 -0
- package/server/services/claude-api-settings-service.mjs +54 -17
- package/server/services/cli-install-service.mjs +3 -2
- package/server/services/copilot-model-discovery-service.mjs +7 -1
- package/server/services/database-file.mjs +23 -5
- package/server/services/deepseek-settings-service.mjs +58 -20
- package/server/services/earlier-database-service.mjs +4 -2
- package/server/services/generated-image-cleanup-service.mjs +2 -1
- package/server/services/generated-image-files.mjs +110 -0
- package/server/services/image-input-paths.mjs +389 -0
- package/server/services/image-operation-service.mjs +6 -2
- package/server/services/media-extract.mjs +653 -0
- package/server/services/media-index-service.mjs +221 -0
- package/server/services/model-refresh-scheduler.mjs +153 -0
- package/server/services/model-variant-catalog-service.mjs +141 -9
- package/server/services/oar-cli-helpers.mjs +72 -9
- package/server/services/oar-state-migration-service.mjs +2 -12
- package/server/services/oar-state-root.mjs +17 -0
- package/server/services/openai-image-preview-turns.mjs +330 -0
- package/server/services/openai-image-spend-service.mjs +516 -0
- package/server/services/openai-image-stream.mjs +170 -0
- package/server/services/plan-usage-openai-image.mjs +148 -0
- package/server/services/provider-model-refresh.mjs +326 -0
- package/server/services/provider-model-state.mjs +395 -0
- package/server/services/relay-singleton-guard.mjs +72 -10
- package/server/services/remote-relay-caller-context.mjs +6 -0
- package/server/services/remote-relay-dispatcher.mjs +386 -22
- package/server/services/remote-relay-registry-service.mjs +3 -1
- package/server/services/session-worker-launch-service.mjs +13 -0
- package/server/services/session-worker-supervisor-service.mjs +19 -1
- package/server/services/session-worker-websocket-service.mjs +15 -0
- package/server/services/token-usage-service.mjs +15 -0
- package/server/services/update-install-service.mjs +2 -2
- package/server/services/worker-launch-note-service.mjs +14 -2
- package/server/services/worker-recycle-service.mjs +145 -11
- package/shared/image-header.mjs +90 -0
- package/shared/model-refresh-schedule.mjs +120 -0
- package/shared/openai-image-options.mjs +299 -0
- package/shared/openai-image-presets.mjs +175 -0
- package/shared/openai-image-previews.mjs +40 -0
- package/shared/openai-image-prices.mjs +291 -0
- package/shared/openai-image-spend.mjs +90 -0
- package/shared/remote-relay-contract.mjs +126 -4
- package/shared/worker-runtime/direct-openai-image.mjs +98 -48
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
// What OpenAI image turns cost, on screen: the relay's spend state (GET
|
|
2
|
+
// OPENAI_IMAGE_USAGE_PATH, then live through OPENAI_IMAGE_USAGE_EVENT), the
|
|
3
|
+
// banner of an image chat, the chat menu's cost line, the image cap section
|
|
4
|
+
// of Settings → Providers → OpenAI, and the image presets the composer and
|
|
5
|
+
// that section share. The wording is openai-image-spend.mjs's.
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
deleteOpenAIImagePreset,
|
|
9
|
+
loadOpenAIImagePresets,
|
|
10
|
+
loadOpenAIImageUsage,
|
|
11
|
+
updateOpenAIImageSpendSettings,
|
|
12
|
+
} from './api-client.js';
|
|
13
|
+
import {
|
|
14
|
+
describeImagePreset,
|
|
15
|
+
imageCapInputText,
|
|
16
|
+
imageChatCostText,
|
|
17
|
+
imageSpendBanner,
|
|
18
|
+
imageSpendOverrideButtons,
|
|
19
|
+
imageSpendSettingsLine,
|
|
20
|
+
normalizeImagePresets,
|
|
21
|
+
normalizeImageSpendState,
|
|
22
|
+
parseImageCapInput,
|
|
23
|
+
formatOpenAIImageUsd,
|
|
24
|
+
} from './openai-image-spend.mjs';
|
|
25
|
+
|
|
26
|
+
let spendState = null;
|
|
27
|
+
let spendLoadedAt = 0;
|
|
28
|
+
let spendRefresh = null;
|
|
29
|
+
const spendListeners = new Set();
|
|
30
|
+
// Whether the chat on screen is an image chat (the banner shows only there).
|
|
31
|
+
let bannerImageChat = false;
|
|
32
|
+
|
|
33
|
+
let presets = normalizeImagePresets(null);
|
|
34
|
+
let presetsLoaded = false;
|
|
35
|
+
let presetsRefresh = null;
|
|
36
|
+
const presetListeners = new Set();
|
|
37
|
+
|
|
38
|
+
let capInputDirty = false;
|
|
39
|
+
let spendSettingsInFlight = false;
|
|
40
|
+
// The cap and today's override as the last OpenAI settings answer named
|
|
41
|
+
// them: the buttons follow them until a spend state is known.
|
|
42
|
+
let settingsSpend = null;
|
|
43
|
+
|
|
44
|
+
const SPEND_STALE_MS = 60_000;
|
|
45
|
+
|
|
46
|
+
function el(id) {
|
|
47
|
+
return typeof document === 'undefined' ? null : document.getElementById(id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function notify(listeners, value) {
|
|
51
|
+
for (const listener of listeners) {
|
|
52
|
+
try { listener(value); } catch {}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- The spend state -------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
export function currentOpenAIImageSpendState() {
|
|
59
|
+
return spendState;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Called with the state after every change (the Check Usage modal listens). */
|
|
63
|
+
export function onOpenAIImageSpendChange(listener) {
|
|
64
|
+
if (typeof listener !== 'function') return () => {};
|
|
65
|
+
spendListeners.add(listener);
|
|
66
|
+
return () => spendListeners.delete(listener);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A state from the relay (the GET, the socket event): kept and shown. */
|
|
70
|
+
export function applyOpenAIImageSpendState(raw) {
|
|
71
|
+
const state = normalizeImageSpendState(raw);
|
|
72
|
+
if (!state) return spendState;
|
|
73
|
+
// The day's state; a chat's cost belongs to the request that asked for it.
|
|
74
|
+
delete state.conversation;
|
|
75
|
+
spendState = state;
|
|
76
|
+
spendLoadedAt = Date.now();
|
|
77
|
+
renderSpendBanner();
|
|
78
|
+
renderSpendSettings();
|
|
79
|
+
notify(spendListeners, spendState);
|
|
80
|
+
return spendState;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Reads the state again (serialised); the state, or null when the relay has none. */
|
|
84
|
+
export async function refreshOpenAIImageSpendState() {
|
|
85
|
+
if (spendRefresh) return spendRefresh;
|
|
86
|
+
spendRefresh = (async () => {
|
|
87
|
+
const answer = await loadOpenAIImageUsage();
|
|
88
|
+
return answer ? applyOpenAIImageSpendState(answer) : null;
|
|
89
|
+
})();
|
|
90
|
+
try {
|
|
91
|
+
return await spendRefresh;
|
|
92
|
+
} finally {
|
|
93
|
+
spendRefresh = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function refreshSpendIfStale() {
|
|
98
|
+
if (!spendState || Date.now() - spendLoadedAt > SPEND_STALE_MS) void refreshOpenAIImageSpendState();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- The banner of an image chat ---------------------------------------------
|
|
102
|
+
|
|
103
|
+
function renderSpendBanner() {
|
|
104
|
+
const banner = el('openai-image-spend-banner');
|
|
105
|
+
if (!banner) return;
|
|
106
|
+
const content = bannerImageChat ? imageSpendBanner(spendState) : null;
|
|
107
|
+
if (!content) {
|
|
108
|
+
banner.hidden = true;
|
|
109
|
+
banner.textContent = '';
|
|
110
|
+
delete banner.dataset.kind;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
banner.textContent = content.text;
|
|
114
|
+
banner.dataset.kind = content.kind;
|
|
115
|
+
banner.title = content.text;
|
|
116
|
+
banner.hidden = false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The chat on screen (or the composer's model) is an image chat, or not. */
|
|
120
|
+
export function syncOpenAIImageSpendBanner({ imageChat = false } = {}) {
|
|
121
|
+
bannerImageChat = imageChat === true;
|
|
122
|
+
if (bannerImageChat) refreshSpendIfStale();
|
|
123
|
+
renderSpendBanner();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---- The chat menu's cost line ---------------------------------------------
|
|
127
|
+
|
|
128
|
+
let chatCostRequest = 0;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Fills the chat menu's "OpenAI images: USD … in this chat" line for an
|
|
132
|
+
* image chat (asked from the relay each time the menu opens); hidden for
|
|
133
|
+
* any other chat.
|
|
134
|
+
*/
|
|
135
|
+
export async function syncChatMenuImageCost({ conversationId = '', imageChat = false } = {}) {
|
|
136
|
+
const row = el('chat-menu-image-cost');
|
|
137
|
+
const value = el('chat-menu-image-cost-value');
|
|
138
|
+
if (!row || !value) return;
|
|
139
|
+
const id = String(conversationId || '').trim();
|
|
140
|
+
const request = ++chatCostRequest;
|
|
141
|
+
if (!imageChat || !id) {
|
|
142
|
+
row.hidden = true;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
row.hidden = false;
|
|
146
|
+
value.textContent = 'OpenAI images: …';
|
|
147
|
+
const answer = await loadOpenAIImageUsage({ conversationId: id });
|
|
148
|
+
if (request !== chatCostRequest) return;
|
|
149
|
+
const state = normalizeImageSpendState(answer);
|
|
150
|
+
if (!state) {
|
|
151
|
+
value.textContent = 'OpenAI images: cost not available';
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
value.textContent = state.conversation
|
|
155
|
+
? imageChatCostText(state.conversation)
|
|
156
|
+
: 'OpenAI images: cost not available';
|
|
157
|
+
applyOpenAIImageSpendState(answer);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---- Settings → Providers → OpenAI: the cap ----------------------------------
|
|
161
|
+
|
|
162
|
+
function showSpendError(text) {
|
|
163
|
+
const error = el('openai-image-spend-error');
|
|
164
|
+
const input = el('openai-image-cap-input');
|
|
165
|
+
const message = String(text || '');
|
|
166
|
+
if (error) {
|
|
167
|
+
error.textContent = message;
|
|
168
|
+
error.hidden = !message;
|
|
169
|
+
}
|
|
170
|
+
if (input) {
|
|
171
|
+
if (message && input.dataset.invalid === '1') input.setAttribute('aria-invalid', 'true');
|
|
172
|
+
else input.removeAttribute('aria-invalid');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function setSpendControlsDisabled(disabled) {
|
|
177
|
+
for (const id of ['openai-image-cap-input', 'openai-image-cap-save-btn', 'openai-image-allow-today-btn', 'openai-image-stop-allowing-btn']) {
|
|
178
|
+
const control = el(id);
|
|
179
|
+
if (control) control.disabled = disabled;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderSpendSettings() {
|
|
184
|
+
const line = el('openai-image-spend-line');
|
|
185
|
+
if (line) {
|
|
186
|
+
line.textContent = spendState ? imageSpendSettingsLine(spendState) : 'Today\'s image spend is not known yet.';
|
|
187
|
+
line.dataset.state = spendState?.reached && !spendState?.allowedToday ? 'error' : (spendState?.warning ? 'warning' : 'saved');
|
|
188
|
+
}
|
|
189
|
+
const buttons = imageSpendOverrideButtons(spendState || settingsSpend);
|
|
190
|
+
const allow = el('openai-image-allow-today-btn');
|
|
191
|
+
if (allow) {
|
|
192
|
+
allow.hidden = !buttons.allow.shown;
|
|
193
|
+
if (buttons.allow.label) allow.textContent = buttons.allow.label;
|
|
194
|
+
}
|
|
195
|
+
const stop = el('openai-image-stop-allowing-btn');
|
|
196
|
+
if (stop) stop.hidden = !buttons.stop.shown;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The cap field from an OpenAI settings answer (GET, POST or the socket
|
|
201
|
+
* event): only an answer that names the cap changes it, and never while the
|
|
202
|
+
* field is being edited.
|
|
203
|
+
*/
|
|
204
|
+
export function applyOpenAIImageSpendSettings(settings = {}, { force = false } = {}) {
|
|
205
|
+
const named = settings && Object.prototype.hasOwnProperty.call(settings, 'imageDailyCapUsd');
|
|
206
|
+
if (named) {
|
|
207
|
+
settingsSpend = normalizeImageSpendState({
|
|
208
|
+
capUsd: settings.imageDailyCapUsd,
|
|
209
|
+
allowedToday: settings.imagesAllowedToday === true,
|
|
210
|
+
allowedAt: settings.imagesAllowedAt || null,
|
|
211
|
+
});
|
|
212
|
+
if (!spendState) renderSpendSettings();
|
|
213
|
+
}
|
|
214
|
+
const input = el('openai-image-cap-input');
|
|
215
|
+
if (!input) return;
|
|
216
|
+
bindSpendSettings();
|
|
217
|
+
if ((!named && !force) || (capInputDirty && !force)) return;
|
|
218
|
+
input.value = imageCapInputText(named ? settings.imageDailyCapUsd : undefined);
|
|
219
|
+
input.dataset.invalid = '';
|
|
220
|
+
capInputDirty = false;
|
|
221
|
+
showSpendError('');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Called when the Settings modal opens. */
|
|
225
|
+
export async function syncOpenAIImageSpendSettings() {
|
|
226
|
+
capInputDirty = false;
|
|
227
|
+
bindSpendSettings();
|
|
228
|
+
showSpendError('');
|
|
229
|
+
renderSpendSettings();
|
|
230
|
+
renderSettingsPresets();
|
|
231
|
+
await Promise.all([refreshOpenAIImageSpendState(), loadOpenAIImagePresetList({ force: true })]);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function runSpendUpdate(patch, successText) {
|
|
235
|
+
if (spendSettingsInFlight) return;
|
|
236
|
+
spendSettingsInFlight = true;
|
|
237
|
+
setSpendControlsDisabled(true);
|
|
238
|
+
showSpendError('');
|
|
239
|
+
try {
|
|
240
|
+
const result = await updateOpenAIImageSpendSettings(patch);
|
|
241
|
+
if (result) applyOpenAIImageSpendSettings(result);
|
|
242
|
+
// The relay sends the new state to every owner page; this one reads it too.
|
|
243
|
+
await refreshOpenAIImageSpendState();
|
|
244
|
+
const status = el('openai-image-spend-status');
|
|
245
|
+
if (status) status.textContent = successText;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
// A paired relay's request (403) and a refused amount say why.
|
|
248
|
+
showSpendError(error?.message || 'Could not update the image cap.');
|
|
249
|
+
} finally {
|
|
250
|
+
spendSettingsInFlight = false;
|
|
251
|
+
setSpendControlsDisabled(false);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function saveOpenAIImageCap() {
|
|
256
|
+
const input = el('openai-image-cap-input');
|
|
257
|
+
// A number field reads '' for text it cannot parse.
|
|
258
|
+
const parsed = input?.validity?.badInput ? parseImageCapInput('not a number') : parseImageCapInput(input?.value);
|
|
259
|
+
if (!parsed.ok) {
|
|
260
|
+
if (input) input.dataset.invalid = '1';
|
|
261
|
+
showSpendError(parsed.error);
|
|
262
|
+
input?.focus?.();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (input) input.dataset.invalid = '';
|
|
266
|
+
await runSpendUpdate(
|
|
267
|
+
{ imageDailyCapUsd: parsed.capUsd },
|
|
268
|
+
parsed.capUsd > 0 ? `Daily image cap saved: ${formatOpenAIImageUsd(parsed.capUsd)}.` : 'Daily image cap off: image turns are not stopped by spend.',
|
|
269
|
+
);
|
|
270
|
+
capInputDirty = false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function setOpenAIImagesAllowedToday(allowed) {
|
|
274
|
+
await runSpendUpdate(
|
|
275
|
+
{ allowImagesToday: allowed === true },
|
|
276
|
+
allowed === true ? 'Image turns are allowed past the cap until the end of the relay\'s day.' : 'Image turns stop at the cap again.',
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The cap refusal's button: the cap field, ready to raise. */
|
|
281
|
+
export function focusOpenAIImageCapInput() {
|
|
282
|
+
const input = el('openai-image-cap-input');
|
|
283
|
+
if (!input) return;
|
|
284
|
+
input.scrollIntoView?.({ block: 'center' });
|
|
285
|
+
input.focus?.();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function bindSpendSettings() {
|
|
289
|
+
const input = el('openai-image-cap-input');
|
|
290
|
+
if (!input || input.dataset.imageSpendBound === '1') return;
|
|
291
|
+
input.dataset.imageSpendBound = '1';
|
|
292
|
+
input.addEventListener('input', () => {
|
|
293
|
+
capInputDirty = true;
|
|
294
|
+
input.dataset.invalid = '';
|
|
295
|
+
showSpendError('');
|
|
296
|
+
});
|
|
297
|
+
input.addEventListener('keydown', (event) => {
|
|
298
|
+
if (event.key === 'Enter') {
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
void saveOpenAIImageCap();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
el('openai-image-cap-save-btn')?.addEventListener('click', () => { void saveOpenAIImageCap(); });
|
|
304
|
+
el('openai-image-allow-today-btn')?.addEventListener('click', () => { void setOpenAIImagesAllowedToday(true); });
|
|
305
|
+
el('openai-image-stop-allowing-btn')?.addEventListener('click', () => { void setOpenAIImagesAllowedToday(false); });
|
|
306
|
+
el('openai-image-presets-list')?.addEventListener('click', (event) => {
|
|
307
|
+
const button = event.target?.closest?.('[data-image-preset-delete]');
|
|
308
|
+
if (button) void removeOwnImagePreset(button.dataset.imagePresetDelete);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---- Presets ---------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
export function currentOpenAIImagePresets() {
|
|
315
|
+
return presets;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Called with the list after every change (the composer's select listens). */
|
|
319
|
+
export function onOpenAIImagePresetsChange(listener) {
|
|
320
|
+
if (typeof listener !== 'function') return () => {};
|
|
321
|
+
presetListeners.add(listener);
|
|
322
|
+
return () => presetListeners.delete(listener);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** A presets answer of the relay (GET, PUT, DELETE): kept and shown. */
|
|
326
|
+
export function applyOpenAIImagePresets(answer) {
|
|
327
|
+
if (!Array.isArray(answer?.presets)) return presets;
|
|
328
|
+
presets = normalizeImagePresets(answer);
|
|
329
|
+
presetsLoaded = true;
|
|
330
|
+
renderSettingsPresets();
|
|
331
|
+
notify(presetListeners, presets);
|
|
332
|
+
return presets;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Reads the list once (or again with `force`); the built-ins stand in until then. */
|
|
336
|
+
export async function loadOpenAIImagePresetList({ force = false } = {}) {
|
|
337
|
+
if (presetsLoaded && !force) return presets;
|
|
338
|
+
if (presetsRefresh) return presetsRefresh;
|
|
339
|
+
presetsRefresh = (async () => {
|
|
340
|
+
const answer = await loadOpenAIImagePresets();
|
|
341
|
+
return answer ? applyOpenAIImagePresets(answer) : presets;
|
|
342
|
+
})();
|
|
343
|
+
try {
|
|
344
|
+
return await presetsRefresh;
|
|
345
|
+
} finally {
|
|
346
|
+
presetsRefresh = null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function renderSettingsPresets() {
|
|
351
|
+
const list = el('openai-image-presets-list');
|
|
352
|
+
if (!list) return;
|
|
353
|
+
list.replaceChildren(...presets.map((preset) => {
|
|
354
|
+
const item = document.createElement('li');
|
|
355
|
+
item.className = 'openai-image-preset-row';
|
|
356
|
+
item.dataset.testid = 'openai-image-preset-row';
|
|
357
|
+
item.dataset.presetName = preset.name;
|
|
358
|
+
const text = document.createElement('span');
|
|
359
|
+
text.className = 'openai-image-preset-text';
|
|
360
|
+
const name = document.createElement('strong');
|
|
361
|
+
name.textContent = preset.name;
|
|
362
|
+
const details = document.createElement('span');
|
|
363
|
+
details.className = 'openai-image-preset-details';
|
|
364
|
+
details.textContent = describeImagePreset(preset);
|
|
365
|
+
text.append(name, details);
|
|
366
|
+
item.append(text);
|
|
367
|
+
if (preset.builtIn) {
|
|
368
|
+
const tag = document.createElement('span');
|
|
369
|
+
tag.className = 'openai-image-preset-tag';
|
|
370
|
+
tag.textContent = 'built-in';
|
|
371
|
+
item.append(tag);
|
|
372
|
+
} else {
|
|
373
|
+
const button = document.createElement('button');
|
|
374
|
+
button.type = 'button';
|
|
375
|
+
button.className = 'summary-btn';
|
|
376
|
+
button.textContent = 'Delete';
|
|
377
|
+
button.dataset.imagePresetDelete = preset.name;
|
|
378
|
+
button.dataset.testid = 'openai-image-preset-delete';
|
|
379
|
+
button.setAttribute('aria-label', `Delete the preset ${preset.name}`);
|
|
380
|
+
item.append(button);
|
|
381
|
+
}
|
|
382
|
+
return item;
|
|
383
|
+
}));
|
|
384
|
+
const own = presets.filter((preset) => !preset.builtIn).length;
|
|
385
|
+
const note = el('openai-image-presets-note');
|
|
386
|
+
if (note) {
|
|
387
|
+
note.textContent = own
|
|
388
|
+
? `${own} own preset${own === 1 ? '' : 's'}. Save more from an image chat's ⚙ settings (Save as preset…).`
|
|
389
|
+
: 'No own presets yet: save one from an image chat\'s ⚙ settings (Save as preset…).';
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function removeOwnImagePreset(name) {
|
|
394
|
+
const preset = presets.find((entry) => entry.name === name && !entry.builtIn);
|
|
395
|
+
if (!preset) return;
|
|
396
|
+
if (!confirm(`Delete the image preset "${preset.name}"?`)) return;
|
|
397
|
+
const error = el('openai-image-presets-error');
|
|
398
|
+
if (error) {
|
|
399
|
+
error.textContent = '';
|
|
400
|
+
error.hidden = true;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const answer = await deleteOpenAIImagePreset(preset.name);
|
|
404
|
+
if (answer) applyOpenAIImagePresets(answer);
|
|
405
|
+
} catch (failure) {
|
|
406
|
+
if (error) {
|
|
407
|
+
error.textContent = failure?.message || 'Could not delete the preset.';
|
|
408
|
+
error.hidden = false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function __resetOpenAIImageCostUiForTests() {
|
|
414
|
+
spendState = null;
|
|
415
|
+
spendLoadedAt = 0;
|
|
416
|
+
spendRefresh = null;
|
|
417
|
+
spendListeners.clear();
|
|
418
|
+
bannerImageChat = false;
|
|
419
|
+
presets = normalizeImagePresets(null);
|
|
420
|
+
presetsLoaded = false;
|
|
421
|
+
presetsRefresh = null;
|
|
422
|
+
presetListeners.clear();
|
|
423
|
+
capInputDirty = false;
|
|
424
|
+
spendSettingsInFlight = false;
|
|
425
|
+
settingsSpend = null;
|
|
426
|
+
}
|