yaxa-svelte 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +66 -21
  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 +42 -0
  22. package/dist/components/seo/Seo.svelte +16 -9
  23. package/dist/index.d.ts +21 -2
  24. package/dist/index.js +12 -1
  25. package/dist/server/ai/index.d.ts +2 -0
  26. package/dist/server/ai/index.js +1 -0
  27. package/dist/server/ai/stream.d.ts +24 -0
  28. package/dist/server/ai/stream.js +178 -0
  29. package/dist/server/ai/stream.test.d.ts +1 -0
  30. package/dist/server/ai/stream.test.js +83 -0
  31. package/dist/server/ai/types.d.ts +43 -0
  32. package/dist/server/ai/types.js +1 -0
  33. package/dist/server/index.d.ts +6 -0
  34. package/dist/server/index.js +4 -0
  35. package/dist/site/context.svelte.d.ts +3 -0
  36. package/dist/site/context.svelte.js +7 -0
  37. package/dist/site/hook.d.ts +5 -1
  38. package/dist/site/hook.js +11 -1
  39. package/dist/site/index.d.ts +26 -0
  40. package/dist/site/index.js +25 -0
  41. package/dist/site/llms.d.ts +27 -0
  42. package/dist/site/llms.js +58 -0
  43. package/dist/site/schema.d.ts +45 -0
  44. package/dist/site/schema.js +43 -0
  45. package/dist/site/seo-helpers.d.ts +1 -0
  46. package/dist/site/site-export.test.d.ts +1 -0
  47. package/dist/site/site-export.test.js +193 -0
  48. package/dist/site/skill.d.ts +26 -0
  49. package/dist/site/skill.js +69 -0
  50. package/dist/site/useSeo.svelte.d.ts +23 -0
  51. package/dist/site/useSeo.svelte.js +40 -0
  52. package/dist/site/useSeo.test.d.ts +1 -0
  53. package/dist/site/useSeo.test.js +35 -0
  54. package/package.json +5 -1
@@ -1,9 +1,29 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { faqVariants } from './FAQ.svelte';
3
+ import FAQ from './FAQ.svelte';
4
+ import { renderComponent } from '../../testing/index.js';
3
5
  describe('FAQ Block', () => {
4
6
  it('generates default variant classes', () => {
5
7
  const classes = faqVariants();
6
8
  expect(classes).toContain('w-full');
7
9
  expect(classes).toContain('max-w-3xl');
8
10
  });
11
+ it('renders FAQ and injects Schema.org FAQPage json-ld when enabled', () => {
12
+ const items = [
13
+ { id: 1, question: 'How does it work?', answer: 'It works seamlessly with Svelte 5.' }
14
+ ];
15
+ const { cleanup, target } = renderComponent(FAQ, {
16
+ items,
17
+ enableSchema: true
18
+ });
19
+ expect(target.textContent).toContain('How does it work?');
20
+ expect(target.textContent).toContain('Frequently Asked Questions');
21
+ const scriptTag = document.querySelector('head script[type="application/ld+json"]');
22
+ if (scriptTag) {
23
+ const json = JSON.parse(scriptTag.textContent || '{}');
24
+ expect(json['@type']).toBe('FAQPage');
25
+ expect(json.mainEntity[0].name).toBe('How does it work?');
26
+ }
27
+ cleanup();
28
+ });
9
29
  });
@@ -1,13 +1,5 @@
1
- <script lang="ts">
2
- import Card from '../layout/Card.svelte';
3
- import Button from '../elements/Button.svelte';
4
- import Input from '../forms/Input.svelte';
5
- import FormField from '../forms/FormField.svelte';
6
- import InputOTP from '../forms/InputOTP.svelte';
7
- import Divider from '../layout/Divider.svelte';
8
- import Alert from '../overlays/Alert.svelte';
9
- import Icon from '../elements/Icon.svelte';
10
- import { useAuth, type SocialProvider } from '../../composables/useAuth.svelte';
1
+ <script module lang="ts">
2
+ import type { SocialProvider } from '../../composables/useAuth.svelte';
11
3
 
12
4
  export type AuthCardMode =
13
5
  | 'signin'
@@ -18,7 +10,7 @@
18
10
  | '2fa-verify'
19
11
  | 'email-change';
20
12
 
21
- interface Props {
13
+ export interface AuthCardProps {
22
14
  title?: string;
23
15
  subtitle?: string;
24
16
  mode?: AuthCardMode;
@@ -33,6 +25,18 @@
33
25
  onsuccess?: () => void;
34
26
  onerror?: (err: string) => void;
35
27
  }
28
+ </script>
29
+
30
+ <script lang="ts">
31
+ import Card from '../layout/Card.svelte';
32
+ import Button from '../elements/Button.svelte';
33
+ import Input from '../forms/Input.svelte';
34
+ import FormField from '../forms/FormField.svelte';
35
+ import InputOTP from '../forms/InputOTP.svelte';
36
+ import Divider from '../layout/Divider.svelte';
37
+ import Alert from '../overlays/Alert.svelte';
38
+ import Icon from '../elements/Icon.svelte';
39
+ import { useAuth } from '../../composables/useAuth.svelte';
36
40
 
37
41
  let {
38
42
  title,
@@ -48,7 +52,7 @@
48
52
  class: className = '',
49
53
  onsuccess,
50
54
  onerror
51
- }: Props = $props();
55
+ }: AuthCardProps = $props();
52
56
 
53
57
  const auth = useAuth();
54
58
 
@@ -1,6 +1,6 @@
1
- import { type SocialProvider } from '../../composables/useAuth.svelte';
1
+ import type { SocialProvider } from '../../composables/useAuth.svelte';
2
2
  export type AuthCardMode = 'signin' | 'signup' | 'magic-link' | 'forgot-password' | 'reset-password' | '2fa-verify' | 'email-change';
3
- interface Props {
3
+ export interface AuthCardProps {
4
4
  title?: string;
5
5
  subtitle?: string;
6
6
  mode?: AuthCardMode;
@@ -15,6 +15,6 @@ interface Props {
15
15
  onsuccess?: () => void;
16
16
  onerror?: (err: string) => void;
17
17
  }
18
- declare const AuthCard: import("svelte").Component<Props, {}, "">;
18
+ declare const AuthCard: import("svelte").Component<AuthCardProps, {}, "">;
19
19
  type AuthCard = ReturnType<typeof AuthCard>;
20
20
  export default AuthCard;
@@ -0,0 +1,13 @@
1
+ <script module lang="ts">
2
+ import type { AuthCardProps } from './AuthCard.svelte';
3
+
4
+ export type ResetPasswordCardProps = Omit<AuthCardProps, 'mode'>;
5
+ </script>
6
+
7
+ <script lang="ts">
8
+ import AuthCard from './AuthCard.svelte';
9
+
10
+ let props: ResetPasswordCardProps = $props();
11
+ </script>
12
+
13
+ <AuthCard mode="reset-password" {...props} />
@@ -0,0 +1,5 @@
1
+ import type { AuthCardProps } from './AuthCard.svelte';
2
+ export type ResetPasswordCardProps = Omit<AuthCardProps, 'mode'>;
3
+ declare const ResetPasswordCard: import("svelte").Component<ResetPasswordCardProps, {}, "">;
4
+ type ResetPasswordCard = ReturnType<typeof ResetPasswordCard>;
5
+ export default ResetPasswordCard;
@@ -0,0 +1,13 @@
1
+ <script module lang="ts">
2
+ import type { AuthCardProps } from './AuthCard.svelte';
3
+
4
+ export type SignInCardProps = Omit<AuthCardProps, 'mode'>;
5
+ </script>
6
+
7
+ <script lang="ts">
8
+ import AuthCard from './AuthCard.svelte';
9
+
10
+ let props: SignInCardProps = $props();
11
+ </script>
12
+
13
+ <AuthCard mode="signin" {...props} />
@@ -0,0 +1,5 @@
1
+ import type { AuthCardProps } from './AuthCard.svelte';
2
+ export type SignInCardProps = Omit<AuthCardProps, 'mode'>;
3
+ declare const SignInCard: import("svelte").Component<SignInCardProps, {}, "">;
4
+ type SignInCard = ReturnType<typeof SignInCard>;
5
+ export default SignInCard;
@@ -0,0 +1,13 @@
1
+ <script module lang="ts">
2
+ import type { AuthCardProps } from './AuthCard.svelte';
3
+
4
+ export type SignUpCardProps = Omit<AuthCardProps, 'mode'>;
5
+ </script>
6
+
7
+ <script lang="ts">
8
+ import AuthCard from './AuthCard.svelte';
9
+
10
+ let props: SignUpCardProps = $props();
11
+ </script>
12
+
13
+ <AuthCard mode="signup" {...props} />
@@ -0,0 +1,5 @@
1
+ import type { AuthCardProps } from './AuthCard.svelte';
2
+ export type SignUpCardProps = Omit<AuthCardProps, 'mode'>;
3
+ declare const SignUpCard: import("svelte").Component<SignUpCardProps, {}, "">;
4
+ type SignUpCard = ReturnType<typeof SignUpCard>;
5
+ export default SignUpCard;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import AuthCard from './AuthCard.svelte';
3
+ import SignInCard from './SignInCard.svelte';
4
+ import SignUpCard from './SignUpCard.svelte';
5
+ import ResetPasswordCard from './ResetPasswordCard.svelte';
6
+ import { renderComponent } from '../../testing/index.js';
7
+ describe('Turnkey Auth Suite Components', () => {
8
+ it('exports all auth card component definitions', () => {
9
+ expect(AuthCard).toBeDefined();
10
+ expect(SignInCard).toBeDefined();
11
+ expect(SignUpCard).toBeDefined();
12
+ expect(ResetPasswordCard).toBeDefined();
13
+ });
14
+ it('renders SignInCard with sign-in fields and action buttons', () => {
15
+ const { cleanup, target } = renderComponent(SignInCard, {
16
+ title: 'Welcome Back',
17
+ showSocial: true
18
+ });
19
+ expect(target.textContent).toContain('Welcome Back');
20
+ expect(target.textContent).toContain('Sign in');
21
+ expect(target.textContent).toContain('GitHub');
22
+ expect(target.textContent).toContain('Google');
23
+ cleanup();
24
+ });
25
+ it('renders SignUpCard with sign-up heading and register button', () => {
26
+ const { cleanup, target } = renderComponent(SignUpCard, {
27
+ title: 'Create Your Account'
28
+ });
29
+ expect(target.textContent).toContain('Create Your Account');
30
+ expect(target.textContent).toContain('Create Account');
31
+ cleanup();
32
+ });
33
+ it('renders ResetPasswordCard with password recovery UI', () => {
34
+ const { cleanup, target } = renderComponent(ResetPasswordCard, {
35
+ title: 'Reset Password',
36
+ resetToken: 'test-token-123'
37
+ });
38
+ expect(target.textContent).toContain('Reset Password');
39
+ expect(target.textContent).toContain('Confirm New Password');
40
+ cleanup();
41
+ });
42
+ });
@@ -39,10 +39,15 @@
39
39
 
40
40
  // Read config from prop or reactive Yaxa context
41
41
  let config = $derived(propConfig || yaxa.config);
42
+ let baseUrl = $derived((config.url || '').replace(/\/$/, ''));
42
43
 
43
- // Dynamic resolution from props OR SvelteKit page.data (e.g. +page.server.ts load data)
44
+ // Dynamic resolution from props OR useSeo() OR SvelteKit page.data (e.g. +page.server.ts load data)
44
45
  let pageData = $derived(page.data || {});
45
- let pageSeo = $derived((pageData.seo as Record<string, unknown>) || {});
46
+ let pageSeo = $derived(
47
+ (yaxa.pageSeo as unknown as Record<string, unknown>) ||
48
+ (pageData.seo as Record<string, unknown>) ||
49
+ {}
50
+ );
46
51
 
47
52
  let resolvedTitle = $derived(
48
53
  propTitle ||
@@ -73,9 +78,9 @@
73
78
  let currentUrl = $derived.by(() => {
74
79
  if (propCanonical) return propCanonical;
75
80
  try {
76
- return `${config.url}${page.url.pathname}`;
81
+ return `${baseUrl}${page.url.pathname}`;
77
82
  } catch {
78
- return config.url;
83
+ return baseUrl;
79
84
  }
80
85
  });
81
86
 
@@ -83,7 +88,9 @@
83
88
 
84
89
  let resolvedOgImageUrl = $derived.by(() => {
85
90
  if (typeof ogImage === 'string') {
86
- return ogImage.startsWith('http') ? ogImage : `${config.url}${ogImage}`;
91
+ return ogImage.startsWith('http')
92
+ ? ogImage
93
+ : `${baseUrl}${ogImage.startsWith('/') ? '' : '/'}${ogImage}`;
87
94
  }
88
95
  if (typeof ogImage === 'object' && ogImage !== null) {
89
96
  const pairs: string[] = [];
@@ -101,7 +108,7 @@
101
108
  pairs.push(`badge=${encodeURIComponent(computeProjectBadge(config.project))}`);
102
109
  }
103
110
  if (img.theme) pairs.push(`theme=${encodeURIComponent(img.theme)}`);
104
- return `${config.url}/api/og?${pairs.join('&')}`;
111
+ return `${baseUrl}/api/og?${pairs.join('&')}`;
105
112
  }
106
113
  if (resolvedTitle && resolvedTitle !== config.name) {
107
114
  const pairs: string[] = [
@@ -111,9 +118,9 @@
111
118
  if (config.project) {
112
119
  pairs.push(`badge=${encodeURIComponent(computeProjectBadge(config.project))}`);
113
120
  }
114
- return `${config.url}/api/og?${pairs.join('&')}`;
121
+ return `${baseUrl}/api/og?${pairs.join('&')}`;
115
122
  }
116
- return `${config.url}${config.seo?.defaultOgImage || '/api/og'}`;
123
+ return `${baseUrl}${config.seo?.defaultOgImage || '/api/og'}`;
117
124
  });
118
125
 
119
126
  let robotsContent = $derived.by(() => {
@@ -156,7 +163,7 @@
156
163
  <meta name="keywords" content={keywords.join(', ')} />
157
164
  {/if}
158
165
  <link rel="canonical" href={currentUrl} />
159
- <link rel="sitemap" type="application/xml" href="{config.url}/sitemap.xml" />
166
+ <link rel="sitemap" type="application/xml" href="{baseUrl}/sitemap.xml" />
160
167
  <meta name="robots" content={robotsContent} />
161
168
  <meta name="theme-color" content={config.theme?.primaryColor || '#ff3e00'} />
162
169
  {#if config.project?.license}
package/dist/index.d.ts CHANGED
@@ -45,6 +45,8 @@ export { default as AiThought } from './components/ai/AiThought.svelte';
45
45
  export type { AiThoughtProps } from './components/ai/AiThought.svelte';
46
46
  export { default as AiToolCall } from './components/ai/AiToolCall.svelte';
47
47
  export type { AiToolCallProps, ToolCallStatus } from './components/ai/AiToolCall.svelte';
48
+ export { useAiChat } from './components/ai/useAiChat.svelte';
49
+ export type { UseAiChatOptions, UseAiChatReturn } from './components/ai/useAiChat.svelte';
48
50
  export { default as LineChart } from './components/charts/LineChart.svelte';
49
51
  export type { LineChartProps, LineSeries, ChartDataPoint } from './components/charts/LineChart.svelte';
50
52
  export { default as BarChart } from './components/charts/BarChart.svelte';
@@ -181,10 +183,15 @@ export { createSitemapXslHandler, generateSitemapXsl } from './site/sitemap-xsl'
181
183
  export { createOgImageHandler, generateOgSvg } from './site/og';
182
184
  export type { OgImageOptions } from './site/og';
183
185
  export { createManifestHandler } from './site/manifest';
184
- export { generateWebSiteSchema, generateOrganizationSchema, generateSoftwareApplicationSchema, generateSoftwareSourceCodeSchema, generatePersonSchema, generateArticleSchema, generateBreadcrumbSchema } from './site/schema';
185
- export type { SchemaType, BreadcrumbItem, ArticleSchemaData } from './site/schema';
186
+ export { createLlmsTxtHandler, generateLlmsTxt } from './site/llms';
187
+ export type { LlmsTxtOptions, LlmsSection, LlmsLink } from './site/llms';
188
+ export { createSkillHandler, generateSkillMd } from './site/skill';
189
+ export type { SkillOptions, SkillAction } from './site/skill';
190
+ export { generateWebSiteSchema, generateOrganizationSchema, generateSoftwareApplicationSchema, generateSoftwareSourceCodeSchema, generatePersonSchema, generateArticleSchema, generateBreadcrumbSchema, generateFaqSchema, generateProductSchema } from './site/schema';
191
+ export type { SchemaType, BreadcrumbItem, ArticleSchemaData, FaqItem, ProductSchemaData } from './site/schema';
186
192
  export { definePageSeo } from './site/seo-helpers';
187
193
  export type { PageSeoConfig } from './site/seo-helpers';
194
+ export { useSeo } from './site/useSeo.svelte';
188
195
  export { yaxa } from './vite/index';
189
196
  export type { YaxaPluginOptions } from './vite/index';
190
197
  export { default as Gate } from './components/saas/Gate.svelte';
@@ -195,6 +202,18 @@ export type { OrgSwitcherProps, OrgItem } from './components/saas/OrgSwitcher.sv
195
202
  export { default as PricingCard } from './components/saas/PricingCard.svelte';
196
203
  export { default as PricingTable } from './components/saas/PricingTable.svelte';
197
204
  export { default as SubscriptionCard } from './components/saas/SubscriptionCard.svelte';
205
+ export { default as AuthCard } from './components/saas/AuthCard.svelte';
206
+ export type { AuthCardProps, AuthCardMode } from './components/saas/AuthCard.svelte';
207
+ export { default as SignInCard } from './components/saas/SignInCard.svelte';
208
+ export type { SignInCardProps } from './components/saas/SignInCard.svelte';
209
+ export { default as SignUpCard } from './components/saas/SignUpCard.svelte';
210
+ export type { SignUpCardProps } from './components/saas/SignUpCard.svelte';
211
+ export { default as ResetPasswordCard } from './components/saas/ResetPasswordCard.svelte';
212
+ export type { ResetPasswordCardProps } from './components/saas/ResetPasswordCard.svelte';
213
+ export { default as PasskeyUI } from './components/saas/PasskeyUI.svelte';
214
+ export { default as TwoFactorModal } from './components/saas/TwoFactorModal.svelte';
215
+ export { useAuth } from './composables/useAuth.svelte';
216
+ export type { SocialProvider } from './composables/useAuth.svelte';
198
217
  export { useGate } from './composables/useGate.svelte';
199
218
  export type { UseGateOptions } from './composables/useGate.svelte';
200
219
  export { useUpload } from './composables/useUpload.svelte';
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ export { default as AiChat } from './components/ai/AiChat.svelte';
30
30
  export { default as PromptBar } from './components/ai/PromptBar.svelte';
31
31
  export { default as AiThought } from './components/ai/AiThought.svelte';
32
32
  export { default as AiToolCall } from './components/ai/AiToolCall.svelte';
33
+ export { useAiChat } from './components/ai/useAiChat.svelte';
33
34
  // Native Theme-Aware SVG Charts
34
35
  export { default as LineChart } from './components/charts/LineChart.svelte';
35
36
  export { default as BarChart } from './components/charts/BarChart.svelte';
@@ -120,8 +121,11 @@ export { createSitemapHandler } from './site/sitemap';
120
121
  export { createSitemapXslHandler, generateSitemapXsl } from './site/sitemap-xsl';
121
122
  export { createOgImageHandler, generateOgSvg } from './site/og';
122
123
  export { createManifestHandler } from './site/manifest';
123
- export { generateWebSiteSchema, generateOrganizationSchema, generateSoftwareApplicationSchema, generateSoftwareSourceCodeSchema, generatePersonSchema, generateArticleSchema, generateBreadcrumbSchema } from './site/schema';
124
+ export { createLlmsTxtHandler, generateLlmsTxt } from './site/llms';
125
+ export { createSkillHandler, generateSkillMd } from './site/skill';
126
+ export { generateWebSiteSchema, generateOrganizationSchema, generateSoftwareApplicationSchema, generateSoftwareSourceCodeSchema, generatePersonSchema, generateArticleSchema, generateBreadcrumbSchema, generateFaqSchema, generateProductSchema } from './site/schema';
124
127
  export { definePageSeo } from './site/seo-helpers';
128
+ export { useSeo } from './site/useSeo.svelte';
125
129
  export { yaxa } from './vite/index';
126
130
  // SaaS Suite: UI Components & Composables (Pure UI & Context Runes)
127
131
  export { default as Gate } from './components/saas/Gate.svelte';
@@ -130,6 +134,13 @@ export { default as OrgSwitcher, orgSwitcherVariants } from './components/saas/O
130
134
  export { default as PricingCard } from './components/saas/PricingCard.svelte';
131
135
  export { default as PricingTable } from './components/saas/PricingTable.svelte';
132
136
  export { default as SubscriptionCard } from './components/saas/SubscriptionCard.svelte';
137
+ export { default as AuthCard } from './components/saas/AuthCard.svelte';
138
+ export { default as SignInCard } from './components/saas/SignInCard.svelte';
139
+ export { default as SignUpCard } from './components/saas/SignUpCard.svelte';
140
+ export { default as ResetPasswordCard } from './components/saas/ResetPasswordCard.svelte';
141
+ export { default as PasskeyUI } from './components/saas/PasskeyUI.svelte';
142
+ export { default as TwoFactorModal } from './components/saas/TwoFactorModal.svelte';
143
+ export { useAuth } from './composables/useAuth.svelte';
133
144
  export { useGate } from './composables/useGate.svelte';
134
145
  export { useUpload } from './composables/useUpload.svelte';
135
146
  // Pre-Built SaaS Blocks
@@ -0,0 +1,2 @@
1
+ export { createAiStreamHandler } from './stream';
2
+ export type { AiStreamChunk, AiStreamContext, AiStreamHandlerFn, CreateAiStreamHandlerOptions } from './types';
@@ -0,0 +1 @@
1
+ export { createAiStreamHandler } from './stream';
@@ -0,0 +1,24 @@
1
+ import type { RequestHandler } from '@sveltejs/kit';
2
+ import type { CreateAiStreamHandlerOptions } from './types';
3
+ /**
4
+ * Creates a turnkey SvelteKit RequestHandler for AI chat streaming.
5
+ * Supports Server-Sent Events (SSE), multi-provider backends (Gemini, OpenAI, Anthropic, Ollama),
6
+ * guarded endpoints (auth/rate-limiting), and a built-in zero-config mock generator for dev/demos.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // src/routes/api/ai/chat/+server.ts
11
+ * import { createAiStreamHandler, requireAuth } from 'yaxa-svelte/server';
12
+ *
13
+ * export const POST = createAiStreamHandler({
14
+ * guards: [requireAuth()],
15
+ * handler: async ({ messages, send }) => {
16
+ * // Integrate your favorite LLM SDK
17
+ * for await (const chunk of streamText({ model: 'gemini-2.5-pro', messages })) {
18
+ * send({ type: 'text', delta: chunk.text });
19
+ * }
20
+ * }
21
+ * });
22
+ * ```
23
+ */
24
+ export declare function createAiStreamHandler(options?: CreateAiStreamHandlerOptions): RequestHandler;
@@ -0,0 +1,178 @@
1
+ import { problemResponse } from '../endpoint/problem';
2
+ /**
3
+ * Creates a turnkey SvelteKit RequestHandler for AI chat streaming.
4
+ * Supports Server-Sent Events (SSE), multi-provider backends (Gemini, OpenAI, Anthropic, Ollama),
5
+ * guarded endpoints (auth/rate-limiting), and a built-in zero-config mock generator for dev/demos.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * // src/routes/api/ai/chat/+server.ts
10
+ * import { createAiStreamHandler, requireAuth } from 'yaxa-svelte/server';
11
+ *
12
+ * export const POST = createAiStreamHandler({
13
+ * guards: [requireAuth()],
14
+ * handler: async ({ messages, send }) => {
15
+ * // Integrate your favorite LLM SDK
16
+ * for await (const chunk of streamText({ model: 'gemini-2.5-pro', messages })) {
17
+ * send({ type: 'text', delta: chunk.text });
18
+ * }
19
+ * }
20
+ * });
21
+ * ```
22
+ */
23
+ export function createAiStreamHandler(options = {}) {
24
+ const mockConfig = options.mock === true || (options.mock === undefined && !options.handler)
25
+ ? { delayMs: 25 }
26
+ : typeof options.mock === 'object'
27
+ ? { delayMs: options.mock.delayMs ?? 25 }
28
+ : null;
29
+ return async (event) => {
30
+ const { request, locals } = event;
31
+ // 1. Evaluate guards if configured
32
+ if (options.guards && options.guards.length > 0) {
33
+ for (const guard of options.guards) {
34
+ const result = await guard(event);
35
+ if (result instanceof Response) {
36
+ return result;
37
+ }
38
+ if (result && typeof result === 'object' && 'status' in result && 'title' in result) {
39
+ return problemResponse(result);
40
+ }
41
+ if (result === false) {
42
+ return problemResponse({
43
+ status: 403,
44
+ title: 'Forbidden',
45
+ detail: 'Access denied by endpoint guard.'
46
+ });
47
+ }
48
+ }
49
+ }
50
+ // 2. Parse request payload
51
+ let messages = [];
52
+ try {
53
+ const body = await request.json();
54
+ messages = Array.isArray(body?.messages) ? body.messages : [];
55
+ }
56
+ catch {
57
+ return problemResponse({
58
+ status: 400,
59
+ title: 'Bad Request',
60
+ detail: 'Invalid JSON request body. Expected an object with a "messages" array.'
61
+ });
62
+ }
63
+ await options.onStart?.(event);
64
+ const user = locals?.user ?? null;
65
+ const encoder = new TextEncoder();
66
+ const signal = request.signal;
67
+ const stream = new ReadableStream({
68
+ async start(controller) {
69
+ let isClosed = false;
70
+ function safeEnqueue(str) {
71
+ if (!isClosed && !signal.aborted) {
72
+ try {
73
+ controller.enqueue(encoder.encode(str));
74
+ }
75
+ catch {
76
+ isClosed = true;
77
+ }
78
+ }
79
+ }
80
+ function sendChunk(chunk) {
81
+ safeEnqueue(`data: ${JSON.stringify(chunk)}\n\n`);
82
+ }
83
+ const ctx = {
84
+ event,
85
+ messages,
86
+ user,
87
+ signal,
88
+ send: sendChunk
89
+ };
90
+ try {
91
+ if (options.handler) {
92
+ const res = await options.handler(ctx);
93
+ if (res && typeof res[Symbol.asyncIterator] === 'function') {
94
+ for await (const chunk of res) {
95
+ if (signal.aborted)
96
+ break;
97
+ sendChunk(chunk);
98
+ }
99
+ }
100
+ }
101
+ else if (mockConfig) {
102
+ // Simulated Realistic AI Flow: Thought -> Streamed Text -> Done
103
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === 'user');
104
+ const prompt = lastUserMsg?.content || 'Hello';
105
+ // 1. Thought start
106
+ sendChunk({
107
+ type: 'thought',
108
+ thought: {
109
+ running: true,
110
+ text: 'Analyzing request and inspecting context...',
111
+ steps: ['Interpreting prompt semantics', 'Checking relevant system tools']
112
+ }
113
+ });
114
+ await new Promise((r) => setTimeout(r, Math.max(10, mockConfig.delayMs * 2)));
115
+ if (signal.aborted)
116
+ return;
117
+ // 2. Thought finish
118
+ sendChunk({
119
+ type: 'thought',
120
+ thought: {
121
+ running: false,
122
+ duration: 0.8,
123
+ text: 'Context analyzed. Formulating response.',
124
+ steps: ['Interpreting prompt semantics', 'Synthesizing output response']
125
+ }
126
+ });
127
+ // 3. Stream response tokens
128
+ const mockText = `I have received your request: "${prompt}".\n\nThis response is streamed live via Yaxa's \`createAiStreamHandler\` using Server-Sent Events (SSE). You can connect this seamlessly with OpenAI, Google Gemini, Anthropic, or local Ollama models.`;
129
+ const words = mockText.split(' ');
130
+ for (let i = 0; i < words.length; i++) {
131
+ if (signal.aborted)
132
+ break;
133
+ const word = words[i] + (i === words.length - 1 ? '' : ' ');
134
+ sendChunk({ type: 'text', delta: word });
135
+ if (mockConfig.delayMs > 0) {
136
+ await new Promise((r) => setTimeout(r, mockConfig.delayMs));
137
+ }
138
+ }
139
+ if (!signal.aborted) {
140
+ sendChunk({ type: 'done' });
141
+ }
142
+ }
143
+ if (!isClosed) {
144
+ isClosed = true;
145
+ controller.close();
146
+ }
147
+ }
148
+ catch (err) {
149
+ if (!isClosed && !signal.aborted) {
150
+ sendChunk({
151
+ type: 'error',
152
+ error: err instanceof Error ? err.message : String(err)
153
+ });
154
+ isClosed = true;
155
+ try {
156
+ controller.close();
157
+ }
158
+ catch {
159
+ // Controller may already be closed
160
+ }
161
+ }
162
+ await options.onError?.(err, event);
163
+ }
164
+ },
165
+ cancel() {
166
+ // Client cancelled stream
167
+ }
168
+ });
169
+ return new Response(stream, {
170
+ headers: {
171
+ 'Content-Type': 'text/event-stream; charset=utf-8',
172
+ 'Cache-Control': 'no-cache, no-transform',
173
+ Connection: 'keep-alive',
174
+ 'X-Accel-Buffering': 'no'
175
+ }
176
+ });
177
+ };
178
+ }
@@ -0,0 +1 @@
1
+ export {};