@lenne.tech/nuxt-extensions 1.6.0 → 1.7.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/CLAUDE.md +64 -18
- package/README.md +72 -0
- package/dist/module.d.mts +2 -1
- package/dist/module.mjs +23 -0
- package/dist/runtime/composables/index.d.ts +7 -0
- package/dist/runtime/composables/index.js +7 -0
- package/dist/runtime/composables/use-lt-ai-admin.d.ts +9 -0
- package/dist/runtime/composables/use-lt-ai-admin.js +36 -0
- package/dist/runtime/composables/use-lt-ai-chat.d.ts +8 -0
- package/dist/runtime/composables/use-lt-ai-chat.js +141 -0
- package/dist/runtime/composables/use-lt-ai-connections.d.ts +7 -0
- package/dist/runtime/composables/use-lt-ai-connections.js +33 -0
- package/dist/runtime/composables/use-lt-ai-placeholders.d.ts +10 -0
- package/dist/runtime/composables/use-lt-ai-placeholders.js +25 -0
- package/dist/runtime/composables/use-lt-ai-prompts.d.ts +13 -0
- package/dist/runtime/composables/use-lt-ai-prompts.js +70 -0
- package/dist/runtime/composables/use-lt-ai-usage.d.ts +7 -0
- package/dist/runtime/composables/use-lt-ai-usage.js +19 -0
- package/dist/runtime/composables/use-lt-ai.d.ts +8 -0
- package/dist/runtime/composables/use-lt-ai.js +68 -0
- package/dist/runtime/lib/ai.d.ts +48 -0
- package/dist/runtime/lib/ai.js +96 -0
- package/dist/runtime/lib/index.d.ts +1 -0
- package/dist/runtime/lib/index.js +1 -0
- package/dist/runtime/plugins/auth-interceptor.client.js +26 -22
- package/dist/runtime/types/ai.d.ts +521 -0
- package/dist/runtime/types/ai.js +0 -0
- package/dist/runtime/types/index.d.ts +1 -0
- package/dist/runtime/types/module.d.ts +16 -0
- package/dist/types.d.mts +3 -1
- package/package.json +13 -3
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readonly, ref } from "#imports";
|
|
2
|
+
import { ltAiRequest } from "../lib/ai.js";
|
|
3
|
+
export function useLtAiPrompts() {
|
|
4
|
+
const prompts = ref([]);
|
|
5
|
+
const loading = ref(false);
|
|
6
|
+
const error = ref(null);
|
|
7
|
+
async function load() {
|
|
8
|
+
loading.value = true;
|
|
9
|
+
error.value = null;
|
|
10
|
+
try {
|
|
11
|
+
prompts.value = await ltAiRequest("GET", "/prompts") || [];
|
|
12
|
+
} catch (err) {
|
|
13
|
+
error.value = err.message;
|
|
14
|
+
prompts.value = [];
|
|
15
|
+
} finally {
|
|
16
|
+
loading.value = false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function create(input) {
|
|
20
|
+
loading.value = true;
|
|
21
|
+
error.value = null;
|
|
22
|
+
try {
|
|
23
|
+
const created = await ltAiRequest("POST", "/prompts", input);
|
|
24
|
+
await load();
|
|
25
|
+
return created;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
error.value = err.message;
|
|
28
|
+
throw err;
|
|
29
|
+
} finally {
|
|
30
|
+
loading.value = false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function update(id, input) {
|
|
34
|
+
loading.value = true;
|
|
35
|
+
error.value = null;
|
|
36
|
+
try {
|
|
37
|
+
const updated = await ltAiRequest("PUT", `/prompts/${encodeURIComponent(id)}`, input);
|
|
38
|
+
await load();
|
|
39
|
+
return updated;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
error.value = err.message;
|
|
42
|
+
throw err;
|
|
43
|
+
} finally {
|
|
44
|
+
loading.value = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function remove(id) {
|
|
48
|
+
loading.value = true;
|
|
49
|
+
error.value = null;
|
|
50
|
+
try {
|
|
51
|
+
const removed = await ltAiRequest("DELETE", `/prompts/${encodeURIComponent(id)}`);
|
|
52
|
+
await load();
|
|
53
|
+
return removed;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
error.value = err.message;
|
|
56
|
+
throw err;
|
|
57
|
+
} finally {
|
|
58
|
+
loading.value = false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
create,
|
|
63
|
+
error: readonly(error),
|
|
64
|
+
load,
|
|
65
|
+
loading: readonly(loading),
|
|
66
|
+
prompts: readonly(prompts),
|
|
67
|
+
remove,
|
|
68
|
+
update
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token/prompt usage for the current user (and tenant), from `GET /ai/usage`.
|
|
3
|
+
* The compact per-response budget is also available on every prompt response
|
|
4
|
+
* (see {@link useLtAiChat}); this composable loads the full breakdown on demand.
|
|
5
|
+
*/
|
|
6
|
+
import type { UseLtAiUsageReturn } from '../types/ai.js';
|
|
7
|
+
export declare function useLtAiUsage(): UseLtAiUsageReturn;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readonly, ref } from "#imports";
|
|
2
|
+
import { ltAiRequest } from "../lib/ai.js";
|
|
3
|
+
export function useLtAiUsage() {
|
|
4
|
+
const usage = ref(null);
|
|
5
|
+
const loading = ref(false);
|
|
6
|
+
const error = ref(null);
|
|
7
|
+
async function load() {
|
|
8
|
+
loading.value = true;
|
|
9
|
+
error.value = null;
|
|
10
|
+
try {
|
|
11
|
+
usage.value = await ltAiRequest("GET", "/usage");
|
|
12
|
+
} catch (err) {
|
|
13
|
+
error.value = err.message;
|
|
14
|
+
} finally {
|
|
15
|
+
loading.value = false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { error: readonly(error), load, loading: readonly(loading), usage: readonly(usage) };
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core AI composable: one-shot prompts and SSE streaming against the
|
|
3
|
+
* @lenne.tech/nest-server AI module (`/ai/prompt`, `/ai/stream`).
|
|
4
|
+
*
|
|
5
|
+
* Headless — pair it with your own UI, or use the higher-level {@link useLtAiChat}.
|
|
6
|
+
*/
|
|
7
|
+
import type { UseLtAiReturn } from '../types/ai.js';
|
|
8
|
+
export declare function useLtAi(): UseLtAiReturn;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readonly, ref } from "#imports";
|
|
2
|
+
import { buildLtAiUrl, ltAiRequest, ltAiResponseError, parseLtAiSseStream } from "../lib/ai.js";
|
|
3
|
+
import { ltAuthFetch } from "../lib/auth-state.js";
|
|
4
|
+
export function useLtAi() {
|
|
5
|
+
const loading = ref(false);
|
|
6
|
+
const streaming = ref(false);
|
|
7
|
+
const error = ref(null);
|
|
8
|
+
async function prompt(input) {
|
|
9
|
+
loading.value = true;
|
|
10
|
+
error.value = null;
|
|
11
|
+
try {
|
|
12
|
+
return await ltAiRequest("POST", "/prompt", input);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
error.value = err.message;
|
|
15
|
+
throw err;
|
|
16
|
+
} finally {
|
|
17
|
+
loading.value = false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function promptStream(input, handlers, options) {
|
|
21
|
+
streaming.value = true;
|
|
22
|
+
error.value = null;
|
|
23
|
+
let final;
|
|
24
|
+
try {
|
|
25
|
+
const response = await ltAuthFetch(buildLtAiUrl("/stream"), {
|
|
26
|
+
body: JSON.stringify(input),
|
|
27
|
+
headers: { Accept: "text/event-stream", "Content-Type": "application/json" },
|
|
28
|
+
method: "POST",
|
|
29
|
+
signal: options?.signal
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw await ltAiResponseError(response);
|
|
33
|
+
}
|
|
34
|
+
await parseLtAiSseStream(response, (event) => {
|
|
35
|
+
handlers?.onEvent?.(event);
|
|
36
|
+
switch (event.type) {
|
|
37
|
+
case "action":
|
|
38
|
+
handlers?.onAction?.(event.action);
|
|
39
|
+
break;
|
|
40
|
+
case "error":
|
|
41
|
+
error.value = event.message;
|
|
42
|
+
handlers?.onError?.(event.message);
|
|
43
|
+
break;
|
|
44
|
+
case "final":
|
|
45
|
+
final = event.response;
|
|
46
|
+
handlers?.onFinal?.(event.response);
|
|
47
|
+
break;
|
|
48
|
+
case "token":
|
|
49
|
+
handlers?.onToken?.(event.token);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return final;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
error.value = err.message;
|
|
56
|
+
throw err;
|
|
57
|
+
} finally {
|
|
58
|
+
streaming.value = false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
error: readonly(error),
|
|
63
|
+
loading: readonly(loading),
|
|
64
|
+
prompt,
|
|
65
|
+
promptStream,
|
|
66
|
+
streaming: readonly(streaming)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI module client helpers: URL building, authenticated JSON requests, and a
|
|
3
|
+
* robust SSE stream parser for `POST /ai/stream`.
|
|
4
|
+
*
|
|
5
|
+
* All requests go through {@link ltAuthFetch} (Cookie/JWT dual-mode, 401 fallback)
|
|
6
|
+
* and {@link buildLtApiUrl} (SSR/proxy/direct URL resolution), so the AI client
|
|
7
|
+
* behaves exactly like the rest of the library.
|
|
8
|
+
*/
|
|
9
|
+
import type { LtAiStreamEvent } from '../types/ai.js';
|
|
10
|
+
/** Resolve the AI base path from runtime config (default `/ai`). */
|
|
11
|
+
export declare function getLtAiBasePath(): string;
|
|
12
|
+
/** Build a full URL for an AI endpoint path (e.g. `/prompt`, `/connections/available`). */
|
|
13
|
+
export declare function buildLtAiUrl(path: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Turn a non-ok AI response into an Error using the backend's (translated) message.
|
|
16
|
+
*
|
|
17
|
+
* The resulting Error carries a numeric `status` field (the HTTP status code) so
|
|
18
|
+
* consumers can branch on response codes without re-parsing the message. The
|
|
19
|
+
* message is capped at ~1 KiB to bound downstream UI rendering / logging.
|
|
20
|
+
*
|
|
21
|
+
* Consumers should pipe the message through `useLtErrorTranslation()` for the
|
|
22
|
+
* user-facing localisation step — this helper preserves the raw backend string.
|
|
23
|
+
*/
|
|
24
|
+
export declare function ltAiResponseError(response: Response): Promise<Error & {
|
|
25
|
+
status: number;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Authenticated JSON request against an AI endpoint. Throws {@link ltAiResponseError}
|
|
29
|
+
* on a non-2xx response, otherwise returns the parsed body (or `undefined` for 204).
|
|
30
|
+
*/
|
|
31
|
+
export declare function ltAiRequest<T>(method: string, path: string, body?: unknown, options?: {
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
}): Promise<T>;
|
|
34
|
+
/**
|
|
35
|
+
* Parse a `text/event-stream` AI response, invoking `onEvent` for each parsed
|
|
36
|
+
* {@link LtAiStreamEvent}. Buffers partial lines across chunks and tolerates
|
|
37
|
+
* malformed/keep-alive lines.
|
|
38
|
+
*
|
|
39
|
+
* `options.signal` lets the caller stop parsing without cancelling the
|
|
40
|
+
* underlying fetch — useful for component-unmount races. When the signal fires
|
|
41
|
+
* the reader is cancelled and the function resolves cleanly.
|
|
42
|
+
*
|
|
43
|
+
* Throws when a single line exceeds `LT_AI_SSE_LINE_LIMIT` (1 MiB) — guards
|
|
44
|
+
* against a misbehaving proxy/backend that never delivers a newline.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseLtAiSseStream(response: Response, onEvent: (event: LtAiStreamEvent) => void, options?: {
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useRuntimeConfig } from "#imports";
|
|
2
|
+
import { buildLtApiUrl, ltAuthFetch } from "./auth-state.js";
|
|
3
|
+
const LT_AI_SSE_LINE_LIMIT = 1024 * 1024;
|
|
4
|
+
const LT_AI_ERROR_MESSAGE_LIMIT = 1024;
|
|
5
|
+
export function getLtAiBasePath() {
|
|
6
|
+
try {
|
|
7
|
+
const rc = useRuntimeConfig();
|
|
8
|
+
const ext = rc.public.ltExtensions;
|
|
9
|
+
return ext?.ai?.basePath || "/ai";
|
|
10
|
+
} catch {
|
|
11
|
+
return "/ai";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function buildLtAiUrl(path) {
|
|
15
|
+
return buildLtApiUrl(`${getLtAiBasePath()}${path}`);
|
|
16
|
+
}
|
|
17
|
+
export async function ltAiResponseError(response) {
|
|
18
|
+
let message = `HTTP ${response.status}`;
|
|
19
|
+
try {
|
|
20
|
+
const body = await response.json();
|
|
21
|
+
const detail = body?.message ?? body?.error;
|
|
22
|
+
if (detail) {
|
|
23
|
+
const raw = Array.isArray(detail) ? detail.join(", ") : String(detail);
|
|
24
|
+
message = raw.length > LT_AI_ERROR_MESSAGE_LIMIT ? `${raw.slice(0, LT_AI_ERROR_MESSAGE_LIMIT)}\u2026` : raw;
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
const error = new Error(message);
|
|
29
|
+
error.status = response.status;
|
|
30
|
+
return error;
|
|
31
|
+
}
|
|
32
|
+
export async function ltAiRequest(method, path, body, options) {
|
|
33
|
+
const init = { method, signal: options?.signal };
|
|
34
|
+
if (body !== void 0) {
|
|
35
|
+
init.headers = { Accept: "application/json", "Content-Type": "application/json" };
|
|
36
|
+
init.body = JSON.stringify(body);
|
|
37
|
+
} else {
|
|
38
|
+
init.headers = { Accept: "application/json" };
|
|
39
|
+
}
|
|
40
|
+
const response = await ltAuthFetch(buildLtAiUrl(path), init);
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw await ltAiResponseError(response);
|
|
43
|
+
}
|
|
44
|
+
if (response.status === 204) {
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
return await response.json();
|
|
48
|
+
}
|
|
49
|
+
export async function parseLtAiSseStream(response, onEvent, options) {
|
|
50
|
+
const body = response.body;
|
|
51
|
+
if (!body) {
|
|
52
|
+
throw new Error("AI stream response has no body");
|
|
53
|
+
}
|
|
54
|
+
const reader = body.getReader();
|
|
55
|
+
const decoder = new TextDecoder();
|
|
56
|
+
let buffer = "";
|
|
57
|
+
const handleLine = (line) => {
|
|
58
|
+
const trimmed = line.trim();
|
|
59
|
+
if (!trimmed.startsWith("data:")) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const payload = trimmed.slice(5).trim();
|
|
63
|
+
if (!payload || payload === "[DONE]") {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
onEvent(JSON.parse(payload));
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
try {
|
|
72
|
+
while (!options?.signal?.aborted) {
|
|
73
|
+
const { done, value } = await reader.read();
|
|
74
|
+
if (done) {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
buffer += decoder.decode(value, { stream: true });
|
|
78
|
+
if (buffer.length > LT_AI_SSE_LINE_LIMIT) {
|
|
79
|
+
throw new Error("AI stream line exceeds maximum allowed size");
|
|
80
|
+
}
|
|
81
|
+
const lines = buffer.split("\n");
|
|
82
|
+
buffer = lines.pop() ?? "";
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
handleLine(line);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (options?.signal?.aborted) {
|
|
88
|
+
reader.cancel().catch(() => {
|
|
89
|
+
});
|
|
90
|
+
} else if (buffer) {
|
|
91
|
+
handleLine(buffer);
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
reader.releaseLock?.();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { attemptLtJwtSwitch, clearLtAuthCookies, createLtAuthFetch, getLtApiBase, getLtAuthCookieNames, getLtAuthMode, getLtJwtToken, isLtAuthenticated, LT_AUTH_STATE_COOKIE_DEFAULT, LT_JWT_TOKEN_COOKIE_DEFAULT, ltAuthFetch, setLtAuthMode, setLtJwtToken, } from './auth-state.js';
|
|
2
2
|
export { clearLtAuthPluginRegistry, createLtAuthClient, getOrCreateLtAuthClient, getLtAuthPluginRegistry, registerLtAuthPlugins, resetLtAuthClientSingleton, type LtAuthClient, } from './auth-client.js';
|
|
3
|
+
export { buildLtAiUrl, getLtAiBasePath, ltAiRequest, ltAiResponseError, parseLtAiSseStream } from './ai.js';
|
|
@@ -69,29 +69,33 @@ export default (nuxtApp) => {
|
|
|
69
69
|
}, 1e3);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
const wrapMarker = "__ltAuthFetchWrapped";
|
|
73
|
+
if (!globalThis[wrapMarker]) {
|
|
74
|
+
globalThis[wrapMarker] = true;
|
|
75
|
+
const originalFetch = globalThis.$fetch;
|
|
76
|
+
globalThis.$fetch = ((url, options) => {
|
|
77
|
+
return originalFetch(url, {
|
|
78
|
+
...options,
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
|
+
onResponseError: (context) => {
|
|
81
|
+
if (options?.onResponseError) {
|
|
82
|
+
options.onResponseError(context);
|
|
83
|
+
}
|
|
84
|
+
if (context.response?.status === 401) {
|
|
85
|
+
handleUnauthorized(url);
|
|
86
|
+
}
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
handleUnauthorized(url);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
88
|
+
});
|
|
85
89
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
90
|
+
const originalNativeFetch = globalThis.fetch;
|
|
91
|
+
globalThis.fetch = async (input, init) => {
|
|
92
|
+
const response = await originalNativeFetch(input, init);
|
|
93
|
+
if (response.status === 401) {
|
|
94
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
95
|
+
handleUnauthorized(url);
|
|
96
|
+
}
|
|
97
|
+
return response;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
96
100
|
nuxtApp.provide("ltHandleUnauthorized", handleUnauthorized);
|
|
97
101
|
};
|