@kedataindo/docflow-core 0.0.5 → 0.0.6
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/index.cjs +285 -2
- package/dist/index.d.cts +148 -4
- package/dist/index.d.ts +148 -4
- package/dist/index.js +274 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -31,12 +31,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
BlockAttributesExtension: () => BlockAttributesExtension,
|
|
34
|
+
CONTEXT_CHAR_CAP: () => CONTEXT_CHAR_CAP,
|
|
34
35
|
EditorContextExtension: () => EditorContextExtension,
|
|
35
36
|
FontSizeExtension: () => FontSizeExtension,
|
|
37
|
+
LOCAL_STORAGE_KEY: () => LOCAL_STORAGE_KEY,
|
|
36
38
|
PAGE_SIZES: () => PAGE_SIZES,
|
|
37
39
|
PaginationPlus: () => PaginationPlus,
|
|
38
40
|
SearchAndReplaceExtension: () => SearchAndReplaceExtension,
|
|
39
41
|
SubdocumentProvider: () => SubdocumentProvider,
|
|
42
|
+
buildAIPrompt: () => buildAIPrompt,
|
|
40
43
|
clearSearch: () => clearSearch,
|
|
41
44
|
collaborationExtensions: () => collaborationExtensions,
|
|
42
45
|
collectExtensions: () => collectExtensions,
|
|
@@ -46,6 +49,10 @@ __export(index_exports, {
|
|
|
46
49
|
definePlugin: () => definePlugin,
|
|
47
50
|
findMatches: () => findMatches,
|
|
48
51
|
getSearchState: () => getSearchState,
|
|
52
|
+
httpKeyStorage: () => httpKeyStorage,
|
|
53
|
+
localStorageKeyStorage: () => localStorageKeyStorage,
|
|
54
|
+
memoryKeyStorage: () => memoryKeyStorage,
|
|
55
|
+
openaiCompatibleProvider: () => openaiCompatibleProvider,
|
|
49
56
|
replaceAll: () => replaceAll,
|
|
50
57
|
replaceCurrent: () => replaceCurrent,
|
|
51
58
|
resolveAction: () => resolveAction,
|
|
@@ -53,7 +60,10 @@ __export(index_exports, {
|
|
|
53
60
|
searchAndReplaceKey: () => searchAndReplaceKey,
|
|
54
61
|
searchNext: () => searchNext,
|
|
55
62
|
searchPrev: () => searchPrev,
|
|
56
|
-
setSearchQuery: () => setSearchQuery
|
|
63
|
+
setSearchQuery: () => setSearchQuery,
|
|
64
|
+
toAIStreamFn: () => toAIStreamFn,
|
|
65
|
+
trimContextAfter: () => trimContextAfter,
|
|
66
|
+
trimContextBefore: () => trimContextBefore
|
|
57
67
|
});
|
|
58
68
|
module.exports = __toCommonJS(index_exports);
|
|
59
69
|
|
|
@@ -1953,15 +1963,281 @@ var SubdocumentProvider = class {
|
|
|
1953
1963
|
this.activeQueue.length = 0;
|
|
1954
1964
|
}
|
|
1955
1965
|
};
|
|
1966
|
+
|
|
1967
|
+
// src/ai/prompts.ts
|
|
1968
|
+
var CONTEXT_CHAR_CAP = 1500;
|
|
1969
|
+
var ONLY_RESULT = "Return ONLY the resulting text \u2014 no preamble, no explanation, no commentary, no markdown fences.";
|
|
1970
|
+
var SYSTEM_PROMPTS = {
|
|
1971
|
+
rewrite: `You are a writing assistant. Rewrite the selected text to improve clarity, flow, and style while preserving its meaning. ${ONLY_RESULT}`,
|
|
1972
|
+
summarize: `You are a writing assistant. Summarize the selected text concisely, keeping every key point. ${ONLY_RESULT}`,
|
|
1973
|
+
grammar: `You are a writing assistant. Fix grammar, spelling, and punctuation in the selected text without changing its meaning or voice. ${ONLY_RESULT}`,
|
|
1974
|
+
tone: `You are a writing assistant. Adjust the tone of the selected text as instructed while preserving its content. ${ONLY_RESULT}`,
|
|
1975
|
+
translate: `You are a writing assistant. Translate the selected text as instructed, preserving structure and formatting. ${ONLY_RESULT}`,
|
|
1976
|
+
expand: `You are a writing assistant. Expand the selected text with relevant detail, staying on topic and matching the existing style. ${ONLY_RESULT}`,
|
|
1977
|
+
shorten: `You are a writing assistant. Shorten the selected text, keeping its essential meaning. ${ONLY_RESULT}`,
|
|
1978
|
+
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}`,
|
|
1979
|
+
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."
|
|
1980
|
+
};
|
|
1981
|
+
function trimContextBefore(text) {
|
|
1982
|
+
return text.length > CONTEXT_CHAR_CAP ? `\u2026${text.slice(-CONTEXT_CHAR_CAP)}` : text;
|
|
1983
|
+
}
|
|
1984
|
+
function trimContextAfter(text) {
|
|
1985
|
+
return text.length > CONTEXT_CHAR_CAP ? `${text.slice(0, CONTEXT_CHAR_CAP)}\u2026` : text;
|
|
1986
|
+
}
|
|
1987
|
+
function quote(label, text) {
|
|
1988
|
+
return `${label}:
|
|
1989
|
+
"""${text}"""`;
|
|
1990
|
+
}
|
|
1991
|
+
function contextSection(req) {
|
|
1992
|
+
const parts = [];
|
|
1993
|
+
if (req.context) {
|
|
1994
|
+
const before = trimContextBefore(req.context.before ?? "");
|
|
1995
|
+
const after = trimContextAfter(req.context.after ?? "");
|
|
1996
|
+
if (before) parts.push(quote("Text before", before));
|
|
1997
|
+
if (after) parts.push(quote("Text after", after));
|
|
1998
|
+
}
|
|
1999
|
+
return parts;
|
|
2000
|
+
}
|
|
2001
|
+
function buildAIPrompt(req) {
|
|
2002
|
+
let system;
|
|
2003
|
+
const userParts = [];
|
|
2004
|
+
switch (req.action) {
|
|
2005
|
+
case "rewrite":
|
|
2006
|
+
case "summarize":
|
|
2007
|
+
case "grammar":
|
|
2008
|
+
case "expand":
|
|
2009
|
+
case "shorten":
|
|
2010
|
+
system = SYSTEM_PROMPTS[req.action];
|
|
2011
|
+
userParts.push(quote("Selected text", req.selection ?? ""));
|
|
2012
|
+
break;
|
|
2013
|
+
case "tone":
|
|
2014
|
+
system = SYSTEM_PROMPTS.tone;
|
|
2015
|
+
userParts.push(quote("Selected text", req.selection ?? ""));
|
|
2016
|
+
userParts.push(`Target tone: ${req.prompt ?? "professional"}`);
|
|
2017
|
+
break;
|
|
2018
|
+
case "translate":
|
|
2019
|
+
system = SYSTEM_PROMPTS.translate;
|
|
2020
|
+
userParts.push(quote("Selected text", req.selection ?? ""));
|
|
2021
|
+
userParts.push(`Target language: ${req.prompt ?? "English"}`);
|
|
2022
|
+
break;
|
|
2023
|
+
case "generate":
|
|
2024
|
+
system = SYSTEM_PROMPTS.generate;
|
|
2025
|
+
userParts.push(`Instruction: ${req.prompt ?? ""}`);
|
|
2026
|
+
break;
|
|
2027
|
+
case "chat":
|
|
2028
|
+
system = SYSTEM_PROMPTS.chat;
|
|
2029
|
+
if (req.selection) userParts.push(quote("Selected text", req.selection));
|
|
2030
|
+
userParts.push(req.prompt ?? "");
|
|
2031
|
+
break;
|
|
2032
|
+
case "draft":
|
|
2033
|
+
throw new Error(
|
|
2034
|
+
"action 'draft' is not built via buildAIPrompt \u2014 use the aiDraft port (Phase 7E)"
|
|
2035
|
+
);
|
|
2036
|
+
default: {
|
|
2037
|
+
const exhaustive = req.action;
|
|
2038
|
+
throw new Error(`Unknown AI action: ${String(exhaustive)}`);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
userParts.push(...contextSection(req));
|
|
2042
|
+
return { system, prompt: userParts.filter(Boolean).join("\n\n") };
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// src/ai/adapter.ts
|
|
2046
|
+
function toAIStreamFn(provider) {
|
|
2047
|
+
return async function* aiStream(req, signal) {
|
|
2048
|
+
const { system, prompt } = buildAIPrompt(req);
|
|
2049
|
+
for await (const event of provider.complete({ system, prompt, signal })) {
|
|
2050
|
+
if (event.type === "delta") {
|
|
2051
|
+
yield event.text;
|
|
2052
|
+
} else if (event.type === "done") {
|
|
2053
|
+
return;
|
|
2054
|
+
} else {
|
|
2055
|
+
throw event.error;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// src/ai/keyStorage.ts
|
|
2062
|
+
function memoryKeyStorage(initial = null) {
|
|
2063
|
+
let value = initial;
|
|
2064
|
+
return {
|
|
2065
|
+
async get() {
|
|
2066
|
+
return value;
|
|
2067
|
+
},
|
|
2068
|
+
async set(next) {
|
|
2069
|
+
value = next;
|
|
2070
|
+
},
|
|
2071
|
+
async clear() {
|
|
2072
|
+
value = null;
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
var LOCAL_STORAGE_KEY = "docflow:ai-config";
|
|
2077
|
+
function localStorageKeyStorage(key2 = LOCAL_STORAGE_KEY, storage) {
|
|
2078
|
+
const store = () => {
|
|
2079
|
+
const s = storage ?? globalThis.localStorage;
|
|
2080
|
+
if (!s) throw new Error("localStorageKeyStorage: no Storage available (SSR?)");
|
|
2081
|
+
return s;
|
|
2082
|
+
};
|
|
2083
|
+
return {
|
|
2084
|
+
async get() {
|
|
2085
|
+
const raw = store().getItem(key2);
|
|
2086
|
+
if (!raw) return null;
|
|
2087
|
+
try {
|
|
2088
|
+
return JSON.parse(raw);
|
|
2089
|
+
} catch {
|
|
2090
|
+
return null;
|
|
2091
|
+
}
|
|
2092
|
+
},
|
|
2093
|
+
async set(value) {
|
|
2094
|
+
store().setItem(key2, JSON.stringify(value));
|
|
2095
|
+
},
|
|
2096
|
+
async clear() {
|
|
2097
|
+
store().removeItem(key2);
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
function httpKeyStorage(urls) {
|
|
2102
|
+
const doFetch = urls.fetchImpl ?? ((...args) => fetch(...args));
|
|
2103
|
+
return {
|
|
2104
|
+
async get() {
|
|
2105
|
+
const res = await doFetch(urls.getUrl, { credentials: "include" });
|
|
2106
|
+
if (res.status === 404) return null;
|
|
2107
|
+
if (!res.ok) throw new Error(`AI config read failed: ${res.status}`);
|
|
2108
|
+
return await res.json();
|
|
2109
|
+
},
|
|
2110
|
+
async set(value) {
|
|
2111
|
+
const res = await doFetch(urls.setUrl, {
|
|
2112
|
+
method: "POST",
|
|
2113
|
+
headers: { "Content-Type": "application/json" },
|
|
2114
|
+
credentials: "include",
|
|
2115
|
+
body: JSON.stringify(value)
|
|
2116
|
+
});
|
|
2117
|
+
if (!res.ok) throw new Error(`AI config write failed: ${res.status}`);
|
|
2118
|
+
},
|
|
2119
|
+
async clear() {
|
|
2120
|
+
const res = await doFetch(urls.deleteUrl, { method: "DELETE", credentials: "include" });
|
|
2121
|
+
if (!res.ok && res.status !== 404) throw new Error(`AI config delete failed: ${res.status}`);
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// src/ai/openaiCompatibleProvider.ts
|
|
2127
|
+
function authHeaders(auth) {
|
|
2128
|
+
switch (auth.type) {
|
|
2129
|
+
case "bearer":
|
|
2130
|
+
return { Authorization: `Bearer ${auth.apiKey}` };
|
|
2131
|
+
case "header":
|
|
2132
|
+
return { [auth.name]: auth.value };
|
|
2133
|
+
case "none":
|
|
2134
|
+
return {};
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
function toError(err) {
|
|
2138
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
2139
|
+
}
|
|
2140
|
+
function openaiCompatibleProvider(config) {
|
|
2141
|
+
const baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2142
|
+
return {
|
|
2143
|
+
async *complete(req) {
|
|
2144
|
+
const controller = new AbortController();
|
|
2145
|
+
const onAbort = () => controller.abort();
|
|
2146
|
+
const callerSignal = req.signal;
|
|
2147
|
+
if (callerSignal) {
|
|
2148
|
+
if (callerSignal.aborted) controller.abort();
|
|
2149
|
+
else callerSignal.addEventListener("abort", onAbort, { once: true });
|
|
2150
|
+
}
|
|
2151
|
+
try {
|
|
2152
|
+
const system = [config.systemPrompt, req.system].filter(Boolean).join("\n\n");
|
|
2153
|
+
const messages = [];
|
|
2154
|
+
if (system) messages.push({ role: "system", content: system });
|
|
2155
|
+
messages.push({ role: "user", content: req.prompt });
|
|
2156
|
+
let res;
|
|
2157
|
+
try {
|
|
2158
|
+
res = await fetch(`${baseUrl}/chat/completions`, {
|
|
2159
|
+
method: "POST",
|
|
2160
|
+
headers: { "Content-Type": "application/json", ...authHeaders(config.auth) },
|
|
2161
|
+
body: JSON.stringify({ model: config.model, messages, stream: true }),
|
|
2162
|
+
signal: controller.signal
|
|
2163
|
+
});
|
|
2164
|
+
} catch (err) {
|
|
2165
|
+
if (controller.signal.aborted) return;
|
|
2166
|
+
yield { type: "error", error: toError(err) };
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
if (!res.ok) {
|
|
2170
|
+
const detail = await res.text().catch(() => "");
|
|
2171
|
+
yield {
|
|
2172
|
+
type: "error",
|
|
2173
|
+
error: new Error(
|
|
2174
|
+
`AI provider responded ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ""}`
|
|
2175
|
+
)
|
|
2176
|
+
};
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
if (!res.body) {
|
|
2180
|
+
yield { type: "error", error: new Error("AI provider response has no body") };
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
const reader = res.body.getReader();
|
|
2184
|
+
const decoder = new TextDecoder();
|
|
2185
|
+
let buffer = "";
|
|
2186
|
+
let sawDone = false;
|
|
2187
|
+
try {
|
|
2188
|
+
for (; ; ) {
|
|
2189
|
+
const { done, value } = await reader.read();
|
|
2190
|
+
if (done) break;
|
|
2191
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2192
|
+
let sep;
|
|
2193
|
+
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
2194
|
+
const block = buffer.slice(0, sep);
|
|
2195
|
+
buffer = buffer.slice(sep + 2);
|
|
2196
|
+
for (const line of block.split("\n")) {
|
|
2197
|
+
if (!line.startsWith("data:")) continue;
|
|
2198
|
+
const data = line.slice("data:".length).trim();
|
|
2199
|
+
if (data === "[DONE]") {
|
|
2200
|
+
sawDone = true;
|
|
2201
|
+
yield { type: "done" };
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
try {
|
|
2205
|
+
const json = JSON.parse(data);
|
|
2206
|
+
const text = json.choices?.[0]?.delta?.content;
|
|
2207
|
+
if (text) yield { type: "delta", text };
|
|
2208
|
+
} catch {
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
} catch (err) {
|
|
2214
|
+
if (!controller.signal.aborted) {
|
|
2215
|
+
yield { type: "error", error: toError(err) };
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
} finally {
|
|
2219
|
+
reader.releaseLock();
|
|
2220
|
+
}
|
|
2221
|
+
if (!sawDone && !controller.signal.aborted) yield { type: "done" };
|
|
2222
|
+
} finally {
|
|
2223
|
+
callerSignal?.removeEventListener("abort", onAbort);
|
|
2224
|
+
controller.abort();
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
1956
2229
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1957
2230
|
0 && (module.exports = {
|
|
1958
2231
|
BlockAttributesExtension,
|
|
2232
|
+
CONTEXT_CHAR_CAP,
|
|
1959
2233
|
EditorContextExtension,
|
|
1960
2234
|
FontSizeExtension,
|
|
2235
|
+
LOCAL_STORAGE_KEY,
|
|
1961
2236
|
PAGE_SIZES,
|
|
1962
2237
|
PaginationPlus,
|
|
1963
2238
|
SearchAndReplaceExtension,
|
|
1964
2239
|
SubdocumentProvider,
|
|
2240
|
+
buildAIPrompt,
|
|
1965
2241
|
clearSearch,
|
|
1966
2242
|
collaborationExtensions,
|
|
1967
2243
|
collectExtensions,
|
|
@@ -1971,6 +2247,10 @@ var SubdocumentProvider = class {
|
|
|
1971
2247
|
definePlugin,
|
|
1972
2248
|
findMatches,
|
|
1973
2249
|
getSearchState,
|
|
2250
|
+
httpKeyStorage,
|
|
2251
|
+
localStorageKeyStorage,
|
|
2252
|
+
memoryKeyStorage,
|
|
2253
|
+
openaiCompatibleProvider,
|
|
1974
2254
|
replaceAll,
|
|
1975
2255
|
replaceCurrent,
|
|
1976
2256
|
resolveAction,
|
|
@@ -1978,5 +2258,8 @@ var SubdocumentProvider = class {
|
|
|
1978
2258
|
searchAndReplaceKey,
|
|
1979
2259
|
searchNext,
|
|
1980
2260
|
searchPrev,
|
|
1981
|
-
setSearchQuery
|
|
2261
|
+
setSearchQuery,
|
|
2262
|
+
toAIStreamFn,
|
|
2263
|
+
trimContextAfter,
|
|
2264
|
+
trimContextBefore
|
|
1982
2265
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -203,12 +203,51 @@ interface CitationPort {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
|
-
*
|
|
206
|
+
* The library's only knowledge of LLMs (pluggable AI provider — issue #119).
|
|
207
|
+
*
|
|
208
|
+
* `AIProvider` is a thin transport: one prompt in, a stream of events out.
|
|
209
|
+
* The library imports only this type — never an implementation. Hosts plug in
|
|
210
|
+
* `openaiCompatibleProvider` (the default we ship) or their own class wrapping
|
|
211
|
+
* an agent SDK, a proxy, or an in-process model. Agent orchestration, tool
|
|
212
|
+
* calling, RAG, and multi-turn memory live behind the host's endpoint, not in
|
|
213
|
+
* the library. See docs/plans/PLUGGABLE_AI_PROVIDER.md §4.1.
|
|
214
|
+
*/
|
|
215
|
+
type StreamEvent = {
|
|
216
|
+
type: 'delta';
|
|
217
|
+
text: string;
|
|
218
|
+
} | {
|
|
219
|
+
type: 'done';
|
|
220
|
+
stopReason?: string;
|
|
221
|
+
} | {
|
|
222
|
+
type: 'error';
|
|
223
|
+
error: Error;
|
|
224
|
+
};
|
|
225
|
+
interface AICompleteRequest {
|
|
226
|
+
system?: string;
|
|
227
|
+
prompt: string;
|
|
228
|
+
signal?: AbortSignal;
|
|
229
|
+
}
|
|
230
|
+
interface AIProvider {
|
|
231
|
+
complete(req: AICompleteRequest): AsyncIterable<StreamEvent>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Shared AI action types (Phase 7; pluggable AI provider — issue #119).
|
|
207
236
|
*
|
|
208
237
|
* The library (aiPlugin) is provider-agnostic: it never names a URL or holds a
|
|
209
|
-
* key. The
|
|
210
|
-
*
|
|
238
|
+
* key. The host injects `aiStream` / `aiDraft` via `EditorOptions` — either a
|
|
239
|
+
* hand-written function or `toAIStreamFn(openaiCompatibleProvider({...}))`
|
|
240
|
+
* (see ./provider.ts, ./openaiCompatibleProvider.ts).
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Factory shape for hosts that hand the library a provider object rather than
|
|
245
|
+
* a plain function (plan §4.2). `toAIStreamFn` adapts an `AIProvider` to the
|
|
246
|
+
* `AIStreamFn` port, so both shapes are accepted at the boundary.
|
|
211
247
|
*/
|
|
248
|
+
type AIProviderFactory = (config: {
|
|
249
|
+
signal?: AbortSignal;
|
|
250
|
+
}) => AIProvider;
|
|
212
251
|
type AIAction = 'rewrite' | 'summarize' | 'grammar' | 'tone' | 'translate' | 'expand' | 'shorten' | 'generate' | 'chat' | 'draft';
|
|
213
252
|
interface AIActionRequest {
|
|
214
253
|
action: AIAction;
|
|
@@ -455,4 +494,109 @@ declare class SubdocumentProvider {
|
|
|
455
494
|
destroy(): void;
|
|
456
495
|
}
|
|
457
496
|
|
|
458
|
-
|
|
497
|
+
declare function toAIStreamFn(provider: AIProvider): AIStreamFn;
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Per-action prompt assembly (pluggable AI provider — issue #119).
|
|
501
|
+
*
|
|
502
|
+
* Ported from the Phase 7 server proxy (`apps/server/src/ai/context.ts`) so
|
|
503
|
+
* the browser can call the LLM directly: the selection + BOUNDED surrounding
|
|
504
|
+
* text is sent — never the whole document (cost + privacy). Untrusted document
|
|
505
|
+
* text stays in the user prompt; instructions live in the system prompt
|
|
506
|
+
* (prompt-injection hygiene).
|
|
507
|
+
*/
|
|
508
|
+
|
|
509
|
+
/** Max characters of surrounding context sent on each side of the selection. */
|
|
510
|
+
declare const CONTEXT_CHAR_CAP = 1500;
|
|
511
|
+
/** Last `CONTEXT_CHAR_CAP` chars of the preceding text (nearest context wins). */
|
|
512
|
+
declare function trimContextBefore(text: string): string;
|
|
513
|
+
/** First `CONTEXT_CHAR_CAP` chars of the following text. */
|
|
514
|
+
declare function trimContextAfter(text: string): string;
|
|
515
|
+
/**
|
|
516
|
+
* Assemble the per-action system prompt + user prompt from a bounded request.
|
|
517
|
+
* `draft` is rejected: RAG-cited drafting goes through the `aiDraft` port,
|
|
518
|
+
* which owns its grounded-prompt assembly (Phase 7E).
|
|
519
|
+
*/
|
|
520
|
+
declare function buildAIPrompt(req: AIActionRequest): {
|
|
521
|
+
system: string;
|
|
522
|
+
prompt: string;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Where the AI provider's config (base URL + credentials + model) lives —
|
|
527
|
+
* the host decides (pluggable AI provider — issue #119, plan §4.4).
|
|
528
|
+
*
|
|
529
|
+
* The URL and the key are a pair and travel together. The library ships three
|
|
530
|
+
* reference implementations; hosts may write their own (e.g. keyed per-tenant
|
|
531
|
+
* in their own backend). No URL is hardcoded anywhere — `httpKeyStorage`
|
|
532
|
+
* receives its endpoints from the host, keeping the library backend-agnostic
|
|
533
|
+
* (docs/LIBRARY_CONTRACT.md rule 3).
|
|
534
|
+
*/
|
|
535
|
+
type Auth = {
|
|
536
|
+
type: 'bearer';
|
|
537
|
+
apiKey: string;
|
|
538
|
+
} | {
|
|
539
|
+
type: 'header';
|
|
540
|
+
name: string;
|
|
541
|
+
value: string;
|
|
542
|
+
} | {
|
|
543
|
+
type: 'none';
|
|
544
|
+
};
|
|
545
|
+
interface AIConfig {
|
|
546
|
+
baseUrl: string;
|
|
547
|
+
auth: Auth;
|
|
548
|
+
model: string;
|
|
549
|
+
}
|
|
550
|
+
interface KeyStorage {
|
|
551
|
+
get(): Promise<AIConfig | null>;
|
|
552
|
+
set(value: AIConfig): Promise<void>;
|
|
553
|
+
clear(): Promise<void>;
|
|
554
|
+
}
|
|
555
|
+
/** In-memory storage — for tests and SSR (no persistence). */
|
|
556
|
+
declare function memoryKeyStorage(initial?: AIConfig | null): KeyStorage;
|
|
557
|
+
declare const LOCAL_STORAGE_KEY = "docflow:ai-config";
|
|
558
|
+
/**
|
|
559
|
+
* Browser `localStorage` persistence — for embedded apps that want the config
|
|
560
|
+
* kept client-side. Note: this stores AI *credentials*, not document data, so
|
|
561
|
+
* it is the host's deliberate choice, not a library persistence leak.
|
|
562
|
+
*/
|
|
563
|
+
declare function localStorageKeyStorage(key?: string, storage?: Storage): KeyStorage;
|
|
564
|
+
interface HttpKeyStorageUrls {
|
|
565
|
+
/** GET → 200 with the AIConfig JSON body, or 404 when unset. */
|
|
566
|
+
getUrl: string;
|
|
567
|
+
/** POST with the AIConfig JSON body. */
|
|
568
|
+
setUrl: string;
|
|
569
|
+
/** DELETE to clear. */
|
|
570
|
+
deleteUrl: string;
|
|
571
|
+
/** Optional fetch override (tests, custom credentials mode). Defaults to global fetch. */
|
|
572
|
+
fetchImpl?: typeof fetch;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Server-backed storage — for our SaaS web app (`/api/ai/config`) and any
|
|
576
|
+
* embedded app that wants credentials kept on its own backend. Endpoints are
|
|
577
|
+
* host-supplied; the library never names a path.
|
|
578
|
+
*/
|
|
579
|
+
declare function httpKeyStorage(urls: HttpKeyStorageUrls): KeyStorage;
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Default `AIProvider` implementation (pluggable AI provider — issue #119,
|
|
583
|
+
* plan §4.3): OpenAI-shaped `POST {baseUrl}/chat/completions` with SSE
|
|
584
|
+
* streaming. Covers OpenAI, Azure-style gateways, Ollama, vLLM, and most
|
|
585
|
+
* OpenAI-compatible proxies.
|
|
586
|
+
*
|
|
587
|
+
* Cancellation-safe: closing the async iterator early (e.g. the user accepts
|
|
588
|
+
* or cancels mid-stream) aborts the in-flight `fetch`; an aborted `req.signal`
|
|
589
|
+
* ends the stream silently rather than yielding an error event.
|
|
590
|
+
*/
|
|
591
|
+
|
|
592
|
+
interface OpenAICompatibleConfig {
|
|
593
|
+
/** Base URL of the OpenAI-compatible endpoint, e.g. `https://api.openai.com/v1`. */
|
|
594
|
+
baseUrl: string;
|
|
595
|
+
auth: Auth;
|
|
596
|
+
model: string;
|
|
597
|
+
/** Prepended to every call's system prompt so hosts don't repeat it. */
|
|
598
|
+
systemPrompt?: string;
|
|
599
|
+
}
|
|
600
|
+
declare function openaiCompatibleProvider(config: OpenAICompatibleConfig): AIProvider;
|
|
601
|
+
|
|
602
|
+
export { type AIAction, type AIActionRequest, type AICompleteRequest, type AIConfig, type AIDraftCitation, type AIDraftEvent, type AIDraftFn, type AIProvider, type AIProviderFactory, type AIStreamFn, type Auth, type AwarenessState, BlockAttributesExtension, CONTEXT_CHAR_CAP, type CitationPort, type CollaborationOptions, type CollaborationSetup, type CslDate, type CslItemData, type CslName, type DocsEditor, type DocsEditorPlugin, EditorContextExtension, type EditorContextOptions, type EditorOptions, FontSizeExtension, type HttpKeyStorageUrls, type ImageUploadHandler, type ImageUploadResult, type KeyStorage, LOCAL_STORAGE_KEY, type OpenAICompatibleConfig, SearchAndReplaceExtension, type SearchMatch, type SearchState, type SlashCommand, type StreamEvent, type SubdocState, SubdocumentProvider, type SubdocumentProviderOptions, type ToolbarItem, buildAIPrompt, clearSearch, collaborationExtensions, collectExtensions, createActionMap, createCollaboration, createEditor, definePlugin, findMatches, getSearchState, httpKeyStorage, localStorageKeyStorage, memoryKeyStorage, openaiCompatibleProvider, replaceAll, replaceCurrent, resolveAction, sanitizePastedHTML, searchAndReplaceKey, searchNext, searchPrev, setSearchQuery, toAIStreamFn, trimContextAfter, trimContextBefore };
|
package/dist/index.d.ts
CHANGED
|
@@ -203,12 +203,51 @@ interface CitationPort {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
|
-
*
|
|
206
|
+
* The library's only knowledge of LLMs (pluggable AI provider — issue #119).
|
|
207
|
+
*
|
|
208
|
+
* `AIProvider` is a thin transport: one prompt in, a stream of events out.
|
|
209
|
+
* The library imports only this type — never an implementation. Hosts plug in
|
|
210
|
+
* `openaiCompatibleProvider` (the default we ship) or their own class wrapping
|
|
211
|
+
* an agent SDK, a proxy, or an in-process model. Agent orchestration, tool
|
|
212
|
+
* calling, RAG, and multi-turn memory live behind the host's endpoint, not in
|
|
213
|
+
* the library. See docs/plans/PLUGGABLE_AI_PROVIDER.md §4.1.
|
|
214
|
+
*/
|
|
215
|
+
type StreamEvent = {
|
|
216
|
+
type: 'delta';
|
|
217
|
+
text: string;
|
|
218
|
+
} | {
|
|
219
|
+
type: 'done';
|
|
220
|
+
stopReason?: string;
|
|
221
|
+
} | {
|
|
222
|
+
type: 'error';
|
|
223
|
+
error: Error;
|
|
224
|
+
};
|
|
225
|
+
interface AICompleteRequest {
|
|
226
|
+
system?: string;
|
|
227
|
+
prompt: string;
|
|
228
|
+
signal?: AbortSignal;
|
|
229
|
+
}
|
|
230
|
+
interface AIProvider {
|
|
231
|
+
complete(req: AICompleteRequest): AsyncIterable<StreamEvent>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Shared AI action types (Phase 7; pluggable AI provider — issue #119).
|
|
207
236
|
*
|
|
208
237
|
* The library (aiPlugin) is provider-agnostic: it never names a URL or holds a
|
|
209
|
-
* key. The
|
|
210
|
-
*
|
|
238
|
+
* key. The host injects `aiStream` / `aiDraft` via `EditorOptions` — either a
|
|
239
|
+
* hand-written function or `toAIStreamFn(openaiCompatibleProvider({...}))`
|
|
240
|
+
* (see ./provider.ts, ./openaiCompatibleProvider.ts).
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Factory shape for hosts that hand the library a provider object rather than
|
|
245
|
+
* a plain function (plan §4.2). `toAIStreamFn` adapts an `AIProvider` to the
|
|
246
|
+
* `AIStreamFn` port, so both shapes are accepted at the boundary.
|
|
211
247
|
*/
|
|
248
|
+
type AIProviderFactory = (config: {
|
|
249
|
+
signal?: AbortSignal;
|
|
250
|
+
}) => AIProvider;
|
|
212
251
|
type AIAction = 'rewrite' | 'summarize' | 'grammar' | 'tone' | 'translate' | 'expand' | 'shorten' | 'generate' | 'chat' | 'draft';
|
|
213
252
|
interface AIActionRequest {
|
|
214
253
|
action: AIAction;
|
|
@@ -455,4 +494,109 @@ declare class SubdocumentProvider {
|
|
|
455
494
|
destroy(): void;
|
|
456
495
|
}
|
|
457
496
|
|
|
458
|
-
|
|
497
|
+
declare function toAIStreamFn(provider: AIProvider): AIStreamFn;
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Per-action prompt assembly (pluggable AI provider — issue #119).
|
|
501
|
+
*
|
|
502
|
+
* Ported from the Phase 7 server proxy (`apps/server/src/ai/context.ts`) so
|
|
503
|
+
* the browser can call the LLM directly: the selection + BOUNDED surrounding
|
|
504
|
+
* text is sent — never the whole document (cost + privacy). Untrusted document
|
|
505
|
+
* text stays in the user prompt; instructions live in the system prompt
|
|
506
|
+
* (prompt-injection hygiene).
|
|
507
|
+
*/
|
|
508
|
+
|
|
509
|
+
/** Max characters of surrounding context sent on each side of the selection. */
|
|
510
|
+
declare const CONTEXT_CHAR_CAP = 1500;
|
|
511
|
+
/** Last `CONTEXT_CHAR_CAP` chars of the preceding text (nearest context wins). */
|
|
512
|
+
declare function trimContextBefore(text: string): string;
|
|
513
|
+
/** First `CONTEXT_CHAR_CAP` chars of the following text. */
|
|
514
|
+
declare function trimContextAfter(text: string): string;
|
|
515
|
+
/**
|
|
516
|
+
* Assemble the per-action system prompt + user prompt from a bounded request.
|
|
517
|
+
* `draft` is rejected: RAG-cited drafting goes through the `aiDraft` port,
|
|
518
|
+
* which owns its grounded-prompt assembly (Phase 7E).
|
|
519
|
+
*/
|
|
520
|
+
declare function buildAIPrompt(req: AIActionRequest): {
|
|
521
|
+
system: string;
|
|
522
|
+
prompt: string;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Where the AI provider's config (base URL + credentials + model) lives —
|
|
527
|
+
* the host decides (pluggable AI provider — issue #119, plan §4.4).
|
|
528
|
+
*
|
|
529
|
+
* The URL and the key are a pair and travel together. The library ships three
|
|
530
|
+
* reference implementations; hosts may write their own (e.g. keyed per-tenant
|
|
531
|
+
* in their own backend). No URL is hardcoded anywhere — `httpKeyStorage`
|
|
532
|
+
* receives its endpoints from the host, keeping the library backend-agnostic
|
|
533
|
+
* (docs/LIBRARY_CONTRACT.md rule 3).
|
|
534
|
+
*/
|
|
535
|
+
type Auth = {
|
|
536
|
+
type: 'bearer';
|
|
537
|
+
apiKey: string;
|
|
538
|
+
} | {
|
|
539
|
+
type: 'header';
|
|
540
|
+
name: string;
|
|
541
|
+
value: string;
|
|
542
|
+
} | {
|
|
543
|
+
type: 'none';
|
|
544
|
+
};
|
|
545
|
+
interface AIConfig {
|
|
546
|
+
baseUrl: string;
|
|
547
|
+
auth: Auth;
|
|
548
|
+
model: string;
|
|
549
|
+
}
|
|
550
|
+
interface KeyStorage {
|
|
551
|
+
get(): Promise<AIConfig | null>;
|
|
552
|
+
set(value: AIConfig): Promise<void>;
|
|
553
|
+
clear(): Promise<void>;
|
|
554
|
+
}
|
|
555
|
+
/** In-memory storage — for tests and SSR (no persistence). */
|
|
556
|
+
declare function memoryKeyStorage(initial?: AIConfig | null): KeyStorage;
|
|
557
|
+
declare const LOCAL_STORAGE_KEY = "docflow:ai-config";
|
|
558
|
+
/**
|
|
559
|
+
* Browser `localStorage` persistence — for embedded apps that want the config
|
|
560
|
+
* kept client-side. Note: this stores AI *credentials*, not document data, so
|
|
561
|
+
* it is the host's deliberate choice, not a library persistence leak.
|
|
562
|
+
*/
|
|
563
|
+
declare function localStorageKeyStorage(key?: string, storage?: Storage): KeyStorage;
|
|
564
|
+
interface HttpKeyStorageUrls {
|
|
565
|
+
/** GET → 200 with the AIConfig JSON body, or 404 when unset. */
|
|
566
|
+
getUrl: string;
|
|
567
|
+
/** POST with the AIConfig JSON body. */
|
|
568
|
+
setUrl: string;
|
|
569
|
+
/** DELETE to clear. */
|
|
570
|
+
deleteUrl: string;
|
|
571
|
+
/** Optional fetch override (tests, custom credentials mode). Defaults to global fetch. */
|
|
572
|
+
fetchImpl?: typeof fetch;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Server-backed storage — for our SaaS web app (`/api/ai/config`) and any
|
|
576
|
+
* embedded app that wants credentials kept on its own backend. Endpoints are
|
|
577
|
+
* host-supplied; the library never names a path.
|
|
578
|
+
*/
|
|
579
|
+
declare function httpKeyStorage(urls: HttpKeyStorageUrls): KeyStorage;
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Default `AIProvider` implementation (pluggable AI provider — issue #119,
|
|
583
|
+
* plan §4.3): OpenAI-shaped `POST {baseUrl}/chat/completions` with SSE
|
|
584
|
+
* streaming. Covers OpenAI, Azure-style gateways, Ollama, vLLM, and most
|
|
585
|
+
* OpenAI-compatible proxies.
|
|
586
|
+
*
|
|
587
|
+
* Cancellation-safe: closing the async iterator early (e.g. the user accepts
|
|
588
|
+
* or cancels mid-stream) aborts the in-flight `fetch`; an aborted `req.signal`
|
|
589
|
+
* ends the stream silently rather than yielding an error event.
|
|
590
|
+
*/
|
|
591
|
+
|
|
592
|
+
interface OpenAICompatibleConfig {
|
|
593
|
+
/** Base URL of the OpenAI-compatible endpoint, e.g. `https://api.openai.com/v1`. */
|
|
594
|
+
baseUrl: string;
|
|
595
|
+
auth: Auth;
|
|
596
|
+
model: string;
|
|
597
|
+
/** Prepended to every call's system prompt so hosts don't repeat it. */
|
|
598
|
+
systemPrompt?: string;
|
|
599
|
+
}
|
|
600
|
+
declare function openaiCompatibleProvider(config: OpenAICompatibleConfig): AIProvider;
|
|
601
|
+
|
|
602
|
+
export { type AIAction, type AIActionRequest, type AICompleteRequest, type AIConfig, type AIDraftCitation, type AIDraftEvent, type AIDraftFn, type AIProvider, type AIProviderFactory, type AIStreamFn, type Auth, type AwarenessState, BlockAttributesExtension, CONTEXT_CHAR_CAP, type CitationPort, type CollaborationOptions, type CollaborationSetup, type CslDate, type CslItemData, type CslName, type DocsEditor, type DocsEditorPlugin, EditorContextExtension, type EditorContextOptions, type EditorOptions, FontSizeExtension, type HttpKeyStorageUrls, type ImageUploadHandler, type ImageUploadResult, type KeyStorage, LOCAL_STORAGE_KEY, type OpenAICompatibleConfig, SearchAndReplaceExtension, type SearchMatch, type SearchState, type SlashCommand, type StreamEvent, type SubdocState, SubdocumentProvider, type SubdocumentProviderOptions, type ToolbarItem, buildAIPrompt, clearSearch, collaborationExtensions, collectExtensions, createActionMap, createCollaboration, createEditor, definePlugin, findMatches, getSearchState, httpKeyStorage, localStorageKeyStorage, memoryKeyStorage, openaiCompatibleProvider, replaceAll, replaceCurrent, resolveAction, sanitizePastedHTML, searchAndReplaceKey, searchNext, searchPrev, setSearchQuery, toAIStreamFn, trimContextAfter, trimContextBefore };
|
package/dist/index.js
CHANGED
|
@@ -1894,14 +1894,280 @@ var SubdocumentProvider = class {
|
|
|
1894
1894
|
this.activeQueue.length = 0;
|
|
1895
1895
|
}
|
|
1896
1896
|
};
|
|
1897
|
+
|
|
1898
|
+
// src/ai/prompts.ts
|
|
1899
|
+
var CONTEXT_CHAR_CAP = 1500;
|
|
1900
|
+
var ONLY_RESULT = "Return ONLY the resulting text \u2014 no preamble, no explanation, no commentary, no markdown fences.";
|
|
1901
|
+
var SYSTEM_PROMPTS = {
|
|
1902
|
+
rewrite: `You are a writing assistant. Rewrite the selected text to improve clarity, flow, and style while preserving its meaning. ${ONLY_RESULT}`,
|
|
1903
|
+
summarize: `You are a writing assistant. Summarize the selected text concisely, keeping every key point. ${ONLY_RESULT}`,
|
|
1904
|
+
grammar: `You are a writing assistant. Fix grammar, spelling, and punctuation in the selected text without changing its meaning or voice. ${ONLY_RESULT}`,
|
|
1905
|
+
tone: `You are a writing assistant. Adjust the tone of the selected text as instructed while preserving its content. ${ONLY_RESULT}`,
|
|
1906
|
+
translate: `You are a writing assistant. Translate the selected text as instructed, preserving structure and formatting. ${ONLY_RESULT}`,
|
|
1907
|
+
expand: `You are a writing assistant. Expand the selected text with relevant detail, staying on topic and matching the existing style. ${ONLY_RESULT}`,
|
|
1908
|
+
shorten: `You are a writing assistant. Shorten the selected text, keeping its essential meaning. ${ONLY_RESULT}`,
|
|
1909
|
+
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}`,
|
|
1910
|
+
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."
|
|
1911
|
+
};
|
|
1912
|
+
function trimContextBefore(text) {
|
|
1913
|
+
return text.length > CONTEXT_CHAR_CAP ? `\u2026${text.slice(-CONTEXT_CHAR_CAP)}` : text;
|
|
1914
|
+
}
|
|
1915
|
+
function trimContextAfter(text) {
|
|
1916
|
+
return text.length > CONTEXT_CHAR_CAP ? `${text.slice(0, CONTEXT_CHAR_CAP)}\u2026` : text;
|
|
1917
|
+
}
|
|
1918
|
+
function quote(label, text) {
|
|
1919
|
+
return `${label}:
|
|
1920
|
+
"""${text}"""`;
|
|
1921
|
+
}
|
|
1922
|
+
function contextSection(req) {
|
|
1923
|
+
const parts = [];
|
|
1924
|
+
if (req.context) {
|
|
1925
|
+
const before = trimContextBefore(req.context.before ?? "");
|
|
1926
|
+
const after = trimContextAfter(req.context.after ?? "");
|
|
1927
|
+
if (before) parts.push(quote("Text before", before));
|
|
1928
|
+
if (after) parts.push(quote("Text after", after));
|
|
1929
|
+
}
|
|
1930
|
+
return parts;
|
|
1931
|
+
}
|
|
1932
|
+
function buildAIPrompt(req) {
|
|
1933
|
+
let system;
|
|
1934
|
+
const userParts = [];
|
|
1935
|
+
switch (req.action) {
|
|
1936
|
+
case "rewrite":
|
|
1937
|
+
case "summarize":
|
|
1938
|
+
case "grammar":
|
|
1939
|
+
case "expand":
|
|
1940
|
+
case "shorten":
|
|
1941
|
+
system = SYSTEM_PROMPTS[req.action];
|
|
1942
|
+
userParts.push(quote("Selected text", req.selection ?? ""));
|
|
1943
|
+
break;
|
|
1944
|
+
case "tone":
|
|
1945
|
+
system = SYSTEM_PROMPTS.tone;
|
|
1946
|
+
userParts.push(quote("Selected text", req.selection ?? ""));
|
|
1947
|
+
userParts.push(`Target tone: ${req.prompt ?? "professional"}`);
|
|
1948
|
+
break;
|
|
1949
|
+
case "translate":
|
|
1950
|
+
system = SYSTEM_PROMPTS.translate;
|
|
1951
|
+
userParts.push(quote("Selected text", req.selection ?? ""));
|
|
1952
|
+
userParts.push(`Target language: ${req.prompt ?? "English"}`);
|
|
1953
|
+
break;
|
|
1954
|
+
case "generate":
|
|
1955
|
+
system = SYSTEM_PROMPTS.generate;
|
|
1956
|
+
userParts.push(`Instruction: ${req.prompt ?? ""}`);
|
|
1957
|
+
break;
|
|
1958
|
+
case "chat":
|
|
1959
|
+
system = SYSTEM_PROMPTS.chat;
|
|
1960
|
+
if (req.selection) userParts.push(quote("Selected text", req.selection));
|
|
1961
|
+
userParts.push(req.prompt ?? "");
|
|
1962
|
+
break;
|
|
1963
|
+
case "draft":
|
|
1964
|
+
throw new Error(
|
|
1965
|
+
"action 'draft' is not built via buildAIPrompt \u2014 use the aiDraft port (Phase 7E)"
|
|
1966
|
+
);
|
|
1967
|
+
default: {
|
|
1968
|
+
const exhaustive = req.action;
|
|
1969
|
+
throw new Error(`Unknown AI action: ${String(exhaustive)}`);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
userParts.push(...contextSection(req));
|
|
1973
|
+
return { system, prompt: userParts.filter(Boolean).join("\n\n") };
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// src/ai/adapter.ts
|
|
1977
|
+
function toAIStreamFn(provider) {
|
|
1978
|
+
return async function* aiStream(req, signal) {
|
|
1979
|
+
const { system, prompt } = buildAIPrompt(req);
|
|
1980
|
+
for await (const event of provider.complete({ system, prompt, signal })) {
|
|
1981
|
+
if (event.type === "delta") {
|
|
1982
|
+
yield event.text;
|
|
1983
|
+
} else if (event.type === "done") {
|
|
1984
|
+
return;
|
|
1985
|
+
} else {
|
|
1986
|
+
throw event.error;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// src/ai/keyStorage.ts
|
|
1993
|
+
function memoryKeyStorage(initial = null) {
|
|
1994
|
+
let value = initial;
|
|
1995
|
+
return {
|
|
1996
|
+
async get() {
|
|
1997
|
+
return value;
|
|
1998
|
+
},
|
|
1999
|
+
async set(next) {
|
|
2000
|
+
value = next;
|
|
2001
|
+
},
|
|
2002
|
+
async clear() {
|
|
2003
|
+
value = null;
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
var LOCAL_STORAGE_KEY = "docflow:ai-config";
|
|
2008
|
+
function localStorageKeyStorage(key2 = LOCAL_STORAGE_KEY, storage) {
|
|
2009
|
+
const store = () => {
|
|
2010
|
+
const s = storage ?? globalThis.localStorage;
|
|
2011
|
+
if (!s) throw new Error("localStorageKeyStorage: no Storage available (SSR?)");
|
|
2012
|
+
return s;
|
|
2013
|
+
};
|
|
2014
|
+
return {
|
|
2015
|
+
async get() {
|
|
2016
|
+
const raw = store().getItem(key2);
|
|
2017
|
+
if (!raw) return null;
|
|
2018
|
+
try {
|
|
2019
|
+
return JSON.parse(raw);
|
|
2020
|
+
} catch {
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
},
|
|
2024
|
+
async set(value) {
|
|
2025
|
+
store().setItem(key2, JSON.stringify(value));
|
|
2026
|
+
},
|
|
2027
|
+
async clear() {
|
|
2028
|
+
store().removeItem(key2);
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
function httpKeyStorage(urls) {
|
|
2033
|
+
const doFetch = urls.fetchImpl ?? ((...args) => fetch(...args));
|
|
2034
|
+
return {
|
|
2035
|
+
async get() {
|
|
2036
|
+
const res = await doFetch(urls.getUrl, { credentials: "include" });
|
|
2037
|
+
if (res.status === 404) return null;
|
|
2038
|
+
if (!res.ok) throw new Error(`AI config read failed: ${res.status}`);
|
|
2039
|
+
return await res.json();
|
|
2040
|
+
},
|
|
2041
|
+
async set(value) {
|
|
2042
|
+
const res = await doFetch(urls.setUrl, {
|
|
2043
|
+
method: "POST",
|
|
2044
|
+
headers: { "Content-Type": "application/json" },
|
|
2045
|
+
credentials: "include",
|
|
2046
|
+
body: JSON.stringify(value)
|
|
2047
|
+
});
|
|
2048
|
+
if (!res.ok) throw new Error(`AI config write failed: ${res.status}`);
|
|
2049
|
+
},
|
|
2050
|
+
async clear() {
|
|
2051
|
+
const res = await doFetch(urls.deleteUrl, { method: "DELETE", credentials: "include" });
|
|
2052
|
+
if (!res.ok && res.status !== 404) throw new Error(`AI config delete failed: ${res.status}`);
|
|
2053
|
+
}
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// src/ai/openaiCompatibleProvider.ts
|
|
2058
|
+
function authHeaders(auth) {
|
|
2059
|
+
switch (auth.type) {
|
|
2060
|
+
case "bearer":
|
|
2061
|
+
return { Authorization: `Bearer ${auth.apiKey}` };
|
|
2062
|
+
case "header":
|
|
2063
|
+
return { [auth.name]: auth.value };
|
|
2064
|
+
case "none":
|
|
2065
|
+
return {};
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
function toError(err) {
|
|
2069
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
2070
|
+
}
|
|
2071
|
+
function openaiCompatibleProvider(config) {
|
|
2072
|
+
const baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2073
|
+
return {
|
|
2074
|
+
async *complete(req) {
|
|
2075
|
+
const controller = new AbortController();
|
|
2076
|
+
const onAbort = () => controller.abort();
|
|
2077
|
+
const callerSignal = req.signal;
|
|
2078
|
+
if (callerSignal) {
|
|
2079
|
+
if (callerSignal.aborted) controller.abort();
|
|
2080
|
+
else callerSignal.addEventListener("abort", onAbort, { once: true });
|
|
2081
|
+
}
|
|
2082
|
+
try {
|
|
2083
|
+
const system = [config.systemPrompt, req.system].filter(Boolean).join("\n\n");
|
|
2084
|
+
const messages = [];
|
|
2085
|
+
if (system) messages.push({ role: "system", content: system });
|
|
2086
|
+
messages.push({ role: "user", content: req.prompt });
|
|
2087
|
+
let res;
|
|
2088
|
+
try {
|
|
2089
|
+
res = await fetch(`${baseUrl}/chat/completions`, {
|
|
2090
|
+
method: "POST",
|
|
2091
|
+
headers: { "Content-Type": "application/json", ...authHeaders(config.auth) },
|
|
2092
|
+
body: JSON.stringify({ model: config.model, messages, stream: true }),
|
|
2093
|
+
signal: controller.signal
|
|
2094
|
+
});
|
|
2095
|
+
} catch (err) {
|
|
2096
|
+
if (controller.signal.aborted) return;
|
|
2097
|
+
yield { type: "error", error: toError(err) };
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
if (!res.ok) {
|
|
2101
|
+
const detail = await res.text().catch(() => "");
|
|
2102
|
+
yield {
|
|
2103
|
+
type: "error",
|
|
2104
|
+
error: new Error(
|
|
2105
|
+
`AI provider responded ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ""}`
|
|
2106
|
+
)
|
|
2107
|
+
};
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
if (!res.body) {
|
|
2111
|
+
yield { type: "error", error: new Error("AI provider response has no body") };
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
const reader = res.body.getReader();
|
|
2115
|
+
const decoder = new TextDecoder();
|
|
2116
|
+
let buffer = "";
|
|
2117
|
+
let sawDone = false;
|
|
2118
|
+
try {
|
|
2119
|
+
for (; ; ) {
|
|
2120
|
+
const { done, value } = await reader.read();
|
|
2121
|
+
if (done) break;
|
|
2122
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2123
|
+
let sep;
|
|
2124
|
+
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
2125
|
+
const block = buffer.slice(0, sep);
|
|
2126
|
+
buffer = buffer.slice(sep + 2);
|
|
2127
|
+
for (const line of block.split("\n")) {
|
|
2128
|
+
if (!line.startsWith("data:")) continue;
|
|
2129
|
+
const data = line.slice("data:".length).trim();
|
|
2130
|
+
if (data === "[DONE]") {
|
|
2131
|
+
sawDone = true;
|
|
2132
|
+
yield { type: "done" };
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
try {
|
|
2136
|
+
const json = JSON.parse(data);
|
|
2137
|
+
const text = json.choices?.[0]?.delta?.content;
|
|
2138
|
+
if (text) yield { type: "delta", text };
|
|
2139
|
+
} catch {
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
} catch (err) {
|
|
2145
|
+
if (!controller.signal.aborted) {
|
|
2146
|
+
yield { type: "error", error: toError(err) };
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
} finally {
|
|
2150
|
+
reader.releaseLock();
|
|
2151
|
+
}
|
|
2152
|
+
if (!sawDone && !controller.signal.aborted) yield { type: "done" };
|
|
2153
|
+
} finally {
|
|
2154
|
+
callerSignal?.removeEventListener("abort", onAbort);
|
|
2155
|
+
controller.abort();
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
1897
2160
|
export {
|
|
1898
2161
|
BlockAttributesExtension,
|
|
2162
|
+
CONTEXT_CHAR_CAP,
|
|
1899
2163
|
EditorContextExtension,
|
|
1900
2164
|
FontSizeExtension,
|
|
2165
|
+
LOCAL_STORAGE_KEY,
|
|
1901
2166
|
PAGE_SIZES,
|
|
1902
2167
|
PaginationPlus,
|
|
1903
2168
|
SearchAndReplaceExtension,
|
|
1904
2169
|
SubdocumentProvider,
|
|
2170
|
+
buildAIPrompt,
|
|
1905
2171
|
clearSearch,
|
|
1906
2172
|
collaborationExtensions,
|
|
1907
2173
|
collectExtensions,
|
|
@@ -1911,6 +2177,10 @@ export {
|
|
|
1911
2177
|
definePlugin,
|
|
1912
2178
|
findMatches,
|
|
1913
2179
|
getSearchState,
|
|
2180
|
+
httpKeyStorage,
|
|
2181
|
+
localStorageKeyStorage,
|
|
2182
|
+
memoryKeyStorage,
|
|
2183
|
+
openaiCompatibleProvider,
|
|
1914
2184
|
replaceAll,
|
|
1915
2185
|
replaceCurrent,
|
|
1916
2186
|
resolveAction,
|
|
@@ -1918,5 +2188,8 @@ export {
|
|
|
1918
2188
|
searchAndReplaceKey,
|
|
1919
2189
|
searchNext,
|
|
1920
2190
|
searchPrev,
|
|
1921
|
-
setSearchQuery
|
|
2191
|
+
setSearchQuery,
|
|
2192
|
+
toAIStreamFn,
|
|
2193
|
+
trimContextAfter,
|
|
2194
|
+
trimContextBefore
|
|
1922
2195
|
};
|