@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.
Files changed (32) hide show
  1. package/CLAUDE.md +64 -18
  2. package/README.md +72 -0
  3. package/dist/module.d.mts +1 -4
  4. package/dist/module.mjs +23 -0
  5. package/dist/runtime/composables/index.d.ts +7 -0
  6. package/dist/runtime/composables/index.js +7 -0
  7. package/dist/runtime/composables/use-lt-ai-admin.d.ts +9 -0
  8. package/dist/runtime/composables/use-lt-ai-admin.js +36 -0
  9. package/dist/runtime/composables/use-lt-ai-chat.d.ts +8 -0
  10. package/dist/runtime/composables/use-lt-ai-chat.js +141 -0
  11. package/dist/runtime/composables/use-lt-ai-connections.d.ts +7 -0
  12. package/dist/runtime/composables/use-lt-ai-connections.js +33 -0
  13. package/dist/runtime/composables/use-lt-ai-placeholders.d.ts +10 -0
  14. package/dist/runtime/composables/use-lt-ai-placeholders.js +25 -0
  15. package/dist/runtime/composables/use-lt-ai-prompts.d.ts +13 -0
  16. package/dist/runtime/composables/use-lt-ai-prompts.js +70 -0
  17. package/dist/runtime/composables/use-lt-ai-usage.d.ts +7 -0
  18. package/dist/runtime/composables/use-lt-ai-usage.js +19 -0
  19. package/dist/runtime/composables/use-lt-ai.d.ts +8 -0
  20. package/dist/runtime/composables/use-lt-ai.js +68 -0
  21. package/dist/runtime/lib/ai.d.ts +48 -0
  22. package/dist/runtime/lib/ai.js +96 -0
  23. package/dist/runtime/lib/auth-client.d.ts +8 -0
  24. package/dist/runtime/lib/index.d.ts +1 -0
  25. package/dist/runtime/lib/index.js +1 -0
  26. package/dist/runtime/plugins/auth-interceptor.client.js +26 -22
  27. package/dist/runtime/types/ai.d.ts +521 -0
  28. package/dist/runtime/types/ai.js +0 -0
  29. package/dist/runtime/types/index.d.ts +2 -1
  30. package/dist/runtime/types/module.d.ts +16 -0
  31. package/dist/types.d.mts +2 -8
  32. 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, helpers)
14
- │ ├── middleware/ # Route middleware (auth)
15
- │ ├── plugins/ # Nuxt plugins (auth initialization)
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
- | `useBetterAuth()` | Authentication state, login, logout, session management |
27
- | `useAuthClient()` | Raw Better Auth client access |
28
- | `useTusUpload()` | TUS resumable file uploads |
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
- lennetech: {
38
- // API base URL for auth proxy
39
- apiUrl: 'http://localhost:3000',
40
- // Enable/disable features
41
- betterAuth: true,
42
- tusUpload: true,
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 `useBetterAuth()`
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 `useBetterAuth().signIn()` / `.signUp()`
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 `useBetterAuth()`** for authentication — never implement auth manually
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 { LtAuthClientConfig, LtAuthMode, LtAuthResponse, LtAuthState, LtPasskeyAuthResult, LtPasskeyRegisterResult, LtUser, UseLtAuthReturn } from '../dist/runtime/types/auth.js';
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
+ }