@mikedown/core 0.1.0 → 0.3.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/CHANGELOG.md +20 -0
- package/README.md +183 -3
- package/dist/ai/engines-page.d.ts +130 -0
- package/dist/ai/engines-page.js +10 -0
- package/dist/ai/engines-page.js.map +1 -0
- package/dist/ai/index.d.ts +1531 -0
- package/dist/ai/index.js +395 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai-ui-C7ENTWWU.js +3370 -0
- package/dist/ai-ui-C7ENTWWU.js.map +1 -0
- package/dist/chunk-7A5HEPLK.js +4910 -0
- package/dist/chunk-7A5HEPLK.js.map +1 -0
- package/dist/chunk-GWCRPPCX.js +18 -0
- package/dist/chunk-GWCRPPCX.js.map +1 -0
- package/dist/chunk-JZWKEBCI.js +3899 -0
- package/dist/chunk-JZWKEBCI.js.map +1 -0
- package/dist/chunk-L4FO63DF.js +113 -0
- package/dist/chunk-L4FO63DF.js.map +1 -0
- package/dist/chunk-LXZG7IKN.js +39 -0
- package/dist/chunk-LXZG7IKN.js.map +1 -0
- package/dist/chunk-QDVY7WW6.js +165 -0
- package/dist/chunk-QDVY7WW6.js.map +1 -0
- package/dist/chunk-QWKT57TR.js +62 -0
- package/dist/chunk-QWKT57TR.js.map +1 -0
- package/dist/chunk-TSP7BAV3.js +22 -0
- package/dist/chunk-TSP7BAV3.js.map +1 -0
- package/dist/index.d.ts +62 -441
- package/dist/index.js +6871 -1247
- package/dist/index.js.map +1 -1
- package/dist/schema-DYzxkuSb.d.ts +46 -0
- package/dist/shared/anchoring.js +4 -16
- package/dist/shared/anchoring.js.map +1 -1
- package/dist/shared/imagePaste.js +3 -57
- package/dist/shared/imagePaste.js.map +1 -1
- package/dist/shared/sha1.js +3 -57
- package/dist/shared/sha1.js.map +1 -1
- package/dist/shared/wordCount.js +4 -12
- package/dist/shared/wordCount.js.map +1 -1
- package/dist/styles.css +3700 -94
- package/dist/types-CgEeIUhi.d.ts +432 -0
- package/dist/types-CsVNVSNY.d.ts +597 -0
- package/package.json +17 -2
|
@@ -0,0 +1,3370 @@
|
|
|
1
|
+
import {
|
|
2
|
+
aiRewriteHighlightKey,
|
|
3
|
+
cmRewriteRange,
|
|
4
|
+
getPortal,
|
|
5
|
+
setCmRewriteRange
|
|
6
|
+
} from "./chunk-L4FO63DF.js";
|
|
7
|
+
import {
|
|
8
|
+
AI_DETECT_LOOKING,
|
|
9
|
+
AI_EXPERIMENTAL_DISCLAIMER,
|
|
10
|
+
ICONS,
|
|
11
|
+
aiIndicatorState,
|
|
12
|
+
aiNextRouteChanged,
|
|
13
|
+
bestLocalServer,
|
|
14
|
+
button,
|
|
15
|
+
createAiSettingsSession,
|
|
16
|
+
detectLocalServers,
|
|
17
|
+
el,
|
|
18
|
+
engineDisplayName,
|
|
19
|
+
engineKindLabel,
|
|
20
|
+
experimentalBadge,
|
|
21
|
+
experimentalDisclaimer,
|
|
22
|
+
keepFocus,
|
|
23
|
+
kindGlyphHtml,
|
|
24
|
+
localityPills,
|
|
25
|
+
mountAiEnginesPage,
|
|
26
|
+
nextId,
|
|
27
|
+
note,
|
|
28
|
+
parseLocalNetworksText,
|
|
29
|
+
privacyCard,
|
|
30
|
+
privacyLine,
|
|
31
|
+
routeLabel,
|
|
32
|
+
scanForSetup,
|
|
33
|
+
setUpLocalEngine,
|
|
34
|
+
statusBadge,
|
|
35
|
+
switchInput,
|
|
36
|
+
taskRouteNote,
|
|
37
|
+
withSavedAiKeys
|
|
38
|
+
} from "./chunk-7A5HEPLK.js";
|
|
39
|
+
import "./chunk-LXZG7IKN.js";
|
|
40
|
+
import {
|
|
41
|
+
AI_AUDIENCES,
|
|
42
|
+
AI_ENGINE_PRESETS,
|
|
43
|
+
AI_FEATURES,
|
|
44
|
+
AI_NO_CONSENTS_TEXT,
|
|
45
|
+
AI_REWRITE_VARIANTS,
|
|
46
|
+
AI_TASKS,
|
|
47
|
+
CHARS_PER_TOKEN,
|
|
48
|
+
DEFAULT_CONTEXT_TOKENS,
|
|
49
|
+
PROPERTY_VOCABULARY_MAX,
|
|
50
|
+
REWRITE_CONTEXT_CHARS,
|
|
51
|
+
aiConsentHost,
|
|
52
|
+
aiConsentStepNote,
|
|
53
|
+
aiConsentSummary,
|
|
54
|
+
aiErrorCopy,
|
|
55
|
+
aiLengthLimitCopy,
|
|
56
|
+
aiProviderPolicy,
|
|
57
|
+
aiResultStats,
|
|
58
|
+
aiResultStatsText,
|
|
59
|
+
aiResultStatsTitle,
|
|
60
|
+
budgetDocument,
|
|
61
|
+
buildExplainMessages,
|
|
62
|
+
buildPropertiesMessages,
|
|
63
|
+
buildRewriteMessages,
|
|
64
|
+
buildSummaryMessages,
|
|
65
|
+
buildTitleMessages,
|
|
66
|
+
checkMarkdownStructure,
|
|
67
|
+
coveragePercent,
|
|
68
|
+
explainMaxTokens,
|
|
69
|
+
finalizeRewriteText,
|
|
70
|
+
finalizeSummary,
|
|
71
|
+
getAiAudience,
|
|
72
|
+
getAiRewriteVariant,
|
|
73
|
+
getAiTask,
|
|
74
|
+
headingsOutline,
|
|
75
|
+
isHostEngineKind,
|
|
76
|
+
isRouteUsable,
|
|
77
|
+
jsonRepairMessages,
|
|
78
|
+
makeAiError,
|
|
79
|
+
nearestThinkingLevel,
|
|
80
|
+
parseAiSettings,
|
|
81
|
+
parsePropertySuggestions,
|
|
82
|
+
parseTitles,
|
|
83
|
+
propertiesMaxTokens,
|
|
84
|
+
renderHeadingOutline,
|
|
85
|
+
resolveAiFeature,
|
|
86
|
+
resolveRoute,
|
|
87
|
+
resolveThinkingLevel,
|
|
88
|
+
rewriteMaxTokens,
|
|
89
|
+
rewriteVisibleText,
|
|
90
|
+
summaryMaxTokens,
|
|
91
|
+
summaryPreviewText,
|
|
92
|
+
summaryVisibleText,
|
|
93
|
+
thinkingChoices,
|
|
94
|
+
thinkingLevelFrom,
|
|
95
|
+
thinkingMaxTokens,
|
|
96
|
+
titleMaxTokens,
|
|
97
|
+
toAiError,
|
|
98
|
+
whatIsSent,
|
|
99
|
+
withThinkingLevel
|
|
100
|
+
} from "./chunk-JZWKEBCI.js";
|
|
101
|
+
import {
|
|
102
|
+
hostOfUrl
|
|
103
|
+
} from "./chunk-QDVY7WW6.js";
|
|
104
|
+
|
|
105
|
+
// src/ai-prefs.ts
|
|
106
|
+
var LANGUAGES_KEY = "mikedown.ai.recentLanguages";
|
|
107
|
+
var AUDIENCE_KEY = "mikedown.ai.audience";
|
|
108
|
+
var THINKING_KEY = "mikedown.ai.thinking";
|
|
109
|
+
var MAX_RECENT_LANGUAGES = 3;
|
|
110
|
+
var LANGUAGE_MAX_CHARS = 60;
|
|
111
|
+
var AI_TRANSLATE_LANGUAGES = [
|
|
112
|
+
"English",
|
|
113
|
+
"Spanish",
|
|
114
|
+
"French",
|
|
115
|
+
"German",
|
|
116
|
+
"Portuguese",
|
|
117
|
+
"Italian",
|
|
118
|
+
"Dutch",
|
|
119
|
+
"Japanese",
|
|
120
|
+
"Chinese (Simplified)",
|
|
121
|
+
"Korean",
|
|
122
|
+
"Arabic",
|
|
123
|
+
"Hindi"
|
|
124
|
+
];
|
|
125
|
+
var sameLanguage = (a, b) => a.toLocaleLowerCase() === b.toLocaleLowerCase();
|
|
126
|
+
function cleanLanguage(language) {
|
|
127
|
+
return language.replace(/\s+/g, " ").trim().slice(0, LANGUAGE_MAX_CHARS);
|
|
128
|
+
}
|
|
129
|
+
function loadRecentLanguages() {
|
|
130
|
+
try {
|
|
131
|
+
const raw = localStorage.getItem(LANGUAGES_KEY);
|
|
132
|
+
if (!raw) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
const parsed = JSON.parse(raw);
|
|
136
|
+
if (!Array.isArray(parsed)) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const item of parsed) {
|
|
141
|
+
if (typeof item !== "string") {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const lang = cleanLanguage(item);
|
|
145
|
+
if (lang && !out.some((l) => sameLanguage(l, lang))) {
|
|
146
|
+
out.push(lang);
|
|
147
|
+
}
|
|
148
|
+
if (out.length >= MAX_RECENT_LANGUAGES) {
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
} catch {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function rememberLanguage(language) {
|
|
158
|
+
const lang = cleanLanguage(language);
|
|
159
|
+
if (!lang) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const next = [lang, ...loadRecentLanguages().filter((l) => !sameLanguage(l, lang))].slice(0, MAX_RECENT_LANGUAGES);
|
|
164
|
+
localStorage.setItem(LANGUAGES_KEY, JSON.stringify(next));
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function translateLanguageChoices(recents = loadRecentLanguages()) {
|
|
169
|
+
return {
|
|
170
|
+
recents,
|
|
171
|
+
languages: AI_TRANSLATE_LANGUAGES.filter((l) => !recents.some((r) => sameLanguage(r, l)))
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function loadAudience() {
|
|
175
|
+
try {
|
|
176
|
+
const raw = localStorage.getItem(AUDIENCE_KEY);
|
|
177
|
+
return raw && getAiAudience(raw) ? raw : null;
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function rememberAudience(audience) {
|
|
183
|
+
if (!getAiAudience(audience)) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
localStorage.setItem(AUDIENCE_KEY, audience);
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
var THINKING_LEVELS = ["off", "low", "medium", "high"];
|
|
192
|
+
function loadThinkingLevel() {
|
|
193
|
+
try {
|
|
194
|
+
const raw = localStorage.getItem(THINKING_KEY);
|
|
195
|
+
return raw && THINKING_LEVELS.includes(raw) ? raw : null;
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function rememberThinkingLevel(level) {
|
|
201
|
+
try {
|
|
202
|
+
if (level) {
|
|
203
|
+
localStorage.setItem(THINKING_KEY, level);
|
|
204
|
+
} else {
|
|
205
|
+
localStorage.removeItem(THINKING_KEY);
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function thinkingLevelFor(kind) {
|
|
211
|
+
return nearestThinkingLevel(kind, loadThinkingLevel());
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/ai-settings.ts
|
|
215
|
+
function buildAiSettingsSection(ctx, opts) {
|
|
216
|
+
const session = createAiSettingsSession(opts);
|
|
217
|
+
const { tier } = session;
|
|
218
|
+
const canManage = tier === 2 && !!opts.openEnginesPage;
|
|
219
|
+
const live = el("div", "ai-live");
|
|
220
|
+
live.setAttribute("role", "status");
|
|
221
|
+
live.setAttribute("aria-live", "polite");
|
|
222
|
+
let liveTimer;
|
|
223
|
+
function announce(text) {
|
|
224
|
+
window.clearTimeout(liveTimer);
|
|
225
|
+
live.textContent = "";
|
|
226
|
+
liveTimer = window.setTimeout(() => {
|
|
227
|
+
live.textContent = text;
|
|
228
|
+
}, 40);
|
|
229
|
+
}
|
|
230
|
+
const rows = [];
|
|
231
|
+
const intro = el("div", "ai-experimental-intro");
|
|
232
|
+
const introHead = el("div", "ai-experimental-head");
|
|
233
|
+
introHead.append(el("span", "ai-experimental-title", "AI features"), experimentalBadge());
|
|
234
|
+
intro.append(introHead, experimentalDisclaimer());
|
|
235
|
+
rows.push(intro);
|
|
236
|
+
const masterField = ctx.makeCheckboxRow(
|
|
237
|
+
"Enable AI features",
|
|
238
|
+
tier === 2 ? "Writing help from engines you choose. While this is off, nothing is sent anywhere." : "Writing help from the AI service this app provides. While this is off, nothing is sent anywhere.",
|
|
239
|
+
session.draft.enabled
|
|
240
|
+
);
|
|
241
|
+
masterField.row.classList.add("ai-master");
|
|
242
|
+
masterField.input.dataset.fk = "ai-enabled";
|
|
243
|
+
masterField.input.addEventListener("change", () => {
|
|
244
|
+
session.draft = { ...session.draft, enabled: masterField.input.checked };
|
|
245
|
+
});
|
|
246
|
+
const toolbarField = ctx.makeCheckboxRow(
|
|
247
|
+
"Show AI button on the toolbar",
|
|
248
|
+
"Opens the AI actions. While AI is off, it offers to turn AI on and set it up.",
|
|
249
|
+
session.draft.showToolbarButton
|
|
250
|
+
);
|
|
251
|
+
toolbarField.input.dataset.fk = "ai-toolbar-button";
|
|
252
|
+
toolbarField.input.addEventListener("change", () => {
|
|
253
|
+
session.draft = { ...session.draft, showToolbarButton: toolbarField.input.checked };
|
|
254
|
+
});
|
|
255
|
+
const cloudField = ctx.makeCheckboxRow(
|
|
256
|
+
"Allow cloud engines",
|
|
257
|
+
"Lets tasks use engines outside this network, such as Anthropic or OpenAI. Local engines never need it.",
|
|
258
|
+
session.draft.allowCloud
|
|
259
|
+
);
|
|
260
|
+
cloudField.input.dataset.fk = "ai-allow-cloud";
|
|
261
|
+
cloudField.row.append(privacyCard(session.draft.engines.map((e) => ({ policy: aiProviderPolicy(e) })), {
|
|
262
|
+
openLink: ctx.openLink,
|
|
263
|
+
extra: aiConsentStepNote(!!session.ai.requestCloudConsent),
|
|
264
|
+
className: "ai-row-note"
|
|
265
|
+
}));
|
|
266
|
+
cloudField.input.addEventListener("change", () => {
|
|
267
|
+
session.draft = { ...session.draft, allowCloud: cloudField.input.checked };
|
|
268
|
+
renderEngines();
|
|
269
|
+
});
|
|
270
|
+
const listConsents = session.ai.listConsents?.bind(session.ai);
|
|
271
|
+
const forgetConsent = session.ai.forgetConsent?.bind(session.ai);
|
|
272
|
+
const consentsRow = ctx.makeRow("Cloud consents", "Cloud engines you have allowed or refused here. Forget an answer to be asked again.");
|
|
273
|
+
consentsRow.classList.add("ai-section");
|
|
274
|
+
const consentsBody = el("div", "ai-consents");
|
|
275
|
+
consentsRow.append(consentsBody);
|
|
276
|
+
function engineNameFor(engineId) {
|
|
277
|
+
const engine = session.draft.engines.find((e) => e.id === engineId);
|
|
278
|
+
return engine ? engineDisplayName(engine) : engineId;
|
|
279
|
+
}
|
|
280
|
+
function renderConsents(records, problem) {
|
|
281
|
+
keepFocus(consentsBody, () => {
|
|
282
|
+
consentsBody.replaceChildren();
|
|
283
|
+
if (problem) {
|
|
284
|
+
consentsBody.append(note("error", problem));
|
|
285
|
+
}
|
|
286
|
+
if (records.length === 0) {
|
|
287
|
+
if (!problem) {
|
|
288
|
+
consentsBody.append(el("div", "ai-consents-empty", AI_NO_CONSENTS_TEXT));
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const list = el("ul", "ai-consents-list");
|
|
293
|
+
list.setAttribute("aria-label", "Cloud consents");
|
|
294
|
+
for (const record of records) {
|
|
295
|
+
const name = engineNameFor(record.engineId);
|
|
296
|
+
const item = el("li", "ai-consent");
|
|
297
|
+
const forget = button("settings-btn settings-btn--secondary ai-consent-forget", "Forget", { ariaLabel: `Forget the answer for ${name}`, fk: `consent-forget:${record.engineId}` });
|
|
298
|
+
forget.addEventListener("click", () => {
|
|
299
|
+
forget.disabled = true;
|
|
300
|
+
void forgetConsent?.(record.engineId).then(
|
|
301
|
+
() => {
|
|
302
|
+
opts.onConsentForgotten?.(record.engineId);
|
|
303
|
+
announce(`Forgot the answer for ${name}.`);
|
|
304
|
+
return refreshConsents();
|
|
305
|
+
},
|
|
306
|
+
(err) => refreshConsents(toAiError(err).message)
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
item.append(el("span", "ai-consent-text", aiConsentSummary(record, name)), forget);
|
|
310
|
+
list.append(item);
|
|
311
|
+
}
|
|
312
|
+
consentsBody.append(list);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
async function refreshConsents(problem) {
|
|
316
|
+
if (!listConsents) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
renderConsents(await listConsents(), problem);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
renderConsents([], toAiError(err).message);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const enginesRow = ctx.makeRow("Engines", "Servers that run the models. Local engines keep your text on this network.");
|
|
326
|
+
enginesRow.classList.add("ai-section");
|
|
327
|
+
const enginesBody = el("div", "ai-engines-body");
|
|
328
|
+
enginesRow.append(enginesBody);
|
|
329
|
+
function openPage(initial) {
|
|
330
|
+
const pendingEnable = session.draft.enabled && !session.original.enabled;
|
|
331
|
+
opts.openEnginesPage?.({
|
|
332
|
+
initial,
|
|
333
|
+
session: { ...session.fork(), pendingEnable },
|
|
334
|
+
onDone: (patch, saved) => {
|
|
335
|
+
session.absorb(saved, patch);
|
|
336
|
+
cloudField.input.checked = session.draft.allowCloud;
|
|
337
|
+
masterField.input.checked = session.draft.enabled;
|
|
338
|
+
renderEngines();
|
|
339
|
+
if (Object.keys(patch).length > 0) {
|
|
340
|
+
announce("Engines and tasks saved.");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function renderEngines() {
|
|
346
|
+
keepFocus(enginesBody, () => {
|
|
347
|
+
enginesBody.replaceChildren();
|
|
348
|
+
const draft = session.draft;
|
|
349
|
+
if (draft.engines.length === 0) {
|
|
350
|
+
const empty = el("div", "ai-summary-empty");
|
|
351
|
+
empty.append(
|
|
352
|
+
el("div", "ai-empty-title", "No engines yet"),
|
|
353
|
+
el("div", "ai-empty-text", `Two ways to connect: paste a cloud API key, or run models on your own machine.${draft.enabled ? "" : " Nothing is sent until AI is on."}`)
|
|
354
|
+
);
|
|
355
|
+
enginesBody.append(empty);
|
|
356
|
+
} else {
|
|
357
|
+
const list = el("div", "ai-engines ai-engines--compact");
|
|
358
|
+
list.setAttribute("role", "list");
|
|
359
|
+
list.setAttribute("aria-label", "Engines");
|
|
360
|
+
for (const engine of draft.engines) {
|
|
361
|
+
list.append(buildEngineItem(engine));
|
|
362
|
+
}
|
|
363
|
+
enginesBody.append(list, buildRouteLine());
|
|
364
|
+
}
|
|
365
|
+
const manage = button("settings-btn settings-btn--secondary ai-manage-btn", draft.engines.length === 0 ? "Add engine\u2026" : "Manage engines\u2026", { fk: "manage-engines" });
|
|
366
|
+
manage.addEventListener("click", () => openPage());
|
|
367
|
+
const actions = el("div", "ai-summary-actions");
|
|
368
|
+
actions.append(manage);
|
|
369
|
+
enginesBody.append(actions);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function buildEngineItem(engine) {
|
|
373
|
+
const item = el("div", "ai-engine ai-engine--compact");
|
|
374
|
+
item.setAttribute("role", "listitem");
|
|
375
|
+
item.dataset.engineId = engine.id;
|
|
376
|
+
item.classList.toggle("is-disabled", !engine.enabled);
|
|
377
|
+
const name = engineDisplayName(engine);
|
|
378
|
+
const head = el("div", "ai-engine-head");
|
|
379
|
+
const glyph = el("span", "ai-engine-glyph");
|
|
380
|
+
glyph.innerHTML = kindGlyphHtml(engine.kind, "ai-glyph--sm");
|
|
381
|
+
const open = button("ai-engine-main ai-engine-open", "", { ariaLabel: `${name}: open in AI Engines`, fk: `eng:${engine.id}:open` });
|
|
382
|
+
const titleLine = el("span", "ai-engine-title");
|
|
383
|
+
titleLine.append(el("span", "ai-engine-name", name), ...localityPills(engine, session.draft));
|
|
384
|
+
open.title = engineKindLabel(engine.kind);
|
|
385
|
+
open.append(titleLine);
|
|
386
|
+
open.addEventListener("click", () => openPage({ engineId: engine.id }));
|
|
387
|
+
const enabled = switchInput(engine.enabled, `Enable ${name}`, `eng:${engine.id}:enabled-row`);
|
|
388
|
+
enabled.addEventListener("change", () => {
|
|
389
|
+
session.updateEngine(engine.id, { enabled: enabled.checked });
|
|
390
|
+
renderEngines();
|
|
391
|
+
});
|
|
392
|
+
const actions = el("div", "ai-engine-actions");
|
|
393
|
+
actions.append(statusBadge(session.statuses.get(engine.id), void 0, { compact: true }), enabled);
|
|
394
|
+
head.append(glyph, open, actions);
|
|
395
|
+
item.append(head);
|
|
396
|
+
return item;
|
|
397
|
+
}
|
|
398
|
+
function buildRouteLine() {
|
|
399
|
+
const draft = session.draft;
|
|
400
|
+
const line = el("div", "ai-route-line");
|
|
401
|
+
const labelId = nextId("ai-route-line");
|
|
402
|
+
const label = el("span", "ai-route-line-label", "Default model");
|
|
403
|
+
label.id = labelId;
|
|
404
|
+
const value = el("span", "ai-route-line-value", draft.routing.default ? routeLabel(draft, draft.routing.default) : "None set");
|
|
405
|
+
value.classList.toggle("is-none", !draft.routing.default);
|
|
406
|
+
const change = button("ai-link-btn", "Change\u2026", { fk: "route:change", ariaLabel: "Change the default model" });
|
|
407
|
+
change.setAttribute("aria-describedby", labelId);
|
|
408
|
+
change.addEventListener("click", () => openPage({ section: "routing" }));
|
|
409
|
+
line.append(label, value, change);
|
|
410
|
+
const n = draft.routing.default ? taskRouteNote({ ...draft, routing: { ...draft.routing, tasks: {} } }, session.statusList(), "chat") : { level: "info", text: "Tasks have nowhere to go until a default model is set." };
|
|
411
|
+
if (n) {
|
|
412
|
+
line.append(note(n.level, n.text, "ai-row-note"));
|
|
413
|
+
}
|
|
414
|
+
return line;
|
|
415
|
+
}
|
|
416
|
+
const featuresRow = ctx.makeRow("Features", "Turn each feature on or off.");
|
|
417
|
+
featuresRow.classList.add("ai-section");
|
|
418
|
+
const featureRows = [];
|
|
419
|
+
const taskRows = [];
|
|
420
|
+
for (const def of AI_FEATURES) {
|
|
421
|
+
const planned = def.status === "planned";
|
|
422
|
+
const resolved = resolveAiFeature(session.draft.features, def.id);
|
|
423
|
+
const row = el("div", "settings-row settings-row--inline ai-feature");
|
|
424
|
+
row.classList.toggle("is-planned", planned);
|
|
425
|
+
const text = el("div", "settings-row-text");
|
|
426
|
+
const labelId = nextId("ai-feature");
|
|
427
|
+
const lbl = el("span", "settings-label", def.label);
|
|
428
|
+
lbl.id = labelId;
|
|
429
|
+
text.append(lbl, el("span", "settings-desc", def.description));
|
|
430
|
+
const controls = el("div", "ai-feature-controls");
|
|
431
|
+
if (planned) {
|
|
432
|
+
controls.append(el("span", "ai-pill ai-pill--soon", "Coming later"));
|
|
433
|
+
row.setAttribute("aria-disabled", "true");
|
|
434
|
+
} else {
|
|
435
|
+
if (tier === 2) {
|
|
436
|
+
taskRows.push(buildTaskRow(def));
|
|
437
|
+
}
|
|
438
|
+
const toggle = switchInput(resolved.enabled, def.label, `feature:${def.id}:enabled`);
|
|
439
|
+
toggle.setAttribute("aria-labelledby", labelId);
|
|
440
|
+
toggle.removeAttribute("aria-label");
|
|
441
|
+
toggle.addEventListener("change", () => {
|
|
442
|
+
const draft = session.draft;
|
|
443
|
+
session.draft = { ...draft, features: { ...draft.features, [def.id]: { ...draft.features[def.id] ?? {}, enabled: toggle.checked } } };
|
|
444
|
+
});
|
|
445
|
+
controls.append(toggle);
|
|
446
|
+
}
|
|
447
|
+
row.append(text, controls);
|
|
448
|
+
featureRows.push(row);
|
|
449
|
+
}
|
|
450
|
+
featuresRow.append(ctx.makeGroup(...featureRows));
|
|
451
|
+
if (taskRows.length > 0) {
|
|
452
|
+
featuresRow.append(...buildTaskDisclosure(taskRows));
|
|
453
|
+
}
|
|
454
|
+
function buildTaskRow(def) {
|
|
455
|
+
const defaultTask = getAiTask(def.defaultTask);
|
|
456
|
+
const row = el("div", "settings-row settings-row--inline ai-feature-task");
|
|
457
|
+
const labelId = nextId("ai-feature-task");
|
|
458
|
+
const lbl = el("span", "settings-label", def.label);
|
|
459
|
+
lbl.id = labelId;
|
|
460
|
+
const select = el("select", "settings-input settings-input--small");
|
|
461
|
+
select.dataset.fk = `feature:${def.id}:task`;
|
|
462
|
+
select.setAttribute("aria-label", `Task route for ${def.label}`);
|
|
463
|
+
const dflt = el("option", "", `Default task (${defaultTask?.label ?? def.defaultTask})`);
|
|
464
|
+
dflt.value = "";
|
|
465
|
+
select.append(dflt);
|
|
466
|
+
for (const task of AI_TASKS) {
|
|
467
|
+
if (task.hostMethod) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
const o = el("option", "", task.label);
|
|
471
|
+
o.value = task.id;
|
|
472
|
+
select.append(o);
|
|
473
|
+
}
|
|
474
|
+
const saved = session.draft.features[def.id]?.task;
|
|
475
|
+
if (saved && !AI_TASKS.some((t) => t.id === saved)) {
|
|
476
|
+
const o = el("option", "", saved);
|
|
477
|
+
o.value = saved;
|
|
478
|
+
select.append(o);
|
|
479
|
+
}
|
|
480
|
+
select.value = saved ?? "";
|
|
481
|
+
select.addEventListener("change", () => {
|
|
482
|
+
const draft = session.draft;
|
|
483
|
+
const entry = { ...draft.features[def.id] ?? {} };
|
|
484
|
+
if (select.value) {
|
|
485
|
+
entry.task = select.value;
|
|
486
|
+
} else {
|
|
487
|
+
delete entry.task;
|
|
488
|
+
}
|
|
489
|
+
session.draft = { ...draft, features: { ...draft.features, [def.id]: entry } };
|
|
490
|
+
});
|
|
491
|
+
row.append(lbl, select);
|
|
492
|
+
return row;
|
|
493
|
+
}
|
|
494
|
+
function buildTaskDisclosure(taskRowsIn) {
|
|
495
|
+
const panelId = nextId("ai-feature-tasks");
|
|
496
|
+
const toggle = button("ai-disclosure ai-feature-tasks-toggle", "Advanced: task per feature", { html: ICONS.chevron, fk: "features:advanced" });
|
|
497
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
498
|
+
toggle.setAttribute("aria-controls", panelId);
|
|
499
|
+
const panel = el("div", "ai-advanced ai-feature-tasks");
|
|
500
|
+
panel.id = panelId;
|
|
501
|
+
panel.hidden = true;
|
|
502
|
+
panel.append(el("div", "settings-desc", "Each feature uses its own task\u2019s model (see Tasks on the AI Engines page). Pick another task to borrow its model."), ctx.makeGroup(...taskRowsIn));
|
|
503
|
+
toggle.addEventListener("click", () => {
|
|
504
|
+
panel.hidden = !panel.hidden;
|
|
505
|
+
toggle.setAttribute("aria-expanded", String(!panel.hidden));
|
|
506
|
+
});
|
|
507
|
+
return [toggle, panel];
|
|
508
|
+
}
|
|
509
|
+
const localNetsRow = ctx.makeRow("Local networks", "Networks and host names that count as local, so an engine there never needs Allow cloud.");
|
|
510
|
+
localNetsRow.classList.add("ai-section");
|
|
511
|
+
{
|
|
512
|
+
const panelId = nextId("ai-local-networks");
|
|
513
|
+
const toggle = button("ai-disclosure ai-local-networks-toggle", "Advanced: local networks", { html: ICONS.chevron, fk: "local-networks:advanced" });
|
|
514
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
515
|
+
toggle.setAttribute("aria-controls", panelId);
|
|
516
|
+
const panel = el("div", "ai-advanced ai-local-networks");
|
|
517
|
+
panel.id = panelId;
|
|
518
|
+
panel.hidden = true;
|
|
519
|
+
const area = el("textarea", "settings-input ai-mono ai-overrides-text");
|
|
520
|
+
area.rows = 4;
|
|
521
|
+
area.spellcheck = false;
|
|
522
|
+
area.dataset.fk = "ai-local-networks";
|
|
523
|
+
area.placeholder = "10.0.98.0/24\n.ts.net";
|
|
524
|
+
area.value = session.draft.localNetworks.join("\n");
|
|
525
|
+
area.setAttribute("aria-label", "Local networks, one per line");
|
|
526
|
+
const err = el("div", "ai-field-error");
|
|
527
|
+
err.id = nextId("ai-local-networks-error");
|
|
528
|
+
err.setAttribute("aria-live", "polite");
|
|
529
|
+
area.setAttribute("aria-describedby", err.id);
|
|
530
|
+
area.addEventListener("input", () => {
|
|
531
|
+
const parsed = parseLocalNetworksText(area.value);
|
|
532
|
+
if (!parsed.ok) {
|
|
533
|
+
err.textContent = parsed.error;
|
|
534
|
+
area.setAttribute("aria-invalid", "true");
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
err.textContent = "";
|
|
538
|
+
area.removeAttribute("aria-invalid");
|
|
539
|
+
session.draft = { ...session.draft, localNetworks: parsed.value };
|
|
540
|
+
if (canManage) {
|
|
541
|
+
renderEngines();
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
area.addEventListener("blur", () => {
|
|
545
|
+
if (area.getAttribute("aria-invalid") !== "true") {
|
|
546
|
+
area.value = session.draft.localNetworks.join("\n");
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
panel.append(
|
|
550
|
+
el("div", "settings-desc", "One per line. An IPv4 range such as 10.0.98.0/24 (a WireGuard network, say) or a host-name suffix such as .ts.net. Loopback, link-local, .local, .lan and .home.arpa always count. Leave it blank for the defaults (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16). Saved to your user settings only, never to a folder."),
|
|
551
|
+
area,
|
|
552
|
+
err
|
|
553
|
+
);
|
|
554
|
+
toggle.addEventListener("click", () => {
|
|
555
|
+
panel.hidden = !panel.hidden;
|
|
556
|
+
toggle.setAttribute("aria-expanded", String(!panel.hidden));
|
|
557
|
+
});
|
|
558
|
+
localNetsRow.append(toggle, panel);
|
|
559
|
+
}
|
|
560
|
+
rows.push(masterField.row);
|
|
561
|
+
if (tier === 2) {
|
|
562
|
+
if (canManage) {
|
|
563
|
+
rows.push(enginesRow);
|
|
564
|
+
renderEngines();
|
|
565
|
+
}
|
|
566
|
+
rows.push(cloudField.row);
|
|
567
|
+
if (listConsents && forgetConsent) {
|
|
568
|
+
rows.push(consentsRow);
|
|
569
|
+
void refreshConsents();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
rows.push(toolbarField.row, featuresRow);
|
|
573
|
+
if (tier === 2) {
|
|
574
|
+
rows.push(localNetsRow);
|
|
575
|
+
}
|
|
576
|
+
rows.push(live);
|
|
577
|
+
const unsubscribe = canManage ? opts.subscribeStatuses?.((list) => {
|
|
578
|
+
if (!enginesRow.isConnected) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
for (const s of list) {
|
|
582
|
+
session.statuses.set(s.engineId, s);
|
|
583
|
+
}
|
|
584
|
+
renderEngines();
|
|
585
|
+
}) : void 0;
|
|
586
|
+
if (canManage) {
|
|
587
|
+
void session.refreshStatuses().then((ok) => {
|
|
588
|
+
if (ok && enginesRow.isConnected) {
|
|
589
|
+
renderEngines();
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
const close = () => {
|
|
594
|
+
window.clearTimeout(liveTimer);
|
|
595
|
+
unsubscribe?.();
|
|
596
|
+
};
|
|
597
|
+
return {
|
|
598
|
+
rows,
|
|
599
|
+
collect: () => session.collect(),
|
|
600
|
+
onDiscarded: () => {
|
|
601
|
+
close();
|
|
602
|
+
session.discard();
|
|
603
|
+
},
|
|
604
|
+
onSaved: () => {
|
|
605
|
+
close();
|
|
606
|
+
opts.onSaved(session.commit());
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/ai-toolbar.ts
|
|
612
|
+
var AI_TURN_ON_PATCH = { "ai.enabled": true };
|
|
613
|
+
function aiSetupTarget(tier, settings) {
|
|
614
|
+
if (tier !== 2) {
|
|
615
|
+
return { kind: "tab" };
|
|
616
|
+
}
|
|
617
|
+
const byRoute = settings.routing.default?.engineId;
|
|
618
|
+
const engineId = byRoute && settings.engines.some((e) => e.id === byRoute) ? byRoute : settings.engines[0]?.id;
|
|
619
|
+
return engineId ? { kind: "page", engineId } : { kind: "wizard" };
|
|
620
|
+
}
|
|
621
|
+
var THINKING_DEFAULT = "default";
|
|
622
|
+
function thinkingRowEntry(row) {
|
|
623
|
+
const short = (label) => label === "Medium" ? "Med" : label;
|
|
624
|
+
return {
|
|
625
|
+
type: "segmented",
|
|
626
|
+
id: "ai-thinking",
|
|
627
|
+
label: "Thinking",
|
|
628
|
+
title: "Thinking level for the next AI requests",
|
|
629
|
+
options: [
|
|
630
|
+
{ value: THINKING_DEFAULT, label: "Default", title: "Thinking: the model's default", spoken: "Default" },
|
|
631
|
+
...row.choices.map((c) => ({ value: c.level, label: short(c.label), title: `Thinking: ${c.label}`, spoken: c.label }))
|
|
632
|
+
],
|
|
633
|
+
value: () => row.value() ?? THINKING_DEFAULT,
|
|
634
|
+
onChange: (v) => row.onChange(v === THINKING_DEFAULT ? null : v),
|
|
635
|
+
classes: { group: "ai-rw-think", label: "ai-rw-think-label", segment: "ai-rw-think-seg" }
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
var SELECT_TEXT_HINT = "Select text first";
|
|
639
|
+
var UNREACHABLE_REASON = /could not reach|no answer|timed out|ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|fetch failed|connection refused/i;
|
|
640
|
+
function aiMenuStatusLine(reason) {
|
|
641
|
+
const failed = /^(.+?) failed its last connection test/.exec(reason);
|
|
642
|
+
if (!failed) {
|
|
643
|
+
return { label: reason };
|
|
644
|
+
}
|
|
645
|
+
return UNREACHABLE_REASON.test(reason) ? { label: `${failed[1]} isn't reachable`, detail: reason } : { label: `${failed[1]} failed its last connection test`, detail: reason.slice(failed[0].length).replace(/^[:.\s]+/, "") || void 0 };
|
|
646
|
+
}
|
|
647
|
+
function sharedDisabledReason(states) {
|
|
648
|
+
const counts = /* @__PURE__ */ new Map();
|
|
649
|
+
for (const state of states) {
|
|
650
|
+
if (state && !state.enabled && state.hint !== SELECT_TEXT_HINT) {
|
|
651
|
+
counts.set(state.hint, (counts.get(state.hint) ?? 0) + 1);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
let best = null;
|
|
655
|
+
for (const [hint, n] of counts) {
|
|
656
|
+
if (n >= 2 && (best === null || n > counts.get(best))) {
|
|
657
|
+
best = hint;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return best;
|
|
661
|
+
}
|
|
662
|
+
function aiToolbarMenuEntries(input) {
|
|
663
|
+
const entries = [];
|
|
664
|
+
const separate = () => {
|
|
665
|
+
if (entries.length > 0 && entries[entries.length - 1].type !== "separator") {
|
|
666
|
+
entries.push({ type: "separator" });
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
const shared = sharedDisabledReason([input.rewrite, input.explain, input.translate, input.summarize, input.titles, input.properties]);
|
|
670
|
+
if (shared) {
|
|
671
|
+
const { label, detail } = aiMenuStatusLine(shared);
|
|
672
|
+
entries.push({ type: "status", id: "ai-status", label, ...detail ? { detail } : {} });
|
|
673
|
+
if (input.onRetest) {
|
|
674
|
+
entries.push({ type: "action", id: "ai-retest", label: "Look again", action: () => input.onRetest?.() });
|
|
675
|
+
}
|
|
676
|
+
entries.push({ type: "separator" });
|
|
677
|
+
}
|
|
678
|
+
const add = (id, label, icon2, state, run) => {
|
|
679
|
+
if (!state) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
entries.push({
|
|
683
|
+
type: "action",
|
|
684
|
+
id,
|
|
685
|
+
label,
|
|
686
|
+
icon: icon2 ?? input.icons.rewrite,
|
|
687
|
+
hint: state.enabled || state.hint === shared ? void 0 : state.hint,
|
|
688
|
+
isDisabled: () => !state.enabled,
|
|
689
|
+
action: run
|
|
690
|
+
});
|
|
691
|
+
};
|
|
692
|
+
add("ai-rewrite", "Rewrite selection", input.icons.rewrite, input.rewrite, () => input.onRewrite());
|
|
693
|
+
add("ai-explain", "Explain selection", input.icons.explain, input.explain, () => input.onExplain?.());
|
|
694
|
+
add("ai-translate", "Translate\u2026", input.icons.translate, input.translate, () => input.onTranslate?.());
|
|
695
|
+
separate();
|
|
696
|
+
add("ai-summarize", "Summarize document\u2026", input.icons.summarize, input.summarize, () => input.onSummarize?.());
|
|
697
|
+
add("ai-titles", "Suggest titles\u2026", input.icons.titles, input.titles, () => input.onSuggestTitles?.());
|
|
698
|
+
add("ai-properties", "Suggest properties", input.icons.properties, input.properties, () => input.onSuggestProperties?.());
|
|
699
|
+
if (input.thinking && input.thinking.choices.length > 0) {
|
|
700
|
+
separate();
|
|
701
|
+
entries.push(thinkingRowEntry(input.thinking));
|
|
702
|
+
}
|
|
703
|
+
separate();
|
|
704
|
+
entries.push({
|
|
705
|
+
type: "action",
|
|
706
|
+
id: "ai-manage",
|
|
707
|
+
label: input.canManageEngines ? "Manage engines\u2026" : "AI settings\u2026",
|
|
708
|
+
icon: input.icons.manage,
|
|
709
|
+
action: () => input.onManage()
|
|
710
|
+
});
|
|
711
|
+
return entries;
|
|
712
|
+
}
|
|
713
|
+
var AI_FIRST_RUN_VERBS = "Rewrite, translate, explain, summarize, suggest titles and tags.";
|
|
714
|
+
var AI_FIRST_RUN_NOTICE = "AI features are experimental, and AI output can be wrong, so review it.";
|
|
715
|
+
var AI_FIRST_RUN_NONE = "Connect a provider you already use. Running models on this machine is under Advanced.";
|
|
716
|
+
function aiDetectFoundText(name, models) {
|
|
717
|
+
const count = models === void 0 ? "" : `, ${models} ${models === 1 ? "model" : "models"}`;
|
|
718
|
+
return `${name} is running on this computer${count}.`;
|
|
719
|
+
}
|
|
720
|
+
var openPanel = null;
|
|
721
|
+
function isAiFirstRunPanelOpen() {
|
|
722
|
+
return openPanel !== null;
|
|
723
|
+
}
|
|
724
|
+
function hideAiFirstRunPanel() {
|
|
725
|
+
openPanel?.close(false);
|
|
726
|
+
}
|
|
727
|
+
function showAiFirstRunPanel(opts) {
|
|
728
|
+
hideAiFirstRunPanel();
|
|
729
|
+
const { anchor } = opts;
|
|
730
|
+
const tier1 = opts.tier === 1;
|
|
731
|
+
const setUpLabel = tier1 ? "Turn on" : "Set up";
|
|
732
|
+
const panel = el("div", "ai-first-run");
|
|
733
|
+
panel.setAttribute("role", "dialog");
|
|
734
|
+
const titleId = nextId("ai-first-run-title");
|
|
735
|
+
const textId = nextId("ai-first-run-text");
|
|
736
|
+
panel.setAttribute("aria-labelledby", titleId);
|
|
737
|
+
panel.setAttribute("aria-describedby", textId);
|
|
738
|
+
const head = el("div", "ai-experimental-head");
|
|
739
|
+
const title = el("span", "ai-experimental-title", "AI features");
|
|
740
|
+
title.id = titleId;
|
|
741
|
+
head.append(title, experimentalBadge());
|
|
742
|
+
const verbs = el("p", "ai-first-run-verbs", AI_FIRST_RUN_VERBS);
|
|
743
|
+
const text = el("p", "ai-disclaimer ai-first-run-notice", `${AI_FIRST_RUN_NOTICE} `);
|
|
744
|
+
text.id = textId;
|
|
745
|
+
const notice = button("ai-link-btn ai-first-run-notice-toggle", "Read the full notice", { fk: "ai-first-run:notice" });
|
|
746
|
+
notice.setAttribute("aria-expanded", "false");
|
|
747
|
+
text.append(notice);
|
|
748
|
+
const fullNotice = el("p", "ai-disclaimer ai-first-run-notice-full", AI_EXPERIMENTAL_DISCLAIMER);
|
|
749
|
+
fullNotice.id = nextId("ai-first-run-notice-full");
|
|
750
|
+
fullNotice.hidden = true;
|
|
751
|
+
notice.setAttribute("aria-controls", fullNotice.id);
|
|
752
|
+
const status = el("div", "ai-first-run-status");
|
|
753
|
+
status.setAttribute("role", "status");
|
|
754
|
+
status.setAttribute("aria-live", "polite");
|
|
755
|
+
status.hidden = true;
|
|
756
|
+
const actions = el("div", "ai-first-run-actions");
|
|
757
|
+
const other = button("ai-link-btn ai-first-run-other", "Set up another way\u2026", { fk: "ai-first-run:other" });
|
|
758
|
+
other.hidden = true;
|
|
759
|
+
const cancel = button("settings-btn settings-btn--secondary", "Cancel", { fk: "ai-first-run:cancel" });
|
|
760
|
+
const primary = button("settings-btn settings-btn--primary", setUpLabel, { fk: "ai-first-run:turn-on" });
|
|
761
|
+
actions.append(other, cancel, primary);
|
|
762
|
+
const privacy = privacyLine(() => setUp());
|
|
763
|
+
privacy.hidden = true;
|
|
764
|
+
const learnMore = privacy.querySelector("button");
|
|
765
|
+
panel.append(head, verbs, text, fullNotice, status, privacy, actions);
|
|
766
|
+
let found = null;
|
|
767
|
+
let busy = false;
|
|
768
|
+
const detectAbort = new AbortController();
|
|
769
|
+
function setStatus(state, line = "") {
|
|
770
|
+
status.hidden = state === "hidden";
|
|
771
|
+
status.dataset.state = state;
|
|
772
|
+
status.replaceChildren();
|
|
773
|
+
if (state === "hidden") {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (state === "found") {
|
|
777
|
+
const dot = el("span", "ai-first-run-dot");
|
|
778
|
+
dot.setAttribute("aria-hidden", "true");
|
|
779
|
+
status.append(dot);
|
|
780
|
+
}
|
|
781
|
+
status.append(el("span", "", line));
|
|
782
|
+
}
|
|
783
|
+
let closed = false;
|
|
784
|
+
function close(restore) {
|
|
785
|
+
if (closed) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
closed = true;
|
|
789
|
+
detectAbort.abort();
|
|
790
|
+
document.removeEventListener("mousedown", onOutside, true);
|
|
791
|
+
panel.removeEventListener("keydown", onKeydown);
|
|
792
|
+
panel.remove();
|
|
793
|
+
if (openPanel?.element === panel) {
|
|
794
|
+
openPanel = null;
|
|
795
|
+
}
|
|
796
|
+
if (restore && anchor.isConnected) {
|
|
797
|
+
anchor.focus({ preventScroll: true });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function onOutside(e) {
|
|
801
|
+
const target = e.target;
|
|
802
|
+
if (!busy && !panel.contains(target) && !anchor.contains(target)) {
|
|
803
|
+
close(false);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function onKeydown(e) {
|
|
807
|
+
if (e.key === "Escape") {
|
|
808
|
+
e.preventDefault();
|
|
809
|
+
e.stopPropagation();
|
|
810
|
+
if (!busy) {
|
|
811
|
+
close(true);
|
|
812
|
+
}
|
|
813
|
+
} else if (e.key === "Tab") {
|
|
814
|
+
const order = [notice, learnMore, other, cancel, primary].filter((b) => !b.hidden && !b.closest("[hidden]") && !b.disabled);
|
|
815
|
+
if (order.length === 0) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
e.preventDefault();
|
|
819
|
+
const at = order.indexOf(document.activeElement);
|
|
820
|
+
const next = at < 0 ? 0 : (at + (e.shiftKey ? order.length - 1 : 1)) % order.length;
|
|
821
|
+
order[next].focus();
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function setUp() {
|
|
825
|
+
close(false);
|
|
826
|
+
opts.onTurnOn();
|
|
827
|
+
}
|
|
828
|
+
async function useLocal(server) {
|
|
829
|
+
if (!opts.onUseLocal) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
busy = true;
|
|
833
|
+
for (const b of [notice, learnMore, other, cancel, primary]) {
|
|
834
|
+
b.disabled = true;
|
|
835
|
+
}
|
|
836
|
+
setStatus("working", `Setting up ${server.name}\u2026`);
|
|
837
|
+
try {
|
|
838
|
+
await opts.onUseLocal(server.presetId);
|
|
839
|
+
busy = false;
|
|
840
|
+
close(true);
|
|
841
|
+
} catch (err) {
|
|
842
|
+
busy = false;
|
|
843
|
+
if (closed) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
found = null;
|
|
847
|
+
for (const b of [notice, learnMore, other, cancel, primary]) {
|
|
848
|
+
b.disabled = false;
|
|
849
|
+
}
|
|
850
|
+
other.hidden = true;
|
|
851
|
+
privacy.hidden = true;
|
|
852
|
+
primary.textContent = setUpLabel;
|
|
853
|
+
setStatus("error", err instanceof Error && err.message ? err.message : `${server.name} could not be set up.`);
|
|
854
|
+
primary.focus({ preventScroll: true });
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
notice.addEventListener("click", () => {
|
|
858
|
+
fullNotice.hidden = !fullNotice.hidden;
|
|
859
|
+
notice.setAttribute("aria-expanded", String(!fullNotice.hidden));
|
|
860
|
+
notice.textContent = fullNotice.hidden ? "Read the full notice" : "Hide the full notice";
|
|
861
|
+
position(panel, anchor);
|
|
862
|
+
});
|
|
863
|
+
cancel.addEventListener("click", () => close(true));
|
|
864
|
+
other.addEventListener("click", () => setUp());
|
|
865
|
+
primary.addEventListener("click", () => {
|
|
866
|
+
if (found) {
|
|
867
|
+
void useLocal(found);
|
|
868
|
+
} else {
|
|
869
|
+
setUp();
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
panel.addEventListener("keydown", onKeydown);
|
|
873
|
+
document.addEventListener("mousedown", onOutside, true);
|
|
874
|
+
if (opts.detect && !tier1) {
|
|
875
|
+
setStatus("looking", AI_DETECT_LOOKING);
|
|
876
|
+
void opts.detect(detectAbort.signal).then(
|
|
877
|
+
(result) => {
|
|
878
|
+
if (closed || busy) {
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (result.kind === "found" && opts.onUseLocal) {
|
|
882
|
+
found = result;
|
|
883
|
+
primary.textContent = `Use ${result.name}`;
|
|
884
|
+
other.hidden = false;
|
|
885
|
+
privacy.hidden = false;
|
|
886
|
+
setStatus("found", aiDetectFoundText(result.name, result.models));
|
|
887
|
+
} else if (result.kind === "none") {
|
|
888
|
+
setStatus("none", AI_FIRST_RUN_NONE);
|
|
889
|
+
} else {
|
|
890
|
+
setStatus("hidden");
|
|
891
|
+
}
|
|
892
|
+
position(panel, anchor);
|
|
893
|
+
},
|
|
894
|
+
() => {
|
|
895
|
+
if (!closed && !busy) {
|
|
896
|
+
setStatus("hidden");
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
getPortal().appendChild(panel);
|
|
902
|
+
position(panel, anchor);
|
|
903
|
+
openPanel = { element: panel, close };
|
|
904
|
+
primary.focus({ preventScroll: true });
|
|
905
|
+
return { element: panel, close: () => close(true) };
|
|
906
|
+
}
|
|
907
|
+
function position(panel, anchor) {
|
|
908
|
+
const a = anchor.getBoundingClientRect();
|
|
909
|
+
const p = panel.getBoundingClientRect();
|
|
910
|
+
const vw = window.innerWidth;
|
|
911
|
+
const vh = window.innerHeight;
|
|
912
|
+
const bounds = anchor.closest(".mikedown-root")?.getBoundingClientRect();
|
|
913
|
+
const minLeft = Math.max(8, (bounds?.left ?? 0) + 8);
|
|
914
|
+
const maxLeft = Math.min(vw, bounds?.right ?? vw) - p.width - 8;
|
|
915
|
+
const left = Math.max(minLeft, Math.min(a.left + a.width / 2 - p.width / 2, maxLeft));
|
|
916
|
+
const below = vh - a.bottom >= p.height + 6 || vh - a.bottom >= a.top;
|
|
917
|
+
const top = below ? a.bottom + 6 : a.top - p.height - 6;
|
|
918
|
+
panel.style.left = `${left}px`;
|
|
919
|
+
panel.style.top = `${Math.max(8, Math.min(top, vh - p.height - 8))}px`;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// src/ai-request.ts
|
|
923
|
+
function aiRunThinkingOn(thinking) {
|
|
924
|
+
return !!thinking && (thinking.sent ?? thinking.configured) !== "off";
|
|
925
|
+
}
|
|
926
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
927
|
+
function thinkingRequestOverrides(kind, modelOverrides, level, opts = {}) {
|
|
928
|
+
const out = withThinkingLevel(kind, void 0, level, opts);
|
|
929
|
+
if (!out || kind === "anthropic" || !modelOverrides) {
|
|
930
|
+
return out;
|
|
931
|
+
}
|
|
932
|
+
if (modelOverrides.reasoning_effort !== void 0 && out.reasoning_effort === void 0) {
|
|
933
|
+
out.reasoning_effort = "none";
|
|
934
|
+
}
|
|
935
|
+
const modelKwargs = modelOverrides.chat_template_kwargs;
|
|
936
|
+
const kwargs = isObject(out.chat_template_kwargs) ? out.chat_template_kwargs : {};
|
|
937
|
+
if (isObject(modelKwargs) && modelKwargs.enable_thinking !== void 0 && kwargs.enable_thinking === void 0) {
|
|
938
|
+
out.chat_template_kwargs = { ...kwargs, enable_thinking: true };
|
|
939
|
+
}
|
|
940
|
+
return out;
|
|
941
|
+
}
|
|
942
|
+
function routeThinking(settings, route, req) {
|
|
943
|
+
const engine = settings.engines.find((e) => e.id === route.engineId);
|
|
944
|
+
if (!engine || thinkingChoices(engine.kind).length === 0) {
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
const model = engine.models.find((m) => m.id === route.modelId);
|
|
948
|
+
const configured = thinkingLevelFrom(engine.kind, model?.requestOverrides);
|
|
949
|
+
const wanted = req.thinking;
|
|
950
|
+
const explicit = typeof wanted === "function" ? wanted(engine.kind) : wanted ?? null;
|
|
951
|
+
const { level, auto } = resolveThinkingLevel(engine.kind, explicit, configured, req.job ?? req.feature, { modelId: route.modelId });
|
|
952
|
+
const overrides = level ? thinkingRequestOverrides(engine.kind, model?.requestOverrides, level, { local: route.isLocal }) : void 0;
|
|
953
|
+
return {
|
|
954
|
+
info: { kind: engine.kind, configured, sent: overrides ? level : null, ...overrides && auto ? { auto: true } : {} },
|
|
955
|
+
...overrides ? { overrides } : {},
|
|
956
|
+
contextTokens: model?.capabilities.contextTokens ?? null
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
function promptTokens(messages) {
|
|
960
|
+
let chars = 0;
|
|
961
|
+
for (const m of messages) {
|
|
962
|
+
if (typeof m.content === "string") {
|
|
963
|
+
chars += m.content.length;
|
|
964
|
+
} else {
|
|
965
|
+
for (const part of m.content) {
|
|
966
|
+
if (part.type === "text") {
|
|
967
|
+
chars += part.text.length;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
973
|
+
}
|
|
974
|
+
function requestMaxTokens(req, thinking) {
|
|
975
|
+
if (req.maxTokens === void 0 || !thinking) {
|
|
976
|
+
return req.maxTokens;
|
|
977
|
+
}
|
|
978
|
+
const level = thinking.info.sent ?? thinking.info.configured;
|
|
979
|
+
return thinkingMaxTokens(req.maxTokens, level, { contextTokens: thinking.contextTokens, promptTokens: promptTokens(req.messages) });
|
|
980
|
+
}
|
|
981
|
+
var requestCounter = 0;
|
|
982
|
+
function nextRequestId(feature) {
|
|
983
|
+
requestCounter += 1;
|
|
984
|
+
return `ai-${feature}-${Date.now().toString(36)}-${requestCounter}`;
|
|
985
|
+
}
|
|
986
|
+
function routeUnusableError(r) {
|
|
987
|
+
const map = {
|
|
988
|
+
cloudBlocked: "cloudBlocked",
|
|
989
|
+
consentDenied: "consentDenied",
|
|
990
|
+
probeFailed: "unreachable",
|
|
991
|
+
modelMissing: "modelNotFound",
|
|
992
|
+
modelNotListed: "modelNotFound"
|
|
993
|
+
};
|
|
994
|
+
const e = makeAiError(map[r.unusable] ?? "unknown", r.reason);
|
|
995
|
+
return r.engineId ? { ...e, engineId: r.engineId } : e;
|
|
996
|
+
}
|
|
997
|
+
function isTier2(ai) {
|
|
998
|
+
return typeof ai.listEngines === "function";
|
|
999
|
+
}
|
|
1000
|
+
function withFailures(statuses, failed) {
|
|
1001
|
+
if (failed.size === 0) {
|
|
1002
|
+
return [...statuses];
|
|
1003
|
+
}
|
|
1004
|
+
const out = statuses.map((s) => failed.has(s.engineId) ? { ...s, ok: false, error: failed.get(s.engineId) } : s);
|
|
1005
|
+
for (const [engineId, error] of failed) {
|
|
1006
|
+
if (!out.some((s) => s.engineId === engineId)) {
|
|
1007
|
+
out.push({ engineId, ok: false, isLocal: true, hasKey: false, error, models: [] });
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return out;
|
|
1011
|
+
}
|
|
1012
|
+
async function runAiCompletion(ctx, req, events, signal) {
|
|
1013
|
+
const tier2 = isTier2(ctx.ai);
|
|
1014
|
+
const failed = /* @__PURE__ */ new Map();
|
|
1015
|
+
const refused = {};
|
|
1016
|
+
const once = {};
|
|
1017
|
+
let retried = false;
|
|
1018
|
+
for (let pass = 0; pass < 12; pass++) {
|
|
1019
|
+
if (signal.aborted) {
|
|
1020
|
+
throw makeAiError("cancelled");
|
|
1021
|
+
}
|
|
1022
|
+
let route = null;
|
|
1023
|
+
if (tier2) {
|
|
1024
|
+
const consent = { ...ctx.consent, ...once, ...refused };
|
|
1025
|
+
const resolved = resolveRoute(ctx.settings(), withFailures(ctx.statuses(), failed), consent, { feature: req.feature, ...req.task ? { task: req.task } : {} });
|
|
1026
|
+
if (!isRouteUsable(resolved)) {
|
|
1027
|
+
throw routeUnusableError(resolved);
|
|
1028
|
+
}
|
|
1029
|
+
if (resolved.needsConsent) {
|
|
1030
|
+
const reply = ctx.ai.requestCloudConsent ? await ctx.ai.requestCloudConsent(resolved.engineId, { feature: req.feature, ...req.sends ? { sends: req.sends } : {} }) : true;
|
|
1031
|
+
if (signal.aborted) {
|
|
1032
|
+
throw makeAiError("cancelled");
|
|
1033
|
+
}
|
|
1034
|
+
if (reply === true) {
|
|
1035
|
+
ctx.consent[resolved.engineId] = true;
|
|
1036
|
+
} else if (reply === "once") {
|
|
1037
|
+
once[resolved.engineId] = true;
|
|
1038
|
+
} else {
|
|
1039
|
+
refused[resolved.engineId] = false;
|
|
1040
|
+
if (reply === "never") {
|
|
1041
|
+
ctx.consent[resolved.engineId] = false;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
route = resolved;
|
|
1047
|
+
}
|
|
1048
|
+
const thinking = route ? routeThinking(ctx.settings(), route, req) : null;
|
|
1049
|
+
const maxTokens = requestMaxTokens(req, thinking);
|
|
1050
|
+
events.onRoute?.(route, thinking?.info ?? null);
|
|
1051
|
+
let text = "";
|
|
1052
|
+
let gotToken = false;
|
|
1053
|
+
try {
|
|
1054
|
+
const result = await ctx.ai.complete({
|
|
1055
|
+
requestId: nextRequestId(req.feature),
|
|
1056
|
+
feature: req.feature,
|
|
1057
|
+
task: route?.task ?? req.task ?? resolveAiFeature(ctx.settings().features, req.feature).task,
|
|
1058
|
+
...route ? { route: { engineId: route.engineId, modelId: route.modelId } } : {},
|
|
1059
|
+
messages: req.messages,
|
|
1060
|
+
...req.format ? { format: req.format } : {},
|
|
1061
|
+
...maxTokens !== void 0 ? { maxTokens } : {},
|
|
1062
|
+
...req.temperature !== void 0 ? { temperature: req.temperature } : {},
|
|
1063
|
+
...thinking?.overrides ? { requestOverrides: thinking.overrides } : {}
|
|
1064
|
+
}, (delta) => {
|
|
1065
|
+
if (signal.aborted) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
gotToken = true;
|
|
1069
|
+
text += delta;
|
|
1070
|
+
events.onToken?.(delta, text);
|
|
1071
|
+
}, signal);
|
|
1072
|
+
return { result, route, viaFallback: route?.viaFallback ?? false, thinking: thinking?.info ?? null };
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
const e = signal.aborted ? makeAiError("cancelled") : toAiError(err);
|
|
1075
|
+
if (e.code === "unreachable" && !gotToken && !retried && route) {
|
|
1076
|
+
retried = true;
|
|
1077
|
+
failed.set(route.engineId, e.message);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
throw route && !e.engineId && e.code !== "cancelled" ? { ...e, engineId: route.engineId } : e;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
throw makeAiError("unknown", "No engine answered.");
|
|
1084
|
+
}
|
|
1085
|
+
function aiFeatureAvailability(ai, settings, statuses, consent, feature) {
|
|
1086
|
+
if (!ai || !settings.enabled) {
|
|
1087
|
+
return { state: "hidden" };
|
|
1088
|
+
}
|
|
1089
|
+
if (!resolveAiFeature(settings.features, feature).enabled) {
|
|
1090
|
+
return { state: "hidden" };
|
|
1091
|
+
}
|
|
1092
|
+
if (!isTier2(ai)) {
|
|
1093
|
+
return { state: "enabled" };
|
|
1094
|
+
}
|
|
1095
|
+
const answers = {};
|
|
1096
|
+
for (const [id, v] of Object.entries(consent)) {
|
|
1097
|
+
if (typeof v === "boolean") {
|
|
1098
|
+
answers[id] = v;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
const r = resolveRoute(settings, statuses, answers, { feature });
|
|
1102
|
+
return isRouteUsable(r) ? { state: "enabled" } : { state: "disabled", reason: r.reason };
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// src/ai-rewrite.ts
|
|
1106
|
+
import { elementFromString, posToDOMRect } from "@tiptap/core";
|
|
1107
|
+
import { TextSelection } from "@tiptap/pm/state";
|
|
1108
|
+
import { DOMParser as PmDOMParser, Fragment } from "@tiptap/pm/model";
|
|
1109
|
+
import { closeHistory } from "@tiptap/pm/history";
|
|
1110
|
+
import { EditorView as CmEditorView } from "@codemirror/view";
|
|
1111
|
+
import { isolateHistory } from "@codemirror/commands";
|
|
1112
|
+
|
|
1113
|
+
// src/ai-markdown-preview.ts
|
|
1114
|
+
var FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
1115
|
+
var HEADING = /^ {0,3}(#{1,6})[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/;
|
|
1116
|
+
var ITEM = /^([ \t]*)([-*+]|\d{1,9}[.)])[ \t]+(.*)$/;
|
|
1117
|
+
var QUOTE = /^ {0,3}>[ \t]?(.*)$/;
|
|
1118
|
+
var RULE = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/;
|
|
1119
|
+
var isSpace = (c) => c === void 0 || /\s/.test(c);
|
|
1120
|
+
var isWord = (c) => c !== void 0 && /[\p{L}\p{N}]/u.test(c);
|
|
1121
|
+
function make(tag, className) {
|
|
1122
|
+
const node = document.createElement(tag);
|
|
1123
|
+
if (className) {
|
|
1124
|
+
node.className = className;
|
|
1125
|
+
}
|
|
1126
|
+
return node;
|
|
1127
|
+
}
|
|
1128
|
+
function findCloser(s, delim, from) {
|
|
1129
|
+
for (let j = s.indexOf(delim, from); j !== -1; j = s.indexOf(delim, j + 1)) {
|
|
1130
|
+
if (s[j - 1] === "\\" || isSpace(s[j - 1])) {
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
if (delim.length === 1 && (s[j + 1] === delim || s[j - 1] === delim)) {
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
if (delim === "_" && isWord(s[j + 1])) {
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
return j;
|
|
1140
|
+
}
|
|
1141
|
+
return -1;
|
|
1142
|
+
}
|
|
1143
|
+
function linkEnd(s, close) {
|
|
1144
|
+
if (s[close + 1] !== "(") {
|
|
1145
|
+
return -1;
|
|
1146
|
+
}
|
|
1147
|
+
let depth = 0;
|
|
1148
|
+
for (let k = close + 1; k < s.length; k++) {
|
|
1149
|
+
if (s[k] === "\n") {
|
|
1150
|
+
return -1;
|
|
1151
|
+
}
|
|
1152
|
+
if (s[k] === "(") {
|
|
1153
|
+
depth++;
|
|
1154
|
+
} else if (s[k] === ")" && --depth === 0) {
|
|
1155
|
+
return k + 1;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return -1;
|
|
1159
|
+
}
|
|
1160
|
+
function matchingBracket(s, open) {
|
|
1161
|
+
let depth = 0;
|
|
1162
|
+
for (let k = open; k < s.length; k++) {
|
|
1163
|
+
if (s[k] === "\\") {
|
|
1164
|
+
k++;
|
|
1165
|
+
} else if (s[k] === "[") {
|
|
1166
|
+
depth++;
|
|
1167
|
+
} else if (s[k] === "]" && --depth === 0) {
|
|
1168
|
+
return k;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return -1;
|
|
1172
|
+
}
|
|
1173
|
+
function appendInline(parent, s) {
|
|
1174
|
+
let buf = "";
|
|
1175
|
+
const flush = () => {
|
|
1176
|
+
if (buf) {
|
|
1177
|
+
parent.appendChild(document.createTextNode(buf));
|
|
1178
|
+
buf = "";
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
const wrap = (tag, inner, className) => {
|
|
1182
|
+
flush();
|
|
1183
|
+
const node = make(tag, className);
|
|
1184
|
+
appendInline(node, inner);
|
|
1185
|
+
parent.appendChild(node);
|
|
1186
|
+
};
|
|
1187
|
+
let i = 0;
|
|
1188
|
+
while (i < s.length) {
|
|
1189
|
+
const c = s[i];
|
|
1190
|
+
if (c === "\\" && i + 1 < s.length && /[\\`*_{}[\]()#+\-.!~>|]/.test(s[i + 1])) {
|
|
1191
|
+
buf += s[i + 1];
|
|
1192
|
+
i += 2;
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
if (c === "`") {
|
|
1196
|
+
let n = 1;
|
|
1197
|
+
while (s[i + n] === "`") {
|
|
1198
|
+
n++;
|
|
1199
|
+
}
|
|
1200
|
+
const run = "`".repeat(n);
|
|
1201
|
+
const end = s.indexOf(run, i + n);
|
|
1202
|
+
if (end !== -1 && s[end + n] !== "`") {
|
|
1203
|
+
flush();
|
|
1204
|
+
const code = make("code");
|
|
1205
|
+
code.textContent = s.slice(i + n, end).replace(/^ (.*) $/, "$1");
|
|
1206
|
+
parent.appendChild(code);
|
|
1207
|
+
i = end + n;
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
buf += run;
|
|
1211
|
+
i += n;
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (c === "*" || c === "_") {
|
|
1215
|
+
const doubled = s[i + 1] === c;
|
|
1216
|
+
const delim = doubled ? c + c : c;
|
|
1217
|
+
const after = s[i + delim.length];
|
|
1218
|
+
const leftOk = c === "*" || !isWord(s[i - 1]);
|
|
1219
|
+
if (leftOk && !isSpace(after) && after !== c) {
|
|
1220
|
+
const end = findCloser(s, delim, i + delim.length);
|
|
1221
|
+
if (end > i + delim.length) {
|
|
1222
|
+
wrap(doubled ? "strong" : "em", s.slice(i + delim.length, end));
|
|
1223
|
+
i = end + delim.length;
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
buf += delim;
|
|
1228
|
+
i += delim.length;
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
if (c === "~" && s[i + 1] === "~" && !isSpace(s[i + 2])) {
|
|
1232
|
+
const end = findCloser(s, "~~", i + 2);
|
|
1233
|
+
if (end > i + 2) {
|
|
1234
|
+
wrap("del", s.slice(i + 2, end));
|
|
1235
|
+
i = end + 2;
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (c === "[" || c === "!" && s[i + 1] === "[") {
|
|
1240
|
+
const open = c === "!" ? i + 1 : i;
|
|
1241
|
+
const close = matchingBracket(s, open);
|
|
1242
|
+
const end = close === -1 ? -1 : linkEnd(s, close);
|
|
1243
|
+
if (end !== -1) {
|
|
1244
|
+
wrap("span", s.slice(open + 1, close), c === "!" ? "ai-rw-md-image" : "ai-rw-md-link");
|
|
1245
|
+
i = end;
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
buf += c;
|
|
1250
|
+
i++;
|
|
1251
|
+
}
|
|
1252
|
+
flush();
|
|
1253
|
+
}
|
|
1254
|
+
var indentOf = (ws) => ws.replace(/\t/g, " ").length;
|
|
1255
|
+
function renderPreviewMarkdown(into, markdown) {
|
|
1256
|
+
const out = [];
|
|
1257
|
+
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
1258
|
+
let i = 0;
|
|
1259
|
+
const startsBlock = (line) => FENCE.test(line) || HEADING.test(line) || ITEM.test(line) || QUOTE.test(line) || RULE.test(line);
|
|
1260
|
+
while (i < lines.length) {
|
|
1261
|
+
const line = lines[i];
|
|
1262
|
+
if (!line.trim()) {
|
|
1263
|
+
i++;
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
const fence = FENCE.exec(line);
|
|
1267
|
+
if (fence) {
|
|
1268
|
+
const marker = fence[1];
|
|
1269
|
+
const body = [];
|
|
1270
|
+
i++;
|
|
1271
|
+
while (i < lines.length && !new RegExp(`^ {0,3}${marker[0]}{${marker.length},}[ \\t]*$`).test(lines[i])) {
|
|
1272
|
+
body.push(lines[i++]);
|
|
1273
|
+
}
|
|
1274
|
+
i++;
|
|
1275
|
+
const pre = make("pre", "ai-rw-md-pre");
|
|
1276
|
+
const code = make("code");
|
|
1277
|
+
code.textContent = body.join("\n");
|
|
1278
|
+
pre.appendChild(code);
|
|
1279
|
+
out.push(pre);
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
const heading = HEADING.exec(line);
|
|
1283
|
+
if (heading) {
|
|
1284
|
+
const node = make("div", "ai-rw-md-h");
|
|
1285
|
+
node.dataset.level = String(heading[1].length);
|
|
1286
|
+
appendInline(node, heading[2]);
|
|
1287
|
+
out.push(node);
|
|
1288
|
+
i++;
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
if (RULE.test(line)) {
|
|
1292
|
+
out.push(make("hr", "ai-rw-md-hr"));
|
|
1293
|
+
i++;
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
if (QUOTE.test(line)) {
|
|
1297
|
+
const inner = [];
|
|
1298
|
+
while (i < lines.length && QUOTE.test(lines[i])) {
|
|
1299
|
+
inner.push(QUOTE.exec(lines[i++])[1]);
|
|
1300
|
+
}
|
|
1301
|
+
const quote = make("blockquote", "ai-rw-md-quote");
|
|
1302
|
+
renderPreviewMarkdown(quote, inner.join("\n"));
|
|
1303
|
+
out.push(quote);
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
if (ITEM.test(line)) {
|
|
1307
|
+
const stack = [];
|
|
1308
|
+
const roots = [];
|
|
1309
|
+
while (i < lines.length) {
|
|
1310
|
+
const m = ITEM.exec(lines[i]);
|
|
1311
|
+
if (!m) {
|
|
1312
|
+
if (lines[i].trim() && /^[ \t]+\S/.test(lines[i]) && !startsBlock(lines[i]) && stack.length) {
|
|
1313
|
+
const item = stack[stack.length - 1].item;
|
|
1314
|
+
item.appendChild(document.createTextNode(`
|
|
1315
|
+
${lines[i].trim()}`));
|
|
1316
|
+
i++;
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
if (!lines[i].trim() && ITEM.test(lines[i + 1] ?? "")) {
|
|
1320
|
+
i++;
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
const indent = indentOf(m[1]);
|
|
1326
|
+
const ordered = /\d/.test(m[2]);
|
|
1327
|
+
while (stack.length && indent < stack[stack.length - 1].indent) {
|
|
1328
|
+
stack.pop();
|
|
1329
|
+
}
|
|
1330
|
+
let top = stack[stack.length - 1];
|
|
1331
|
+
if (top && indent === top.indent && ordered !== top.ordered) {
|
|
1332
|
+
stack.pop();
|
|
1333
|
+
top = stack[stack.length - 1];
|
|
1334
|
+
}
|
|
1335
|
+
if (!top || indent > top.indent) {
|
|
1336
|
+
const list = make(ordered ? "ol" : "ul", "ai-rw-md-list");
|
|
1337
|
+
if (ordered) {
|
|
1338
|
+
const start = parseInt(m[2], 10);
|
|
1339
|
+
if (start !== 1) {
|
|
1340
|
+
list.setAttribute("start", String(start));
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (top?.item) {
|
|
1344
|
+
top.item.appendChild(list);
|
|
1345
|
+
} else {
|
|
1346
|
+
roots.push(list);
|
|
1347
|
+
}
|
|
1348
|
+
top = { indent, ordered, list, item: null };
|
|
1349
|
+
stack.push(top);
|
|
1350
|
+
}
|
|
1351
|
+
const li = make("li");
|
|
1352
|
+
appendInline(li, m[3]);
|
|
1353
|
+
top.list.appendChild(li);
|
|
1354
|
+
top.item = li;
|
|
1355
|
+
i++;
|
|
1356
|
+
}
|
|
1357
|
+
out.push(...roots);
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
const para = [];
|
|
1361
|
+
while (i < lines.length && lines[i].trim() && (para.length === 0 || !startsBlock(lines[i]))) {
|
|
1362
|
+
para.push(lines[i++]);
|
|
1363
|
+
}
|
|
1364
|
+
const p = make("p", "ai-rw-md-p");
|
|
1365
|
+
appendInline(p, para.join("\n"));
|
|
1366
|
+
out.push(p);
|
|
1367
|
+
}
|
|
1368
|
+
into.replaceChildren(...out);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// src/ai-rewrite.ts
|
|
1372
|
+
var nextTargetId = 0;
|
|
1373
|
+
function serializeFragment(editor, fragment) {
|
|
1374
|
+
const storage = editor.storage.markdown;
|
|
1375
|
+
try {
|
|
1376
|
+
const md = storage?.serializer?.serialize(fragment);
|
|
1377
|
+
if (typeof md === "string") {
|
|
1378
|
+
return md;
|
|
1379
|
+
}
|
|
1380
|
+
} catch {
|
|
1381
|
+
}
|
|
1382
|
+
return fragment.textBetween(0, fragment.size, "\n\n");
|
|
1383
|
+
}
|
|
1384
|
+
function serializeRange(editor, doc, from, to) {
|
|
1385
|
+
const content = doc.slice(from, to).content;
|
|
1386
|
+
const parent = doc.resolve(from).parent;
|
|
1387
|
+
const paragraph = editor.state.schema.nodes.paragraph;
|
|
1388
|
+
if (content.firstChild?.isInline && !parent.type.spec.code && paragraph) {
|
|
1389
|
+
return serializeFragment(editor, Fragment.from(paragraph.create(null, content)));
|
|
1390
|
+
}
|
|
1391
|
+
return serializeFragment(editor, content);
|
|
1392
|
+
}
|
|
1393
|
+
function markdownStorage(editor) {
|
|
1394
|
+
return editor.storage.markdown;
|
|
1395
|
+
}
|
|
1396
|
+
function parseBlocks(editor, markdown) {
|
|
1397
|
+
const html = markdownStorage(editor)?.parser?.parse(markdown) ?? markdown;
|
|
1398
|
+
return PmDOMParser.fromSchema(editor.state.schema).parse(elementFromString(html)).content;
|
|
1399
|
+
}
|
|
1400
|
+
function blockquoteMarkdown(text) {
|
|
1401
|
+
return text.trim().split("\n").map((line) => line.trim() ? `> ${line}` : ">").join("\n");
|
|
1402
|
+
}
|
|
1403
|
+
function visibleTopRect(el3) {
|
|
1404
|
+
const r = el3.getBoundingClientRect();
|
|
1405
|
+
const top = Math.max(r.top, 8);
|
|
1406
|
+
return new DOMRect(r.left, top, Math.max(1, r.width), 1);
|
|
1407
|
+
}
|
|
1408
|
+
function rectUnion(a, b) {
|
|
1409
|
+
const left = Math.min(a.left, b.left);
|
|
1410
|
+
const top = Math.min(a.top, b.top);
|
|
1411
|
+
return new DOMRect(left, top, Math.max(a.right, b.right) - left, Math.max(a.bottom, b.bottom) - top);
|
|
1412
|
+
}
|
|
1413
|
+
function richRewriteTarget(editor) {
|
|
1414
|
+
const { state } = editor;
|
|
1415
|
+
const sel = state.selection;
|
|
1416
|
+
if ("$anchorCell" in sel) {
|
|
1417
|
+
return null;
|
|
1418
|
+
}
|
|
1419
|
+
let from = sel.from;
|
|
1420
|
+
let to = sel.to;
|
|
1421
|
+
if (sel.empty) {
|
|
1422
|
+
const $f = sel.$from;
|
|
1423
|
+
if (!$f.parent.isTextblock || $f.parent.type.spec.code || !$f.parent.textContent.trim()) {
|
|
1424
|
+
return null;
|
|
1425
|
+
}
|
|
1426
|
+
from = $f.start();
|
|
1427
|
+
to = $f.end();
|
|
1428
|
+
}
|
|
1429
|
+
const doc = state.doc;
|
|
1430
|
+
if (!doc.textBetween(from, to, " ").trim()) {
|
|
1431
|
+
return null;
|
|
1432
|
+
}
|
|
1433
|
+
const text = serializeRange(editor, doc, from, to);
|
|
1434
|
+
if (!text.trim()) {
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1437
|
+
const $from = doc.resolve(from);
|
|
1438
|
+
const $to = doc.resolve(to);
|
|
1439
|
+
const beforeStart = $from.depth > 0 ? $from.posAtIndex(Math.max(0, $from.index(0) - 1), 0) : 0;
|
|
1440
|
+
const afterEnd = $to.depth > 0 ? $to.posAtIndex(Math.min(doc.childCount, $to.index(0) + 2), 0) : doc.content.size;
|
|
1441
|
+
const before = beforeStart < from ? serializeRange(editor, doc, beforeStart, from) : "";
|
|
1442
|
+
const after = afterEnd > to ? serializeRange(editor, doc, to, afterEnd) : "";
|
|
1443
|
+
const id = ++nextTargetId;
|
|
1444
|
+
editor.view.dispatch(state.tr.setMeta(aiRewriteHighlightKey, { range: { from, to, id } }).setMeta("addToHistory", false));
|
|
1445
|
+
const range = () => {
|
|
1446
|
+
if (editor.isDestroyed) {
|
|
1447
|
+
return null;
|
|
1448
|
+
}
|
|
1449
|
+
const r = aiRewriteHighlightKey.getState(editor.state);
|
|
1450
|
+
return r && r.id === id ? r : null;
|
|
1451
|
+
};
|
|
1452
|
+
return {
|
|
1453
|
+
mode: "rich",
|
|
1454
|
+
scope: "selection",
|
|
1455
|
+
text,
|
|
1456
|
+
before,
|
|
1457
|
+
after,
|
|
1458
|
+
multiline: text.trim().includes("\n"),
|
|
1459
|
+
alive: () => !!range(),
|
|
1460
|
+
rect() {
|
|
1461
|
+
const r = range();
|
|
1462
|
+
if (!r) {
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
try {
|
|
1466
|
+
return posToDOMRect(editor.view, r.from, r.to);
|
|
1467
|
+
} catch {
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
},
|
|
1471
|
+
accept(markdown) {
|
|
1472
|
+
const r = range();
|
|
1473
|
+
if (!r || editor.isDestroyed) {
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
const current = editor.state;
|
|
1477
|
+
const html = markdownStorage(editor)?.parser?.parse(markdown, { inline: true }) ?? markdown;
|
|
1478
|
+
const slice = PmDOMParser.fromSchema(current.schema).parseSlice(elementFromString(html), {
|
|
1479
|
+
preserveWhitespace: true,
|
|
1480
|
+
context: current.doc.resolve(r.from)
|
|
1481
|
+
});
|
|
1482
|
+
let { from: from2, to: to2 } = r;
|
|
1483
|
+
const $a = current.doc.resolve(from2);
|
|
1484
|
+
const $b = current.doc.resolve(to2);
|
|
1485
|
+
if (slice.content.firstChild?.isBlock && $a.parent.isTextblock && $b.parent.isTextblock && $a.parentOffset === 0 && $b.parentOffset === $b.parent.content.size && $a.depth === $b.depth && $a.node($a.depth - 1) === $b.node($b.depth - 1)) {
|
|
1486
|
+
from2 = $a.before();
|
|
1487
|
+
to2 = $b.after();
|
|
1488
|
+
}
|
|
1489
|
+
const tr = closeHistory(current.tr).replaceRange(from2, to2, slice);
|
|
1490
|
+
tr.setMeta(aiRewriteHighlightKey, { range: null });
|
|
1491
|
+
const end = tr.mapping.map(to2, 1);
|
|
1492
|
+
try {
|
|
1493
|
+
tr.setSelection(TextSelection.between(tr.doc.resolve(Math.min(from2, tr.doc.content.size)), tr.doc.resolve(Math.min(end, tr.doc.content.size))));
|
|
1494
|
+
} catch {
|
|
1495
|
+
}
|
|
1496
|
+
editor.view.dispatch(tr.scrollIntoView());
|
|
1497
|
+
return true;
|
|
1498
|
+
},
|
|
1499
|
+
insertBelow(markdown) {
|
|
1500
|
+
const r = range();
|
|
1501
|
+
if (!r || editor.isDestroyed) {
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
const current = editor.state;
|
|
1505
|
+
const content = parseBlocks(editor, blockquoteMarkdown(markdown));
|
|
1506
|
+
if (!content.size) {
|
|
1507
|
+
return false;
|
|
1508
|
+
}
|
|
1509
|
+
const $to2 = current.doc.resolve(r.to);
|
|
1510
|
+
const pos = $to2.depth > 0 ? $to2.after(1) : r.to;
|
|
1511
|
+
editor.view.dispatch(closeHistory(current.tr).insert(pos, content).scrollIntoView());
|
|
1512
|
+
return true;
|
|
1513
|
+
},
|
|
1514
|
+
release() {
|
|
1515
|
+
if (!range()) {
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
editor.view.dispatch(editor.state.tr.setMeta(aiRewriteHighlightKey, { range: null }).setMeta("addToHistory", false));
|
|
1519
|
+
},
|
|
1520
|
+
focus() {
|
|
1521
|
+
if (!editor.isDestroyed) {
|
|
1522
|
+
editor.view.focus();
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
function sourceRewriteTarget(view) {
|
|
1528
|
+
const { state } = view;
|
|
1529
|
+
const sel = state.selection.main;
|
|
1530
|
+
let { from, to } = sel;
|
|
1531
|
+
if (sel.empty) {
|
|
1532
|
+
const line = state.doc.lineAt(from);
|
|
1533
|
+
if (!line.text.trim()) {
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
from = line.from;
|
|
1537
|
+
to = line.to;
|
|
1538
|
+
}
|
|
1539
|
+
const text = state.sliceDoc(from, to);
|
|
1540
|
+
if (!text.trim()) {
|
|
1541
|
+
return null;
|
|
1542
|
+
}
|
|
1543
|
+
const before = state.sliceDoc(Math.max(0, from - REWRITE_CONTEXT_CHARS * 2), from);
|
|
1544
|
+
const after = state.sliceDoc(to, Math.min(state.doc.length, to + REWRITE_CONTEXT_CHARS * 2));
|
|
1545
|
+
const id = ++nextTargetId;
|
|
1546
|
+
view.dispatch({ effects: setCmRewriteRange.of({ from, to, id }) });
|
|
1547
|
+
const range = () => {
|
|
1548
|
+
const r = view.state.field(cmRewriteRange, false);
|
|
1549
|
+
return r && r.id === id ? r : null;
|
|
1550
|
+
};
|
|
1551
|
+
return {
|
|
1552
|
+
mode: "source",
|
|
1553
|
+
scope: "selection",
|
|
1554
|
+
text,
|
|
1555
|
+
before,
|
|
1556
|
+
after,
|
|
1557
|
+
multiline: text.trim().includes("\n"),
|
|
1558
|
+
alive: () => !!range(),
|
|
1559
|
+
rect() {
|
|
1560
|
+
const r = range();
|
|
1561
|
+
if (!r) {
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
const a = view.coordsAtPos(r.from);
|
|
1565
|
+
const b = view.coordsAtPos(r.to, -1);
|
|
1566
|
+
if (!a || !b) {
|
|
1567
|
+
return a ? rectUnion(a, a) : b ? rectUnion(b, b) : null;
|
|
1568
|
+
}
|
|
1569
|
+
if (b.top > a.top + 1) {
|
|
1570
|
+
const content = view.contentDOM.getBoundingClientRect();
|
|
1571
|
+
return rectUnion(a, { ...b, left: content.left, right: Math.max(a.right, b.right) });
|
|
1572
|
+
}
|
|
1573
|
+
return rectUnion(a, b);
|
|
1574
|
+
},
|
|
1575
|
+
accept(markdown) {
|
|
1576
|
+
const r = range();
|
|
1577
|
+
if (!r) {
|
|
1578
|
+
return false;
|
|
1579
|
+
}
|
|
1580
|
+
view.dispatch({
|
|
1581
|
+
changes: { from: r.from, to: r.to, insert: markdown },
|
|
1582
|
+
selection: { anchor: r.from, head: r.from + markdown.length },
|
|
1583
|
+
effects: setCmRewriteRange.of(null),
|
|
1584
|
+
annotations: isolateHistory.of("full"),
|
|
1585
|
+
userEvent: "input.ai",
|
|
1586
|
+
scrollIntoView: true
|
|
1587
|
+
});
|
|
1588
|
+
return true;
|
|
1589
|
+
},
|
|
1590
|
+
insertBelow(markdown) {
|
|
1591
|
+
const r = range();
|
|
1592
|
+
if (!r) {
|
|
1593
|
+
return false;
|
|
1594
|
+
}
|
|
1595
|
+
const doc = view.state.doc;
|
|
1596
|
+
const endLine = doc.lineAt(r.to);
|
|
1597
|
+
const line = r.to > r.from && endLine.from === r.to ? doc.lineAt(r.to - 1) : endLine;
|
|
1598
|
+
const next = line.number < doc.lines ? doc.line(line.number + 1) : null;
|
|
1599
|
+
const insert = `
|
|
1600
|
+
|
|
1601
|
+
${blockquoteMarkdown(markdown)}${next && next.text.trim() ? "\n" : ""}`;
|
|
1602
|
+
view.dispatch({
|
|
1603
|
+
changes: { from: line.to, insert },
|
|
1604
|
+
annotations: isolateHistory.of("full"),
|
|
1605
|
+
userEvent: "input.ai",
|
|
1606
|
+
scrollIntoView: true
|
|
1607
|
+
});
|
|
1608
|
+
return true;
|
|
1609
|
+
},
|
|
1610
|
+
release() {
|
|
1611
|
+
if (range()) {
|
|
1612
|
+
view.dispatch({ effects: setCmRewriteRange.of(null) });
|
|
1613
|
+
}
|
|
1614
|
+
},
|
|
1615
|
+
focus() {
|
|
1616
|
+
view.focus();
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
function richDocumentTarget(editor, opts = {}) {
|
|
1621
|
+
if (editor.isDestroyed) {
|
|
1622
|
+
return null;
|
|
1623
|
+
}
|
|
1624
|
+
const text = markdownStorage(editor)?.getMarkdown?.() ?? editor.state.doc.textBetween(0, editor.state.doc.content.size, "\n\n");
|
|
1625
|
+
if (!text.trim()) {
|
|
1626
|
+
return null;
|
|
1627
|
+
}
|
|
1628
|
+
return {
|
|
1629
|
+
mode: "rich",
|
|
1630
|
+
scope: "document",
|
|
1631
|
+
text,
|
|
1632
|
+
before: "",
|
|
1633
|
+
after: "",
|
|
1634
|
+
multiline: false,
|
|
1635
|
+
alive: () => !editor.isDestroyed,
|
|
1636
|
+
rect: () => editor.isDestroyed ? null : visibleTopRect(editor.view.dom),
|
|
1637
|
+
accept(markdown) {
|
|
1638
|
+
if (editor.isDestroyed) {
|
|
1639
|
+
return false;
|
|
1640
|
+
}
|
|
1641
|
+
const current = editor.state;
|
|
1642
|
+
const content = parseBlocks(editor, markdown);
|
|
1643
|
+
if (!content.size) {
|
|
1644
|
+
return false;
|
|
1645
|
+
}
|
|
1646
|
+
const tr = closeHistory(current.tr).replaceWith(0, current.doc.content.size, content);
|
|
1647
|
+
tr.setMeta(aiRewriteHighlightKey, { range: null });
|
|
1648
|
+
tr.setSelection(TextSelection.atStart(tr.doc));
|
|
1649
|
+
editor.view.dispatch(tr.scrollIntoView());
|
|
1650
|
+
opts.scrollToTop?.();
|
|
1651
|
+
return true;
|
|
1652
|
+
},
|
|
1653
|
+
insertBelow: () => false,
|
|
1654
|
+
release() {
|
|
1655
|
+
},
|
|
1656
|
+
focus() {
|
|
1657
|
+
if (!editor.isDestroyed) {
|
|
1658
|
+
editor.view.focus();
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
function sourceDocumentTarget(view, bodyStart) {
|
|
1664
|
+
const full = view.state.doc.toString();
|
|
1665
|
+
const text = full.slice(bodyStart(full));
|
|
1666
|
+
if (!text.trim()) {
|
|
1667
|
+
return null;
|
|
1668
|
+
}
|
|
1669
|
+
return {
|
|
1670
|
+
mode: "source",
|
|
1671
|
+
scope: "document",
|
|
1672
|
+
text,
|
|
1673
|
+
before: "",
|
|
1674
|
+
after: "",
|
|
1675
|
+
multiline: false,
|
|
1676
|
+
alive: () => view.dom.isConnected,
|
|
1677
|
+
rect: () => visibleTopRect(view.scrollDOM),
|
|
1678
|
+
accept(markdown) {
|
|
1679
|
+
const current = view.state.doc.toString();
|
|
1680
|
+
const from = bodyStart(current);
|
|
1681
|
+
view.dispatch({
|
|
1682
|
+
changes: { from, to: current.length, insert: markdown },
|
|
1683
|
+
selection: { anchor: from },
|
|
1684
|
+
effects: [setCmRewriteRange.of(null), CmEditorView.scrollIntoView(0, { y: "start" })],
|
|
1685
|
+
annotations: isolateHistory.of("full"),
|
|
1686
|
+
userEvent: "input.ai"
|
|
1687
|
+
});
|
|
1688
|
+
return true;
|
|
1689
|
+
},
|
|
1690
|
+
insertBelow: () => false,
|
|
1691
|
+
release() {
|
|
1692
|
+
},
|
|
1693
|
+
focus() {
|
|
1694
|
+
view.focus();
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
function cursorInsertTarget(editor, slash) {
|
|
1699
|
+
if (editor.isDestroyed) {
|
|
1700
|
+
return null;
|
|
1701
|
+
}
|
|
1702
|
+
const { state } = editor;
|
|
1703
|
+
const id = ++nextTargetId;
|
|
1704
|
+
const head = state.selection.head;
|
|
1705
|
+
const initial = slash && slash.to > slash.from ? { from: slash.from, to: slash.to, id } : { from: head, to: head, id, anchor: true };
|
|
1706
|
+
editor.view.dispatch(state.tr.setMeta(aiRewriteHighlightKey, { range: initial }).setMeta("addToHistory", false));
|
|
1707
|
+
const range = () => {
|
|
1708
|
+
if (editor.isDestroyed) {
|
|
1709
|
+
return null;
|
|
1710
|
+
}
|
|
1711
|
+
const r = aiRewriteHighlightKey.getState(editor.state);
|
|
1712
|
+
return r && r.id === id ? r : null;
|
|
1713
|
+
};
|
|
1714
|
+
return {
|
|
1715
|
+
mode: "rich",
|
|
1716
|
+
scope: "cursor",
|
|
1717
|
+
text: "",
|
|
1718
|
+
before: "",
|
|
1719
|
+
after: "",
|
|
1720
|
+
multiline: false,
|
|
1721
|
+
alive: () => !!range(),
|
|
1722
|
+
range() {
|
|
1723
|
+
const r = range();
|
|
1724
|
+
return r ? { from: r.from, to: r.to } : null;
|
|
1725
|
+
},
|
|
1726
|
+
rect() {
|
|
1727
|
+
const r = range();
|
|
1728
|
+
if (!r) {
|
|
1729
|
+
return null;
|
|
1730
|
+
}
|
|
1731
|
+
try {
|
|
1732
|
+
return posToDOMRect(editor.view, r.from, r.to);
|
|
1733
|
+
} catch {
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
},
|
|
1737
|
+
accept(markdown) {
|
|
1738
|
+
const r = range();
|
|
1739
|
+
if (!r || editor.isDestroyed) {
|
|
1740
|
+
return false;
|
|
1741
|
+
}
|
|
1742
|
+
const content = parseBlocks(editor, markdown);
|
|
1743
|
+
if (!content.size) {
|
|
1744
|
+
return false;
|
|
1745
|
+
}
|
|
1746
|
+
const current = editor.state;
|
|
1747
|
+
const tr = closeHistory(current.tr);
|
|
1748
|
+
let at = r.from;
|
|
1749
|
+
if (r.to > r.from) {
|
|
1750
|
+
const $q = current.doc.resolve(r.from);
|
|
1751
|
+
if ($q.parentOffset > 0 && current.doc.textBetween(r.from - 1, r.from) === " ") {
|
|
1752
|
+
at = r.from - 1;
|
|
1753
|
+
}
|
|
1754
|
+
tr.delete(at, r.to);
|
|
1755
|
+
}
|
|
1756
|
+
tr.setMeta(aiRewriteHighlightKey, { range: null });
|
|
1757
|
+
const $at = tr.doc.resolve(Math.min(at, tr.doc.content.size));
|
|
1758
|
+
let from = $at.pos;
|
|
1759
|
+
let to = $at.pos;
|
|
1760
|
+
if ($at.depth > 0) {
|
|
1761
|
+
const block = $at.node(1);
|
|
1762
|
+
const empty = block.type.name === "paragraph" && block.content.size === 0;
|
|
1763
|
+
from = empty ? $at.before(1) : $at.after(1);
|
|
1764
|
+
to = empty ? $at.after(1) : from;
|
|
1765
|
+
}
|
|
1766
|
+
tr.replaceWith(from, to, content);
|
|
1767
|
+
const end = Math.min(from + content.size, tr.doc.content.size);
|
|
1768
|
+
try {
|
|
1769
|
+
tr.setSelection(TextSelection.near(tr.doc.resolve(end), -1));
|
|
1770
|
+
} catch {
|
|
1771
|
+
}
|
|
1772
|
+
editor.view.dispatch(tr.scrollIntoView());
|
|
1773
|
+
return true;
|
|
1774
|
+
},
|
|
1775
|
+
insertBelow: () => false,
|
|
1776
|
+
release() {
|
|
1777
|
+
if (!range()) {
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
editor.view.dispatch(editor.state.tr.setMeta(aiRewriteHighlightKey, { range: null }).setMeta("addToHistory", false));
|
|
1781
|
+
},
|
|
1782
|
+
focus() {
|
|
1783
|
+
if (!editor.isDestroyed) {
|
|
1784
|
+
editor.view.focus();
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
function sourceCursorTarget(view, bodyStart) {
|
|
1790
|
+
const id = ++nextTargetId;
|
|
1791
|
+
const head = view.state.selection.main.head;
|
|
1792
|
+
view.dispatch({ effects: setCmRewriteRange.of({ from: head, to: head, id, anchor: true }) });
|
|
1793
|
+
const range = () => {
|
|
1794
|
+
const r = view.state.field(cmRewriteRange, false);
|
|
1795
|
+
return r && r.id === id ? r : null;
|
|
1796
|
+
};
|
|
1797
|
+
return {
|
|
1798
|
+
mode: "source",
|
|
1799
|
+
scope: "cursor",
|
|
1800
|
+
text: "",
|
|
1801
|
+
before: "",
|
|
1802
|
+
after: "",
|
|
1803
|
+
multiline: false,
|
|
1804
|
+
alive: () => !!range() && view.dom.isConnected,
|
|
1805
|
+
range() {
|
|
1806
|
+
const r = range();
|
|
1807
|
+
return r ? { from: r.from, to: r.to } : null;
|
|
1808
|
+
},
|
|
1809
|
+
rect() {
|
|
1810
|
+
const r = range();
|
|
1811
|
+
const c = r ? view.coordsAtPos(r.from) : null;
|
|
1812
|
+
return c ? rectUnion(c, c) : null;
|
|
1813
|
+
},
|
|
1814
|
+
accept(markdown) {
|
|
1815
|
+
const r = range();
|
|
1816
|
+
const md = markdown.trim();
|
|
1817
|
+
if (!r || !md) {
|
|
1818
|
+
return false;
|
|
1819
|
+
}
|
|
1820
|
+
const doc = view.state.doc;
|
|
1821
|
+
const start = bodyStart ? bodyStart(doc.toString()) : 0;
|
|
1822
|
+
const line = doc.lineAt(Math.max(r.from, Math.min(start, doc.length)));
|
|
1823
|
+
const prev = line.number > 1 ? doc.line(line.number - 1) : null;
|
|
1824
|
+
const next = line.number < doc.lines ? doc.line(line.number + 1) : null;
|
|
1825
|
+
const textAfter = !!next && !!next.text.trim();
|
|
1826
|
+
let changes;
|
|
1827
|
+
let caret;
|
|
1828
|
+
if (!line.text.trim()) {
|
|
1829
|
+
const lead = prev && prev.text.trim() && line.from >= start ? "\n" : "";
|
|
1830
|
+
changes = { from: line.from, to: line.to, insert: `${lead}${md}${textAfter ? "\n" : ""}` };
|
|
1831
|
+
caret = line.from + lead.length + md.length;
|
|
1832
|
+
} else {
|
|
1833
|
+
changes = { from: line.to, insert: `
|
|
1834
|
+
|
|
1835
|
+
${md}${textAfter ? "\n" : ""}` };
|
|
1836
|
+
caret = line.to + 2 + md.length;
|
|
1837
|
+
}
|
|
1838
|
+
view.dispatch({
|
|
1839
|
+
changes,
|
|
1840
|
+
selection: { anchor: caret },
|
|
1841
|
+
effects: setCmRewriteRange.of(null),
|
|
1842
|
+
annotations: isolateHistory.of("full"),
|
|
1843
|
+
userEvent: "input.ai",
|
|
1844
|
+
scrollIntoView: true
|
|
1845
|
+
});
|
|
1846
|
+
return true;
|
|
1847
|
+
},
|
|
1848
|
+
insertBelow: () => false,
|
|
1849
|
+
release() {
|
|
1850
|
+
if (range()) {
|
|
1851
|
+
view.dispatch({ effects: setCmRewriteRange.of(null) });
|
|
1852
|
+
}
|
|
1853
|
+
},
|
|
1854
|
+
focus() {
|
|
1855
|
+
view.focus();
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
var AI_TITLE_EFFECT_HINTS = {
|
|
1860
|
+
frontmatter: "Sets title: in Properties",
|
|
1861
|
+
h1: "Replaces the H1",
|
|
1862
|
+
insertH1: "Inserts an H1"
|
|
1863
|
+
};
|
|
1864
|
+
function yamlHasTitle(yaml) {
|
|
1865
|
+
return /^title[ \t]*:/m.test(yaml ?? "");
|
|
1866
|
+
}
|
|
1867
|
+
function setYamlTitle(yaml, title) {
|
|
1868
|
+
const lines = yaml.split("\n");
|
|
1869
|
+
const i = lines.findIndex((l) => /^title[ \t]*:/.test(l));
|
|
1870
|
+
if (i < 0) {
|
|
1871
|
+
return yaml;
|
|
1872
|
+
}
|
|
1873
|
+
let end = i + 1;
|
|
1874
|
+
while (end < lines.length && /^[ \t]+\S/.test(lines[end])) {
|
|
1875
|
+
end++;
|
|
1876
|
+
}
|
|
1877
|
+
lines.splice(i, end - i, `title: ${yamlScalar(title)}`);
|
|
1878
|
+
return lines.join("\n");
|
|
1879
|
+
}
|
|
1880
|
+
function yamlScalar(s) {
|
|
1881
|
+
const plain = !/[:#[\]{},&*!|>'"%@`]/.test(s) && !/^\s|\s$/.test(s) && !/^(-?\d+(\.\d+)?|true|false|null|yes|no|on|off|~|-.*)$/i.test(s);
|
|
1882
|
+
return plain && s ? s : `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
1883
|
+
}
|
|
1884
|
+
function applyRichTitle(editor, effect, title, remove) {
|
|
1885
|
+
if (editor.isDestroyed || !title.trim()) {
|
|
1886
|
+
return false;
|
|
1887
|
+
}
|
|
1888
|
+
const { schema } = editor.state;
|
|
1889
|
+
const heading = schema.nodes.heading;
|
|
1890
|
+
if (!heading) {
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
const tr = closeHistory(editor.state.tr);
|
|
1894
|
+
if (remove && remove.to > remove.from) {
|
|
1895
|
+
tr.delete(remove.from, remove.to);
|
|
1896
|
+
}
|
|
1897
|
+
tr.setMeta(aiRewriteHighlightKey, { range: null });
|
|
1898
|
+
let h1 = null;
|
|
1899
|
+
if (effect === "h1") {
|
|
1900
|
+
tr.doc.descendants((node, pos) => {
|
|
1901
|
+
if (h1) {
|
|
1902
|
+
return false;
|
|
1903
|
+
}
|
|
1904
|
+
if (node.type === heading && node.attrs.level === 1) {
|
|
1905
|
+
h1 = { pos, size: node.content.size };
|
|
1906
|
+
return false;
|
|
1907
|
+
}
|
|
1908
|
+
return !node.isTextblock;
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
const found = h1;
|
|
1912
|
+
if (found) {
|
|
1913
|
+
tr.replaceWith(found.pos + 1, found.pos + 1 + found.size, schema.text(title));
|
|
1914
|
+
} else {
|
|
1915
|
+
tr.insert(0, heading.create({ level: 1 }, schema.text(title)));
|
|
1916
|
+
}
|
|
1917
|
+
editor.view.dispatch(tr.scrollIntoView());
|
|
1918
|
+
return true;
|
|
1919
|
+
}
|
|
1920
|
+
function sourceH1Line(markdown, from) {
|
|
1921
|
+
let pos = from;
|
|
1922
|
+
let fence = null;
|
|
1923
|
+
while (pos <= markdown.length) {
|
|
1924
|
+
const nl = markdown.indexOf("\n", pos);
|
|
1925
|
+
const end = nl < 0 ? markdown.length : nl;
|
|
1926
|
+
const line = markdown.slice(pos, end);
|
|
1927
|
+
const f = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
1928
|
+
if (fence) {
|
|
1929
|
+
if (f && f[1][0] === fence[0] && f[1].length >= fence.length && !line.trim().slice(f[1].length).trim()) {
|
|
1930
|
+
fence = null;
|
|
1931
|
+
}
|
|
1932
|
+
} else if (f) {
|
|
1933
|
+
fence = f[1];
|
|
1934
|
+
} else {
|
|
1935
|
+
const m = /^( {0,3}#[ \t]+)/.exec(line);
|
|
1936
|
+
if (m) {
|
|
1937
|
+
return { from: pos, to: end, prefix: m[1] };
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
if (nl < 0) {
|
|
1941
|
+
break;
|
|
1942
|
+
}
|
|
1943
|
+
pos = nl + 1;
|
|
1944
|
+
}
|
|
1945
|
+
return null;
|
|
1946
|
+
}
|
|
1947
|
+
function applySourceTitle(view, effect, title, frontmatter) {
|
|
1948
|
+
if (!title.trim()) {
|
|
1949
|
+
return false;
|
|
1950
|
+
}
|
|
1951
|
+
const full = view.state.doc.toString();
|
|
1952
|
+
let change;
|
|
1953
|
+
if (effect === "frontmatter" && frontmatter.yaml !== null && yamlHasTitle(frontmatter.yaml)) {
|
|
1954
|
+
const yamlFrom = full.indexOf("\n") + 1;
|
|
1955
|
+
change = { from: yamlFrom, to: yamlFrom + frontmatter.yaml.length, insert: setYamlTitle(frontmatter.yaml, title) };
|
|
1956
|
+
} else {
|
|
1957
|
+
const h1 = effect === "insertH1" ? null : sourceH1Line(full, frontmatter.bodyStart);
|
|
1958
|
+
change = h1 ? { from: h1.from + h1.prefix.length, to: h1.to, insert: title } : { from: frontmatter.bodyStart, to: frontmatter.bodyStart, insert: `# ${title}
|
|
1959
|
+
|
|
1960
|
+
` };
|
|
1961
|
+
}
|
|
1962
|
+
view.dispatch({
|
|
1963
|
+
changes: change,
|
|
1964
|
+
effects: setCmRewriteRange.of(null),
|
|
1965
|
+
annotations: isolateHistory.of("full"),
|
|
1966
|
+
userEvent: "input.ai"
|
|
1967
|
+
});
|
|
1968
|
+
return true;
|
|
1969
|
+
}
|
|
1970
|
+
var icon = (body, size = 14) => `<svg width="${size}" height="${size}" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
|
|
1971
|
+
var SPARK = icon('<path d="M8 2.2l1.25 3.3 3.3 1.25-3.3 1.25L8 11.3 6.75 8 3.45 6.75 6.75 5.5z"/><path d="M12.6 10.6l.5 1.3 1.3.5-1.3.5-.5 1.3-.5-1.3-1.3-.5 1.3-.5z"/>');
|
|
1972
|
+
var CLOSE = icon('<path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/>', 12);
|
|
1973
|
+
var LOCAL = icon('<path d="M2.5 7.5L8 3l5.5 4.5M4 6.5V13h8V6.5"/>', 11);
|
|
1974
|
+
var CLOUD = icon('<path d="M4.5 12.5h7a2.75 2.75 0 0 0 .3-5.48A4 4 0 0 0 4.1 7.6 2.5 2.5 0 0 0 4.5 12.5z"/>', 11);
|
|
1975
|
+
var ALERT = icon('<path d="M8 2.5l6 10.5H2z"/><path d="M8 6.5v3M8 11.4v.1"/>', 16);
|
|
1976
|
+
var WARN = icon('<path d="M8 2.5l6 10.5H2z"/><path d="M8 6.5v3M8 11.4v.1"/>', 12);
|
|
1977
|
+
var BACK = icon('<path d="M9.5 4L5.5 8l4 4"/>', 14);
|
|
1978
|
+
var CHECK = icon('<path d="M3.5 8.5l3 3 6-7"/>', 12);
|
|
1979
|
+
function el2(tag, className, text) {
|
|
1980
|
+
const node = document.createElement(tag);
|
|
1981
|
+
if (className) {
|
|
1982
|
+
node.className = className;
|
|
1983
|
+
}
|
|
1984
|
+
if (text !== void 0) {
|
|
1985
|
+
node.textContent = text;
|
|
1986
|
+
}
|
|
1987
|
+
return node;
|
|
1988
|
+
}
|
|
1989
|
+
function button2(className, label, onClick) {
|
|
1990
|
+
const b = el2("button", className, label);
|
|
1991
|
+
b.type = "button";
|
|
1992
|
+
b.addEventListener("mousedown", (e) => e.preventDefault());
|
|
1993
|
+
b.addEventListener("click", () => {
|
|
1994
|
+
if (b.getAttribute("aria-disabled") !== "true") {
|
|
1995
|
+
onClick();
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
1998
|
+
return b;
|
|
1999
|
+
}
|
|
2000
|
+
var GROUP_LABELS = {
|
|
2001
|
+
length: "Length",
|
|
2002
|
+
tone: "Tone",
|
|
2003
|
+
audience: "Audience",
|
|
2004
|
+
translate: "Translate",
|
|
2005
|
+
explain: "Explain",
|
|
2006
|
+
custom: "Custom"
|
|
2007
|
+
};
|
|
2008
|
+
function aiRewriteMenuGroups() {
|
|
2009
|
+
const groups = [];
|
|
2010
|
+
for (const v of AI_REWRITE_VARIANTS) {
|
|
2011
|
+
let g = groups.find((x) => x.group === v.group);
|
|
2012
|
+
if (!g) {
|
|
2013
|
+
g = { group: v.group, label: GROUP_LABELS[v.group], variants: [] };
|
|
2014
|
+
groups.push(g);
|
|
2015
|
+
}
|
|
2016
|
+
g.variants.push(v);
|
|
2017
|
+
}
|
|
2018
|
+
return groups;
|
|
2019
|
+
}
|
|
2020
|
+
var STRUCTURE_CHECKED = /* @__PURE__ */ new Set(["translate", "simpler", "formal", "casual", "expand"]);
|
|
2021
|
+
var STRUCTURE_NOUNS = {
|
|
2022
|
+
heading: ["heading", "headings"],
|
|
2023
|
+
fencedCodeBlock: ["code block", "code blocks"],
|
|
2024
|
+
inlineCode: ["inline code span", "inline code spans"],
|
|
2025
|
+
link: ["link", "links"],
|
|
2026
|
+
image: ["image", "images"],
|
|
2027
|
+
listItem: ["list item", "list items"],
|
|
2028
|
+
tableRow: ["table row", "table rows"],
|
|
2029
|
+
wikilink: ["wikilink", "wikilinks"],
|
|
2030
|
+
footnote: ["footnote reference", "footnote references"]
|
|
2031
|
+
};
|
|
2032
|
+
function structureWarningText(diff) {
|
|
2033
|
+
if (diff.ok) {
|
|
2034
|
+
return "";
|
|
2035
|
+
}
|
|
2036
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
2037
|
+
for (const m of diff.mismatches) {
|
|
2038
|
+
const seen = byKind.get(m.kind);
|
|
2039
|
+
if (seen) {
|
|
2040
|
+
seen.original += m.original;
|
|
2041
|
+
seen.result += m.result;
|
|
2042
|
+
} else {
|
|
2043
|
+
byKind.set(m.kind, { original: m.original, result: m.result, message: m.message });
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
const parts = [];
|
|
2047
|
+
for (const [kind, m] of byKind) {
|
|
2048
|
+
const [one, many] = STRUCTURE_NOUNS[kind];
|
|
2049
|
+
const d = m.original - m.result;
|
|
2050
|
+
const noun = Math.abs(d) === 1 ? one : many;
|
|
2051
|
+
if (d > 0) {
|
|
2052
|
+
parts.push(`${d} ${noun} missing in the result`);
|
|
2053
|
+
} else if (d < 0) {
|
|
2054
|
+
parts.push(`${-d} extra ${noun} in the result`);
|
|
2055
|
+
} else if (kind === "link" || kind === "image") {
|
|
2056
|
+
parts.push(`${many} point somewhere else in the result`);
|
|
2057
|
+
} else {
|
|
2058
|
+
parts.push(`${many} differ in the result`);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
return `Check: ${parts.join("; ")}`;
|
|
2062
|
+
}
|
|
2063
|
+
function documentMaxTokens(text) {
|
|
2064
|
+
return Math.max(1024, Math.min(16384, Math.ceil(text.length / CHARS_PER_TOKEN * 1.6) + 256));
|
|
2065
|
+
}
|
|
2066
|
+
var DOCUMENT_RESERVE_TOKENS = 768;
|
|
2067
|
+
function budgetTranslation(body, contextTokens) {
|
|
2068
|
+
let maxTokens = documentMaxTokens(body);
|
|
2069
|
+
let b = budgetDocument(body, { contextTokens, outputTokens: maxTokens, reserveTokens: DOCUMENT_RESERVE_TOKENS });
|
|
2070
|
+
if (b.truncated) {
|
|
2071
|
+
const room = Math.max(0, contextTokens - DOCUMENT_RESERVE_TOKENS);
|
|
2072
|
+
maxTokens = Math.max(1024, Math.min(16384, Math.floor(room * 1.6 / 2.6)));
|
|
2073
|
+
b = budgetDocument(body, { contextTokens, outputTokens: maxTokens, reserveTokens: DOCUMENT_RESERVE_TOKENS });
|
|
2074
|
+
if (b.truncated) {
|
|
2075
|
+
maxTokens = Math.min(maxTokens, documentMaxTokens(b.text));
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
return b.truncated ? { text: b.text, rest: body.slice(b.text.length), coverage: b.coverage, maxTokens } : { text: body, rest: "", coverage: 1, maxTokens };
|
|
2079
|
+
}
|
|
2080
|
+
function coverageNoteText(job, coverage) {
|
|
2081
|
+
if (coverage >= 1) {
|
|
2082
|
+
return "";
|
|
2083
|
+
}
|
|
2084
|
+
const pct = coveragePercent(coverage);
|
|
2085
|
+
if (job === "translate") {
|
|
2086
|
+
return `Translates the first ${pct}% of the document; the rest did not fit the model's context and stays as it is.`;
|
|
2087
|
+
}
|
|
2088
|
+
if (job === "titles") {
|
|
2089
|
+
return `Based on the first ${pct}% of the document and all of its headings; the rest did not fit the model's context.`;
|
|
2090
|
+
}
|
|
2091
|
+
return `Covers the first ${pct}% of the document; the rest did not fit the model's context.`;
|
|
2092
|
+
}
|
|
2093
|
+
var SUMMARY_KIND_LABELS = { tldr: "TL;DR", outline: "Outline" };
|
|
2094
|
+
var REPAIR_NOTE = "Fixing the answer format\u2026";
|
|
2095
|
+
function hasParam(def, params) {
|
|
2096
|
+
if (def?.param === "language") {
|
|
2097
|
+
return !!params.language?.trim();
|
|
2098
|
+
}
|
|
2099
|
+
if (def?.param === "audience") {
|
|
2100
|
+
return !!params.audience && !!getAiAudience(params.audience);
|
|
2101
|
+
}
|
|
2102
|
+
return true;
|
|
2103
|
+
}
|
|
2104
|
+
var plainLabel = (label) => label.replace(/…$/, "");
|
|
2105
|
+
var thinkingSegmentLabel = (label) => label === "Medium" ? "Med" : label;
|
|
2106
|
+
function thinkingInEffect(t) {
|
|
2107
|
+
return t ? t.sent ?? t.configured : null;
|
|
2108
|
+
}
|
|
2109
|
+
var THINKING_DEFAULT2 = "default";
|
|
2110
|
+
var thinkingSelected = (t) => (t && !t.auto ? t.sent : null) ?? THINKING_DEFAULT2;
|
|
2111
|
+
var capitalizeFirst = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
2112
|
+
function createAiRewritePopover(deps) {
|
|
2113
|
+
let target = null;
|
|
2114
|
+
let root = null;
|
|
2115
|
+
let phase = "choose";
|
|
2116
|
+
let variant = "shorter";
|
|
2117
|
+
let instruction = "";
|
|
2118
|
+
let params = {};
|
|
2119
|
+
let picker = null;
|
|
2120
|
+
let wholeDocument = false;
|
|
2121
|
+
let docOnly = false;
|
|
2122
|
+
let controller = null;
|
|
2123
|
+
let runSeq = 0;
|
|
2124
|
+
let raw = "";
|
|
2125
|
+
let finalText = "";
|
|
2126
|
+
let summaryCutOff = false;
|
|
2127
|
+
let route = null;
|
|
2128
|
+
let served = null;
|
|
2129
|
+
let stats = null;
|
|
2130
|
+
let viaFallback = false;
|
|
2131
|
+
let thinking = null;
|
|
2132
|
+
let error = null;
|
|
2133
|
+
let errorCopy = null;
|
|
2134
|
+
let unavailable = false;
|
|
2135
|
+
let note2 = "";
|
|
2136
|
+
let warning = null;
|
|
2137
|
+
let job = "rewrite";
|
|
2138
|
+
let summaryJob = null;
|
|
2139
|
+
let titleJob = null;
|
|
2140
|
+
let titles = [];
|
|
2141
|
+
let coverage = 1;
|
|
2142
|
+
let sentText = "";
|
|
2143
|
+
let untranslated = "";
|
|
2144
|
+
let repairing = false;
|
|
2145
|
+
let side = null;
|
|
2146
|
+
let frame = 0;
|
|
2147
|
+
let copiedTimer;
|
|
2148
|
+
let detach = [];
|
|
2149
|
+
let titleEl;
|
|
2150
|
+
let badgeEl;
|
|
2151
|
+
let statsEl;
|
|
2152
|
+
let thinkEl;
|
|
2153
|
+
let chooseEl;
|
|
2154
|
+
let menuEl;
|
|
2155
|
+
let formEl;
|
|
2156
|
+
let pickerEl;
|
|
2157
|
+
let inputEl;
|
|
2158
|
+
let bodyEl;
|
|
2159
|
+
let oldTextEl;
|
|
2160
|
+
let outEl;
|
|
2161
|
+
let caretEl;
|
|
2162
|
+
let newLabelEl;
|
|
2163
|
+
let newPaneEl;
|
|
2164
|
+
let errorEl;
|
|
2165
|
+
let warnEl;
|
|
2166
|
+
let footEl;
|
|
2167
|
+
let acceptBtn;
|
|
2168
|
+
let retryBtn;
|
|
2169
|
+
let copyBtn;
|
|
2170
|
+
let noteEl;
|
|
2171
|
+
let closeBtn;
|
|
2172
|
+
let liveEl;
|
|
2173
|
+
let kindEl;
|
|
2174
|
+
let coverageEl;
|
|
2175
|
+
let titlesBoxEl;
|
|
2176
|
+
let titlesEl;
|
|
2177
|
+
let titleHintEl;
|
|
2178
|
+
function abort() {
|
|
2179
|
+
runSeq += 1;
|
|
2180
|
+
controller?.abort();
|
|
2181
|
+
controller = null;
|
|
2182
|
+
}
|
|
2183
|
+
function close(opts = {}) {
|
|
2184
|
+
if (!target) {
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
abort();
|
|
2188
|
+
cancelAnimationFrame(frame);
|
|
2189
|
+
window.clearTimeout(copiedTimer);
|
|
2190
|
+
for (const off of detach) {
|
|
2191
|
+
off();
|
|
2192
|
+
}
|
|
2193
|
+
detach = [];
|
|
2194
|
+
root?.remove();
|
|
2195
|
+
root = null;
|
|
2196
|
+
syncBusy();
|
|
2197
|
+
const t = target;
|
|
2198
|
+
target = null;
|
|
2199
|
+
picker = null;
|
|
2200
|
+
t.release();
|
|
2201
|
+
if (opts.refocus !== false) {
|
|
2202
|
+
t.focus();
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
function outputMode() {
|
|
2206
|
+
if (job === "summary") {
|
|
2207
|
+
return "insert";
|
|
2208
|
+
}
|
|
2209
|
+
if (job === "titles") {
|
|
2210
|
+
return "choose";
|
|
2211
|
+
}
|
|
2212
|
+
if (getAiRewriteVariant(variant)?.output === "readOnly") {
|
|
2213
|
+
return "readOnly";
|
|
2214
|
+
}
|
|
2215
|
+
return target?.scope === "document" ? "replaceDocument" : "replace";
|
|
2216
|
+
}
|
|
2217
|
+
function build() {
|
|
2218
|
+
const box = el2("div", "mikedown-ai-rewrite");
|
|
2219
|
+
box.setAttribute("role", "dialog");
|
|
2220
|
+
box.setAttribute("aria-label", "Rewrite with AI");
|
|
2221
|
+
box.tabIndex = -1;
|
|
2222
|
+
const head = el2("div", "ai-rw-head");
|
|
2223
|
+
const spark = el2("span", "ai-rw-spark");
|
|
2224
|
+
spark.innerHTML = SPARK;
|
|
2225
|
+
titleEl = el2("span", "ai-rw-title", "Rewrite");
|
|
2226
|
+
thinkEl = el2("div", "ai-rw-think");
|
|
2227
|
+
thinkEl.setAttribute("role", "radiogroup");
|
|
2228
|
+
thinkEl.setAttribute("aria-label", "Thinking");
|
|
2229
|
+
thinkEl.hidden = true;
|
|
2230
|
+
thinkEl.addEventListener("keydown", onThinkingKey);
|
|
2231
|
+
badgeEl = el2("span", "ai-rw-badge");
|
|
2232
|
+
badgeEl.hidden = true;
|
|
2233
|
+
closeBtn = button2("ai-rw-icon-btn", "", () => close());
|
|
2234
|
+
closeBtn.innerHTML = CLOSE;
|
|
2235
|
+
kindEl = el2("div", "ai-rw-kind");
|
|
2236
|
+
kindEl.setAttribute("role", "group");
|
|
2237
|
+
kindEl.setAttribute("aria-label", "Summary kind");
|
|
2238
|
+
kindEl.hidden = true;
|
|
2239
|
+
for (const k of ["tldr", "outline"]) {
|
|
2240
|
+
const seg = button2("ai-rw-kind-seg", SUMMARY_KIND_LABELS[k], () => pickKind(k));
|
|
2241
|
+
seg.dataset.kind = k;
|
|
2242
|
+
seg.title = k === "tldr" ? "A short TL;DR of the document (runs again)" : "An outline of the document (runs again)";
|
|
2243
|
+
kindEl.append(seg);
|
|
2244
|
+
}
|
|
2245
|
+
head.append(spark, titleEl, kindEl, thinkEl, closeBtn);
|
|
2246
|
+
chooseEl = el2("div", "ai-rw-choose");
|
|
2247
|
+
menuEl = el2("div", "ai-rw-menu");
|
|
2248
|
+
menuEl.setAttribute("role", "menu");
|
|
2249
|
+
menuEl.setAttribute("aria-label", "Rewrite options");
|
|
2250
|
+
for (const g of aiRewriteMenuGroups()) {
|
|
2251
|
+
if (g.group === "custom") {
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
2254
|
+
const section = el2("div", "ai-rw-group");
|
|
2255
|
+
section.dataset.group = g.group;
|
|
2256
|
+
section.setAttribute("role", "group");
|
|
2257
|
+
section.setAttribute("aria-label", g.label);
|
|
2258
|
+
const heading = el2("div", "ai-rw-group-label", g.label);
|
|
2259
|
+
heading.setAttribute("aria-hidden", "true");
|
|
2260
|
+
const chips = el2("div", "ai-rw-chips");
|
|
2261
|
+
for (const v of g.variants) {
|
|
2262
|
+
const chip = button2("ai-rw-chip", v.label, () => choose(v));
|
|
2263
|
+
chip.setAttribute("role", "menuitem");
|
|
2264
|
+
if (v.title) {
|
|
2265
|
+
chip.title = v.title;
|
|
2266
|
+
}
|
|
2267
|
+
chip.dataset.variant = v.id;
|
|
2268
|
+
if (v.param) {
|
|
2269
|
+
chip.setAttribute("aria-haspopup", "menu");
|
|
2270
|
+
}
|
|
2271
|
+
chips.append(chip);
|
|
2272
|
+
}
|
|
2273
|
+
section.append(heading, chips);
|
|
2274
|
+
menuEl.append(section);
|
|
2275
|
+
}
|
|
2276
|
+
formEl = el2("form", "ai-rw-form");
|
|
2277
|
+
inputEl = el2("input", "ai-rw-input");
|
|
2278
|
+
inputEl.type = "text";
|
|
2279
|
+
inputEl.placeholder = 'Describe a change, e.g. "make it friendlier"';
|
|
2280
|
+
inputEl.setAttribute("aria-label", "Custom rewrite instruction");
|
|
2281
|
+
inputEl.maxLength = 1e3;
|
|
2282
|
+
const go = button2("ai-rw-btn ai-rw-btn--primary", "Rewrite", () => submitCustom());
|
|
2283
|
+
formEl.append(inputEl, go);
|
|
2284
|
+
formEl.addEventListener("submit", (e) => {
|
|
2285
|
+
e.preventDefault();
|
|
2286
|
+
submitCustom();
|
|
2287
|
+
});
|
|
2288
|
+
pickerEl = el2("div", "ai-rw-picker");
|
|
2289
|
+
chooseEl.append(menuEl, formEl, pickerEl);
|
|
2290
|
+
bodyEl = el2("div", "ai-rw-body");
|
|
2291
|
+
const oldPane = el2("div", "ai-rw-pane ai-rw-pane--old");
|
|
2292
|
+
oldTextEl = el2("div", "ai-rw-text");
|
|
2293
|
+
oldPane.append(el2("div", "ai-rw-label", "Original"), oldTextEl);
|
|
2294
|
+
newPaneEl = el2("div", "ai-rw-pane ai-rw-pane--new");
|
|
2295
|
+
const newText = el2("div", "ai-rw-text");
|
|
2296
|
+
outEl = el2("div", "ai-rw-out");
|
|
2297
|
+
caretEl = el2("span", "ai-rw-caret");
|
|
2298
|
+
newText.append(outEl);
|
|
2299
|
+
newLabelEl = el2("div", "ai-rw-label", "Rewrite");
|
|
2300
|
+
newPaneEl.append(newLabelEl, newText);
|
|
2301
|
+
bodyEl.append(oldPane, newPaneEl);
|
|
2302
|
+
titlesBoxEl = el2("div", "ai-rw-titles-box");
|
|
2303
|
+
titlesEl = el2("div", "ai-rw-titles");
|
|
2304
|
+
titlesEl.setAttribute("role", "menu");
|
|
2305
|
+
titlesEl.setAttribute("aria-label", "Suggested titles");
|
|
2306
|
+
titleHintEl = el2("div", "ai-rw-titles-hint");
|
|
2307
|
+
titlesBoxEl.append(titlesEl, titleHintEl);
|
|
2308
|
+
errorEl = el2("div", "ai-rw-error");
|
|
2309
|
+
errorEl.setAttribute("role", "alert");
|
|
2310
|
+
coverageEl = el2("div", "ai-rw-coverage");
|
|
2311
|
+
warnEl = el2("div", "ai-rw-warn");
|
|
2312
|
+
footEl = el2("div", "ai-rw-foot");
|
|
2313
|
+
acceptBtn = button2("ai-rw-btn ai-rw-btn--primary", "Accept", () => accept());
|
|
2314
|
+
retryBtn = button2("ai-rw-btn", "Retry", () => rerun());
|
|
2315
|
+
copyBtn = button2("ai-rw-btn", "Copy", () => copy());
|
|
2316
|
+
noteEl = el2("span", "ai-rw-note");
|
|
2317
|
+
statsEl = el2("span", "ai-rw-stats");
|
|
2318
|
+
statsEl.hidden = true;
|
|
2319
|
+
footEl.append(acceptBtn, retryBtn, copyBtn, noteEl, statsEl, badgeEl);
|
|
2320
|
+
liveEl = el2("div", "ai-rw-sr");
|
|
2321
|
+
liveEl.setAttribute("aria-live", "polite");
|
|
2322
|
+
box.append(head, chooseEl, bodyEl, titlesBoxEl, errorEl, coverageEl, warnEl, footEl, liveEl);
|
|
2323
|
+
return box;
|
|
2324
|
+
}
|
|
2325
|
+
function submitCustom() {
|
|
2326
|
+
const text = inputEl.value.trim();
|
|
2327
|
+
if (!text) {
|
|
2328
|
+
inputEl.focus();
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
start("custom", text);
|
|
2332
|
+
}
|
|
2333
|
+
function choose(def) {
|
|
2334
|
+
if (def.param) {
|
|
2335
|
+
openPicker(def.param === "language" ? "language" : "audience");
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
start(def.id);
|
|
2339
|
+
}
|
|
2340
|
+
function pickerOption(label, checked, onPick) {
|
|
2341
|
+
const b = button2("ai-rw-option", "", onPick);
|
|
2342
|
+
b.setAttribute("role", "menuitemradio");
|
|
2343
|
+
b.setAttribute("aria-checked", checked ? "true" : "false");
|
|
2344
|
+
const mark = el2("span", "ai-rw-option-check");
|
|
2345
|
+
mark.innerHTML = CHECK;
|
|
2346
|
+
b.append(mark, el2("span", "ai-rw-option-label", label));
|
|
2347
|
+
return b;
|
|
2348
|
+
}
|
|
2349
|
+
function pickerSeparator() {
|
|
2350
|
+
const sep = el2("div", "ai-rw-option-sep");
|
|
2351
|
+
sep.setAttribute("role", "separator");
|
|
2352
|
+
return sep;
|
|
2353
|
+
}
|
|
2354
|
+
function renderPicker() {
|
|
2355
|
+
if (!picker) {
|
|
2356
|
+
pickerEl.replaceChildren();
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
const head = el2("div", "ai-rw-picker-head");
|
|
2360
|
+
const back = button2("ai-rw-icon-btn ai-rw-back", "", () => closePicker());
|
|
2361
|
+
back.innerHTML = BACK;
|
|
2362
|
+
back.setAttribute("aria-label", "Back (Esc)");
|
|
2363
|
+
back.title = "Back (Esc)";
|
|
2364
|
+
back.hidden = docOnly;
|
|
2365
|
+
head.append(back, el2("span", "ai-rw-picker-title", picker === "language" ? "Translate to" : "Reading level"));
|
|
2366
|
+
const list = el2("div", "ai-rw-picker-list");
|
|
2367
|
+
list.setAttribute("role", "menu");
|
|
2368
|
+
list.setAttribute("aria-label", picker === "language" ? "Language" : "Audience");
|
|
2369
|
+
const parts = [head, list];
|
|
2370
|
+
if (picker === "language") {
|
|
2371
|
+
const { recents, languages } = translateLanguageChoices();
|
|
2372
|
+
recents.forEach((lang, i) => list.append(pickerOption(lang, i === 0, () => pickLanguage(lang))));
|
|
2373
|
+
if (recents.length) {
|
|
2374
|
+
list.append(pickerSeparator());
|
|
2375
|
+
}
|
|
2376
|
+
for (const lang of languages) {
|
|
2377
|
+
list.append(pickerOption(lang, false, () => pickLanguage(lang)));
|
|
2378
|
+
}
|
|
2379
|
+
list.append(pickerSeparator());
|
|
2380
|
+
const otherForm = el2("form", "ai-rw-form ai-rw-other");
|
|
2381
|
+
otherForm.hidden = true;
|
|
2382
|
+
const otherInput = el2("input", "ai-rw-input");
|
|
2383
|
+
otherInput.type = "text";
|
|
2384
|
+
otherInput.placeholder = "Language, e.g. Swahili";
|
|
2385
|
+
otherInput.setAttribute("aria-label", "Other language");
|
|
2386
|
+
otherInput.maxLength = 60;
|
|
2387
|
+
otherForm.append(otherInput, button2("ai-rw-btn ai-rw-btn--primary", "Translate", () => pickLanguage(otherInput.value)));
|
|
2388
|
+
otherForm.addEventListener("submit", (e) => {
|
|
2389
|
+
e.preventDefault();
|
|
2390
|
+
pickLanguage(otherInput.value);
|
|
2391
|
+
});
|
|
2392
|
+
const other = button2("ai-rw-option ai-rw-option--other", "", () => {
|
|
2393
|
+
otherForm.hidden = false;
|
|
2394
|
+
other.setAttribute("aria-expanded", "true");
|
|
2395
|
+
otherInput.focus();
|
|
2396
|
+
position2();
|
|
2397
|
+
});
|
|
2398
|
+
other.setAttribute("role", "menuitem");
|
|
2399
|
+
other.setAttribute("aria-expanded", "false");
|
|
2400
|
+
other.append(el2("span", "ai-rw-option-check"), el2("span", "ai-rw-option-label", "Other\u2026"));
|
|
2401
|
+
list.append(other);
|
|
2402
|
+
parts.push(otherForm);
|
|
2403
|
+
if (docOnly || deps.documentTarget) {
|
|
2404
|
+
const toggle = el2("label", "ai-rw-toggle");
|
|
2405
|
+
const box = el2("input");
|
|
2406
|
+
box.type = "checkbox";
|
|
2407
|
+
box.checked = wholeDocument;
|
|
2408
|
+
box.disabled = docOnly;
|
|
2409
|
+
box.addEventListener("change", () => {
|
|
2410
|
+
wholeDocument = box.checked;
|
|
2411
|
+
});
|
|
2412
|
+
toggle.append(box, el2("span", "", "Whole document"));
|
|
2413
|
+
toggle.title = docOnly ? "Nothing is selected, so the whole document is translated." : "Translate the whole document instead of the selection (frontmatter is kept).";
|
|
2414
|
+
const foot = el2("div", "ai-rw-picker-foot");
|
|
2415
|
+
foot.append(toggle);
|
|
2416
|
+
parts.push(foot);
|
|
2417
|
+
}
|
|
2418
|
+
} else {
|
|
2419
|
+
const remembered = loadAudience();
|
|
2420
|
+
for (const a of AI_AUDIENCES) {
|
|
2421
|
+
list.append(pickerOption(a.label, a.id === remembered, () => pickAudience(a.id)));
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
pickerEl.replaceChildren(...parts);
|
|
2425
|
+
}
|
|
2426
|
+
function openPicker(kind) {
|
|
2427
|
+
picker = kind;
|
|
2428
|
+
variant = kind === "language" ? "translate" : "readingLevel";
|
|
2429
|
+
phase = "choose";
|
|
2430
|
+
renderPicker();
|
|
2431
|
+
render();
|
|
2432
|
+
position2();
|
|
2433
|
+
const items = [...pickerEl.querySelectorAll(".ai-rw-picker-list > button")];
|
|
2434
|
+
(items.find((b) => b.getAttribute("aria-checked") === "true") ?? items[0])?.focus({ preventScroll: true });
|
|
2435
|
+
}
|
|
2436
|
+
function closePicker() {
|
|
2437
|
+
if (docOnly) {
|
|
2438
|
+
close();
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
const was = variant;
|
|
2442
|
+
picker = null;
|
|
2443
|
+
renderPicker();
|
|
2444
|
+
render();
|
|
2445
|
+
position2();
|
|
2446
|
+
menuEl.querySelector(`.ai-rw-chip[data-variant="${was}"]`)?.focus({ preventScroll: true });
|
|
2447
|
+
}
|
|
2448
|
+
function pickLanguage(language) {
|
|
2449
|
+
const lang = language.replace(/\s+/g, " ").trim();
|
|
2450
|
+
if (!lang) {
|
|
2451
|
+
pickerEl.querySelector(".ai-rw-other .ai-rw-input")?.focus();
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
params = { language: lang };
|
|
2455
|
+
if (wholeDocument && !useDocumentTarget()) {
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
start("translate");
|
|
2459
|
+
}
|
|
2460
|
+
function pickAudience(audience) {
|
|
2461
|
+
params = { audience };
|
|
2462
|
+
start("readingLevel");
|
|
2463
|
+
}
|
|
2464
|
+
function useDocumentTarget() {
|
|
2465
|
+
if (!target) {
|
|
2466
|
+
return false;
|
|
2467
|
+
}
|
|
2468
|
+
if (target.scope === "document") {
|
|
2469
|
+
return true;
|
|
2470
|
+
}
|
|
2471
|
+
const doc = deps.documentTarget?.() ?? null;
|
|
2472
|
+
if (!doc) {
|
|
2473
|
+
return false;
|
|
2474
|
+
}
|
|
2475
|
+
target.release();
|
|
2476
|
+
target = doc;
|
|
2477
|
+
side = null;
|
|
2478
|
+
return true;
|
|
2479
|
+
}
|
|
2480
|
+
function headTitle() {
|
|
2481
|
+
if (job === "summary") {
|
|
2482
|
+
return summaryJob?.kind === "outline" ? "Outline" : "Summary";
|
|
2483
|
+
}
|
|
2484
|
+
if (job === "titles") {
|
|
2485
|
+
return "Suggest titles";
|
|
2486
|
+
}
|
|
2487
|
+
if (unavailable) {
|
|
2488
|
+
return "Rewrite";
|
|
2489
|
+
}
|
|
2490
|
+
if (phase === "choose") {
|
|
2491
|
+
return picker === "language" ? "Translate" : picker === "audience" ? "Reading level" : "Rewrite";
|
|
2492
|
+
}
|
|
2493
|
+
if (variant === "explain") {
|
|
2494
|
+
return "Explain";
|
|
2495
|
+
}
|
|
2496
|
+
if (variant === "custom") {
|
|
2497
|
+
return "Rewrite \xB7 Custom";
|
|
2498
|
+
}
|
|
2499
|
+
if (variant === "translate") {
|
|
2500
|
+
return `Translate \xB7 ${params.language ?? ""}${target?.scope === "document" ? " \xB7 Whole document" : ""}`;
|
|
2501
|
+
}
|
|
2502
|
+
if (variant === "readingLevel") {
|
|
2503
|
+
return `Reading level \xB7 ${(params.audience && getAiAudience(params.audience)?.label) ?? ""}`;
|
|
2504
|
+
}
|
|
2505
|
+
return `Rewrite \xB7 ${plainLabel(getAiRewriteVariant(variant)?.label ?? "")}`;
|
|
2506
|
+
}
|
|
2507
|
+
function renderThinking() {
|
|
2508
|
+
const choices = thinking && phase !== "choose" ? thinkingChoices(thinking.kind) : [];
|
|
2509
|
+
thinkEl.hidden = choices.length === 0;
|
|
2510
|
+
if (choices.length === 0) {
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
const current = thinkingSelected(thinking);
|
|
2514
|
+
const focused = thinkEl.contains(document.activeElement) ? document.activeElement.dataset.level : void 0;
|
|
2515
|
+
const label = el2("span", "ai-rw-think-label", "Thinking");
|
|
2516
|
+
label.setAttribute("aria-hidden", "true");
|
|
2517
|
+
const options = [
|
|
2518
|
+
{ level: THINKING_DEFAULT2, label: "Default", title: "Thinking: picked for the task" },
|
|
2519
|
+
...choices.map((c) => ({ level: c.level, label: thinkingSegmentLabel(c.label), title: `Thinking: ${c.label}` }))
|
|
2520
|
+
];
|
|
2521
|
+
const stop = focused && options.some((o) => o.level === focused) ? focused : current;
|
|
2522
|
+
const segments = options.map((o) => {
|
|
2523
|
+
const seg = button2("ai-rw-think-seg", o.label, () => pickThinking(o.level === THINKING_DEFAULT2 ? null : o.level));
|
|
2524
|
+
seg.dataset.level = o.level;
|
|
2525
|
+
seg.setAttribute("role", "radio");
|
|
2526
|
+
seg.setAttribute("aria-checked", o.level === current ? "true" : "false");
|
|
2527
|
+
seg.setAttribute("aria-label", o.level === THINKING_DEFAULT2 ? "Default" : choices.find((c) => c.level === o.level)?.label ?? o.label);
|
|
2528
|
+
seg.tabIndex = o.level === stop ? 0 : -1;
|
|
2529
|
+
seg.title = `${o.title} (runs again)`;
|
|
2530
|
+
return seg;
|
|
2531
|
+
});
|
|
2532
|
+
const effective = thinkingInEffect(thinking);
|
|
2533
|
+
thinkEl.title = thinking?.sent && thinking.auto ? `Thinking: Default, ${choices.find((c) => c.level === thinking?.sent)?.label ?? thinking.sent} for this task` : thinking?.sent ? `Thinking: ${choices.find((c) => c.level === thinking?.sent)?.label ?? thinking.sent}` : effective ? `Thinking: the model's setting (${choices.find((c) => c.level === effective)?.label ?? effective})` : "Thinking: the model's default";
|
|
2534
|
+
thinkEl.replaceChildren(label, ...segments);
|
|
2535
|
+
if (focused) {
|
|
2536
|
+
segments.find((b) => b.dataset.level === focused)?.focus({ preventScroll: true });
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
function onThinkingKey(e) {
|
|
2540
|
+
if (!/^(Arrow(Left|Right|Up|Down)|Home|End)$/.test(e.key)) {
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
const segs = [...thinkEl.querySelectorAll(".ai-rw-think-seg")];
|
|
2544
|
+
const at = segs.indexOf(document.activeElement);
|
|
2545
|
+
if (at < 0) {
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
e.preventDefault();
|
|
2549
|
+
e.stopPropagation();
|
|
2550
|
+
const forward = e.key === "ArrowRight" || e.key === "ArrowDown";
|
|
2551
|
+
const next = e.key === "Home" ? 0 : e.key === "End" ? segs.length - 1 : (at + (forward ? 1 : -1) + segs.length) % segs.length;
|
|
2552
|
+
for (const [i, b] of segs.entries()) {
|
|
2553
|
+
b.tabIndex = i === next ? 0 : -1;
|
|
2554
|
+
}
|
|
2555
|
+
segs[next].focus({ preventScroll: true });
|
|
2556
|
+
}
|
|
2557
|
+
function renderBadge() {
|
|
2558
|
+
renderThinking();
|
|
2559
|
+
const b = deps.badge(route, served, served ? viaFallback : route?.viaFallback ?? false, jobSends());
|
|
2560
|
+
badgeEl.hidden = !b;
|
|
2561
|
+
if (!b) {
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
badgeEl.dataset.where = b.where ?? "";
|
|
2565
|
+
badgeEl.classList.toggle("is-pending", !served);
|
|
2566
|
+
const level = served ? thinkingInEffect(thinking) : null;
|
|
2567
|
+
const levelLabel = level && thinking ? thinkingChoices(thinking.kind).find((c) => c.level === level)?.label : void 0;
|
|
2568
|
+
let title = b.title;
|
|
2569
|
+
if (b.where === "cloud") {
|
|
2570
|
+
const j = jobSends();
|
|
2571
|
+
const what = whatIsSent(j.feature, j.sends);
|
|
2572
|
+
if (!title.includes(what)) {
|
|
2573
|
+
title = `${title} ${served ? "Sent" : "Sends"} ${what}.`;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
badgeEl.title = levelLabel ? `${title} Thinking: ${levelLabel.toLowerCase()}.` : title;
|
|
2577
|
+
const glyph = el2("span", "ai-rw-badge-glyph");
|
|
2578
|
+
glyph.innerHTML = b.where === "cloud" ? CLOUD : LOCAL;
|
|
2579
|
+
const text = el2("span", "ai-rw-badge-text");
|
|
2580
|
+
const cut = capitalizeFirst(b.text).split(" \xB7 ");
|
|
2581
|
+
if (cut.length >= 3) {
|
|
2582
|
+
text.append(el2("span", "ai-rw-badge-engine", `${cut.slice(0, 2).join(" \xB7 ")} \xB7 `), el2("span", "ai-rw-badge-model", cut.slice(2).join(" \xB7 ")));
|
|
2583
|
+
} else {
|
|
2584
|
+
text.append(el2("span", "ai-rw-badge-engine", capitalizeFirst(b.text)));
|
|
2585
|
+
}
|
|
2586
|
+
const parts = [glyph, text];
|
|
2587
|
+
if (b.fallback) {
|
|
2588
|
+
parts.push(el2("span", "ai-rw-badge-fallback", "(fallback)"));
|
|
2589
|
+
}
|
|
2590
|
+
badgeEl.replaceChildren(...parts);
|
|
2591
|
+
}
|
|
2592
|
+
function placeCaret() {
|
|
2593
|
+
let host = outEl;
|
|
2594
|
+
for (let last = host.lastElementChild; last && /^(UL|OL|LI|BLOCKQUOTE|P|DIV)$/.test(last.tagName); last = host.lastElementChild) {
|
|
2595
|
+
host = last;
|
|
2596
|
+
}
|
|
2597
|
+
host.append(caretEl);
|
|
2598
|
+
}
|
|
2599
|
+
function renderOut() {
|
|
2600
|
+
const shown = job === "titles" ? "" : job === "summary" ? (phase === "done" && summaryJob ? summaryPreviewText(raw, summaryJob.kind, summaryCutOff) : summaryVisibleText(raw)).trim() : phase === "done" ? finalText.trim() : rewriteVisibleText(raw).trimStart();
|
|
2601
|
+
renderPreviewMarkdown(outEl, shown);
|
|
2602
|
+
placeCaret();
|
|
2603
|
+
newPaneEl.classList.toggle("is-waiting", phase === "running" && !shown);
|
|
2604
|
+
if (phase === "running" && !shown) {
|
|
2605
|
+
const name = deps.badge(route, null, false)?.text.split(" \xB7 ")[1];
|
|
2606
|
+
outEl.replaceChildren();
|
|
2607
|
+
placeCaret();
|
|
2608
|
+
outEl.dataset.placeholder = repairing ? REPAIR_NOTE : job === "titles" && raw ? "Suggesting titles\u2026" : name ? `Waiting for ${name}\u2026` : "Waiting for the engine\u2026";
|
|
2609
|
+
} else {
|
|
2610
|
+
delete outEl.dataset.placeholder;
|
|
2611
|
+
}
|
|
2612
|
+
const scroller = outEl.parentElement;
|
|
2613
|
+
if (scroller && phase === "running" && scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight < 40) {
|
|
2614
|
+
scroller.scrollTop = scroller.scrollHeight;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
function scheduleOut() {
|
|
2618
|
+
if (frame) {
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
frame = requestAnimationFrame(() => {
|
|
2622
|
+
frame = 0;
|
|
2623
|
+
if (root) {
|
|
2624
|
+
renderOut();
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
let busyReported = false;
|
|
2629
|
+
function syncBusy() {
|
|
2630
|
+
const running = !!root && phase === "running";
|
|
2631
|
+
if (running === busyReported) {
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
busyReported = running;
|
|
2635
|
+
deps.onBusy?.(running);
|
|
2636
|
+
}
|
|
2637
|
+
function render() {
|
|
2638
|
+
syncBusy();
|
|
2639
|
+
if (!root || !target) {
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
const mode = outputMode();
|
|
2643
|
+
root.dataset.phase = phase;
|
|
2644
|
+
root.dataset.output = mode;
|
|
2645
|
+
root.dataset.layout = mode === "replace" && target.multiline ? "split" : "single";
|
|
2646
|
+
root.setAttribute("aria-busy", phase === "running" ? "true" : "false");
|
|
2647
|
+
root.setAttribute("aria-label", job === "summary" ? "Summarize with AI" : job === "titles" ? "Suggest titles with AI" : variant === "explain" && phase !== "choose" ? "Explain with AI" : "Rewrite with AI");
|
|
2648
|
+
titleEl.textContent = headTitle();
|
|
2649
|
+
if (variant === "custom" && instruction && phase !== "choose") {
|
|
2650
|
+
titleEl.title = instruction;
|
|
2651
|
+
} else {
|
|
2652
|
+
titleEl.title = titleEl.textContent;
|
|
2653
|
+
}
|
|
2654
|
+
chooseEl.hidden = phase !== "choose";
|
|
2655
|
+
menuEl.hidden = !!picker;
|
|
2656
|
+
formEl.hidden = !!picker;
|
|
2657
|
+
pickerEl.hidden = !picker;
|
|
2658
|
+
bodyEl.hidden = phase === "choose" || phase === "error" || mode === "choose" && phase === "done";
|
|
2659
|
+
titlesBoxEl.hidden = !(mode === "choose" && phase === "done");
|
|
2660
|
+
kindEl.hidden = job !== "summary";
|
|
2661
|
+
for (const seg of kindEl.querySelectorAll(".ai-rw-kind-seg")) {
|
|
2662
|
+
seg.setAttribute("aria-pressed", seg.dataset.kind === summaryJob?.kind ? "true" : "false");
|
|
2663
|
+
}
|
|
2664
|
+
errorEl.hidden = phase !== "error";
|
|
2665
|
+
footEl.hidden = phase === "choose" || phase === "error";
|
|
2666
|
+
newLabelEl.textContent = mode === "readOnly" ? "Explanation" : variant === "translate" ? "Translation" : "Rewrite";
|
|
2667
|
+
renderBadge();
|
|
2668
|
+
renderOut();
|
|
2669
|
+
const ready = phase === "done" && !!finalText.trim();
|
|
2670
|
+
acceptBtn.hidden = phase === "error" || mode === "choose";
|
|
2671
|
+
acceptBtn.setAttribute("aria-disabled", ready ? "false" : "true");
|
|
2672
|
+
acceptBtn.textContent = mode === "readOnly" ? "Insert below" : mode === "insert" ? "Insert" : mode === "replaceDocument" ? "Replace document" : "Accept";
|
|
2673
|
+
acceptBtn.title = mode === "readOnly" ? "Insert the explanation as a quote below the selection" : mode === "insert" ? "Insert at the cursor (Enter)" : mode === "replaceDocument" ? "Replace the document body; frontmatter is kept (Enter)" : "Replace the selection (Enter)";
|
|
2674
|
+
copyBtn.hidden = phase === "error" || mode === "choose";
|
|
2675
|
+
copyBtn.setAttribute("aria-disabled", (phase === "done" ? finalText : rewriteVisibleText(raw)).trim() ? "false" : "true");
|
|
2676
|
+
retryBtn.hidden = phase === "error";
|
|
2677
|
+
noteEl.textContent = repairing ? REPAIR_NOTE : note2;
|
|
2678
|
+
statsEl.hidden = !(phase === "done" && stats);
|
|
2679
|
+
if (phase === "done" && stats) {
|
|
2680
|
+
statsEl.textContent = aiResultStatsText(stats);
|
|
2681
|
+
statsEl.title = aiResultStatsTitle(stats);
|
|
2682
|
+
}
|
|
2683
|
+
const covered = job === "rewrite" ? target.scope === "document" ? "translate" : null : job;
|
|
2684
|
+
const covText = covered && (phase === "running" || phase === "done") ? coverageNoteText(covered, coverage) : "";
|
|
2685
|
+
coverageEl.hidden = !covText;
|
|
2686
|
+
coverageEl.textContent = covText;
|
|
2687
|
+
const closeLabel = mode === "readOnly" ? "Close (Esc)" : "Cancel (Esc)";
|
|
2688
|
+
closeBtn.setAttribute("aria-label", closeLabel);
|
|
2689
|
+
closeBtn.title = closeLabel;
|
|
2690
|
+
warnEl.hidden = !(phase === "done" && warning);
|
|
2691
|
+
if (phase === "done" && warning) {
|
|
2692
|
+
const glyph = el2("span", "ai-rw-warn-icon");
|
|
2693
|
+
glyph.innerHTML = WARN;
|
|
2694
|
+
warnEl.replaceChildren(glyph, el2("span", "ai-rw-warn-text", warning.text));
|
|
2695
|
+
warnEl.title = warning.title;
|
|
2696
|
+
}
|
|
2697
|
+
if (phase === "error" && error) {
|
|
2698
|
+
renderError(error);
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
function errorEngine(e) {
|
|
2702
|
+
return e.engineId ? deps.engineName?.(e.engineId) ?? null : null;
|
|
2703
|
+
}
|
|
2704
|
+
function copyFor(e) {
|
|
2705
|
+
if (unavailable) {
|
|
2706
|
+
return { title: "No engine can rewrite right now", message: e.message, action: "openSettings" };
|
|
2707
|
+
}
|
|
2708
|
+
const engine = errorEngine(e);
|
|
2709
|
+
const model = route && (!e.engineId || route.engineId === e.engineId) ? route.modelId : void 0;
|
|
2710
|
+
return errorCopy ?? aiErrorCopy(e.code, { ...engine ? { engine } : {}, ...model ? { model } : {} });
|
|
2711
|
+
}
|
|
2712
|
+
function renderError(e) {
|
|
2713
|
+
const copyText = copyFor(e);
|
|
2714
|
+
const iconEl = el2("span", "ai-rw-error-icon");
|
|
2715
|
+
iconEl.innerHTML = ALERT;
|
|
2716
|
+
const textBox = el2("div", "ai-rw-error-text");
|
|
2717
|
+
textBox.append(el2("div", "ai-rw-error-title", copyText.title));
|
|
2718
|
+
const detail = e.message && e.message !== copyText.message ? e.message : "";
|
|
2719
|
+
textBox.append(el2("div", "ai-rw-error-message", e.code === "unknown" && detail ? detail : copyText.message));
|
|
2720
|
+
if (detail && e.code === "modelLoadFailed") {
|
|
2721
|
+
const more = el2("details", "ai-rw-error-more");
|
|
2722
|
+
more.append(el2("summary", void 0, "Details"), el2("div", "ai-rw-error-detail", detail));
|
|
2723
|
+
textBox.append(more);
|
|
2724
|
+
} else if (detail && e.code !== "unknown") {
|
|
2725
|
+
textBox.append(el2("div", "ai-rw-error-detail", detail));
|
|
2726
|
+
}
|
|
2727
|
+
if (e.code === "contextOverflow" && target) {
|
|
2728
|
+
const what = target.scope === "selection" ? "The selection" : "The document";
|
|
2729
|
+
const size = job === "summary" ? summaryJob?.markdown.length ?? 0 : job === "titles" ? titleJob?.markdown.length ?? 0 : target.text.length;
|
|
2730
|
+
textBox.append(el2("div", "ai-rw-error-detail", `${what} is ${size.toLocaleString()} characters.`));
|
|
2731
|
+
}
|
|
2732
|
+
const actions = el2("div", "ai-rw-error-actions");
|
|
2733
|
+
const onEngine = (copyText.action === "openEngine" || copyText.action === "chooseModel") && !!e.engineId;
|
|
2734
|
+
const settingsLabel = copyText.action === "chooseModel" ? "Choose model" : onEngine ? `Open ${errorEngine(e) ?? "engine"}` : "Open AI settings";
|
|
2735
|
+
const primary = copyText.action === "openSettings" || copyText.action === "chooseModel" || onEngine;
|
|
2736
|
+
const settingsBtn = button2(`ai-rw-btn${primary ? " ai-rw-btn--primary" : ""}`, settingsLabel, () => {
|
|
2737
|
+
close({ refocus: false });
|
|
2738
|
+
if (onEngine) {
|
|
2739
|
+
deps.openSettings(e.engineId);
|
|
2740
|
+
} else {
|
|
2741
|
+
deps.openSettings();
|
|
2742
|
+
}
|
|
2743
|
+
});
|
|
2744
|
+
actions.append(settingsBtn);
|
|
2745
|
+
if (!unavailable && copyText.action !== "shortenSelection" && e.code !== "cloudBlocked") {
|
|
2746
|
+
actions.append(button2(`ai-rw-btn${copyText.action === "retry" ? " ai-rw-btn--primary" : ""}`, e.code === "badOutput" ? "Try again" : "Retry", () => rerun()));
|
|
2747
|
+
}
|
|
2748
|
+
actions.append(button2("ai-rw-btn", "Close", () => close()));
|
|
2749
|
+
errorEl.replaceChildren(iconEl, textBox, actions);
|
|
2750
|
+
}
|
|
2751
|
+
function position2() {
|
|
2752
|
+
if (!root || !target) {
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
if (!target.alive()) {
|
|
2756
|
+
close({ refocus: false });
|
|
2757
|
+
return;
|
|
2758
|
+
}
|
|
2759
|
+
const rect = target.rect();
|
|
2760
|
+
if (!rect) {
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2763
|
+
const vw = window.innerWidth;
|
|
2764
|
+
const vh = window.innerHeight;
|
|
2765
|
+
const gap = 8;
|
|
2766
|
+
const w = root.offsetWidth || 440;
|
|
2767
|
+
const h = root.offsetHeight || 200;
|
|
2768
|
+
if (!side) {
|
|
2769
|
+
const reserve = Math.min(vh * 0.55, 380);
|
|
2770
|
+
side = rect.bottom + gap + reserve <= vh - gap || rect.top - gap - reserve < gap ? "below" : "above";
|
|
2771
|
+
}
|
|
2772
|
+
let top = side === "below" ? rect.bottom + gap : rect.top - gap - h;
|
|
2773
|
+
top = Math.max(gap, Math.min(top, vh - h - gap));
|
|
2774
|
+
const left = Math.max(gap, Math.min(rect.left, vw - w - gap));
|
|
2775
|
+
root.style.top = `${Math.round(top)}px`;
|
|
2776
|
+
root.style.left = `${Math.round(left)}px`;
|
|
2777
|
+
}
|
|
2778
|
+
function moveFocus(e) {
|
|
2779
|
+
const inTitles = outputMode() === "choose" && phase === "done";
|
|
2780
|
+
if (!root || phase !== "choose" && !inTitles || !/^(Arrow(Up|Down|Left|Right)|Home|End)$/.test(e.key)) {
|
|
2781
|
+
return false;
|
|
2782
|
+
}
|
|
2783
|
+
const selector = inTitles ? ".ai-rw-titles > button" : picker ? ".ai-rw-picker-list > button" : ".ai-rw-menu .ai-rw-chip";
|
|
2784
|
+
const items = [...root.querySelectorAll(selector)];
|
|
2785
|
+
const i = items.indexOf(e.target);
|
|
2786
|
+
if (i < 0) {
|
|
2787
|
+
return false;
|
|
2788
|
+
}
|
|
2789
|
+
const forward = e.key === "ArrowDown" || e.key === "ArrowRight";
|
|
2790
|
+
const next = e.key === "Home" ? 0 : e.key === "End" ? items.length - 1 : (i + (forward ? 1 : -1) + items.length) % items.length;
|
|
2791
|
+
items[next].focus({ preventScroll: true });
|
|
2792
|
+
return true;
|
|
2793
|
+
}
|
|
2794
|
+
function mount(t) {
|
|
2795
|
+
close({ refocus: false });
|
|
2796
|
+
target = t;
|
|
2797
|
+
side = null;
|
|
2798
|
+
raw = "";
|
|
2799
|
+
finalText = "";
|
|
2800
|
+
summaryCutOff = false;
|
|
2801
|
+
route = null;
|
|
2802
|
+
served = null;
|
|
2803
|
+
stats = null;
|
|
2804
|
+
viaFallback = false;
|
|
2805
|
+
thinking = null;
|
|
2806
|
+
error = null;
|
|
2807
|
+
unavailable = false;
|
|
2808
|
+
note2 = "";
|
|
2809
|
+
warning = null;
|
|
2810
|
+
picker = null;
|
|
2811
|
+
params = {};
|
|
2812
|
+
variant = "shorter";
|
|
2813
|
+
instruction = "";
|
|
2814
|
+
docOnly = t.scope === "document";
|
|
2815
|
+
wholeDocument = docOnly;
|
|
2816
|
+
job = "rewrite";
|
|
2817
|
+
summaryJob = null;
|
|
2818
|
+
titleJob = null;
|
|
2819
|
+
titles = [];
|
|
2820
|
+
coverage = 1;
|
|
2821
|
+
sentText = "";
|
|
2822
|
+
untranslated = "";
|
|
2823
|
+
repairing = false;
|
|
2824
|
+
root = build();
|
|
2825
|
+
getPortal().appendChild(root);
|
|
2826
|
+
const onKey = (e) => {
|
|
2827
|
+
if (!root) {
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
if (e.key === "Escape") {
|
|
2831
|
+
e.preventDefault();
|
|
2832
|
+
e.stopPropagation();
|
|
2833
|
+
if (phase === "choose" && picker) {
|
|
2834
|
+
closePicker();
|
|
2835
|
+
} else {
|
|
2836
|
+
close();
|
|
2837
|
+
}
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
if (root.contains(e.target) && moveFocus(e)) {
|
|
2841
|
+
e.preventDefault();
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
const mode = outputMode();
|
|
2845
|
+
if (e.key === "Enter" && phase === "done" && mode !== "readOnly" && mode !== "choose" && !e.shiftKey && !e.altKey) {
|
|
2846
|
+
const t2 = e.target;
|
|
2847
|
+
const inField = !!t2 && (t2.tagName === "INPUT" || t2.tagName === "TEXTAREA");
|
|
2848
|
+
const onOtherButton = !!t2 && t2.tagName === "BUTTON" && t2 !== acceptBtn && root.contains(t2);
|
|
2849
|
+
if (!inField && !onOtherButton || e.metaKey || e.ctrlKey) {
|
|
2850
|
+
e.preventDefault();
|
|
2851
|
+
e.stopPropagation();
|
|
2852
|
+
accept();
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
};
|
|
2856
|
+
const onDown = (e) => {
|
|
2857
|
+
if (root && !root.contains(e.target)) {
|
|
2858
|
+
close({ refocus: false });
|
|
2859
|
+
}
|
|
2860
|
+
};
|
|
2861
|
+
const onMove = () => position2();
|
|
2862
|
+
document.addEventListener("keydown", onKey, true);
|
|
2863
|
+
document.addEventListener("mousedown", onDown, true);
|
|
2864
|
+
window.addEventListener("resize", onMove);
|
|
2865
|
+
document.addEventListener("scroll", onMove, true);
|
|
2866
|
+
detach.push(
|
|
2867
|
+
() => document.removeEventListener("keydown", onKey, true),
|
|
2868
|
+
() => document.removeEventListener("mousedown", onDown, true),
|
|
2869
|
+
() => window.removeEventListener("resize", onMove),
|
|
2870
|
+
() => document.removeEventListener("scroll", onMove, true)
|
|
2871
|
+
);
|
|
2872
|
+
}
|
|
2873
|
+
function pickThinking(level) {
|
|
2874
|
+
if (!thinking || phase === "choose") {
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
if ((level ?? THINKING_DEFAULT2) === thinkingSelected(thinking)) {
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
rememberThinkingLevel(level);
|
|
2881
|
+
thinking = { ...thinking, sent: level, auto: false };
|
|
2882
|
+
const label = level ? thinkingChoices(thinking.kind).find((c) => c.level === level)?.label ?? level : "Default";
|
|
2883
|
+
rerun();
|
|
2884
|
+
liveEl.textContent = `Thinking: ${label}`;
|
|
2885
|
+
}
|
|
2886
|
+
function rerun() {
|
|
2887
|
+
if (job === "summary") {
|
|
2888
|
+
startSummary();
|
|
2889
|
+
} else if (job === "titles") {
|
|
2890
|
+
startTitles();
|
|
2891
|
+
} else {
|
|
2892
|
+
start(variant, instruction);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
function contextTokensFor(feature) {
|
|
2896
|
+
return deps.routeInfo?.(feature).contextTokens ?? DEFAULT_CONTEXT_TOKENS;
|
|
2897
|
+
}
|
|
2898
|
+
function beginRun() {
|
|
2899
|
+
abort();
|
|
2900
|
+
const ac = new AbortController();
|
|
2901
|
+
controller = ac;
|
|
2902
|
+
phase = "running";
|
|
2903
|
+
raw = "";
|
|
2904
|
+
finalText = "";
|
|
2905
|
+
summaryCutOff = false;
|
|
2906
|
+
served = null;
|
|
2907
|
+
stats = null;
|
|
2908
|
+
error = null;
|
|
2909
|
+
errorCopy = null;
|
|
2910
|
+
note2 = "";
|
|
2911
|
+
warning = null;
|
|
2912
|
+
repairing = false;
|
|
2913
|
+
return { seq: runSeq, signal: ac.signal };
|
|
2914
|
+
}
|
|
2915
|
+
function runEvents(seq) {
|
|
2916
|
+
return {
|
|
2917
|
+
onRoute: (r, th) => {
|
|
2918
|
+
if (seq !== runSeq) {
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
route = r;
|
|
2922
|
+
thinking = th;
|
|
2923
|
+
raw = "";
|
|
2924
|
+
renderBadge();
|
|
2925
|
+
renderOut();
|
|
2926
|
+
},
|
|
2927
|
+
onToken: (_delta, text) => {
|
|
2928
|
+
if (seq !== runSeq) {
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
raw = text;
|
|
2932
|
+
scheduleOut();
|
|
2933
|
+
}
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
function jobSends() {
|
|
2937
|
+
if (job === "summary") {
|
|
2938
|
+
return { feature: "summary", sends: "document" };
|
|
2939
|
+
}
|
|
2940
|
+
if (job === "titles") {
|
|
2941
|
+
return { feature: "title", sends: "document" };
|
|
2942
|
+
}
|
|
2943
|
+
return { feature: "rewrite", sends: target?.scope === "document" ? "document" : "selection" };
|
|
2944
|
+
}
|
|
2945
|
+
function noteServed(outcome) {
|
|
2946
|
+
const by = { ...outcome.result.served, ...jobSends() };
|
|
2947
|
+
served = by;
|
|
2948
|
+
stats = aiResultStats(outcome.result);
|
|
2949
|
+
viaFallback = outcome.viaFallback;
|
|
2950
|
+
thinking = outcome.thinking;
|
|
2951
|
+
deps.onServed(by, outcome.viaFallback);
|
|
2952
|
+
}
|
|
2953
|
+
function finishNote(outcome) {
|
|
2954
|
+
const reason = outcome.result.finishReason;
|
|
2955
|
+
if (reason === "length") {
|
|
2956
|
+
return aiLengthLimitCopy({ thinking: aiRunThinkingOn(outcome.thinking), empty: false }).message;
|
|
2957
|
+
}
|
|
2958
|
+
return reason === "error" ? "The stream ended early." : "";
|
|
2959
|
+
}
|
|
2960
|
+
function showLengthError(outcome) {
|
|
2961
|
+
const copy2 = aiLengthLimitCopy({ thinking: aiRunThinkingOn(outcome.thinking), empty: true });
|
|
2962
|
+
errorCopy = copy2;
|
|
2963
|
+
showError(makeAiError("badOutput", copy2.message));
|
|
2964
|
+
}
|
|
2965
|
+
function showError(e) {
|
|
2966
|
+
phase = "error";
|
|
2967
|
+
error = e;
|
|
2968
|
+
repairing = false;
|
|
2969
|
+
render();
|
|
2970
|
+
position2();
|
|
2971
|
+
liveEl.textContent = `${copyFor(e).title}.`;
|
|
2972
|
+
errorEl.querySelector(".ai-rw-btn--primary, .ai-rw-btn")?.focus({ preventScroll: true });
|
|
2973
|
+
}
|
|
2974
|
+
function runFailed(seq) {
|
|
2975
|
+
return (err) => {
|
|
2976
|
+
if (seq !== runSeq || !root) {
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
controller = null;
|
|
2980
|
+
const e = toAiError(err);
|
|
2981
|
+
if (e.code === "cancelled") {
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
showError(e);
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2987
|
+
function pickKind(kind) {
|
|
2988
|
+
if (!summaryJob || summaryJob.kind === kind) {
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
summaryJob.kind = kind;
|
|
2992
|
+
startSummary();
|
|
2993
|
+
}
|
|
2994
|
+
function startSummary() {
|
|
2995
|
+
const t = target;
|
|
2996
|
+
const sj = summaryJob;
|
|
2997
|
+
if (!t || !root || !sj) {
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
const { seq, signal } = beginRun();
|
|
3001
|
+
const kind = sj.kind;
|
|
3002
|
+
const b = budgetDocument(sj.markdown, {
|
|
3003
|
+
contextTokens: contextTokensFor("summary"),
|
|
3004
|
+
outputTokens: summaryMaxTokens(kind, sj.markdown.length),
|
|
3005
|
+
reserveTokens: DOCUMENT_RESERVE_TOKENS
|
|
3006
|
+
});
|
|
3007
|
+
coverage = b.coverage;
|
|
3008
|
+
render();
|
|
3009
|
+
position2();
|
|
3010
|
+
root.focus({ preventScroll: true });
|
|
3011
|
+
deps.run({
|
|
3012
|
+
feature: "summary",
|
|
3013
|
+
messages: buildSummaryMessages({ document: b.text, kind, coverage: b.coverage }),
|
|
3014
|
+
maxTokens: summaryMaxTokens(kind, b.text.length),
|
|
3015
|
+
temperature: 0.3,
|
|
3016
|
+
thinking: thinkingLevelFor,
|
|
3017
|
+
job: "summary"
|
|
3018
|
+
}, runEvents(seq), signal).then((outcome) => {
|
|
3019
|
+
if (seq !== runSeq || !root) {
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
controller = null;
|
|
3023
|
+
noteServed(outcome);
|
|
3024
|
+
raw = outcome.result.text || raw;
|
|
3025
|
+
const cutOff = outcome.result.finishReason === "length";
|
|
3026
|
+
summaryCutOff = cutOff;
|
|
3027
|
+
finalText = finalizeSummary(raw, kind, coverage, cutOff);
|
|
3028
|
+
if (!finalText.trim()) {
|
|
3029
|
+
if (cutOff) {
|
|
3030
|
+
showLengthError(outcome);
|
|
3031
|
+
} else {
|
|
3032
|
+
showError(makeAiError("badOutput"));
|
|
3033
|
+
}
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
phase = "done";
|
|
3037
|
+
note2 = finishNote(outcome);
|
|
3038
|
+
render();
|
|
3039
|
+
position2();
|
|
3040
|
+
liveEl.textContent = `${kind === "outline" ? "Outline" : "Summary"} ready. Press Enter to insert or Escape to cancel.`;
|
|
3041
|
+
acceptBtn.focus({ preventScroll: true });
|
|
3042
|
+
}, runFailed(seq));
|
|
3043
|
+
}
|
|
3044
|
+
function startTitles() {
|
|
3045
|
+
const t = target;
|
|
3046
|
+
const tj = titleJob;
|
|
3047
|
+
if (!t || !root || !tj) {
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
const { seq, signal } = beginRun();
|
|
3051
|
+
titles = [];
|
|
3052
|
+
const info = deps.routeInfo?.("title");
|
|
3053
|
+
const outline = headingsOutline(tj.markdown);
|
|
3054
|
+
const b = budgetDocument(tj.markdown, {
|
|
3055
|
+
contextTokens: info?.contextTokens ?? DEFAULT_CONTEXT_TOKENS,
|
|
3056
|
+
outputTokens: titleMaxTokens(3),
|
|
3057
|
+
reserveTokens: DOCUMENT_RESERVE_TOKENS + Math.ceil(renderHeadingOutline(outline).length / CHARS_PER_TOKEN)
|
|
3058
|
+
});
|
|
3059
|
+
coverage = b.coverage;
|
|
3060
|
+
render();
|
|
3061
|
+
position2();
|
|
3062
|
+
root.focus({ preventScroll: true });
|
|
3063
|
+
const messages = buildTitleMessages({ document: b.text, outline, existingTitle: tj.existingTitle, count: 3 });
|
|
3064
|
+
const request = (msgs) => deps.run({
|
|
3065
|
+
feature: "title",
|
|
3066
|
+
messages: msgs,
|
|
3067
|
+
maxTokens: titleMaxTokens(3),
|
|
3068
|
+
temperature: 0.7,
|
|
3069
|
+
thinking: thinkingLevelFor,
|
|
3070
|
+
job: "title",
|
|
3071
|
+
...info?.json ? { format: "json" } : {}
|
|
3072
|
+
}, runEvents(seq), signal);
|
|
3073
|
+
request(messages).then(async (first) => {
|
|
3074
|
+
if (seq !== runSeq || !root) {
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
noteServed(first);
|
|
3078
|
+
const firstText = first.result.text || raw;
|
|
3079
|
+
if (!rewriteVisibleText(firstText).trim() && first.result.finishReason === "length") {
|
|
3080
|
+
controller = null;
|
|
3081
|
+
showLengthError(first);
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
let last = first;
|
|
3085
|
+
let parsed = parseTitles(firstText, tj.existingTitle);
|
|
3086
|
+
if (!parsed.ok) {
|
|
3087
|
+
repairing = true;
|
|
3088
|
+
render();
|
|
3089
|
+
const second = await request([...messages, ...jsonRepairMessages(firstText, parsed.error)]);
|
|
3090
|
+
if (seq !== runSeq || !root) {
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
noteServed(second);
|
|
3094
|
+
repairing = false;
|
|
3095
|
+
last = second;
|
|
3096
|
+
parsed = parseTitles(second.result.text || raw, tj.existingTitle);
|
|
3097
|
+
if (!parsed.ok) {
|
|
3098
|
+
if (second.result.finishReason === "length") {
|
|
3099
|
+
controller = null;
|
|
3100
|
+
showLengthError(second);
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
throw makeAiError("badOutput", parsed.error);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
controller = null;
|
|
3107
|
+
titles = parsed.titles.slice(0, 3);
|
|
3108
|
+
phase = "done";
|
|
3109
|
+
note2 = finishNote(last);
|
|
3110
|
+
renderTitles();
|
|
3111
|
+
render();
|
|
3112
|
+
position2();
|
|
3113
|
+
liveEl.textContent = `${titles.length} ${titles.length === 1 ? "title" : "titles"} ready. ${AI_TITLE_EFFECT_HINTS[tj.effect]}. Escape cancels.`;
|
|
3114
|
+
titlesEl.querySelector("button")?.focus({ preventScroll: true });
|
|
3115
|
+
}).catch(runFailed(seq));
|
|
3116
|
+
}
|
|
3117
|
+
function renderTitles() {
|
|
3118
|
+
titlesEl.replaceChildren(...titles.map((title) => {
|
|
3119
|
+
const b = button2("ai-rw-option ai-rw-title-option", "", () => pickTitle(title));
|
|
3120
|
+
b.setAttribute("role", "menuitem");
|
|
3121
|
+
b.append(el2("span", "ai-rw-option-label", title));
|
|
3122
|
+
return b;
|
|
3123
|
+
}));
|
|
3124
|
+
titleHintEl.textContent = titleJob ? AI_TITLE_EFFECT_HINTS[titleJob.effect] : "";
|
|
3125
|
+
}
|
|
3126
|
+
function pickTitle(title) {
|
|
3127
|
+
const t = target;
|
|
3128
|
+
const tj = titleJob;
|
|
3129
|
+
if (!t || !tj || phase !== "done") {
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
3132
|
+
const ok = tj.apply(title);
|
|
3133
|
+
close({ refocus: true });
|
|
3134
|
+
if (!ok) {
|
|
3135
|
+
t.focus();
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
function start(v, custom) {
|
|
3139
|
+
const t = target;
|
|
3140
|
+
if (!t || !root) {
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
const { seq, signal } = beginRun();
|
|
3144
|
+
variant = v;
|
|
3145
|
+
instruction = v === "custom" ? custom ?? instruction : "";
|
|
3146
|
+
picker = null;
|
|
3147
|
+
oldTextEl.textContent = t.scope === "document" ? "" : t.text.trim();
|
|
3148
|
+
const def = getAiRewriteVariant(v);
|
|
3149
|
+
if (def?.param === "language" && params.language) {
|
|
3150
|
+
rememberLanguage(params.language);
|
|
3151
|
+
}
|
|
3152
|
+
if (def?.param === "audience" && params.audience) {
|
|
3153
|
+
rememberAudience(params.audience);
|
|
3154
|
+
}
|
|
3155
|
+
const readOnly = def?.output === "readOnly";
|
|
3156
|
+
sentText = t.text;
|
|
3157
|
+
untranslated = "";
|
|
3158
|
+
coverage = 1;
|
|
3159
|
+
let maxTokens = readOnly ? explainMaxTokens() : rewriteMaxTokens(t.text, v);
|
|
3160
|
+
if (t.scope === "document" && !readOnly) {
|
|
3161
|
+
const b = budgetTranslation(t.text, contextTokensFor("rewrite"));
|
|
3162
|
+
sentText = b.text;
|
|
3163
|
+
untranslated = b.rest;
|
|
3164
|
+
coverage = b.coverage;
|
|
3165
|
+
maxTokens = b.maxTokens;
|
|
3166
|
+
}
|
|
3167
|
+
render();
|
|
3168
|
+
position2();
|
|
3169
|
+
root.focus({ preventScroll: true });
|
|
3170
|
+
const messages = readOnly ? buildExplainMessages({ selection: t.text, before: t.before, after: t.after, title: t.title }) : buildRewriteMessages({
|
|
3171
|
+
variant: v,
|
|
3172
|
+
instruction,
|
|
3173
|
+
selection: sentText,
|
|
3174
|
+
before: t.before,
|
|
3175
|
+
after: t.after,
|
|
3176
|
+
params: def?.param ? params : void 0,
|
|
3177
|
+
scope: t.scope === "document" ? "document" : "selection"
|
|
3178
|
+
});
|
|
3179
|
+
deps.run({
|
|
3180
|
+
feature: "rewrite",
|
|
3181
|
+
...t.scope === "document" ? { sends: "document" } : {},
|
|
3182
|
+
messages,
|
|
3183
|
+
maxTokens,
|
|
3184
|
+
temperature: v === "fixFlow" || v === "translate" ? 0.2 : 0.5,
|
|
3185
|
+
thinking: thinkingLevelFor,
|
|
3186
|
+
// Explain keeps the model's own thinking; every other variant is a short edit (Off).
|
|
3187
|
+
job: readOnly ? "explain" : v === "translate" ? "translate" : "rewrite"
|
|
3188
|
+
}, runEvents(seq), signal).then((outcome) => {
|
|
3189
|
+
if (seq !== runSeq || !root) {
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
controller = null;
|
|
3193
|
+
noteServed(outcome);
|
|
3194
|
+
finalText = finalizeRewriteText(outcome.result.text || raw, readOnly ? "" : sentText);
|
|
3195
|
+
if (!finalText.trim()) {
|
|
3196
|
+
phase = "error";
|
|
3197
|
+
if (outcome.result.finishReason === "length") {
|
|
3198
|
+
errorCopy = aiLengthLimitCopy({ thinking: aiRunThinkingOn(outcome.thinking), empty: true });
|
|
3199
|
+
error = makeAiError("badOutput", errorCopy.message);
|
|
3200
|
+
} else {
|
|
3201
|
+
error = { code: "unknown", message: "The engine sent back no text." };
|
|
3202
|
+
}
|
|
3203
|
+
} else {
|
|
3204
|
+
phase = "done";
|
|
3205
|
+
note2 = finishNote(outcome);
|
|
3206
|
+
if (STRUCTURE_CHECKED.has(v)) {
|
|
3207
|
+
const diff = checkMarkdownStructure(sentText, finalText);
|
|
3208
|
+
const text = structureWarningText(diff);
|
|
3209
|
+
warning = text ? { text, title: diff.messages.join("\n") } : null;
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
render();
|
|
3213
|
+
position2();
|
|
3214
|
+
liveEl.textContent = phase !== "done" ? "Rewrite failed." : readOnly ? "Explanation ready. Insert below adds it to the document; Escape closes." : `${variant === "translate" ? "Translation" : "Rewrite"} ready. Press Enter to accept or Escape to cancel.${warning ? ` ${warning.text}.` : ""}`;
|
|
3215
|
+
if (phase === "done") {
|
|
3216
|
+
(readOnly ? root : acceptBtn).focus({ preventScroll: true });
|
|
3217
|
+
}
|
|
3218
|
+
}, runFailed(seq));
|
|
3219
|
+
}
|
|
3220
|
+
function accept() {
|
|
3221
|
+
const t = target;
|
|
3222
|
+
const mode = outputMode();
|
|
3223
|
+
if (!t || phase !== "done" || !finalText.trim() || mode === "choose") {
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
const ok = mode === "readOnly" ? t.insertBelow(finalText) : mode === "replaceDocument" && untranslated.trim() ? t.accept(`${finalText.trimEnd()}
|
|
3227
|
+
|
|
3228
|
+
${untranslated.trimStart()}`) : t.accept(finalText);
|
|
3229
|
+
close({ refocus: true });
|
|
3230
|
+
if (!ok) {
|
|
3231
|
+
t.focus();
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
function copy() {
|
|
3235
|
+
const text = (phase === "done" ? finalText : rewriteVisibleText(raw)).trim();
|
|
3236
|
+
if (!text) {
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
void navigator.clipboard?.writeText(text).then(() => {
|
|
3240
|
+
if (!root) {
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
copyBtn.textContent = "Copied";
|
|
3244
|
+
window.clearTimeout(copiedTimer);
|
|
3245
|
+
copiedTimer = window.setTimeout(() => {
|
|
3246
|
+
copyBtn.textContent = "Copy";
|
|
3247
|
+
}, 1200);
|
|
3248
|
+
}, () => {
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
return {
|
|
3252
|
+
open(t, choice) {
|
|
3253
|
+
mount(t);
|
|
3254
|
+
params = { ...choice?.params };
|
|
3255
|
+
wholeDocument = docOnly || !!choice?.wholeDocument;
|
|
3256
|
+
const def = choice ? getAiRewriteVariant(choice.variant) : void 0;
|
|
3257
|
+
if (docOnly && (!def || def.id !== "translate" || !hasParam(def, params))) {
|
|
3258
|
+
openPicker("language");
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
if (def && def.param && !hasParam(def, params)) {
|
|
3262
|
+
openPicker(def.param === "language" ? "language" : "audience");
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
if (def && (def.id !== "custom" || choice?.instruction?.trim())) {
|
|
3266
|
+
if (def.id === "translate" && wholeDocument && !useDocumentTarget()) {
|
|
3267
|
+
wholeDocument = false;
|
|
3268
|
+
openPicker("language");
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
start(def.id, choice?.instruction);
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
phase = "choose";
|
|
3275
|
+
render();
|
|
3276
|
+
position2();
|
|
3277
|
+
inputEl.focus({ preventScroll: true });
|
|
3278
|
+
},
|
|
3279
|
+
openSummary(t, sj) {
|
|
3280
|
+
mount(t);
|
|
3281
|
+
job = "summary";
|
|
3282
|
+
summaryJob = { ...sj };
|
|
3283
|
+
startSummary();
|
|
3284
|
+
},
|
|
3285
|
+
openTitles(t, tj) {
|
|
3286
|
+
mount(t);
|
|
3287
|
+
job = "titles";
|
|
3288
|
+
titleJob = tj;
|
|
3289
|
+
startTitles();
|
|
3290
|
+
},
|
|
3291
|
+
openUnavailable(t, reason) {
|
|
3292
|
+
mount(t);
|
|
3293
|
+
phase = "error";
|
|
3294
|
+
error = { code: "unknown", message: reason };
|
|
3295
|
+
unavailable = true;
|
|
3296
|
+
render();
|
|
3297
|
+
position2();
|
|
3298
|
+
errorEl.querySelector(".ai-rw-btn")?.focus({ preventScroll: true });
|
|
3299
|
+
},
|
|
3300
|
+
close: () => close(),
|
|
3301
|
+
isOpen: () => !!target,
|
|
3302
|
+
destroy() {
|
|
3303
|
+
close({ refocus: false });
|
|
3304
|
+
}
|
|
3305
|
+
};
|
|
3306
|
+
}
|
|
3307
|
+
export {
|
|
3308
|
+
AI_ENGINE_PRESETS,
|
|
3309
|
+
AI_TURN_ON_PATCH,
|
|
3310
|
+
CHARS_PER_TOKEN,
|
|
3311
|
+
DEFAULT_CONTEXT_TOKENS,
|
|
3312
|
+
PROPERTY_VOCABULARY_MAX,
|
|
3313
|
+
aiConsentHost,
|
|
3314
|
+
aiErrorCopy,
|
|
3315
|
+
aiFeatureAvailability,
|
|
3316
|
+
aiIndicatorState,
|
|
3317
|
+
aiLengthLimitCopy,
|
|
3318
|
+
aiNextRouteChanged,
|
|
3319
|
+
aiRewriteMenuGroups,
|
|
3320
|
+
aiRunThinkingOn,
|
|
3321
|
+
aiSetupTarget,
|
|
3322
|
+
aiToolbarMenuEntries,
|
|
3323
|
+
applyRichTitle,
|
|
3324
|
+
applySourceTitle,
|
|
3325
|
+
bestLocalServer,
|
|
3326
|
+
budgetDocument,
|
|
3327
|
+
buildAiSettingsSection,
|
|
3328
|
+
buildPropertiesMessages,
|
|
3329
|
+
createAiRewritePopover,
|
|
3330
|
+
cursorInsertTarget,
|
|
3331
|
+
detectLocalServers,
|
|
3332
|
+
engineDisplayName,
|
|
3333
|
+
experimentalBadge,
|
|
3334
|
+
headingsOutline,
|
|
3335
|
+
hideAiFirstRunPanel,
|
|
3336
|
+
hostOfUrl,
|
|
3337
|
+
isAiFirstRunPanelOpen,
|
|
3338
|
+
isHostEngineKind,
|
|
3339
|
+
isRouteUsable,
|
|
3340
|
+
jsonRepairMessages,
|
|
3341
|
+
loadThinkingLevel,
|
|
3342
|
+
makeAiError,
|
|
3343
|
+
mountAiEnginesPage,
|
|
3344
|
+
parseAiSettings,
|
|
3345
|
+
parsePropertySuggestions,
|
|
3346
|
+
privacyCard,
|
|
3347
|
+
privacyLine,
|
|
3348
|
+
propertiesMaxTokens,
|
|
3349
|
+
rememberThinkingLevel,
|
|
3350
|
+
renderHeadingOutline,
|
|
3351
|
+
resolveRoute,
|
|
3352
|
+
rewriteVisibleText,
|
|
3353
|
+
richDocumentTarget,
|
|
3354
|
+
richRewriteTarget,
|
|
3355
|
+
runAiCompletion,
|
|
3356
|
+
scanForSetup,
|
|
3357
|
+
setUpLocalEngine,
|
|
3358
|
+
setYamlTitle,
|
|
3359
|
+
showAiFirstRunPanel,
|
|
3360
|
+
sourceCursorTarget,
|
|
3361
|
+
sourceDocumentTarget,
|
|
3362
|
+
sourceRewriteTarget,
|
|
3363
|
+
thinkingChoices,
|
|
3364
|
+
thinkingLevelFor,
|
|
3365
|
+
toAiError,
|
|
3366
|
+
whatIsSent,
|
|
3367
|
+
withSavedAiKeys,
|
|
3368
|
+
yamlHasTitle
|
|
3369
|
+
};
|
|
3370
|
+
//# sourceMappingURL=ai-ui-C7ENTWWU.js.map
|