@kedataindo/docflow-core 0.0.32 → 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/index.cjs +310 -0
- package/dist/ai/index.d.cts +1 -0
- package/dist/ai/index.d.ts +1 -0
- package/dist/ai/index.js +24 -0
- package/dist/chunk-HMOWKFPE.js +275 -0
- package/dist/index-BsIFJXPk.d.cts +313 -0
- package/dist/index-BsIFJXPk.d.ts +313 -0
- package/dist/index.d.cts +3 -313
- package/dist/index.d.ts +3 -313
- package/dist/index.js +13 -263
- package/package.json +6 -1
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/ai/index.ts
|
|
21
|
+
var ai_exports = {};
|
|
22
|
+
__export(ai_exports, {
|
|
23
|
+
CONTEXT_CHAR_CAP: () => CONTEXT_CHAR_CAP,
|
|
24
|
+
LOCAL_STORAGE_KEY: () => LOCAL_STORAGE_KEY,
|
|
25
|
+
buildAIPrompt: () => buildAIPrompt,
|
|
26
|
+
httpKeyStorage: () => httpKeyStorage,
|
|
27
|
+
localStorageKeyStorage: () => localStorageKeyStorage,
|
|
28
|
+
memoryKeyStorage: () => memoryKeyStorage,
|
|
29
|
+
openaiCompatibleProvider: () => openaiCompatibleProvider,
|
|
30
|
+
toAIStreamFn: () => toAIStreamFn,
|
|
31
|
+
trimContextAfter: () => trimContextAfter,
|
|
32
|
+
trimContextBefore: () => trimContextBefore
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(ai_exports);
|
|
35
|
+
|
|
36
|
+
// src/ai/prompts.ts
|
|
37
|
+
var CONTEXT_CHAR_CAP = 1500;
|
|
38
|
+
var ONLY_RESULT = "Return ONLY the resulting text \u2014 no preamble, no explanation, no commentary, no markdown fences.";
|
|
39
|
+
var SYSTEM_PROMPTS = {
|
|
40
|
+
rewrite: `You are a writing assistant. Rewrite the selected text to improve clarity, flow, and style while preserving its meaning. ${ONLY_RESULT}`,
|
|
41
|
+
summarize: `You are a writing assistant. Summarize the selected text concisely, keeping every key point. ${ONLY_RESULT}`,
|
|
42
|
+
grammar: `You are a writing assistant. Fix grammar, spelling, and punctuation in the selected text without changing its meaning or voice. ${ONLY_RESULT}`,
|
|
43
|
+
tone: `You are a writing assistant. Adjust the tone of the selected text as instructed while preserving its content. ${ONLY_RESULT}`,
|
|
44
|
+
translate: `You are a writing assistant. Translate the selected text as instructed, preserving structure and formatting. ${ONLY_RESULT}`,
|
|
45
|
+
expand: `You are a writing assistant. Expand the selected text with relevant detail, staying on topic and matching the existing style. ${ONLY_RESULT}`,
|
|
46
|
+
shorten: `You are a writing assistant. Shorten the selected text, keeping its essential meaning. ${ONLY_RESULT}`,
|
|
47
|
+
generate: `You are a writing assistant embedded in a document editor. Write the text the user asks for so it fits the surrounding context. ${ONLY_RESULT}`,
|
|
48
|
+
chat: "You are a helpful writing assistant embedded in a document editor. Answer the user\u2019s question. Use the provided document context when it is relevant; say so when it is not enough to answer."
|
|
49
|
+
};
|
|
50
|
+
function trimContextBefore(text) {
|
|
51
|
+
return text.length > CONTEXT_CHAR_CAP ? `\u2026${text.slice(-CONTEXT_CHAR_CAP)}` : text;
|
|
52
|
+
}
|
|
53
|
+
function trimContextAfter(text) {
|
|
54
|
+
return text.length > CONTEXT_CHAR_CAP ? `${text.slice(0, CONTEXT_CHAR_CAP)}\u2026` : text;
|
|
55
|
+
}
|
|
56
|
+
function quote(label, text) {
|
|
57
|
+
return `${label}:
|
|
58
|
+
"""${text}"""`;
|
|
59
|
+
}
|
|
60
|
+
function contextSection(req) {
|
|
61
|
+
const parts = [];
|
|
62
|
+
if (req.context) {
|
|
63
|
+
const before = trimContextBefore(req.context.before ?? "");
|
|
64
|
+
const after = trimContextAfter(req.context.after ?? "");
|
|
65
|
+
if (before) parts.push(quote("Text before", before));
|
|
66
|
+
if (after) parts.push(quote("Text after", after));
|
|
67
|
+
}
|
|
68
|
+
return parts;
|
|
69
|
+
}
|
|
70
|
+
function buildAIPrompt(req) {
|
|
71
|
+
let system;
|
|
72
|
+
const userParts = [];
|
|
73
|
+
switch (req.action) {
|
|
74
|
+
case "rewrite":
|
|
75
|
+
case "summarize":
|
|
76
|
+
case "grammar":
|
|
77
|
+
case "expand":
|
|
78
|
+
case "shorten":
|
|
79
|
+
system = SYSTEM_PROMPTS[req.action];
|
|
80
|
+
userParts.push(quote("Selected text", req.selection ?? ""));
|
|
81
|
+
break;
|
|
82
|
+
case "tone":
|
|
83
|
+
system = SYSTEM_PROMPTS.tone;
|
|
84
|
+
userParts.push(quote("Selected text", req.selection ?? ""));
|
|
85
|
+
userParts.push(`Target tone: ${req.prompt ?? "professional"}`);
|
|
86
|
+
break;
|
|
87
|
+
case "translate":
|
|
88
|
+
system = SYSTEM_PROMPTS.translate;
|
|
89
|
+
userParts.push(quote("Selected text", req.selection ?? ""));
|
|
90
|
+
userParts.push(`Target language: ${req.prompt ?? "English"}`);
|
|
91
|
+
break;
|
|
92
|
+
case "generate":
|
|
93
|
+
system = SYSTEM_PROMPTS.generate;
|
|
94
|
+
userParts.push(`Instruction: ${req.prompt ?? ""}`);
|
|
95
|
+
break;
|
|
96
|
+
case "chat":
|
|
97
|
+
system = SYSTEM_PROMPTS.chat;
|
|
98
|
+
if (req.selection) userParts.push(quote("Selected text", req.selection));
|
|
99
|
+
userParts.push(req.prompt ?? "");
|
|
100
|
+
break;
|
|
101
|
+
case "draft":
|
|
102
|
+
throw new Error(
|
|
103
|
+
"action 'draft' is not built via buildAIPrompt \u2014 use the aiDraft port (Phase 7E)"
|
|
104
|
+
);
|
|
105
|
+
default: {
|
|
106
|
+
const exhaustive = req.action;
|
|
107
|
+
throw new Error(`Unknown AI action: ${String(exhaustive)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
userParts.push(...contextSection(req));
|
|
111
|
+
return { system, prompt: userParts.filter(Boolean).join("\n\n") };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/ai/adapter.ts
|
|
115
|
+
function toAIStreamFn(provider) {
|
|
116
|
+
return async function* aiStream(req, signal) {
|
|
117
|
+
const { system, prompt } = buildAIPrompt(req);
|
|
118
|
+
for await (const event of provider.complete({ system, prompt, signal })) {
|
|
119
|
+
if (event.type === "delta") {
|
|
120
|
+
yield event.text;
|
|
121
|
+
} else if (event.type === "done") {
|
|
122
|
+
return;
|
|
123
|
+
} else {
|
|
124
|
+
throw event.error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/ai/keyStorage.ts
|
|
131
|
+
function memoryKeyStorage(initial = null) {
|
|
132
|
+
let value = initial;
|
|
133
|
+
return {
|
|
134
|
+
async get() {
|
|
135
|
+
return value;
|
|
136
|
+
},
|
|
137
|
+
async set(next) {
|
|
138
|
+
value = next;
|
|
139
|
+
},
|
|
140
|
+
async clear() {
|
|
141
|
+
value = null;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
var LOCAL_STORAGE_KEY = "docflow:ai-config";
|
|
146
|
+
function localStorageKeyStorage(key = LOCAL_STORAGE_KEY, storage) {
|
|
147
|
+
const store = () => {
|
|
148
|
+
const s = storage ?? globalThis.localStorage;
|
|
149
|
+
if (!s) throw new Error("localStorageKeyStorage: no Storage available (SSR?)");
|
|
150
|
+
return s;
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
async get() {
|
|
154
|
+
const raw = store().getItem(key);
|
|
155
|
+
if (!raw) return null;
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(raw);
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
async set(value) {
|
|
163
|
+
store().setItem(key, JSON.stringify(value));
|
|
164
|
+
},
|
|
165
|
+
async clear() {
|
|
166
|
+
store().removeItem(key);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function httpKeyStorage(urls) {
|
|
171
|
+
const doFetch = urls.fetchImpl ?? ((...args) => fetch(...args));
|
|
172
|
+
return {
|
|
173
|
+
async get() {
|
|
174
|
+
const res = await doFetch(urls.getUrl, { credentials: "include" });
|
|
175
|
+
if (res.status === 404) return null;
|
|
176
|
+
if (!res.ok) throw new Error(`AI config read failed: ${res.status}`);
|
|
177
|
+
return await res.json();
|
|
178
|
+
},
|
|
179
|
+
async set(value) {
|
|
180
|
+
const res = await doFetch(urls.setUrl, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: { "Content-Type": "application/json" },
|
|
183
|
+
credentials: "include",
|
|
184
|
+
body: JSON.stringify(value)
|
|
185
|
+
});
|
|
186
|
+
if (!res.ok) throw new Error(`AI config write failed: ${res.status}`);
|
|
187
|
+
},
|
|
188
|
+
async clear() {
|
|
189
|
+
const res = await doFetch(urls.deleteUrl, { method: "DELETE", credentials: "include" });
|
|
190
|
+
if (!res.ok && res.status !== 404) throw new Error(`AI config delete failed: ${res.status}`);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/ai/openaiCompatibleProvider.ts
|
|
196
|
+
function authHeaders(auth) {
|
|
197
|
+
switch (auth.type) {
|
|
198
|
+
case "bearer":
|
|
199
|
+
return { Authorization: `Bearer ${auth.apiKey}` };
|
|
200
|
+
case "header":
|
|
201
|
+
return { [auth.name]: auth.value };
|
|
202
|
+
case "none":
|
|
203
|
+
return {};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function toError(err) {
|
|
207
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
208
|
+
}
|
|
209
|
+
function openaiCompatibleProvider(config) {
|
|
210
|
+
const baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
211
|
+
return {
|
|
212
|
+
async *complete(req) {
|
|
213
|
+
const controller = new AbortController();
|
|
214
|
+
const onAbort = () => controller.abort();
|
|
215
|
+
const callerSignal = req.signal;
|
|
216
|
+
if (callerSignal) {
|
|
217
|
+
if (callerSignal.aborted) controller.abort();
|
|
218
|
+
else callerSignal.addEventListener("abort", onAbort, { once: true });
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const system = [config.systemPrompt, req.system].filter(Boolean).join("\n\n");
|
|
222
|
+
const messages = [];
|
|
223
|
+
if (system) messages.push({ role: "system", content: system });
|
|
224
|
+
messages.push({ role: "user", content: req.prompt });
|
|
225
|
+
let res;
|
|
226
|
+
try {
|
|
227
|
+
res = await fetch(`${baseUrl}/chat/completions`, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: { "Content-Type": "application/json", ...authHeaders(config.auth) },
|
|
230
|
+
body: JSON.stringify({ model: config.model, messages, stream: true }),
|
|
231
|
+
signal: controller.signal
|
|
232
|
+
});
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if (controller.signal.aborted) return;
|
|
235
|
+
yield { type: "error", error: toError(err) };
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
const detail = await res.text().catch(() => "");
|
|
240
|
+
yield {
|
|
241
|
+
type: "error",
|
|
242
|
+
error: new Error(
|
|
243
|
+
`AI provider responded ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ""}`
|
|
244
|
+
)
|
|
245
|
+
};
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!res.body) {
|
|
249
|
+
yield { type: "error", error: new Error("AI provider response has no body") };
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const reader = res.body.getReader();
|
|
253
|
+
const decoder = new TextDecoder();
|
|
254
|
+
let buffer = "";
|
|
255
|
+
let sawDone = false;
|
|
256
|
+
try {
|
|
257
|
+
for (; ; ) {
|
|
258
|
+
const { done, value } = await reader.read();
|
|
259
|
+
if (done) break;
|
|
260
|
+
buffer += decoder.decode(value, { stream: true });
|
|
261
|
+
let sep;
|
|
262
|
+
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
263
|
+
const block = buffer.slice(0, sep);
|
|
264
|
+
buffer = buffer.slice(sep + 2);
|
|
265
|
+
for (const line of block.split("\n")) {
|
|
266
|
+
if (!line.startsWith("data:")) continue;
|
|
267
|
+
const data = line.slice("data:".length).trim();
|
|
268
|
+
if (data === "[DONE]") {
|
|
269
|
+
sawDone = true;
|
|
270
|
+
yield { type: "done" };
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const json = JSON.parse(data);
|
|
275
|
+
const text = json.choices?.[0]?.delta?.content;
|
|
276
|
+
if (text) yield { type: "delta", text };
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} catch (err) {
|
|
283
|
+
if (!controller.signal.aborted) {
|
|
284
|
+
yield { type: "error", error: toError(err) };
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
} finally {
|
|
288
|
+
reader.releaseLock();
|
|
289
|
+
}
|
|
290
|
+
if (!sawDone && !controller.signal.aborted) yield { type: "done" };
|
|
291
|
+
} finally {
|
|
292
|
+
callerSignal?.removeEventListener("abort", onAbort);
|
|
293
|
+
controller.abort();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
299
|
+
0 && (module.exports = {
|
|
300
|
+
CONTEXT_CHAR_CAP,
|
|
301
|
+
LOCAL_STORAGE_KEY,
|
|
302
|
+
buildAIPrompt,
|
|
303
|
+
httpKeyStorage,
|
|
304
|
+
localStorageKeyStorage,
|
|
305
|
+
memoryKeyStorage,
|
|
306
|
+
openaiCompatibleProvider,
|
|
307
|
+
toAIStreamFn,
|
|
308
|
+
trimContextAfter,
|
|
309
|
+
trimContextBefore
|
|
310
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { b as AIAction, c as AIActionRequest, d as AICompleteRequest, e as AIConfig, f as AIDraftCitation, g as AIDraftEvent, a as AIDraftFn, h as AIProvider, A as AIStreamFn, j as Auth, k as CONTEXT_CHAR_CAP, l as CslDate, m as CslItemData, n as CslName, H as HttpKeyStorageUrls, K as KeyStorage, L as LOCAL_STORAGE_KEY, O as OpenAICompatibleConfig, S as StreamEvent, p as buildAIPrompt, q as httpKeyStorage, r as localStorageKeyStorage, s as memoryKeyStorage, t as openaiCompatibleProvider, u as toAIStreamFn, v as trimContextAfter, w as trimContextBefore } from '../index-BsIFJXPk.cjs';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { b as AIAction, c as AIActionRequest, d as AICompleteRequest, e as AIConfig, f as AIDraftCitation, g as AIDraftEvent, a as AIDraftFn, h as AIProvider, A as AIStreamFn, j as Auth, k as CONTEXT_CHAR_CAP, l as CslDate, m as CslItemData, n as CslName, H as HttpKeyStorageUrls, K as KeyStorage, L as LOCAL_STORAGE_KEY, O as OpenAICompatibleConfig, S as StreamEvent, p as buildAIPrompt, q as httpKeyStorage, r as localStorageKeyStorage, s as memoryKeyStorage, t as openaiCompatibleProvider, u as toAIStreamFn, v as trimContextAfter, w as trimContextBefore } from '../index-BsIFJXPk.js';
|
package/dist/ai/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONTEXT_CHAR_CAP,
|
|
3
|
+
LOCAL_STORAGE_KEY,
|
|
4
|
+
buildAIPrompt,
|
|
5
|
+
httpKeyStorage,
|
|
6
|
+
localStorageKeyStorage,
|
|
7
|
+
memoryKeyStorage,
|
|
8
|
+
openaiCompatibleProvider,
|
|
9
|
+
toAIStreamFn,
|
|
10
|
+
trimContextAfter,
|
|
11
|
+
trimContextBefore
|
|
12
|
+
} from "../chunk-HMOWKFPE.js";
|
|
13
|
+
export {
|
|
14
|
+
CONTEXT_CHAR_CAP,
|
|
15
|
+
LOCAL_STORAGE_KEY,
|
|
16
|
+
buildAIPrompt,
|
|
17
|
+
httpKeyStorage,
|
|
18
|
+
localStorageKeyStorage,
|
|
19
|
+
memoryKeyStorage,
|
|
20
|
+
openaiCompatibleProvider,
|
|
21
|
+
toAIStreamFn,
|
|
22
|
+
trimContextAfter,
|
|
23
|
+
trimContextBefore
|
|
24
|
+
};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// src/ai/prompts.ts
|
|
2
|
+
var CONTEXT_CHAR_CAP = 1500;
|
|
3
|
+
var ONLY_RESULT = "Return ONLY the resulting text \u2014 no preamble, no explanation, no commentary, no markdown fences.";
|
|
4
|
+
var SYSTEM_PROMPTS = {
|
|
5
|
+
rewrite: `You are a writing assistant. Rewrite the selected text to improve clarity, flow, and style while preserving its meaning. ${ONLY_RESULT}`,
|
|
6
|
+
summarize: `You are a writing assistant. Summarize the selected text concisely, keeping every key point. ${ONLY_RESULT}`,
|
|
7
|
+
grammar: `You are a writing assistant. Fix grammar, spelling, and punctuation in the selected text without changing its meaning or voice. ${ONLY_RESULT}`,
|
|
8
|
+
tone: `You are a writing assistant. Adjust the tone of the selected text as instructed while preserving its content. ${ONLY_RESULT}`,
|
|
9
|
+
translate: `You are a writing assistant. Translate the selected text as instructed, preserving structure and formatting. ${ONLY_RESULT}`,
|
|
10
|
+
expand: `You are a writing assistant. Expand the selected text with relevant detail, staying on topic and matching the existing style. ${ONLY_RESULT}`,
|
|
11
|
+
shorten: `You are a writing assistant. Shorten the selected text, keeping its essential meaning. ${ONLY_RESULT}`,
|
|
12
|
+
generate: `You are a writing assistant embedded in a document editor. Write the text the user asks for so it fits the surrounding context. ${ONLY_RESULT}`,
|
|
13
|
+
chat: "You are a helpful writing assistant embedded in a document editor. Answer the user\u2019s question. Use the provided document context when it is relevant; say so when it is not enough to answer."
|
|
14
|
+
};
|
|
15
|
+
function trimContextBefore(text) {
|
|
16
|
+
return text.length > CONTEXT_CHAR_CAP ? `\u2026${text.slice(-CONTEXT_CHAR_CAP)}` : text;
|
|
17
|
+
}
|
|
18
|
+
function trimContextAfter(text) {
|
|
19
|
+
return text.length > CONTEXT_CHAR_CAP ? `${text.slice(0, CONTEXT_CHAR_CAP)}\u2026` : text;
|
|
20
|
+
}
|
|
21
|
+
function quote(label, text) {
|
|
22
|
+
return `${label}:
|
|
23
|
+
"""${text}"""`;
|
|
24
|
+
}
|
|
25
|
+
function contextSection(req) {
|
|
26
|
+
const parts = [];
|
|
27
|
+
if (req.context) {
|
|
28
|
+
const before = trimContextBefore(req.context.before ?? "");
|
|
29
|
+
const after = trimContextAfter(req.context.after ?? "");
|
|
30
|
+
if (before) parts.push(quote("Text before", before));
|
|
31
|
+
if (after) parts.push(quote("Text after", after));
|
|
32
|
+
}
|
|
33
|
+
return parts;
|
|
34
|
+
}
|
|
35
|
+
function buildAIPrompt(req) {
|
|
36
|
+
let system;
|
|
37
|
+
const userParts = [];
|
|
38
|
+
switch (req.action) {
|
|
39
|
+
case "rewrite":
|
|
40
|
+
case "summarize":
|
|
41
|
+
case "grammar":
|
|
42
|
+
case "expand":
|
|
43
|
+
case "shorten":
|
|
44
|
+
system = SYSTEM_PROMPTS[req.action];
|
|
45
|
+
userParts.push(quote("Selected text", req.selection ?? ""));
|
|
46
|
+
break;
|
|
47
|
+
case "tone":
|
|
48
|
+
system = SYSTEM_PROMPTS.tone;
|
|
49
|
+
userParts.push(quote("Selected text", req.selection ?? ""));
|
|
50
|
+
userParts.push(`Target tone: ${req.prompt ?? "professional"}`);
|
|
51
|
+
break;
|
|
52
|
+
case "translate":
|
|
53
|
+
system = SYSTEM_PROMPTS.translate;
|
|
54
|
+
userParts.push(quote("Selected text", req.selection ?? ""));
|
|
55
|
+
userParts.push(`Target language: ${req.prompt ?? "English"}`);
|
|
56
|
+
break;
|
|
57
|
+
case "generate":
|
|
58
|
+
system = SYSTEM_PROMPTS.generate;
|
|
59
|
+
userParts.push(`Instruction: ${req.prompt ?? ""}`);
|
|
60
|
+
break;
|
|
61
|
+
case "chat":
|
|
62
|
+
system = SYSTEM_PROMPTS.chat;
|
|
63
|
+
if (req.selection) userParts.push(quote("Selected text", req.selection));
|
|
64
|
+
userParts.push(req.prompt ?? "");
|
|
65
|
+
break;
|
|
66
|
+
case "draft":
|
|
67
|
+
throw new Error(
|
|
68
|
+
"action 'draft' is not built via buildAIPrompt \u2014 use the aiDraft port (Phase 7E)"
|
|
69
|
+
);
|
|
70
|
+
default: {
|
|
71
|
+
const exhaustive = req.action;
|
|
72
|
+
throw new Error(`Unknown AI action: ${String(exhaustive)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
userParts.push(...contextSection(req));
|
|
76
|
+
return { system, prompt: userParts.filter(Boolean).join("\n\n") };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/ai/adapter.ts
|
|
80
|
+
function toAIStreamFn(provider) {
|
|
81
|
+
return async function* aiStream(req, signal) {
|
|
82
|
+
const { system, prompt } = buildAIPrompt(req);
|
|
83
|
+
for await (const event of provider.complete({ system, prompt, signal })) {
|
|
84
|
+
if (event.type === "delta") {
|
|
85
|
+
yield event.text;
|
|
86
|
+
} else if (event.type === "done") {
|
|
87
|
+
return;
|
|
88
|
+
} else {
|
|
89
|
+
throw event.error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/ai/keyStorage.ts
|
|
96
|
+
function memoryKeyStorage(initial = null) {
|
|
97
|
+
let value = initial;
|
|
98
|
+
return {
|
|
99
|
+
async get() {
|
|
100
|
+
return value;
|
|
101
|
+
},
|
|
102
|
+
async set(next) {
|
|
103
|
+
value = next;
|
|
104
|
+
},
|
|
105
|
+
async clear() {
|
|
106
|
+
value = null;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
var LOCAL_STORAGE_KEY = "docflow:ai-config";
|
|
111
|
+
function localStorageKeyStorage(key = LOCAL_STORAGE_KEY, storage) {
|
|
112
|
+
const store = () => {
|
|
113
|
+
const s = storage ?? globalThis.localStorage;
|
|
114
|
+
if (!s) throw new Error("localStorageKeyStorage: no Storage available (SSR?)");
|
|
115
|
+
return s;
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
async get() {
|
|
119
|
+
const raw = store().getItem(key);
|
|
120
|
+
if (!raw) return null;
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(raw);
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
async set(value) {
|
|
128
|
+
store().setItem(key, JSON.stringify(value));
|
|
129
|
+
},
|
|
130
|
+
async clear() {
|
|
131
|
+
store().removeItem(key);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function httpKeyStorage(urls) {
|
|
136
|
+
const doFetch = urls.fetchImpl ?? ((...args) => fetch(...args));
|
|
137
|
+
return {
|
|
138
|
+
async get() {
|
|
139
|
+
const res = await doFetch(urls.getUrl, { credentials: "include" });
|
|
140
|
+
if (res.status === 404) return null;
|
|
141
|
+
if (!res.ok) throw new Error(`AI config read failed: ${res.status}`);
|
|
142
|
+
return await res.json();
|
|
143
|
+
},
|
|
144
|
+
async set(value) {
|
|
145
|
+
const res = await doFetch(urls.setUrl, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: { "Content-Type": "application/json" },
|
|
148
|
+
credentials: "include",
|
|
149
|
+
body: JSON.stringify(value)
|
|
150
|
+
});
|
|
151
|
+
if (!res.ok) throw new Error(`AI config write failed: ${res.status}`);
|
|
152
|
+
},
|
|
153
|
+
async clear() {
|
|
154
|
+
const res = await doFetch(urls.deleteUrl, { method: "DELETE", credentials: "include" });
|
|
155
|
+
if (!res.ok && res.status !== 404) throw new Error(`AI config delete failed: ${res.status}`);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/ai/openaiCompatibleProvider.ts
|
|
161
|
+
function authHeaders(auth) {
|
|
162
|
+
switch (auth.type) {
|
|
163
|
+
case "bearer":
|
|
164
|
+
return { Authorization: `Bearer ${auth.apiKey}` };
|
|
165
|
+
case "header":
|
|
166
|
+
return { [auth.name]: auth.value };
|
|
167
|
+
case "none":
|
|
168
|
+
return {};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function toError(err) {
|
|
172
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
173
|
+
}
|
|
174
|
+
function openaiCompatibleProvider(config) {
|
|
175
|
+
const baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
176
|
+
return {
|
|
177
|
+
async *complete(req) {
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
const onAbort = () => controller.abort();
|
|
180
|
+
const callerSignal = req.signal;
|
|
181
|
+
if (callerSignal) {
|
|
182
|
+
if (callerSignal.aborted) controller.abort();
|
|
183
|
+
else callerSignal.addEventListener("abort", onAbort, { once: true });
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const system = [config.systemPrompt, req.system].filter(Boolean).join("\n\n");
|
|
187
|
+
const messages = [];
|
|
188
|
+
if (system) messages.push({ role: "system", content: system });
|
|
189
|
+
messages.push({ role: "user", content: req.prompt });
|
|
190
|
+
let res;
|
|
191
|
+
try {
|
|
192
|
+
res = await fetch(`${baseUrl}/chat/completions`, {
|
|
193
|
+
method: "POST",
|
|
194
|
+
headers: { "Content-Type": "application/json", ...authHeaders(config.auth) },
|
|
195
|
+
body: JSON.stringify({ model: config.model, messages, stream: true }),
|
|
196
|
+
signal: controller.signal
|
|
197
|
+
});
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (controller.signal.aborted) return;
|
|
200
|
+
yield { type: "error", error: toError(err) };
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!res.ok) {
|
|
204
|
+
const detail = await res.text().catch(() => "");
|
|
205
|
+
yield {
|
|
206
|
+
type: "error",
|
|
207
|
+
error: new Error(
|
|
208
|
+
`AI provider responded ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ""}`
|
|
209
|
+
)
|
|
210
|
+
};
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (!res.body) {
|
|
214
|
+
yield { type: "error", error: new Error("AI provider response has no body") };
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const reader = res.body.getReader();
|
|
218
|
+
const decoder = new TextDecoder();
|
|
219
|
+
let buffer = "";
|
|
220
|
+
let sawDone = false;
|
|
221
|
+
try {
|
|
222
|
+
for (; ; ) {
|
|
223
|
+
const { done, value } = await reader.read();
|
|
224
|
+
if (done) break;
|
|
225
|
+
buffer += decoder.decode(value, { stream: true });
|
|
226
|
+
let sep;
|
|
227
|
+
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
228
|
+
const block = buffer.slice(0, sep);
|
|
229
|
+
buffer = buffer.slice(sep + 2);
|
|
230
|
+
for (const line of block.split("\n")) {
|
|
231
|
+
if (!line.startsWith("data:")) continue;
|
|
232
|
+
const data = line.slice("data:".length).trim();
|
|
233
|
+
if (data === "[DONE]") {
|
|
234
|
+
sawDone = true;
|
|
235
|
+
yield { type: "done" };
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const json = JSON.parse(data);
|
|
240
|
+
const text = json.choices?.[0]?.delta?.content;
|
|
241
|
+
if (text) yield { type: "delta", text };
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch (err) {
|
|
248
|
+
if (!controller.signal.aborted) {
|
|
249
|
+
yield { type: "error", error: toError(err) };
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
} finally {
|
|
253
|
+
reader.releaseLock();
|
|
254
|
+
}
|
|
255
|
+
if (!sawDone && !controller.signal.aborted) yield { type: "done" };
|
|
256
|
+
} finally {
|
|
257
|
+
callerSignal?.removeEventListener("abort", onAbort);
|
|
258
|
+
controller.abort();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export {
|
|
265
|
+
CONTEXT_CHAR_CAP,
|
|
266
|
+
trimContextBefore,
|
|
267
|
+
trimContextAfter,
|
|
268
|
+
buildAIPrompt,
|
|
269
|
+
toAIStreamFn,
|
|
270
|
+
memoryKeyStorage,
|
|
271
|
+
LOCAL_STORAGE_KEY,
|
|
272
|
+
localStorageKeyStorage,
|
|
273
|
+
httpKeyStorage,
|
|
274
|
+
openaiCompatibleProvider
|
|
275
|
+
};
|