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.
- package/README.md +68 -23
- package/dist/auth/index.d.ts +7 -1
- package/dist/auth/index.js +3 -0
- package/dist/components/ai/ai.test.js +68 -1
- package/dist/components/ai/index.d.ts +2 -0
- package/dist/components/ai/index.js +1 -0
- package/dist/components/ai/useAiChat.svelte.d.ts +68 -0
- package/dist/components/ai/useAiChat.svelte.js +258 -0
- package/dist/components/blocks/FAQ.svelte +11 -0
- package/dist/components/blocks/FAQ.svelte.d.ts +1 -0
- package/dist/components/blocks/FAQ.test.js +20 -0
- package/dist/components/saas/AuthCard.svelte +16 -12
- package/dist/components/saas/AuthCard.svelte.d.ts +3 -3
- package/dist/components/saas/ResetPasswordCard.svelte +13 -0
- package/dist/components/saas/ResetPasswordCard.svelte.d.ts +5 -0
- package/dist/components/saas/SignInCard.svelte +13 -0
- package/dist/components/saas/SignInCard.svelte.d.ts +5 -0
- package/dist/components/saas/SignUpCard.svelte +13 -0
- package/dist/components/saas/SignUpCard.svelte.d.ts +5 -0
- package/dist/components/saas/auth-cards.test.d.ts +1 -0
- package/dist/components/saas/auth-cards.test.js +50 -0
- package/dist/components/seo/Seo.svelte +16 -9
- package/dist/index.d.ts +9 -2
- package/dist/index.js +5 -1
- package/dist/root-purity.test.d.ts +1 -0
- package/dist/root-purity.test.js +96 -0
- package/dist/server/ai/index.d.ts +2 -0
- package/dist/server/ai/index.js +1 -0
- package/dist/server/ai/stream.d.ts +24 -0
- package/dist/server/ai/stream.js +178 -0
- package/dist/server/ai/stream.test.d.ts +1 -0
- package/dist/server/ai/stream.test.js +83 -0
- package/dist/server/ai/types.d.ts +43 -0
- package/dist/server/ai/types.js +1 -0
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.js +4 -0
- package/dist/site/context.svelte.d.ts +3 -0
- package/dist/site/context.svelte.js +7 -0
- package/dist/site/hook.d.ts +5 -1
- package/dist/site/hook.js +11 -1
- package/dist/site/index.d.ts +26 -0
- package/dist/site/index.js +25 -0
- package/dist/site/llms.d.ts +27 -0
- package/dist/site/llms.js +58 -0
- package/dist/site/schema.d.ts +45 -0
- package/dist/site/schema.js +43 -0
- package/dist/site/seo-helpers.d.ts +1 -0
- package/dist/site/site-export.test.d.ts +1 -0
- package/dist/site/site-export.test.js +193 -0
- package/dist/site/skill.d.ts +26 -0
- package/dist/site/skill.js +69 -0
- package/dist/site/useSeo.svelte.d.ts +23 -0
- package/dist/site/useSeo.svelte.js +40 -0
- package/dist/site/useSeo.test.d.ts +1 -0
- package/dist/site/useSeo.test.js +35 -0
- 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**: `
|
|
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/
|
|
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
|
-
|
|
200
|
+
import { Gate, useGate } from 'yaxa-svelte';
|
|
197
201
|
|
|
198
|
-
|
|
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
|
-
|
|
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/
|
|
516
|
-
| `yaxa-svelte/
|
|
517
|
-
| `yaxa-svelte/
|
|
518
|
-
| `yaxa-svelte/
|
|
519
|
-
| `yaxa-svelte/
|
|
520
|
-
| `yaxa-svelte/
|
|
521
|
-
| `yaxa-svelte/
|
|
522
|
-
| `yaxa-svelte/
|
|
523
|
-
| `yaxa-svelte/
|
|
524
|
-
| `yaxa-svelte/
|
|
525
|
-
| `yaxa-svelte/
|
|
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** | `<
|
|
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** | `
|
|
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
|
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/auth/index.js
CHANGED
|
@@ -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">
|