@lenne.tech/nuxt-extensions 1.6.0 → 1.7.1
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 +1 -4
- 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/auth-client.d.ts +8 -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 +2 -1
- package/dist/runtime/types/module.d.ts +16 -0
- package/dist/types.d.mts +2 -8
- package/package.json +21 -14
package/CLAUDE.md
CHANGED
|
@@ -10,9 +10,9 @@ src/
|
|
|
10
10
|
├── runtime/
|
|
11
11
|
│ ├── composables/ # Auto-imported composables
|
|
12
12
|
│ ├── components/ # Auto-imported components
|
|
13
|
-
│ ├── lib/ # Library exports (auth-client,
|
|
14
|
-
│ ├── middleware/
|
|
15
|
-
│ ├── plugins/ # Nuxt plugins (auth
|
|
13
|
+
│ ├── lib/ # Library exports (auth-client, auth-state, ai)
|
|
14
|
+
│ ├── middleware/ # Route middleware (system setup)
|
|
15
|
+
│ ├── plugins/ # Nuxt plugins (auth interceptor, error translation)
|
|
16
16
|
│ ├── server/ # Nitro server routes (auth proxy)
|
|
17
17
|
│ ├── testing/ # Playwright test helpers
|
|
18
18
|
│ ├── types/ # TypeScript type definitions
|
|
@@ -21,32 +21,70 @@ src/
|
|
|
21
21
|
|
|
22
22
|
## Key Composables
|
|
23
23
|
|
|
24
|
+
### Auth + uploads + utilities
|
|
25
|
+
|
|
26
|
+
| Composable | Purpose |
|
|
27
|
+
|-----------|---------|
|
|
28
|
+
| `useLtAuth()` | Authentication state, login, logout, session management |
|
|
29
|
+
| `useLtAuthClient()` | Raw Better Auth client access |
|
|
30
|
+
| `useSystemSetup()` | First-admin setup flow |
|
|
31
|
+
| `useLtTusUpload()` | TUS resumable file uploads |
|
|
32
|
+
| `useLtFile()` | File utilities (size formatting, URLs) |
|
|
33
|
+
| `useLtShare()` | Web Share API with clipboard fallback |
|
|
34
|
+
| `useLtErrorTranslation()` | Translate backend error codes |
|
|
35
|
+
|
|
36
|
+
### AI assistant (nest-server AI module)
|
|
37
|
+
|
|
24
38
|
| Composable | Purpose |
|
|
25
39
|
|-----------|---------|
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
40
|
+
| `useLtAi()` | One-shot `prompt()` + streaming `promptStream()` (POST `/ai/prompt` / `/ai/stream`) |
|
|
41
|
+
| `useLtAiChat()` | Multi-turn chat: streaming, budget summary, confirmation gate, `stop()`/`clear()`, optional `maxMessages` cap, auto-stop on unmount |
|
|
42
|
+
| `useLtAiConnections()` | User-facing list of available connections + `select()` |
|
|
43
|
+
| `useLtAiUsage()` | Full token/prompt usage breakdown (`GET /ai/usage`) |
|
|
44
|
+
| `useLtAiPrompts()` | User-facing CRUD for re-usable prompt snippets ("Vorlagen", `scope: 'user'` / `'tenant'`) |
|
|
45
|
+
| `useLtAiPlaceholders()` | Loads the backend's `{{placeholder}}` registry for editor sidebars |
|
|
46
|
+
| `useLtAiAdmin()` | Admin CRUD: connections, preferences, budget limits, slots, prompt hints, interactions — server-gated |
|
|
29
47
|
|
|
30
48
|
## Module Configuration
|
|
31
49
|
|
|
32
|
-
Configure in `nuxt.config.ts
|
|
50
|
+
Configure in `nuxt.config.ts` (top-level key is `ltExtensions`):
|
|
33
51
|
|
|
34
52
|
```typescript
|
|
35
53
|
export default defineNuxtConfig({
|
|
36
54
|
modules: ['@lenne.tech/nuxt-extensions'],
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
ltExtensions: {
|
|
56
|
+
auth: {
|
|
57
|
+
enabled: true,
|
|
58
|
+
basePath: '/iam', // Better-Auth endpoint prefix
|
|
59
|
+
loginPath: '/auth/login',
|
|
60
|
+
enableAdmin: true,
|
|
61
|
+
enablePasskey: true,
|
|
62
|
+
enableTwoFactor: true,
|
|
63
|
+
},
|
|
64
|
+
errorTranslation: {
|
|
65
|
+
enabled: true,
|
|
66
|
+
defaultLocale: 'de',
|
|
67
|
+
},
|
|
68
|
+
tus: {
|
|
69
|
+
defaultEndpoint: '/files/upload',
|
|
70
|
+
defaultChunkSize: 5 * 1024 * 1024, // 5MB
|
|
71
|
+
},
|
|
72
|
+
i18n: {
|
|
73
|
+
autoMerge: true,
|
|
74
|
+
},
|
|
75
|
+
ai: {
|
|
76
|
+
enabled: true,
|
|
77
|
+
basePath: '/ai', // Must match the nest-server AI controller
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
45
81
|
```
|
|
46
82
|
|
|
83
|
+
The API base URL is resolved at runtime from `NUXT_PUBLIC_API_URL` (client + SSR fallback) and the optional `NUXT_API_URL` (SSR-only, e.g. for internal cluster addresses). `NUXT_API_URL` is never promoted into the public bundle.
|
|
84
|
+
|
|
47
85
|
## Authentication (Better Auth)
|
|
48
86
|
|
|
49
|
-
- SSR-safe session management via `
|
|
87
|
+
- SSR-safe session management via `useLtAuth()`
|
|
50
88
|
- Auth proxy server route at `/api/auth/**` (proxies to backend)
|
|
51
89
|
- Middleware `auth` for protected routes
|
|
52
90
|
- Passkey (WebAuthn) support via `@better-auth/passkey` peer dependency
|
|
@@ -54,11 +92,18 @@ export default defineNuxtConfig({
|
|
|
54
92
|
|
|
55
93
|
### Auth Flow
|
|
56
94
|
|
|
57
|
-
1. Frontend calls `
|
|
95
|
+
1. Frontend calls `useLtAuth().signIn()` / `.signUp()`
|
|
58
96
|
2. Request goes through Nitro proxy at `/api/auth/**`
|
|
59
97
|
3. Proxy forwards to backend Better Auth endpoints
|
|
60
98
|
4. Session cookie set via `httpOnly` cookie (SSR-safe)
|
|
61
99
|
|
|
100
|
+
## AI Assistant Flow
|
|
101
|
+
|
|
102
|
+
- All AI composables route through `ltAuthFetch` (Cookie/JWT dual mode) and `buildLtAiUrl` (SSR/public URL resolution), so they inherit the same auth + URL behaviour as the rest of the library.
|
|
103
|
+
- `useLtAi.promptStream()` and `useLtAiChat()` consume `POST /ai/stream` via `fetch` + `ReadableStream` (NOT `EventSource` — the endpoint is `POST` with auth). The SSE parser is robust against split chunks, keep-alive comments, malformed lines, and oversized lines (>1 MiB throws).
|
|
104
|
+
- `useLtAiAdmin()` is exposed as a regular composable; admin gating is enforced server-side (`@Restricted(ADMIN)`). Render admin UI behind a frontend route guard for UX, but trust the backend for authorization.
|
|
105
|
+
- The chat composable's `messages` ref is a shallow `Readonly<Ref<LtAiMessage[]>>` so child components can bind individual messages while preserving streaming reactivity.
|
|
106
|
+
|
|
62
107
|
## Testing Helpers
|
|
63
108
|
|
|
64
109
|
Import from `@lenne.tech/nuxt-extensions/testing`:
|
|
@@ -110,6 +155,7 @@ export default defineNuxtRouteMiddleware((to) => {
|
|
|
110
155
|
## Development Rules
|
|
111
156
|
|
|
112
157
|
1. **ALWAYS read source code** in `dist/runtime/` to understand available composables and components
|
|
113
|
-
2. **Use `
|
|
158
|
+
2. **Use `useLtAuth()`** for authentication — never implement auth manually
|
|
114
159
|
3. **Check existing composables** before creating new ones — this module may already provide what you need
|
|
115
160
|
4. **Peer dependencies** (`better-auth`, `@better-auth/passkey`, `tus-js-client`) must be installed in the consuming project
|
|
161
|
+
5. **AI types collision pitfall:** `LtAiPromptInput` is the CRUD input for the user-facing `LtAiPrompt` entity (used by `useLtAiPrompts().create/update`). The execution payload for `useLtAi.prompt()` / `.promptStream()` is `LtAiPromptRunInput`. Do not conflate them.
|
package/README.md
CHANGED
|
@@ -144,6 +144,12 @@ export default defineNuxtConfig({
|
|
|
144
144
|
i18n: {
|
|
145
145
|
autoMerge: true, // Auto-merge locales with @nuxtjs/i18n
|
|
146
146
|
},
|
|
147
|
+
|
|
148
|
+
// AI module configuration
|
|
149
|
+
ai: {
|
|
150
|
+
enabled: true, // Enable AI composables / auto-imports
|
|
151
|
+
basePath: '/ai', // Must match the nest-server AI controller
|
|
152
|
+
},
|
|
147
153
|
},
|
|
148
154
|
});
|
|
149
155
|
```
|
|
@@ -344,6 +350,65 @@ const base64 = ltArrayBufferToBase64Url(buffer);
|
|
|
344
350
|
const uint8 = ltBase64UrlToUint8Array(base64String);
|
|
345
351
|
```
|
|
346
352
|
|
|
353
|
+
## AI Assistant
|
|
354
|
+
|
|
355
|
+
Headless, provider-agnostic composables for the `@lenne.tech/nest-server` **AI module**
|
|
356
|
+
(REST + SSE). All requests use the same auth-aware `ltAuthFetch` (Cookie/JWT) and URL
|
|
357
|
+
resolution as the rest of the library. Configure via `ltExtensions.ai`:
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
// nuxt.config.ts
|
|
361
|
+
ltExtensions: {
|
|
362
|
+
ai: { enabled: true, basePath: '/ai' }, // basePath must match the nest-server AI controller
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Chat (streaming, multi-turn)
|
|
367
|
+
|
|
368
|
+
```vue
|
|
369
|
+
<script setup lang="ts">
|
|
370
|
+
const { budget, confirm, messages, requiresConfirmation, send, streaming } = useLtAiChat();
|
|
371
|
+
</script>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`useLtAiChat()` streams the answer token-by-token, tracks the `conversationId`, exposes
|
|
375
|
+
the per-response `budget` summary, and drives the confirmation flow for mutating/
|
|
376
|
+
destructive actions (`requiresConfirmation` → `confirm()`).
|
|
377
|
+
|
|
378
|
+
### User-facing prompts ("Vorlagen")
|
|
379
|
+
|
|
380
|
+
```vue
|
|
381
|
+
<script setup lang="ts">
|
|
382
|
+
const { create, error, load, loading, prompts, remove, update } = useLtAiPrompts();
|
|
383
|
+
await load();
|
|
384
|
+
// create({ name: 'Summary', content: 'Summarize {{topic}}', scope: 'user' });
|
|
385
|
+
</script>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
`useLtAiPrompts()` lets any signed-in user manage re-usable prompt snippets. `scope: 'user'`
|
|
389
|
+
is private to the owner; `scope: 'tenant'` is shared across the owner's tenant. The list
|
|
390
|
+
returned by `load()` is already server-filtered to what the user is allowed to see.
|
|
391
|
+
|
|
392
|
+
### Lower-level + supporting composables
|
|
393
|
+
|
|
394
|
+
| Composable | Purpose |
|
|
395
|
+
|------------|---------|
|
|
396
|
+
| `useLtAi()` | One-shot `prompt(input)` and streaming `promptStream(input, handlers)` (POST `/ai/stream`) |
|
|
397
|
+
| `useLtAiChat()` | Multi-turn chat state, streaming, budget, confirmation, `stop()`/`clear()`, optional `maxMessages` cap, auto-stop on component unmount |
|
|
398
|
+
| `useLtAiConnections()` | User self-service: available connections + `select()` (`selected`/`locked`) |
|
|
399
|
+
| `useLtAiUsage()` | Full token/prompt usage breakdown (`GET /ai/usage`) |
|
|
400
|
+
| `useLtAiPrompts()` | User-facing CRUD for re-usable prompt snippets ("Vorlagen", `scope: 'user'` / `'tenant'`) |
|
|
401
|
+
| `useLtAiPlaceholders()` | Loads `{{placeholder}}` registry from the backend so slot / prompt editors render a dynamic helper sidebar |
|
|
402
|
+
| `useLtAiAdmin()` | Admin CRUD: connections (+ `detectCapabilities`), preferences, budget-limits, slots (incl. `listEffectiveSlots`/`resetSlot`), prompt hints, interactions |
|
|
403
|
+
|
|
404
|
+
Helpers: `buildLtAiUrl(path)`, `ltAiRequest(method, path, body?)`, `parseLtAiSseStream(response, onEvent, options?)`.
|
|
405
|
+
All AI DTOs are exported as `LtAi*` types. The streaming endpoint is consumed via a
|
|
406
|
+
`fetch` + `ReadableStream` SSE reader (not `EventSource`, since it is a `POST` with auth).
|
|
407
|
+
|
|
408
|
+
> **Security note:** `useLtAiAdmin()` is exposed as a regular composable; admin gating
|
|
409
|
+
> is enforced server-side (`@Restricted(ADMIN)`). Render admin UI behind a frontend
|
|
410
|
+
> route guard for UX, but trust the backend for authorization.
|
|
411
|
+
|
|
347
412
|
## i18n Support
|
|
348
413
|
|
|
349
414
|
The package works **with or without** `@nuxtjs/i18n`:
|
|
@@ -368,6 +433,13 @@ The package works **with or without** `@nuxtjs/i18n`:
|
|
|
368
433
|
| `useLtFile()` | File utilities (size formatting, URLs) |
|
|
369
434
|
| `useLtShare()` | Web Share API with clipboard fallback |
|
|
370
435
|
| `useSystemSetup()` | System setup flow for initial admin user creation |
|
|
436
|
+
| `useLtAi()` | One-shot `prompt()` + streaming `promptStream()` for the nest-server AI module |
|
|
437
|
+
| `useLtAiChat()` | Multi-turn chat state, streaming, budget summary, confirmation gate, `maxMessages` cap |
|
|
438
|
+
| `useLtAiConnections()` | User self-service connection list + `select()` |
|
|
439
|
+
| `useLtAiUsage()` | Token/prompt usage breakdown per user / tenant |
|
|
440
|
+
| `useLtAiPrompts()` | User-facing prompt snippet CRUD (`'user'` / `'tenant'` scope) |
|
|
441
|
+
| `useLtAiPlaceholders()` | Loads the backend's `{{placeholder}}` registry |
|
|
442
|
+
| `useLtAiAdmin()` | Admin CRUD for connections, preferences, budget limits, slots, prompt hints, interactions |
|
|
371
443
|
|
|
372
444
|
### Components
|
|
373
445
|
|
package/dist/module.d.mts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
2
|
import { LtExtensionsModuleOptions } from '../dist/runtime/types/index.js';
|
|
3
|
-
export
|
|
4
|
-
export { LtFileInfo, LtUploadItem, LtUploadOptions, LtUploadProgress, LtUploadStatus, UseLtFileReturn, UseLtTusUploadReturn } from '../dist/runtime/types/upload.js';
|
|
5
|
-
export { LtAuthCookieNamesOptions, LtAuthModuleOptions, LtErrorTranslationModuleOptions, LtExtensionsModuleOptions, LtExtensionsPublicRuntimeConfig, LtI18nModuleOptions, LtSystemSetupModuleOptions, LtTusModuleOptions } from '../dist/runtime/types/module.js';
|
|
6
|
-
export { LtErrorTranslationResponse, LtParsedError, UseLtErrorTranslationReturn } from '../dist/runtime/types/error.js';
|
|
3
|
+
export * from '../dist/runtime/types/index.js';
|
|
7
4
|
|
|
8
5
|
declare const name = "@lenne.tech/nuxt-extensions";
|
|
9
6
|
declare const version = "1.5.2";
|
package/dist/module.mjs
CHANGED
|
@@ -6,6 +6,10 @@ const configKey = "ltExtensions";
|
|
|
6
6
|
const DEFAULT_LT_AUTH_STATE_COOKIE = "lt-auth-state";
|
|
7
7
|
const DEFAULT_LT_JWT_TOKEN_COOKIE = "lt-jwt-token";
|
|
8
8
|
const defaultOptions = {
|
|
9
|
+
ai: {
|
|
10
|
+
basePath: "/ai",
|
|
11
|
+
enabled: true
|
|
12
|
+
},
|
|
9
13
|
auth: {
|
|
10
14
|
basePath: "/iam",
|
|
11
15
|
baseURL: "",
|
|
@@ -51,6 +55,7 @@ const module$1 = defineNuxtModule({
|
|
|
51
55
|
async setup(options, nuxt) {
|
|
52
56
|
const { resolve } = createResolver(import.meta.url);
|
|
53
57
|
const resolvedOptions = {
|
|
58
|
+
ai: { ...defaultOptions.ai, ...options.ai },
|
|
54
59
|
auth: {
|
|
55
60
|
...defaultOptions.auth,
|
|
56
61
|
...options.auth,
|
|
@@ -69,6 +74,10 @@ const module$1 = defineNuxtModule({
|
|
|
69
74
|
rc.public.apiUrl = "";
|
|
70
75
|
}
|
|
71
76
|
nuxt.options.runtimeConfig.public.ltExtensions = {
|
|
77
|
+
ai: {
|
|
78
|
+
basePath: resolvedOptions.ai?.basePath || "/ai",
|
|
79
|
+
enabled: resolvedOptions.ai?.enabled ?? true
|
|
80
|
+
},
|
|
72
81
|
auth: {
|
|
73
82
|
basePath: resolvedOptions.auth?.basePath || "/iam",
|
|
74
83
|
baseURL: resolvedOptions.auth?.baseURL || "",
|
|
@@ -137,6 +146,20 @@ const module$1 = defineNuxtModule({
|
|
|
137
146
|
{ name: "createLtAuthFetch", from: resolve("./runtime/lib/auth-state") },
|
|
138
147
|
{ name: "ltAuthFetch", from: resolve("./runtime/lib/auth-state") }
|
|
139
148
|
]);
|
|
149
|
+
if (resolvedOptions.ai?.enabled !== false) {
|
|
150
|
+
addImports([
|
|
151
|
+
{ name: "useLtAi", from: resolve("./runtime/composables/use-lt-ai") },
|
|
152
|
+
{ name: "useLtAiChat", from: resolve("./runtime/composables/use-lt-ai-chat") },
|
|
153
|
+
{ name: "useLtAiConnections", from: resolve("./runtime/composables/use-lt-ai-connections") },
|
|
154
|
+
{ name: "useLtAiUsage", from: resolve("./runtime/composables/use-lt-ai-usage") },
|
|
155
|
+
{ name: "useLtAiPrompts", from: resolve("./runtime/composables/use-lt-ai-prompts") },
|
|
156
|
+
{ name: "useLtAiPlaceholders", from: resolve("./runtime/composables/use-lt-ai-placeholders") },
|
|
157
|
+
{ name: "useLtAiAdmin", from: resolve("./runtime/composables/use-lt-ai-admin") },
|
|
158
|
+
{ name: "buildLtAiUrl", from: resolve("./runtime/lib/ai") },
|
|
159
|
+
{ name: "ltAiRequest", from: resolve("./runtime/lib/ai") },
|
|
160
|
+
{ name: "parseLtAiSseStream", from: resolve("./runtime/lib/ai") }
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
140
163
|
const transitionComponents = ["LtTransitionFade", "LtTransitionSlide", "LtTransitionSlideBottom", "LtTransitionSlideRevert", "LtTransitionFadeScale"];
|
|
141
164
|
for (const componentName of transitionComponents) {
|
|
142
165
|
addComponent({
|
|
@@ -5,3 +5,10 @@ export { useLtTusUpload } from './use-lt-tus-upload.js';
|
|
|
5
5
|
export { useLtFile } from './use-lt-file.js';
|
|
6
6
|
export { useLtShare, type UseLtShareReturn } from './use-lt-share.js';
|
|
7
7
|
export { useLtErrorTranslation } from './use-lt-error-translation.js';
|
|
8
|
+
export { useLtAi } from './use-lt-ai.js';
|
|
9
|
+
export { useLtAiChat } from './use-lt-ai-chat.js';
|
|
10
|
+
export { useLtAiConnections } from './use-lt-ai-connections.js';
|
|
11
|
+
export { useLtAiUsage } from './use-lt-ai-usage.js';
|
|
12
|
+
export { useLtAiAdmin } from './use-lt-ai-admin.js';
|
|
13
|
+
export { useLtAiPrompts } from './use-lt-ai-prompts.js';
|
|
14
|
+
export { useLtAiPlaceholders } from './use-lt-ai-placeholders.js';
|
|
@@ -5,3 +5,10 @@ export { useLtTusUpload } from "./use-lt-tus-upload.js";
|
|
|
5
5
|
export { useLtFile } from "./use-lt-file.js";
|
|
6
6
|
export { useLtShare } from "./use-lt-share.js";
|
|
7
7
|
export { useLtErrorTranslation } from "./use-lt-error-translation.js";
|
|
8
|
+
export { useLtAi } from "./use-lt-ai.js";
|
|
9
|
+
export { useLtAiChat } from "./use-lt-ai-chat.js";
|
|
10
|
+
export { useLtAiConnections } from "./use-lt-ai-connections.js";
|
|
11
|
+
export { useLtAiUsage } from "./use-lt-ai-usage.js";
|
|
12
|
+
export { useLtAiAdmin } from "./use-lt-ai-admin.js";
|
|
13
|
+
export { useLtAiPrompts } from "./use-lt-ai-prompts.js";
|
|
14
|
+
export { useLtAiPlaceholders } from "./use-lt-ai-placeholders.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin data layer for the AI module (headless): CRUD for connections,
|
|
3
|
+
* connection preferences and budget limits, capability auto-detection, and the
|
|
4
|
+
* interaction audit log. All endpoints are ADMIN-gated server-side.
|
|
5
|
+
*
|
|
6
|
+
* Pair with your own UI (the nuxt-base-starter ships a reference admin area).
|
|
7
|
+
*/
|
|
8
|
+
import type { UseLtAiAdminReturn } from '../types/ai.js';
|
|
9
|
+
export declare function useLtAiAdmin(): UseLtAiAdminReturn;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ltAiRequest } from "../lib/ai.js";
|
|
2
|
+
const seg = (id) => encodeURIComponent(id);
|
|
3
|
+
export function useLtAiAdmin() {
|
|
4
|
+
return {
|
|
5
|
+
// Connections
|
|
6
|
+
createConnection: (input) => ltAiRequest("POST", "/connections", input),
|
|
7
|
+
deleteConnection: (id) => ltAiRequest("DELETE", `/connections/${seg(id)}`),
|
|
8
|
+
detectCapabilities: (id) => ltAiRequest("POST", `/connections/${seg(id)}/detect-capabilities`),
|
|
9
|
+
getConnection: (id) => ltAiRequest("GET", `/connections/${seg(id)}`),
|
|
10
|
+
listConnections: () => ltAiRequest("GET", "/connections"),
|
|
11
|
+
updateConnection: (id, input) => ltAiRequest("PUT", `/connections/${seg(id)}`, input),
|
|
12
|
+
// Connection preferences
|
|
13
|
+
deletePreference: (id) => ltAiRequest("DELETE", `/connections/preferences/${seg(id)}`),
|
|
14
|
+
listPreferences: () => ltAiRequest("GET", "/connections/preferences"),
|
|
15
|
+
setPreference: (input) => ltAiRequest("POST", "/connections/preferences", input),
|
|
16
|
+
// Budget limits
|
|
17
|
+
createBudgetLimit: (input) => ltAiRequest("POST", "/budget-limits", input),
|
|
18
|
+
deleteBudgetLimit: (id) => ltAiRequest("DELETE", `/budget-limits/${seg(id)}`),
|
|
19
|
+
listBudgetLimits: () => ltAiRequest("GET", "/budget-limits"),
|
|
20
|
+
updateBudgetLimit: (id, input) => ltAiRequest("PUT", `/budget-limits/${seg(id)}`, input),
|
|
21
|
+
// System-prompt slots (admin-editable; tenant-scoped)
|
|
22
|
+
createSlot: (input) => ltAiRequest("POST", "/slots", input),
|
|
23
|
+
deleteSlot: (id) => ltAiRequest("DELETE", `/slots/${seg(id)}`),
|
|
24
|
+
listEffectiveSlots: () => ltAiRequest("GET", "/slots/effective"),
|
|
25
|
+
listSlots: () => ltAiRequest("GET", "/slots"),
|
|
26
|
+
resetSlot: (id) => ltAiRequest("POST", `/slots/${seg(id)}/reset`),
|
|
27
|
+
updateSlot: (id, input) => ltAiRequest("PUT", `/slots/${seg(id)}`, input),
|
|
28
|
+
// Learned prompt hints (governed self-improvement loop)
|
|
29
|
+
createPromptHint: (input) => ltAiRequest("POST", "/prompt-hints", input),
|
|
30
|
+
deletePromptHint: (id) => ltAiRequest("DELETE", `/prompt-hints/${seg(id)}`),
|
|
31
|
+
listPromptHints: () => ltAiRequest("GET", "/prompt-hints"),
|
|
32
|
+
updatePromptHint: (id, input) => ltAiRequest("PUT", `/prompt-hints/${seg(id)}`, input),
|
|
33
|
+
// Audit
|
|
34
|
+
listInteractions: () => ltAiRequest("GET", "/interactions")
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-turn chat composable built on {@link useLtAi}. Maintains the message
|
|
3
|
+
* list, streams assistant answers token-by-token, tracks the conversation id and
|
|
4
|
+
* the latest budget summary, and handles the confirmation flow for mutating /
|
|
5
|
+
* destructive actions.
|
|
6
|
+
*/
|
|
7
|
+
import type { UseLtAiChatOptions, UseLtAiChatReturn } from '../types/ai.js';
|
|
8
|
+
export declare function useLtAiChat(options?: UseLtAiChatOptions): UseLtAiChatReturn;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { computed, getCurrentScope, onScopeDispose, reactive, readonly, ref, unref } from "#imports";
|
|
2
|
+
import { useLtAi } from "./use-lt-ai.js";
|
|
3
|
+
export function useLtAiChat(options = {}) {
|
|
4
|
+
const ai = useLtAi();
|
|
5
|
+
const messages = ref([]);
|
|
6
|
+
const conversationId = ref(options.conversationId);
|
|
7
|
+
const budget = ref(null);
|
|
8
|
+
const contextWindow = ref(null);
|
|
9
|
+
const error = ref(null);
|
|
10
|
+
const maxMessages = options.maxMessages;
|
|
11
|
+
let lastPrompt = "";
|
|
12
|
+
let controller;
|
|
13
|
+
const requiresConfirmation = computed(() => {
|
|
14
|
+
const last = messages.value[messages.value.length - 1];
|
|
15
|
+
return !!last && last.role === "assistant" && !!last.requiresConfirmation;
|
|
16
|
+
});
|
|
17
|
+
function resolveConnectionId() {
|
|
18
|
+
return unref(options.connectionId) ?? void 0;
|
|
19
|
+
}
|
|
20
|
+
function trimMessageHistory() {
|
|
21
|
+
if (maxMessages && messages.value.length > maxMessages) {
|
|
22
|
+
messages.value.splice(0, messages.value.length - maxMessages);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function applyFinal(assistant, response) {
|
|
26
|
+
assistant.pending = false;
|
|
27
|
+
assistant.content = response.text || assistant.content;
|
|
28
|
+
assistant.actions = response.actions;
|
|
29
|
+
assistant.budget = response.budget;
|
|
30
|
+
assistant.denied = response.denied;
|
|
31
|
+
assistant.requiresConfirmation = response.requiresConfirmation;
|
|
32
|
+
assistant.pendingActions = response.pendingActions;
|
|
33
|
+
if (response.budget) {
|
|
34
|
+
budget.value = response.budget;
|
|
35
|
+
}
|
|
36
|
+
if (response.contextWindow) {
|
|
37
|
+
contextWindow.value = response.contextWindow;
|
|
38
|
+
}
|
|
39
|
+
if (response.conversationId) {
|
|
40
|
+
conversationId.value = response.conversationId;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function runTurn(input) {
|
|
44
|
+
error.value = null;
|
|
45
|
+
const assistant = reactive({ content: "", createdAt: (/* @__PURE__ */ new Date()).toISOString(), pending: true, role: "assistant" });
|
|
46
|
+
messages.value.push(assistant);
|
|
47
|
+
trimMessageHistory();
|
|
48
|
+
controller = new AbortController();
|
|
49
|
+
const useStream = options.stream !== false;
|
|
50
|
+
try {
|
|
51
|
+
if (useStream) {
|
|
52
|
+
await ai.promptStream(
|
|
53
|
+
input,
|
|
54
|
+
{
|
|
55
|
+
onToken: (token) => {
|
|
56
|
+
assistant.content += token;
|
|
57
|
+
},
|
|
58
|
+
onFinal: (response) => applyFinal(assistant, response)
|
|
59
|
+
},
|
|
60
|
+
{ signal: controller.signal }
|
|
61
|
+
);
|
|
62
|
+
if (assistant.pending) {
|
|
63
|
+
assistant.pending = false;
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
applyFinal(assistant, await ai.prompt(input));
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
assistant.pending = false;
|
|
70
|
+
if (err.name === "AbortError") {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
assistant.error = true;
|
|
74
|
+
error.value = err.message;
|
|
75
|
+
if (!assistant.content) {
|
|
76
|
+
assistant.content = err.message;
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
controller = void 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function send(content) {
|
|
83
|
+
const text = content.trim();
|
|
84
|
+
if (!text || ai.streaming.value) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
lastPrompt = text;
|
|
88
|
+
messages.value.push({ content: text, createdAt: (/* @__PURE__ */ new Date()).toISOString(), role: "user" });
|
|
89
|
+
trimMessageHistory();
|
|
90
|
+
await runTurn({
|
|
91
|
+
connectionId: resolveConnectionId(),
|
|
92
|
+
conversationId: conversationId.value,
|
|
93
|
+
metadata: options.metadata?.(),
|
|
94
|
+
mode: options.mode,
|
|
95
|
+
prompt: text
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function confirm() {
|
|
99
|
+
if (!requiresConfirmation.value || !lastPrompt) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await runTurn({
|
|
103
|
+
confirm: true,
|
|
104
|
+
connectionId: resolveConnectionId(),
|
|
105
|
+
conversationId: conversationId.value,
|
|
106
|
+
metadata: options.metadata?.(),
|
|
107
|
+
mode: options.mode,
|
|
108
|
+
prompt: lastPrompt
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function stop() {
|
|
112
|
+
controller?.abort();
|
|
113
|
+
controller = void 0;
|
|
114
|
+
}
|
|
115
|
+
function clear() {
|
|
116
|
+
stop();
|
|
117
|
+
messages.value = [];
|
|
118
|
+
budget.value = null;
|
|
119
|
+
contextWindow.value = null;
|
|
120
|
+
error.value = null;
|
|
121
|
+
lastPrompt = "";
|
|
122
|
+
}
|
|
123
|
+
if (getCurrentScope()) {
|
|
124
|
+
onScopeDispose(() => stop());
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
budget: readonly(budget),
|
|
128
|
+
contextWindow: readonly(contextWindow),
|
|
129
|
+
clear,
|
|
130
|
+
confirm,
|
|
131
|
+
conversationId: readonly(conversationId),
|
|
132
|
+
error: readonly(error),
|
|
133
|
+
// Returned as a shallow Readonly<Ref> (see UseLtAiChatReturn): consumers cannot
|
|
134
|
+
// reassign the ref but can bind individual messages to child components.
|
|
135
|
+
messages,
|
|
136
|
+
requiresConfirmation,
|
|
137
|
+
send,
|
|
138
|
+
stop,
|
|
139
|
+
streaming: ai.streaming
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User self-service for AI connections: list the connections the current
|
|
3
|
+
* user/tenant may use and pick a personal default. Backed by
|
|
4
|
+
* `GET /ai/connections/available` and `POST /ai/connections/select`.
|
|
5
|
+
*/
|
|
6
|
+
import type { UseLtAiConnectionsReturn } from '../types/ai.js';
|
|
7
|
+
export declare function useLtAiConnections(): UseLtAiConnectionsReturn;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { computed, readonly, ref } from "#imports";
|
|
2
|
+
import { ltAiRequest } from "../lib/ai.js";
|
|
3
|
+
export function useLtAiConnections() {
|
|
4
|
+
const connections = ref([]);
|
|
5
|
+
const loading = ref(false);
|
|
6
|
+
const error = ref(null);
|
|
7
|
+
const selected = computed(() => connections.value.find((c) => c.selected));
|
|
8
|
+
const locked = computed(() => connections.value.some((c) => c.locked));
|
|
9
|
+
async function load() {
|
|
10
|
+
loading.value = true;
|
|
11
|
+
error.value = null;
|
|
12
|
+
try {
|
|
13
|
+
connections.value = await ltAiRequest("GET", "/connections/available");
|
|
14
|
+
} catch (err) {
|
|
15
|
+
error.value = err.message;
|
|
16
|
+
} finally {
|
|
17
|
+
loading.value = false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function select(connectionId) {
|
|
21
|
+
loading.value = true;
|
|
22
|
+
error.value = null;
|
|
23
|
+
try {
|
|
24
|
+
connections.value = await ltAiRequest("POST", "/connections/select", { connectionId });
|
|
25
|
+
} catch (err) {
|
|
26
|
+
error.value = err.message;
|
|
27
|
+
throw err;
|
|
28
|
+
} finally {
|
|
29
|
+
loading.value = false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { connections: readonly(connections), error: readonly(error), load, loading: readonly(loading), locked, select, selected };
|
|
33
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder-list composable. Loads the runtime registry from the backend
|
|
3
|
+
* (`GET /ai/placeholders`) so slot / prompt editors can render a dynamic
|
|
4
|
+
* helper sidebar with the currently supported `{{placeholders}}` — without
|
|
5
|
+
* hard-coding any names in the frontend.
|
|
6
|
+
*
|
|
7
|
+
* Available to any signed-in user (S_USER on the endpoint).
|
|
8
|
+
*/
|
|
9
|
+
import type { UseLtAiPlaceholdersReturn } from '../types/ai.js';
|
|
10
|
+
export declare function useLtAiPlaceholders(): UseLtAiPlaceholdersReturn;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readonly, ref } from "#imports";
|
|
2
|
+
import { ltAiRequest } from "../lib/ai.js";
|
|
3
|
+
export function useLtAiPlaceholders() {
|
|
4
|
+
const placeholders = 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
|
+
placeholders.value = await ltAiRequest("GET", "/placeholders") || [];
|
|
12
|
+
} catch (err) {
|
|
13
|
+
error.value = err.message;
|
|
14
|
+
placeholders.value = [];
|
|
15
|
+
} finally {
|
|
16
|
+
loading.value = false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
error: readonly(error),
|
|
21
|
+
load,
|
|
22
|
+
loading: readonly(loading),
|
|
23
|
+
placeholders: readonly(placeholders)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing prompt composable.
|
|
3
|
+
*
|
|
4
|
+
* Any signed-in user can author re-usable prompts ("Vorlagen") for themselves
|
|
5
|
+
* (`scope: 'user'` = private) or for their tenant (`scope: 'tenant'` = public).
|
|
6
|
+
* `load()` returns every prompt the current user is allowed to see (own +
|
|
7
|
+
* tenant), already filtered by the server.
|
|
8
|
+
*
|
|
9
|
+
* Pair this with your own prompt picker / settings UI. The nuxt-base-starter
|
|
10
|
+
* ships a reference picker inside the AI chat and a settings page.
|
|
11
|
+
*/
|
|
12
|
+
import type { UseLtAiPromptsReturn } from '../types/ai.js';
|
|
13
|
+
export declare function useLtAiPrompts(): UseLtAiPromptsReturn;
|
|
@@ -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
|
+
}
|