yaxa-svelte 2.7.0 → 2.8.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 (56) hide show
  1. package/README.md +68 -23
  2. package/dist/auth/index.d.ts +7 -1
  3. package/dist/auth/index.js +3 -0
  4. package/dist/components/ai/ai.test.js +68 -1
  5. package/dist/components/ai/index.d.ts +2 -0
  6. package/dist/components/ai/index.js +1 -0
  7. package/dist/components/ai/useAiChat.svelte.d.ts +68 -0
  8. package/dist/components/ai/useAiChat.svelte.js +258 -0
  9. package/dist/components/blocks/FAQ.svelte +11 -0
  10. package/dist/components/blocks/FAQ.svelte.d.ts +1 -0
  11. package/dist/components/blocks/FAQ.test.js +20 -0
  12. package/dist/components/saas/AuthCard.svelte +16 -12
  13. package/dist/components/saas/AuthCard.svelte.d.ts +3 -3
  14. package/dist/components/saas/ResetPasswordCard.svelte +13 -0
  15. package/dist/components/saas/ResetPasswordCard.svelte.d.ts +5 -0
  16. package/dist/components/saas/SignInCard.svelte +13 -0
  17. package/dist/components/saas/SignInCard.svelte.d.ts +5 -0
  18. package/dist/components/saas/SignUpCard.svelte +13 -0
  19. package/dist/components/saas/SignUpCard.svelte.d.ts +5 -0
  20. package/dist/components/saas/auth-cards.test.d.ts +1 -0
  21. package/dist/components/saas/auth-cards.test.js +50 -0
  22. package/dist/components/seo/Seo.svelte +16 -9
  23. package/dist/index.d.ts +9 -2
  24. package/dist/index.js +5 -1
  25. package/dist/root-purity.test.d.ts +1 -0
  26. package/dist/root-purity.test.js +96 -0
  27. package/dist/server/ai/index.d.ts +2 -0
  28. package/dist/server/ai/index.js +1 -0
  29. package/dist/server/ai/stream.d.ts +24 -0
  30. package/dist/server/ai/stream.js +178 -0
  31. package/dist/server/ai/stream.test.d.ts +1 -0
  32. package/dist/server/ai/stream.test.js +83 -0
  33. package/dist/server/ai/types.d.ts +43 -0
  34. package/dist/server/ai/types.js +1 -0
  35. package/dist/server/index.d.ts +6 -0
  36. package/dist/server/index.js +4 -0
  37. package/dist/site/context.svelte.d.ts +3 -0
  38. package/dist/site/context.svelte.js +7 -0
  39. package/dist/site/hook.d.ts +5 -1
  40. package/dist/site/hook.js +11 -1
  41. package/dist/site/index.d.ts +26 -0
  42. package/dist/site/index.js +25 -0
  43. package/dist/site/llms.d.ts +27 -0
  44. package/dist/site/llms.js +58 -0
  45. package/dist/site/schema.d.ts +45 -0
  46. package/dist/site/schema.js +43 -0
  47. package/dist/site/seo-helpers.d.ts +1 -0
  48. package/dist/site/site-export.test.d.ts +1 -0
  49. package/dist/site/site-export.test.js +193 -0
  50. package/dist/site/skill.d.ts +26 -0
  51. package/dist/site/skill.js +69 -0
  52. package/dist/site/useSeo.svelte.d.ts +23 -0
  53. package/dist/site/useSeo.svelte.js +40 -0
  54. package/dist/site/useSeo.test.d.ts +1 -0
  55. package/dist/site/useSeo.test.js +35 -0
  56. package/package.json +5 -1
package/README.md CHANGED
@@ -41,7 +41,7 @@ _Nuxt UI v4 & Nuxt UI Pro Equivalent for SvelteKit 2 & 3 & Svelte 5 with Built-i
41
41
  - 🚀 **100% DRY SEO Parity**: Automate dynamic Open Graph cards (`/api/og`), environment-aware `robots.txt`, XML sitemaps with human-readable `sitemap.xsl` stylesheets, PWA manifests, and Schema.org JSON-LD from a single `src/site.config.ts`.
42
42
  - 🔐 **Batteries-Included SaaS Suite**: Pre-integrated **Better-Auth**, multi-dialect **Drizzle ORM** (Neon PostgreSQL & Turso LibSQL/SQLite), **Polar.sh** payments & webhook sync, and **Resend** transactional emails.
43
43
  - 🧩 **50+ Production Components**: Elements, compound `<Skeleton recipe="...">` layouts, AI primitives, charts, advanced forms (`<RichTextEditor>`, `<PhoneInput>`, `<CreditCardInput>`), layout, overlays, sliding segmented `<Tabs>`, native `<Toast>` / `<Toaster>`, and drop-in SaaS widgets (`<AuthCard>`, `<UserMenu>`, `<PricingTable>`, `<SubscriptionCard>`).
44
- - 🛠️ **Composable Runes**: `useAuth`, `useShortcuts`, `useClipboard`, `useColorMode`, `useToast`, `useMediaQuery`, `useDebounce`, `useIdle`, `useGate`.
44
+ - 🛠️ **Composable Runes**: `useShortcuts`, `useClipboard`, `useColorMode`, `useToast`, `useMediaQuery`, `useDebounce`, `useIdle`, `useGate`, `useAuth` (`yaxa-svelte/auth`).
45
45
 
46
46
  ---
47
47
 
@@ -170,7 +170,7 @@ Handle **all 5 SEO endpoints** and **Better-Auth session parsing** in `src/hooks
170
170
  ```ts
171
171
  // src/hooks.server.ts
172
172
  import { sequence } from '@sveltejs/kit/hooks';
173
- import { createYaxaHook } from 'yaxa-svelte/server';
173
+ import { createYaxaHook } from 'yaxa-svelte/seo';
174
174
  import { createYaxaAuth, createYaxaAuthHook } from 'yaxa-svelte/auth';
175
175
  import { siteConfig } from './site.config';
176
176
 
@@ -185,6 +185,10 @@ const authHook = createYaxaAuthHook({
185
185
  export const handle = sequence(yaxaHook, authHook);
186
186
  ```
187
187
 
188
+ > **Pure Static Sites (`@sveltejs/adapter-static`):** Import directly from `yaxa-svelte/seo` (`createSitemapHandler`, `createRobotsHandler`, `createOgImageHandler`, `generateSitemapXsl`) with `export const prerender = true`. Completely headless with **zero DB, auth, or server runtime peer dependencies**.
189
+
190
+ ````
191
+
188
192
  ---
189
193
 
190
194
  ## 🛡️ Declarative Feature Gating (`<Gate />` & `useGate()`)
@@ -193,16 +197,16 @@ Declarative and programmatic subscription tier and RBAC gating with automatic Sv
193
197
 
194
198
  ```svelte
195
199
  <script lang="ts">
196
- import { Gate, useGate } from 'yaxa-svelte';
200
+ import { Gate, useGate } from 'yaxa-svelte';
197
201
 
198
- const gate = useGate();
202
+ const gate = useGate();
199
203
  </script>
200
204
 
201
205
  <!-- 0 props needed when wrapped in <YaxaApp user={data.user}> -->
202
206
  <Gate requiredPlan="pro" preview={true}>
203
- <AdvancedAnalyticsWidget />
207
+ <AdvancedAnalyticsWidget />
204
208
  </Gate>
205
- ```
209
+ ````
206
210
 
207
211
  ---
208
212
 
@@ -399,6 +403,46 @@ await broadcast(`org:${orgId}`, 'invoice.settled', {
399
403
 
400
404
  ---
401
405
 
406
+ ## 🤖 AI Streaming Bridge & Runes Chat (`useAiChat` & `createAiStreamHandler`)
407
+
408
+ Zero-boilerplate bridge connecting SvelteKit streaming endpoints directly to `<AiChat>`, with progressive text streaming, thought duration/step inspection, tool invocation cards, and abortable retries:
409
+
410
+ ```ts
411
+ // 1. Server Streaming Endpoint (src/routes/api/ai/chat/+server.ts)
412
+ import { createAiStreamHandler, requireAuth } from 'yaxa-svelte/server';
413
+
414
+ export const POST = createAiStreamHandler({
415
+ guards: [requireAuth()],
416
+ // Built-in realistic mock streaming for instant zero-config dev & demos,
417
+ // or provide your custom handler with Google Gemini, Anthropic, or OpenAI:
418
+ handler: async function* ({ messages }) {
419
+ yield { type: 'thought', thought: { running: true, text: 'Analyzing user request...' } };
420
+ // Yield text deltas, thoughts, and tool call states
421
+ yield { type: 'text', delta: 'Hello from your AI backend!' };
422
+ yield { type: 'done' };
423
+ }
424
+ });
425
+ ```
426
+
427
+ ```svelte
428
+ <!-- 2. Svelte 5 Client Chat (+page.svelte) -->
429
+ <script lang="ts">
430
+ import { AiChat, useAiChat } from 'yaxa-svelte';
431
+
432
+ const chat = useAiChat({ api: '/api/ai/chat' });
433
+ </script>
434
+
435
+ <AiChat
436
+ messages={chat.messages}
437
+ loading={chat.loading}
438
+ onsend={(text) => chat.send(text)}
439
+ onstop={() => chat.stop()}
440
+ onretry={(msg) => chat.retry(msg)}
441
+ />
442
+ ```
443
+
444
+ ---
445
+
402
446
  ## 🔗 End-to-End Type-Safe Client RPC (`createApiClient`)
403
447
 
404
448
  Zero-codegen end-to-end type safety bridging your server `yaxaEndpoint` routes directly into Svelte 5 components and universal `load` functions. Adds less than 500 bytes gzipped to your client bundle:
@@ -509,20 +553,21 @@ A zero-overhead, runes-native query and mutation cache built specifically for Sv
509
553
 
510
554
  ## 📦 Subpath Exports Architecture
511
555
 
512
- | Subpath | Description |
513
- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
514
- | `yaxa-svelte` | Core UI Primitives, SEO Components, `<Gate />`, Type-Safe Client RPC (`createApiClient`), Runes Query Engine (`createQuery`, `createMutation`, `QueryClient`), Theme System & Composables (`useGate`, `useUpload`, `useSSE`, `useToast`, `useClipboard`, etc.) |
515
- | `yaxa-svelte/admin` | Drizzle ORM Schema Introspection, `<AdminDashboard />`, `createYaxaAdminHook`, `createDrizzleAdmin`, `<DevSandbox />`, `<RecordDrawer />` |
516
- | `yaxa-svelte/server` | `yaxaEndpoint` Pipeline, Declarative Guards, OpenAPI 3.1 & Scalar Docs, RFC 7807, Real-Time SSE (`yaxaSSE`, `broadcast`), Background Jobs (`enqueueJob`, `createJobRunner`), `eventBus`, Server Hooks, DB, Storage & Email |
517
- | `yaxa-svelte/auth` | Better-Auth client & server integration (`useAuth`, `<AuthCard />`, `<PasskeyUI />`, `<TwoFactorModal />`, `createYaxaAuth`) |
518
- | `yaxa-svelte/db` | Multi-dialect Drizzle ORM clients & schemas for Neon (PostgreSQL) and Turso (SQLite/LibSQL) |
519
- | `yaxa-svelte/polar` | Polar.sh billing, customer portal sessions, and webhook synchronization |
520
- | `yaxa-svelte/email` | Resend transactional email templates with automatic development console fallback |
521
- | `yaxa-svelte/storage` | AWS S3 and Cloudflare R2 presigned upload URL generators |
522
- | `yaxa-svelte/testing` | Test mocks & utilities (`createMockSiteConfig`, `createMockUser`, `createTestWrapperProps`) |
523
- | `yaxa-svelte/vite` | Turnkey Vite plugin (`yaxa()`) |
524
- | `yaxa-svelte/yaxa.css` | Standalone turnkey Tailwind CSS v4 stylesheet with base styling, `@theme`, and scrollbars |
525
- | `yaxa-svelte/theme.css` | Pure Tailwind CSS v4 design tokens, `:root` variables, and harmonic radius scale without `@import 'tailwindcss'` |
556
+ | Subpath | Description |
557
+ | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
558
+ | `yaxa-svelte` | Core UI Primitives, SEO Components, `<Gate />`, Type-Safe Client RPC (`createApiClient`), Runes Query Engine (`createQuery`, `createMutation`, `QueryClient`), Theme System & Composables (`useGate`, `useUpload`, `useSSE`, `useToast`, `useClipboard`, etc.) |
559
+ | `yaxa-svelte/seo` | Standalone SEO & Agent Engine: Sitemaps, robots.txt, XSL stylesheets, OpenGraph SVG, Schema.org JSON-LD (FAQPage, Product), WebManifest, llms.txt, skill.md, `useSeo()` runes, and prerendering utilities (Zero DB/Auth dependencies; 100% compatible with `@sveltejs/adapter-static`) |
560
+ | `yaxa-svelte/admin` | Drizzle ORM Schema Introspection, `<AdminDashboard />`, `createYaxaAdminHook`, `createDrizzleAdmin`, `<DevSandbox />`, `<RecordDrawer />` |
561
+ | `yaxa-svelte/server` | `yaxaEndpoint` Pipeline, Declarative Guards, OpenAPI 3.1 & Scalar Docs, RFC 7807, Real-Time SSE (`yaxaSSE`, `broadcast`), AI Streaming (`createAiStreamHandler`), Background Jobs (`enqueueJob`, `createJobRunner`), `eventBus`, Server Hooks, DB, Storage & Email |
562
+ | `yaxa-svelte/auth` | Better-Auth client & server integration (`useAuth`, `<SignInCard />`, `<SignUpCard />`, `<ResetPasswordCard />`, `<AuthCard />`, `<PasskeyUI />`, `<TwoFactorModal />`, `createYaxaAuth`) |
563
+ | `yaxa-svelte/db` | Multi-dialect Drizzle ORM clients & schemas for Neon (PostgreSQL) and Turso (SQLite/LibSQL) |
564
+ | `yaxa-svelte/polar` | Polar.sh billing, customer portal sessions, and webhook synchronization |
565
+ | `yaxa-svelte/email` | Resend transactional email templates with automatic development console fallback |
566
+ | `yaxa-svelte/storage` | AWS S3 and Cloudflare R2 presigned upload URL generators |
567
+ | `yaxa-svelte/testing` | Test mocks & utilities (`createMockSiteConfig`, `createMockUser`, `createTestWrapperProps`) |
568
+ | `yaxa-svelte/vite` | Turnkey Vite plugin (`yaxa()`) |
569
+ | `yaxa-svelte/yaxa.css` | Standalone turnkey Tailwind CSS v4 stylesheet with base styling, `@theme`, and scrollbars |
570
+ | `yaxa-svelte/theme.css` | Pure Tailwind CSS v4 design tokens, `:root` variables, and harmonic radius scale without `@import 'tailwindcss'` |
526
571
 
527
572
  ---
528
573
 
@@ -531,16 +576,16 @@ A zero-overhead, runes-native query and mutation cache built specifically for Sv
531
576
  | Category | Components |
532
577
  | :--------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
533
578
  | **Theming & Studio** | `<ThemeStudio>`, `theme`, `ACCENT_PALETTES`, `NEUTRAL_PALETTES`, `RADIUS_PRESETS`, Harmonic Radius Multiplier Scale, Chart Palette Series Tokens |
534
- | **AI Primitives** | `<AiChat>`, `<PromptBar>`, `<AiThought>`, `<AiToolCall>` |
579
+ | **AI Primitives** | `<AiChat>`, `<PromptBar>`, `<AiThought>`, `<AiToolCall>`, `useAiChat` |
535
580
  | **Charts (Pure SVG)** | `<LineChart>`, `<BarChart>`, `<DonutChart>` (0 external charting dependencies, auto-responsive, dark/light theme aware) |
536
- | **SaaS & Auth Suite** | `<PasskeyUI>` (WebAuthn), `<TwoFactorModal>` (TOTP), `<AuthCard>` (Polymorphic), `<Gate>`, `<UserMenu>`, `<OrgSwitcher>`, `<PricingCard>`, `<PricingTable>`, `<SubscriptionCard>`, `useAuth`, `useGate`, `useUpload` |
581
+ | **SaaS & Auth Suite** | `<Gate>`, `<UserMenu>`, `<OrgSwitcher>`, `<PricingCard>`, `<PricingTable>`, `<SubscriptionCard>`, `useGate`, `useUpload` (Root); `<SignInCard>`, `<SignUpCard>`, `<ResetPasswordCard>`, `<AuthCard>`, `<PasskeyUI>`, `<TwoFactorModal>`, `useAuth` (`yaxa-svelte/auth`) |
537
582
  | **Admin Suite** | `<AdminDashboard>`, `<RecordDrawer>`, `<DevSandbox>`, `<ImpersonationBanner>`, `createYaxaAdminHook`, `createDrizzleAdmin` |
538
583
  | **Elements & Code** | `<CodeDiff>` (Interactive Diff & Changelog), `<CodeBlock>`, `<DataTable>` (Client & Server Modes), `Button`, `ButtonGroup`, `Badge`, `Avatar`, `AvatarGroup`, `Chip`, `Meter`, `MetricCard`, `Kbd`, `Icon`, `Spinner`, `Progress`, `Skeleton`, `Link`, `Logo`, `Tree`, `Timeline`, `Terminal`, `VirtualList`, `Carousel` |
539
584
  | **Forms (Auto-Wired)** | `<Form>`, `<FormField>`, `Input`, `InputOTP`, `Textarea`, `Checkbox`, `Switch`, `Select`, `Combobox`, `MultiSelect`, `NumberInput`, `PhoneInput`, `CreditCardInput`, `RadioGroup`, `Slider`, `ColorPicker`, `ToggleGroup`, `DatePicker`, `DateRangePicker`, `Dropzone`, `<RichTextEditor>` |
540
585
  | **Layout & Blocks** | `Container`, `Header`, `Footer`, `Section`, `Card`, `Divider`, `YaxaApp`, `DashboardShell` (with independent sidebar tokens), `ResizablePanels`, Page Blocks (`Hero`, `SaaS Metrics`, `FAQ`, `Testimonials`, `FeatureGrid`) |
541
586
  | **Overlays & Nav** | `Tabs`, `Breadcrumb`, `Pagination`, `CommandPalette` (`⌘K`), `DropdownMenu`, `ContextMenu`, `Modal`, `Drawer`, `Slideover`, `Popover`, `Tooltip` (with `arrow={false}` & `unstyled`), `Alert`, `Accordion`, `NotificationCenter`, `Stepper` |
542
587
  | **Motion & Actions** | `springTilt`, `smoothSlide`, `staggerFly`, `autosize`, `clickOutside`, `portal`, `sortableItem`, `infiniteScrollAction` |
543
- | **Composables** | `useAuth`, `useGate`, `useUpload`, `useClipboard`, `useShortcuts`, `useColorMode`, `useToast`, `useMediaQuery`, `useDebounce`, `useIdle`, `useInfiniteScroll`, `useSortable`, `useDataTable`, `useLocale` |
588
+ | **Composables** | `useGate`, `useUpload`, `useClipboard`, `useShortcuts`, `useColorMode`, `useToast`, `useMediaQuery`, `useDebounce`, `useIdle`, `useInfiniteScroll`, `useSortable`, `useDataTable`, `useLocale` (Root); `useAuth` (`yaxa-svelte/auth`) |
544
589
 
545
590
  ---
546
591
 
@@ -1,5 +1,11 @@
1
1
  export { default as AuthCard } from '../components/saas/AuthCard.svelte';
2
- export type { AuthCardMode } from '../components/saas/AuthCard.svelte';
2
+ export type { AuthCardProps, AuthCardMode } from '../components/saas/AuthCard.svelte';
3
+ export { default as SignInCard } from '../components/saas/SignInCard.svelte';
4
+ export type { SignInCardProps } from '../components/saas/SignInCard.svelte';
5
+ export { default as SignUpCard } from '../components/saas/SignUpCard.svelte';
6
+ export type { SignUpCardProps } from '../components/saas/SignUpCard.svelte';
7
+ export { default as ResetPasswordCard } from '../components/saas/ResetPasswordCard.svelte';
8
+ export type { ResetPasswordCardProps } from '../components/saas/ResetPasswordCard.svelte';
3
9
  export { default as PasskeyUI, passkeyUiVariants } from '../components/saas/PasskeyUI.svelte';
4
10
  export type { PasskeyUiProps } from '../components/saas/PasskeyUI.svelte';
5
11
  export { default as TwoFactorModal } from '../components/saas/TwoFactorModal.svelte';
@@ -1,5 +1,8 @@
1
1
  // Yaxa SaaS Auth Suite (Better-Auth integration for Svelte 5 & SvelteKit)
2
2
  export { default as AuthCard } from '../components/saas/AuthCard.svelte';
3
+ export { default as SignInCard } from '../components/saas/SignInCard.svelte';
4
+ export { default as SignUpCard } from '../components/saas/SignUpCard.svelte';
5
+ export { default as ResetPasswordCard } from '../components/saas/ResetPasswordCard.svelte';
3
6
  export { default as PasskeyUI, passkeyUiVariants } from '../components/saas/PasskeyUI.svelte';
4
7
  export { default as TwoFactorModal } from '../components/saas/TwoFactorModal.svelte';
5
8
  export { useAuth } from '../composables/useAuth.svelte';
@@ -1,13 +1,80 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
2
  import AiChat from './AiChat.svelte';
3
3
  import PromptBar from './PromptBar.svelte';
4
4
  import AiThought from './AiThought.svelte';
5
5
  import AiToolCall from './AiToolCall.svelte';
6
+ import { useAiChat } from './useAiChat.svelte';
6
7
  describe('AI-Native Components', () => {
7
8
  it('should export all AI component definitions', () => {
8
9
  expect(AiChat).toBeDefined();
9
10
  expect(PromptBar).toBeDefined();
10
11
  expect(AiThought).toBeDefined();
11
12
  expect(AiToolCall).toBeDefined();
13
+ expect(typeof useAiChat).toBe('function');
14
+ });
15
+ });
16
+ describe('useAiChat Runes Composable', () => {
17
+ const originalFetch = globalThis.fetch;
18
+ afterEach(() => {
19
+ globalThis.fetch = originalFetch;
20
+ });
21
+ it('initializes with default and initial messages', () => {
22
+ const chat = useAiChat({
23
+ initialMessages: [{ id: 'init-1', role: 'user', content: 'Initial message' }]
24
+ });
25
+ expect(chat.messages).toHaveLength(1);
26
+ expect(chat.messages[0].content).toBe('Initial message');
27
+ expect(chat.input).toBe('');
28
+ expect(chat.loading).toBe(false);
29
+ expect(chat.error).toBeNull();
30
+ });
31
+ it('streams SSE text deltas and thoughts progressively into assistant message', async () => {
32
+ const encoder = new TextEncoder();
33
+ const stream = new ReadableStream({
34
+ start(controller) {
35
+ controller.enqueue(encoder.encode('data: {"type":"thought","thought":{"running":true,"text":"Analyzing..."}}\n\n'));
36
+ controller.enqueue(encoder.encode('data: {"type":"text","delta":"Hello "}\n\n'));
37
+ controller.enqueue(encoder.encode('data: {"type":"text","delta":"World!"}\n\n'));
38
+ controller.enqueue(encoder.encode('data: {"type":"done"}\n\n'));
39
+ controller.close();
40
+ }
41
+ });
42
+ globalThis.fetch = vi.fn().mockResolvedValue(new Response(stream, {
43
+ headers: { 'Content-Type': 'text/event-stream' }
44
+ }));
45
+ const chat = useAiChat();
46
+ chat.input = 'Say hello';
47
+ await chat.send();
48
+ expect(chat.messages).toHaveLength(2);
49
+ expect(chat.messages[0].role).toBe('user');
50
+ expect(chat.messages[0].content).toBe('Say hello');
51
+ expect(chat.messages[1].role).toBe('assistant');
52
+ expect(chat.messages[1].content).toBe('Hello World!');
53
+ expect(chat.messages[1].thought?.text).toBe('Analyzing...');
54
+ expect(chat.messages[1].isStreaming).toBe(false);
55
+ expect(chat.loading).toBe(false);
56
+ expect(chat.error).toBeNull();
57
+ });
58
+ it('handles retries and message clearing', async () => {
59
+ const encoder = new TextEncoder();
60
+ const mockResponse = () => new Response(new ReadableStream({
61
+ start(controller) {
62
+ controller.enqueue(encoder.encode('data: {"type":"text","delta":"Response 1"}\n\n'));
63
+ controller.enqueue(encoder.encode('data: {"type":"done"}\n\n'));
64
+ controller.close();
65
+ }
66
+ }));
67
+ globalThis.fetch = vi.fn().mockImplementation(() => Promise.resolve(mockResponse()));
68
+ const chat = useAiChat();
69
+ await chat.send('Prompt 1');
70
+ expect(chat.messages).toHaveLength(2);
71
+ expect(chat.messages[1].content).toBe('Response 1');
72
+ // Retrying replaces the assistant answer
73
+ await chat.retry();
74
+ expect(chat.messages).toHaveLength(2);
75
+ expect(chat.messages[0].content).toBe('Prompt 1');
76
+ // Clear empties conversation
77
+ chat.clear();
78
+ expect(chat.messages).toHaveLength(0);
12
79
  });
13
80
  });
@@ -6,3 +6,5 @@ export { default as AiThought } from './AiThought.svelte';
6
6
  export type { AiThoughtProps } from './AiThought.svelte';
7
7
  export { default as AiToolCall } from './AiToolCall.svelte';
8
8
  export type { AiToolCallProps, ToolCallStatus } from './AiToolCall.svelte';
9
+ export { useAiChat } from './useAiChat.svelte';
10
+ export type { UseAiChatOptions, UseAiChatReturn } from './useAiChat.svelte';
@@ -2,3 +2,4 @@ export { default as AiChat } from './AiChat.svelte';
2
2
  export { default as PromptBar } from './PromptBar.svelte';
3
3
  export { default as AiThought } from './AiThought.svelte';
4
4
  export { default as AiToolCall } from './AiToolCall.svelte';
5
+ export { useAiChat } from './useAiChat.svelte';
@@ -0,0 +1,68 @@
1
+ import type { AiMessage } from './AiChat.svelte';
2
+ export interface UseAiChatOptions {
3
+ /**
4
+ * Endpoint URL for streaming chat completions.
5
+ * @default '/api/ai/chat'
6
+ */
7
+ api?: string;
8
+ /**
9
+ * Initial messages loaded into the chat conversation.
10
+ */
11
+ initialMessages?: AiMessage[];
12
+ /**
13
+ * Additional HTTP request headers or function returning headers.
14
+ */
15
+ headers?: Record<string, string> | (() => Record<string, string>);
16
+ /**
17
+ * Additional request body payload merged with `{ messages }`.
18
+ */
19
+ body?: Record<string, unknown> | (() => Record<string, unknown>);
20
+ /**
21
+ * Callback invoked when assistant finishes streaming a response.
22
+ */
23
+ onFinish?: (message: AiMessage) => void;
24
+ /**
25
+ * Callback invoked when an error occurs during streaming or fetch.
26
+ */
27
+ onError?: (error: Error) => void;
28
+ }
29
+ export interface UseAiChatReturn {
30
+ readonly messages: AiMessage[];
31
+ input: string;
32
+ readonly loading: boolean;
33
+ readonly error: string | null;
34
+ send: (content?: string, files?: File[]) => Promise<void>;
35
+ stop: () => void;
36
+ retry: (message?: AiMessage) => Promise<void>;
37
+ clear: () => void;
38
+ setMessages: (newMessages: AiMessage[]) => void;
39
+ }
40
+ /**
41
+ * Svelte 5 reactive runes composable for AI streaming chat.
42
+ * Direct bridge between SvelteKit streaming endpoints (`createAiStreamHandler`) and `<AiChat>`.
43
+ *
44
+ * Automatically coordinates:
45
+ * - Reactive messages array with user and assistant turns
46
+ * - Progressive streaming text deltas
47
+ * - `<AiThought>` real-time reasoning durations and steps
48
+ * - `<AiToolCall>` live tool executions and states
49
+ * - Abortable streams (`stop()`) and conversational retries (`retry()`)
50
+ *
51
+ * @example
52
+ * ```svelte
53
+ * <script lang="ts">
54
+ * import { AiChat, useAiChat } from 'yaxa-svelte';
55
+ *
56
+ * const chat = useAiChat({ api: '/api/ai/chat' });
57
+ * </script>
58
+ *
59
+ * <AiChat
60
+ * messages={chat.messages}
61
+ * loading={chat.loading}
62
+ * onsend={(text) => chat.send(text)}
63
+ * onstop={() => chat.stop()}
64
+ * onretry={(msg) => chat.retry(msg)}
65
+ * />
66
+ * ```
67
+ */
68
+ export declare function useAiChat(options?: UseAiChatOptions): UseAiChatReturn;
@@ -0,0 +1,258 @@
1
+ import { SvelteDate } from 'svelte/reactivity';
2
+ /**
3
+ * Svelte 5 reactive runes composable for AI streaming chat.
4
+ * Direct bridge between SvelteKit streaming endpoints (`createAiStreamHandler`) and `<AiChat>`.
5
+ *
6
+ * Automatically coordinates:
7
+ * - Reactive messages array with user and assistant turns
8
+ * - Progressive streaming text deltas
9
+ * - `<AiThought>` real-time reasoning durations and steps
10
+ * - `<AiToolCall>` live tool executions and states
11
+ * - Abortable streams (`stop()`) and conversational retries (`retry()`)
12
+ *
13
+ * @example
14
+ * ```svelte
15
+ * <script lang="ts">
16
+ * import { AiChat, useAiChat } from 'yaxa-svelte';
17
+ *
18
+ * const chat = useAiChat({ api: '/api/ai/chat' });
19
+ * </script>
20
+ *
21
+ * <AiChat
22
+ * messages={chat.messages}
23
+ * loading={chat.loading}
24
+ * onsend={(text) => chat.send(text)}
25
+ * onstop={() => chat.stop()}
26
+ * onretry={(msg) => chat.retry(msg)}
27
+ * />
28
+ * ```
29
+ */
30
+ export function useAiChat(options = {}) {
31
+ const api = options.api || '/api/ai/chat';
32
+ let messages = $state(options.initialMessages ? options.initialMessages.map((m) => ({ ...m })) : []);
33
+ let input = $state('');
34
+ let loading = $state(false);
35
+ let error = $state(null);
36
+ let abortController = null;
37
+ async function send(content, _files) {
38
+ void _files;
39
+ const prompt = (content !== undefined ? content : input).trim();
40
+ if (!prompt)
41
+ return;
42
+ // Reset input and clear any previous errors
43
+ input = '';
44
+ error = null;
45
+ loading = true;
46
+ // 1. Append User Message
47
+ const userMsg = {
48
+ id: `user_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
49
+ role: 'user',
50
+ content: prompt,
51
+ createdAt: new SvelteDate()
52
+ };
53
+ messages.push(userMsg);
54
+ // 2. Append Assistant Placeholder Message
55
+ messages.push({
56
+ id: `asst_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
57
+ role: 'assistant',
58
+ content: '',
59
+ isStreaming: true,
60
+ createdAt: new SvelteDate()
61
+ });
62
+ const assistantIdx = messages.length - 1;
63
+ abortController = new AbortController();
64
+ try {
65
+ const customHeaders = typeof options.headers === 'function' ? options.headers() : options.headers || {};
66
+ const customBody = typeof options.body === 'function' ? options.body() : options.body || {};
67
+ const response = await fetch(api, {
68
+ method: 'POST',
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ ...customHeaders
72
+ },
73
+ body: JSON.stringify({
74
+ messages: $state.snapshot(messages),
75
+ ...customBody
76
+ }),
77
+ signal: abortController.signal
78
+ });
79
+ if (!response.ok) {
80
+ const errorText = await response.text().catch(() => '');
81
+ let detail = `Request failed with status ${response.status}`;
82
+ try {
83
+ const json = JSON.parse(errorText);
84
+ detail = json.detail || json.message || detail;
85
+ }
86
+ catch {
87
+ // Response body was not JSON, retain default detail message
88
+ }
89
+ throw new Error(detail);
90
+ }
91
+ if (!response.body) {
92
+ throw new Error('Response body is null, cannot stream SSE chunks.');
93
+ }
94
+ const reader = response.body.getReader();
95
+ const decoder = new TextDecoder();
96
+ let buffer = '';
97
+ while (true) {
98
+ const { done, value } = await reader.read();
99
+ if (done)
100
+ break;
101
+ buffer += decoder.decode(value, { stream: true });
102
+ const lines = buffer.split('\n');
103
+ buffer = lines.pop() || '';
104
+ const target = messages[assistantIdx];
105
+ if (!target)
106
+ break;
107
+ for (const line of lines) {
108
+ const trimmed = line.trim();
109
+ if (!trimmed || !trimmed.startsWith('data:'))
110
+ continue;
111
+ const dataStr = trimmed.slice(5).trim();
112
+ if (dataStr === '[DONE]') {
113
+ target.isStreaming = false;
114
+ continue;
115
+ }
116
+ try {
117
+ const chunk = JSON.parse(dataStr);
118
+ if (chunk.type === 'text' && chunk.delta) {
119
+ target.content += chunk.delta;
120
+ }
121
+ else if (chunk.type === 'thought' && chunk.thought) {
122
+ target.thought = {
123
+ ...target.thought,
124
+ ...chunk.thought
125
+ };
126
+ }
127
+ else if (chunk.type === 'tool_call' && chunk.tool) {
128
+ if (!target.tools)
129
+ target.tools = [];
130
+ const existingIdx = target.tools.findIndex((t) => t.name === chunk.tool.name);
131
+ if (existingIdx >= 0) {
132
+ target.tools[existingIdx] = {
133
+ ...target.tools[existingIdx],
134
+ ...chunk.tool
135
+ };
136
+ }
137
+ else {
138
+ target.tools.push(chunk.tool);
139
+ }
140
+ }
141
+ else if (chunk.type === 'error') {
142
+ error = chunk.error || 'AI generation encountered an error.';
143
+ target.isStreaming = false;
144
+ }
145
+ else if (chunk.type === 'done') {
146
+ target.isStreaming = false;
147
+ }
148
+ }
149
+ catch {
150
+ // Ignore unparsable SSE frames
151
+ }
152
+ }
153
+ }
154
+ const finalTarget = messages[assistantIdx];
155
+ if (finalTarget) {
156
+ finalTarget.isStreaming = false;
157
+ options.onFinish?.(finalTarget);
158
+ }
159
+ }
160
+ catch (err) {
161
+ if (err?.name === 'AbortError') {
162
+ // User manually aborted stream via stop()
163
+ }
164
+ else {
165
+ const errMsg = err instanceof Error ? err.message : String(err);
166
+ error = errMsg;
167
+ options.onError?.(err instanceof Error ? err : new Error(errMsg));
168
+ }
169
+ const finalTarget = messages[assistantIdx];
170
+ if (finalTarget) {
171
+ finalTarget.isStreaming = false;
172
+ }
173
+ }
174
+ finally {
175
+ loading = false;
176
+ abortController = null;
177
+ }
178
+ }
179
+ function stop() {
180
+ if (abortController) {
181
+ abortController.abort();
182
+ abortController = null;
183
+ }
184
+ loading = false;
185
+ const last = messages[messages.length - 1];
186
+ if (last && last.role === 'assistant') {
187
+ last.isStreaming = false;
188
+ }
189
+ }
190
+ async function retry(messageToRetry) {
191
+ if (loading)
192
+ stop();
193
+ let targetIdx = -1;
194
+ if (messageToRetry) {
195
+ targetIdx = messages.findIndex((m) => m.id === messageToRetry.id);
196
+ }
197
+ else {
198
+ // Find the last assistant message
199
+ for (let i = messages.length - 1; i >= 0; i--) {
200
+ if (messages[i].role === 'assistant') {
201
+ targetIdx = i;
202
+ break;
203
+ }
204
+ }
205
+ }
206
+ if (targetIdx < 0)
207
+ return;
208
+ // Find the closest preceding user message
209
+ let promptToRetry = '';
210
+ for (let i = targetIdx - 1; i >= 0; i--) {
211
+ if (messages[i].role === 'user') {
212
+ promptToRetry = messages[i].content;
213
+ break;
214
+ }
215
+ }
216
+ // Remove the target message and any following messages
217
+ messages.splice(targetIdx);
218
+ if (promptToRetry) {
219
+ // Also pop the user message so send() doesn't duplicate it
220
+ if (messages.length > 0 && messages[messages.length - 1].role === 'user') {
221
+ messages.pop();
222
+ }
223
+ await send(promptToRetry);
224
+ }
225
+ }
226
+ function clear() {
227
+ if (loading)
228
+ stop();
229
+ messages.length = 0;
230
+ error = null;
231
+ loading = false;
232
+ }
233
+ function setMessages(newMessages) {
234
+ messages = newMessages.map((m) => ({ ...m }));
235
+ }
236
+ return {
237
+ get messages() {
238
+ return messages;
239
+ },
240
+ get input() {
241
+ return input;
242
+ },
243
+ set input(val) {
244
+ input = val;
245
+ },
246
+ get loading() {
247
+ return loading;
248
+ },
249
+ get error() {
250
+ return error;
251
+ },
252
+ send,
253
+ stop,
254
+ retry,
255
+ clear,
256
+ setMessages
257
+ };
258
+ }
@@ -18,6 +18,7 @@
18
18
  badge?: string;
19
19
  items?: FAQItem[];
20
20
  categories?: string[];
21
+ enableSchema?: boolean;
21
22
  class?: string;
22
23
  };
23
24
  </script>
@@ -26,6 +27,7 @@
26
27
  import Icon from '../elements/Icon.svelte';
27
28
  import Badge from '../elements/Badge.svelte';
28
29
  import { smoothSlide } from '../../motion/transitions';
30
+ import { generateFaqSchema } from '../../site/schema.js';
29
31
 
30
32
  let {
31
33
  title = 'Frequently Asked Questions',
@@ -33,9 +35,12 @@
33
35
  badge = 'FAQ',
34
36
  items = [],
35
37
  categories = [],
38
+ enableSchema = true,
36
39
  class: className = ''
37
40
  }: FAQProps = $props();
38
41
 
42
+ let faqSchema = $derived(enableSchema && items.length > 0 ? generateFaqSchema(items) : null);
43
+
39
44
  let selectedCategory = $state('all');
40
45
  let openIds = $state<Array<string | number>>([]);
41
46
 
@@ -58,6 +63,12 @@
58
63
  }
59
64
  </script>
60
65
 
66
+ <svelte:head>
67
+ {#if faqSchema}
68
+ {@html `<script type="application/ld+json">${JSON.stringify(faqSchema)}</` + `script>`}
69
+ {/if}
70
+ </svelte:head>
71
+
61
72
  <section class={faqVariants({ class: className })}>
62
73
  {#if title || description || badge}
63
74
  <div class="mb-10 space-y-3 text-center">