ai-site-pilot 0.4.4 → 0.5.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.
package/README.md CHANGED
@@ -23,6 +23,35 @@ Works with any AI model (Gemini, GPT-4, Claude, Llama) via [OpenRouter](https://
23
23
  npm install ai-site-pilot
24
24
  ```
25
25
 
26
+ ## Setup
27
+
28
+ ### Tailwind CSS Configuration (Required)
29
+
30
+ If you're using Tailwind CSS, add ai-site-pilot to your content config so Tailwind generates the necessary classes:
31
+
32
+ ```js
33
+ // tailwind.config.js or tailwind.config.ts
34
+ module.exports = {
35
+ content: [
36
+ './app/**/*.{js,ts,jsx,tsx}',
37
+ './components/**/*.{js,ts,jsx,tsx}',
38
+ // Add this line:
39
+ './node_modules/ai-site-pilot/dist/**/*.{js,mjs}',
40
+ ],
41
+ // ...
42
+ }
43
+ ```
44
+
45
+ **Without this, the button text ("Ask AI") and other responsive styles won't work correctly.**
46
+
47
+ ### Import Styles
48
+
49
+ Make sure to import the CSS file in your component:
50
+
51
+ ```tsx
52
+ import 'ai-site-pilot/styles.css';
53
+ ```
54
+
26
55
  ## Quick Start
27
56
 
28
57
  ### 1. Get an OpenRouter API Key
@@ -117,18 +146,61 @@ See all models at [openrouter.ai/models](https://openrouter.ai/models)
117
146
 
118
147
  ### `createHandler()`
119
148
 
120
- Creates a Next.js API route handler.
149
+ Creates a Next.js API route handler. You can use either `systemPrompt` (manual) or `siteContent` (auto-generated).
150
+
151
+ **Option 1: Auto-generated prompt with `siteContent`** (Recommended)
121
152
 
122
153
  ```typescript
123
154
  import { createHandler } from 'ai-site-pilot/api';
124
155
 
125
156
  export const POST = createHandler({
126
- // Required
127
- systemPrompt: 'You are a helpful assistant...',
157
+ model: 'google/gemini-2.0-flash-exp:free',
158
+ siteContent: {
159
+ name: 'Acme Dance Studio',
160
+ type: 'dance studio',
161
+ description: 'Premier dance education since 1995',
162
+ personality: 'warm and encouraging',
163
+ pages: ['home', 'classes', 'teachers', 'schedule', 'contact'],
164
+ items: [
165
+ { id: 'ballet', name: 'Ballet', category: 'class', description: 'Classical ballet for ages 3-adult', price: '$80/month' },
166
+ { id: 'jazz', name: 'Jazz', category: 'class', description: 'High-energy jazz for ages 6+', price: '$75/month' },
167
+ { id: 'sarah', name: 'Sarah Johnson', category: 'teacher', description: 'Owner & lead instructor, 15 years experience' },
168
+ ],
169
+ faqs: [
170
+ { question: 'What should I wear?', answer: 'Leotard and ballet slippers for ballet, comfortable athletic wear for jazz.' },
171
+ ],
172
+ contact: {
173
+ email: 'info@acmedance.com',
174
+ phone: '555-1234',
175
+ hours: 'Mon-Sat 9am-8pm',
176
+ },
177
+ },
178
+ tools: [navigateTool, showClassTool],
179
+ });
180
+ ```
181
+
182
+ The AI automatically knows about your content and can answer questions like "What classes do you offer?" with specific details.
183
+
184
+ **Option 2: Manual `systemPrompt`**
185
+
186
+ ```typescript
187
+ export const POST = createHandler({
188
+ systemPrompt: 'You are a helpful assistant for Acme Dance Studio...',
189
+ tools: [navigateTool],
190
+ });
191
+ ```
192
+
193
+ **Full options:**
194
+
195
+ ```typescript
196
+ createHandler({
197
+ // Content (use ONE of these)
198
+ siteContent: { ... }, // Auto-generate prompt from your content
199
+ systemPrompt: '...', // Or write your own prompt
128
200
 
129
201
  // Optional
130
202
  apiKey: process.env.OPENROUTER_API_KEY, // Uses env var by default
131
- model: 'google/gemini-2.0-flash-exp:free', // Default
203
+ model: 'google/gemini-2.0-flash-exp:free', // Default (free!)
132
204
  tools: [myTool1, myTool2],
133
205
  temperature: 0.7,
134
206
  siteUrl: 'https://mysite.com', // Shown in OpenRouter dashboard
@@ -245,8 +317,65 @@ const generateFallback = createFallbackMessageGenerator({
245
317
 
246
318
  - React 18+ or React 19
247
319
  - Next.js 13+ (for API routes)
320
+ - Tailwind CSS (for responsive styles)
248
321
  - OpenRouter API key (free at [openrouter.ai](https://openrouter.ai))
249
322
 
323
+ ## Troubleshooting
324
+
325
+ ### Button only shows icon, no "Ask AI" text
326
+
327
+ Your Tailwind config isn't scanning the package. Add this to your `tailwind.config.js`:
328
+
329
+ ```js
330
+ content: [
331
+ // ... your paths
332
+ './node_modules/ai-site-pilot/dist/**/*.{js,mjs}',
333
+ ]
334
+ ```
335
+
336
+ ### Theme accent color not working
337
+
338
+ Make sure you're using the `accent` prop (preset) or `accentColor` prop (custom hex):
339
+
340
+ ```tsx
341
+ // Using preset
342
+ <SitePilot theme={{ accent: 'pink' }} />
343
+
344
+ // Using custom color
345
+ <SitePilot theme={{ accentColor: '#ec4899' }} />
346
+ ```
347
+
348
+ Don't set CSS variables directly—use the component props.
349
+
350
+ ### Tools not executing
351
+
352
+ 1. Make sure you're handling tool calls in `onToolCall`:
353
+ ```tsx
354
+ <SitePilot
355
+ onToolCall={(name, args) => {
356
+ console.log('Tool called:', name, args);
357
+ // Handle the tool...
358
+ }}
359
+ />
360
+ ```
361
+
362
+ 2. Check browser console for errors
363
+
364
+ ### "I've made some changes" generic message
365
+
366
+ Use `generateFallbackMessage` to customize messages when the AI uses tools without text:
367
+
368
+ ```tsx
369
+ import { createFallbackMessageGenerator } from 'ai-site-pilot';
370
+
371
+ const generateFallback = createFallbackMessageGenerator({
372
+ navigate: (args) => `Navigated to ${args.section}`,
373
+ filter: (args) => `Filtered by ${args.category}`,
374
+ });
375
+
376
+ <SitePilot generateFallbackMessage={generateFallback} />
377
+ ```
378
+
250
379
  ## License
251
380
 
252
381
  MIT
@@ -1,19 +1,85 @@
1
1
  import { T as ToolDefinition } from '../types--7jDyUM6.mjs';
2
2
  import { S as StreamEvent } from '../types-UmyDfs25.mjs';
3
3
 
4
+ /**
5
+ * Auto-generate system prompts from site content
6
+ * Makes it easy for any site to get a well-structured AI assistant
7
+ */
8
+ interface SiteContentItem {
9
+ /** Unique identifier for the item */
10
+ id: string;
11
+ /** Display name */
12
+ name: string;
13
+ /** Category/type (e.g., 'product', 'service', 'team', 'class') */
14
+ category?: string;
15
+ /** Description of the item */
16
+ description?: string;
17
+ /** Additional metadata */
18
+ [key: string]: unknown;
19
+ }
20
+ interface SiteContent {
21
+ /** Name of the business/site */
22
+ name: string;
23
+ /** Brief description of what the site/business does */
24
+ description?: string;
25
+ /** Type of business (e.g., 'dance studio', 'portfolio', 'e-commerce') */
26
+ type?: string;
27
+ /** Personality/tone for the assistant (e.g., 'friendly', 'professional', 'warm') */
28
+ personality?: string;
29
+ /** Available pages/sections users can navigate to */
30
+ pages?: string[];
31
+ /** Content items (products, services, team members, etc.) */
32
+ items?: SiteContentItem[];
33
+ /** FAQs */
34
+ faqs?: Array<{
35
+ question: string;
36
+ answer: string;
37
+ }>;
38
+ /** Contact information */
39
+ contact?: {
40
+ email?: string;
41
+ phone?: string;
42
+ address?: string;
43
+ hours?: string;
44
+ };
45
+ /** Additional context to include in the prompt */
46
+ additionalContext?: string;
47
+ }
48
+ /**
49
+ * Generate a system prompt from site content
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const prompt = generateSystemPrompt({
54
+ * name: 'Acme Dance Studio',
55
+ * type: 'dance studio',
56
+ * personality: 'warm and encouraging',
57
+ * pages: ['home', 'classes', 'teachers', 'schedule', 'contact'],
58
+ * items: [
59
+ * { id: 'ballet', name: 'Ballet', category: 'class', description: 'Classical ballet for ages 3-adult' },
60
+ * { id: 'jazz', name: 'Jazz', category: 'class', description: 'High-energy jazz for ages 6+' },
61
+ * { id: 'sarah', name: 'Sarah Johnson', category: 'teacher', description: 'Owner, 15 years experience' },
62
+ * ],
63
+ * contact: {
64
+ * email: 'info@acmedance.com',
65
+ * phone: '555-1234',
66
+ * },
67
+ * });
68
+ * ```
69
+ */
70
+ declare function generateSystemPrompt(content: SiteContent): string;
71
+
4
72
  /**
5
73
  * Universal chat handler using OpenRouter
6
74
  * Works with any model: Gemini, GPT-4, Claude, Llama, etc.
7
75
  * No SDK required - just standard fetch.
8
76
  */
9
77
 
10
- interface HandlerConfig {
78
+ interface BaseHandlerConfig {
11
79
  /** OpenRouter API key (or set OPENROUTER_API_KEY env var) */
12
80
  apiKey?: string;
13
81
  /** Model to use (e.g., 'google/gemini-2.0-flash-exp:free', 'openai/gpt-4o', 'anthropic/claude-3.5-sonnet') */
14
82
  model?: string;
15
- /** System prompt for the AI */
16
- systemPrompt: string;
17
83
  /** Tool definitions for the AI */
18
84
  tools?: ToolDefinition[];
19
85
  /** Temperature for response generation (0-1) */
@@ -23,6 +89,17 @@ interface HandlerConfig {
23
89
  /** Your app name (shown in OpenRouter dashboard) */
24
90
  siteName?: string;
25
91
  }
92
+ interface HandlerConfigWithPrompt extends BaseHandlerConfig {
93
+ /** System prompt for the AI (use this OR siteContent, not both) */
94
+ systemPrompt: string;
95
+ siteContent?: never;
96
+ }
97
+ interface HandlerConfigWithContent extends BaseHandlerConfig {
98
+ /** Site content to auto-generate system prompt (use this OR systemPrompt, not both) */
99
+ siteContent: SiteContent;
100
+ systemPrompt?: never;
101
+ }
102
+ type HandlerConfig = HandlerConfigWithPrompt | HandlerConfigWithContent;
26
103
  /**
27
104
  * Create a Next.js API route handler using OpenRouter
28
105
  *
@@ -69,4 +146,4 @@ declare function getSSEHeaders(): HeadersInit;
69
146
  */
70
147
  declare function parseSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>): AsyncGenerator<StreamEvent>;
71
148
 
72
- export { type HandlerConfig, createHandler, createSSEEncoder, getSSEHeaders, parseSSEStream };
149
+ export { type HandlerConfig, type SiteContent, type SiteContentItem, createHandler, createSSEEncoder, generateSystemPrompt, getSSEHeaders, parseSSEStream };
@@ -1,19 +1,85 @@
1
1
  import { T as ToolDefinition } from '../types--7jDyUM6.js';
2
2
  import { S as StreamEvent } from '../types-UmyDfs25.js';
3
3
 
4
+ /**
5
+ * Auto-generate system prompts from site content
6
+ * Makes it easy for any site to get a well-structured AI assistant
7
+ */
8
+ interface SiteContentItem {
9
+ /** Unique identifier for the item */
10
+ id: string;
11
+ /** Display name */
12
+ name: string;
13
+ /** Category/type (e.g., 'product', 'service', 'team', 'class') */
14
+ category?: string;
15
+ /** Description of the item */
16
+ description?: string;
17
+ /** Additional metadata */
18
+ [key: string]: unknown;
19
+ }
20
+ interface SiteContent {
21
+ /** Name of the business/site */
22
+ name: string;
23
+ /** Brief description of what the site/business does */
24
+ description?: string;
25
+ /** Type of business (e.g., 'dance studio', 'portfolio', 'e-commerce') */
26
+ type?: string;
27
+ /** Personality/tone for the assistant (e.g., 'friendly', 'professional', 'warm') */
28
+ personality?: string;
29
+ /** Available pages/sections users can navigate to */
30
+ pages?: string[];
31
+ /** Content items (products, services, team members, etc.) */
32
+ items?: SiteContentItem[];
33
+ /** FAQs */
34
+ faqs?: Array<{
35
+ question: string;
36
+ answer: string;
37
+ }>;
38
+ /** Contact information */
39
+ contact?: {
40
+ email?: string;
41
+ phone?: string;
42
+ address?: string;
43
+ hours?: string;
44
+ };
45
+ /** Additional context to include in the prompt */
46
+ additionalContext?: string;
47
+ }
48
+ /**
49
+ * Generate a system prompt from site content
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const prompt = generateSystemPrompt({
54
+ * name: 'Acme Dance Studio',
55
+ * type: 'dance studio',
56
+ * personality: 'warm and encouraging',
57
+ * pages: ['home', 'classes', 'teachers', 'schedule', 'contact'],
58
+ * items: [
59
+ * { id: 'ballet', name: 'Ballet', category: 'class', description: 'Classical ballet for ages 3-adult' },
60
+ * { id: 'jazz', name: 'Jazz', category: 'class', description: 'High-energy jazz for ages 6+' },
61
+ * { id: 'sarah', name: 'Sarah Johnson', category: 'teacher', description: 'Owner, 15 years experience' },
62
+ * ],
63
+ * contact: {
64
+ * email: 'info@acmedance.com',
65
+ * phone: '555-1234',
66
+ * },
67
+ * });
68
+ * ```
69
+ */
70
+ declare function generateSystemPrompt(content: SiteContent): string;
71
+
4
72
  /**
5
73
  * Universal chat handler using OpenRouter
6
74
  * Works with any model: Gemini, GPT-4, Claude, Llama, etc.
7
75
  * No SDK required - just standard fetch.
8
76
  */
9
77
 
10
- interface HandlerConfig {
78
+ interface BaseHandlerConfig {
11
79
  /** OpenRouter API key (or set OPENROUTER_API_KEY env var) */
12
80
  apiKey?: string;
13
81
  /** Model to use (e.g., 'google/gemini-2.0-flash-exp:free', 'openai/gpt-4o', 'anthropic/claude-3.5-sonnet') */
14
82
  model?: string;
15
- /** System prompt for the AI */
16
- systemPrompt: string;
17
83
  /** Tool definitions for the AI */
18
84
  tools?: ToolDefinition[];
19
85
  /** Temperature for response generation (0-1) */
@@ -23,6 +89,17 @@ interface HandlerConfig {
23
89
  /** Your app name (shown in OpenRouter dashboard) */
24
90
  siteName?: string;
25
91
  }
92
+ interface HandlerConfigWithPrompt extends BaseHandlerConfig {
93
+ /** System prompt for the AI (use this OR siteContent, not both) */
94
+ systemPrompt: string;
95
+ siteContent?: never;
96
+ }
97
+ interface HandlerConfigWithContent extends BaseHandlerConfig {
98
+ /** Site content to auto-generate system prompt (use this OR systemPrompt, not both) */
99
+ siteContent: SiteContent;
100
+ systemPrompt?: never;
101
+ }
102
+ type HandlerConfig = HandlerConfigWithPrompt | HandlerConfigWithContent;
26
103
  /**
27
104
  * Create a Next.js API route handler using OpenRouter
28
105
  *
@@ -69,4 +146,4 @@ declare function getSSEHeaders(): HeadersInit;
69
146
  */
70
147
  declare function parseSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>): AsyncGenerator<StreamEvent>;
71
148
 
72
- export { type HandlerConfig, createHandler, createSSEEncoder, getSSEHeaders, parseSSEStream };
149
+ export { type HandlerConfig, type SiteContent, type SiteContentItem, createHandler, createSSEEncoder, generateSystemPrompt, getSSEHeaders, parseSSEStream };
package/dist/api/index.js CHANGED
@@ -59,6 +59,81 @@ async function* parseSSEStream(reader) {
59
59
  }
60
60
  }
61
61
 
62
+ // src/api/generateSystemPrompt.ts
63
+ function generateSystemPrompt(content) {
64
+ const {
65
+ name,
66
+ description,
67
+ type,
68
+ personality = "helpful and friendly",
69
+ pages = [],
70
+ items = [],
71
+ faqs = [],
72
+ contact,
73
+ additionalContext
74
+ } = content;
75
+ const sections = [];
76
+ sections.push(`You are the AI assistant for ${name}${type ? `, a ${type}` : ""}.${description ? ` ${description}` : ""}`);
77
+ sections.push(`Your personality is ${personality}. Be conversational but concise.`);
78
+ const itemsByCategory = items.reduce((acc, item) => {
79
+ const cat = item.category || "other";
80
+ if (!acc[cat]) acc[cat] = [];
81
+ acc[cat].push(item);
82
+ return acc;
83
+ }, {});
84
+ if (Object.keys(itemsByCategory).length > 0) {
85
+ sections.push("\n## Available Content\n");
86
+ for (const [category, categoryItems] of Object.entries(itemsByCategory)) {
87
+ const categoryTitle = category.charAt(0).toUpperCase() + category.slice(1) + "s";
88
+ sections.push(`### ${categoryTitle}`);
89
+ for (const item of categoryItems) {
90
+ let itemLine = `- **${item.name}** (id: "${item.id}")`;
91
+ if (item.description) {
92
+ itemLine += `: ${item.description}`;
93
+ }
94
+ const extraFields = Object.entries(item).filter(([k]) => !["id", "name", "category", "description"].includes(k)).map(([k, v]) => `${k}: ${v}`).join(", ");
95
+ if (extraFields) {
96
+ itemLine += ` [${extraFields}]`;
97
+ }
98
+ sections.push(itemLine);
99
+ }
100
+ sections.push("");
101
+ }
102
+ }
103
+ if (pages.length > 0) {
104
+ sections.push(`## Site Sections
105
+ Users can navigate to: ${pages.join(", ")}`);
106
+ }
107
+ if (faqs.length > 0) {
108
+ sections.push("\n## Frequently Asked Questions");
109
+ for (const faq of faqs) {
110
+ sections.push(`**Q: ${faq.question}**`);
111
+ sections.push(`A: ${faq.answer}
112
+ `);
113
+ }
114
+ }
115
+ if (contact) {
116
+ sections.push("\n## Contact Information");
117
+ if (contact.email) sections.push(`- Email: ${contact.email}`);
118
+ if (contact.phone) sections.push(`- Phone: ${contact.phone}`);
119
+ if (contact.address) sections.push(`- Address: ${contact.address}`);
120
+ if (contact.hours) sections.push(`- Hours: ${contact.hours}`);
121
+ }
122
+ if (additionalContext) {
123
+ sections.push(`
124
+ ## Additional Information
125
+ ${additionalContext}`);
126
+ }
127
+ sections.push(`
128
+ ## Instructions
129
+ - When users ask about specific items, provide detailed information from the content above
130
+ - Use the available tools to navigate users to relevant pages or show them specific items
131
+ - If you don't know something, say so honestly - don't make up information
132
+ - Keep responses concise but helpful
133
+ - Reference specific items by their id when using tools`);
134
+ return sections.join("\n");
135
+ }
136
+
62
137
  // src/api/createHandler.ts
63
138
  function convertTools(tools) {
64
139
  return tools.map((tool) => ({
@@ -87,12 +162,12 @@ function createHandler(config) {
87
162
  const {
88
163
  apiKey = process.env.OPENROUTER_API_KEY,
89
164
  model = "google/gemini-2.0-flash-exp:free",
90
- systemPrompt,
91
165
  tools = [],
92
166
  temperature = 0.7,
93
167
  siteUrl,
94
168
  siteName
95
169
  } = config;
170
+ const systemPrompt = "siteContent" in config && config.siteContent ? generateSystemPrompt(config.siteContent) : config.systemPrompt;
96
171
  return async function POST(req) {
97
172
  if (!apiKey) {
98
173
  return new Response(
@@ -217,6 +292,7 @@ function createHandler(config) {
217
292
 
218
293
  exports.createHandler = createHandler;
219
294
  exports.createSSEEncoder = createSSEEncoder;
295
+ exports.generateSystemPrompt = generateSystemPrompt;
220
296
  exports.getSSEHeaders = getSSEHeaders;
221
297
  exports.parseSSEStream = parseSSEStream;
222
298
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/api/streaming.ts","../../src/api/createHandler.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * SSE streaming utilities\n */\n\nimport type { StreamEvent } from '../types';\n\n/**\n * Create an SSE encoder for streaming responses\n */\nexport function createSSEEncoder() {\n const encoder = new TextEncoder();\n\n return {\n encode(event: StreamEvent): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify(event)}\\n\\n`);\n },\n\n encodeText(content: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'text', content })}\\n\\n`);\n },\n\n encodeTool(name: string, args: Record<string, unknown>): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'tool', name, args })}\\n\\n`);\n },\n\n encodeDone(): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'done' })}\\n\\n`);\n },\n\n encodeError(message: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'error', message })}\\n\\n`);\n },\n };\n}\n\n/**\n * Create SSE response headers\n */\nexport function getSSEHeaders(): HeadersInit {\n return {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n };\n}\n\n/**\n * Parse SSE events from a ReadableStream\n */\nexport async function* parseSSEStream(\n reader: ReadableStreamDefaultReader<Uint8Array>\n): AsyncGenerator<StreamEvent> {\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n try {\n const data = JSON.parse(line.slice(6)) as StreamEvent;\n yield data;\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n}\n","/**\n * Universal chat handler using OpenRouter\n * Works with any model: Gemini, GPT-4, Claude, Llama, etc.\n * No SDK required - just standard fetch.\n */\n\nimport type { ToolDefinition } from '../tools/types';\nimport { createSSEEncoder, getSSEHeaders } from './streaming';\n\nexport interface HandlerConfig {\n /** OpenRouter API key (or set OPENROUTER_API_KEY env var) */\n apiKey?: string;\n /** Model to use (e.g., 'google/gemini-2.0-flash-exp:free', 'openai/gpt-4o', 'anthropic/claude-3.5-sonnet') */\n model?: string;\n /** System prompt for the AI */\n systemPrompt: string;\n /** Tool definitions for the AI */\n tools?: ToolDefinition[];\n /** Temperature for response generation (0-1) */\n temperature?: number;\n /** Your site URL (shown in OpenRouter dashboard) */\n siteUrl?: string;\n /** Your app name (shown in OpenRouter dashboard) */\n siteName?: string;\n}\n\ninterface RequestBody {\n messages: Array<{ role: 'user' | 'assistant'; content: string }>;\n}\n\ninterface OpenRouterMessage {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\ninterface OpenRouterTool {\n type: 'function';\n function: {\n name: string;\n description: string;\n parameters: {\n type: 'object';\n properties: Record<string, unknown>;\n required: string[];\n };\n };\n}\n\n/**\n * Convert tool definitions to OpenAI/OpenRouter format\n */\nfunction convertTools(tools: ToolDefinition[]): OpenRouterTool[] {\n return tools.map(tool => ({\n type: 'function' as const,\n function: {\n name: tool.name,\n description: tool.description,\n parameters: {\n type: 'object' as const,\n properties: Object.fromEntries(\n Object.entries(tool.parameters.properties).map(([key, value]) => [\n key,\n {\n type: value.type,\n description: value.description,\n ...(value.enum && { enum: value.enum }),\n },\n ])\n ),\n required: tool.parameters.required,\n },\n },\n }));\n}\n\n/**\n * Create a Next.js API route handler using OpenRouter\n *\n * Works with any model - just change the model string:\n * - 'google/gemini-2.0-flash-exp:free' (free!)\n * - 'openai/gpt-4o'\n * - 'anthropic/claude-3.5-sonnet'\n * - 'meta-llama/llama-3.1-70b-instruct'\n *\n * @example\n * ```ts\n * // app/api/chat/route.ts\n * import { createHandler } from 'ai-site-pilot/api';\n *\n * export const POST = createHandler({\n * model: 'google/gemini-2.0-flash-exp:free',\n * systemPrompt: 'You are a helpful assistant...',\n * tools: myTools,\n * });\n * ```\n */\nexport function createHandler(config: HandlerConfig) {\n const {\n apiKey = process.env.OPENROUTER_API_KEY,\n model = 'google/gemini-2.0-flash-exp:free',\n systemPrompt,\n tools = [],\n temperature = 0.7,\n siteUrl,\n siteName,\n } = config;\n\n return async function POST(req: Request): Promise<Response> {\n if (!apiKey) {\n return new Response(\n JSON.stringify({ error: 'OpenRouter API key not configured. Get one at https://openrouter.ai' }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n try {\n const body = (await req.json()) as RequestBody;\n const { messages } = body;\n\n // Build messages array with system prompt\n const openRouterMessages: OpenRouterMessage[] = [\n { role: 'system', content: systemPrompt },\n ...messages.map((m) => ({\n role: m.role === 'assistant' ? 'assistant' as const : 'user' as const,\n content: m.content,\n })),\n ];\n\n const sse = createSSEEncoder();\n\n const stream = new ReadableStream({\n async start(controller) {\n try {\n const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n 'HTTP-Referer': siteUrl || '',\n 'X-Title': siteName || '',\n },\n body: JSON.stringify({\n model,\n messages: openRouterMessages,\n temperature,\n stream: true,\n ...(tools.length > 0 && { tools: convertTools(tools) }),\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n console.error('OpenRouter error:', error);\n controller.enqueue(sse.encodeError('Failed to get AI response'));\n controller.enqueue(sse.encodeDone());\n controller.close();\n return;\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n controller.enqueue(sse.encodeError('No response stream'));\n controller.enqueue(sse.encodeDone());\n controller.close();\n return;\n }\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n // Accumulate tool calls across stream chunks\n // OpenRouter streams tool calls in pieces: name first, then arguments in chunks\n const pendingToolCalls: Map<number, { name: string; arguments: string }> = new Map();\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n const data = line.slice(6);\n if (data === '[DONE]') continue;\n\n try {\n const parsed = JSON.parse(data);\n const delta = parsed.choices?.[0]?.delta;\n\n // Handle text content\n if (delta?.content) {\n controller.enqueue(sse.encodeText(delta.content));\n }\n\n // Handle tool calls - accumulate across chunks\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n const index = toolCall.index ?? 0;\n\n // Get or create pending tool call\n let pending = pendingToolCalls.get(index);\n if (!pending) {\n pending = { name: '', arguments: '' };\n pendingToolCalls.set(index, pending);\n }\n\n // Accumulate name (usually comes in first chunk)\n if (toolCall.function?.name) {\n pending.name = toolCall.function.name;\n }\n\n // Accumulate arguments (streamed in chunks)\n if (toolCall.function?.arguments) {\n pending.arguments += toolCall.function.arguments;\n }\n }\n }\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n\n // Emit all accumulated tool calls now that streaming is complete\n for (const [, toolCall] of pendingToolCalls) {\n if (toolCall.name && toolCall.arguments) {\n try {\n const args = JSON.parse(toolCall.arguments);\n controller.enqueue(sse.encodeTool(toolCall.name, args));\n } catch (e) {\n console.error('Failed to parse tool arguments:', toolCall.arguments, e);\n }\n }\n }\n\n controller.enqueue(sse.encodeDone());\n controller.close();\n } catch (error) {\n console.error('Streaming error:', error);\n controller.enqueue(sse.encodeError('An error occurred during streaming'));\n controller.close();\n }\n },\n });\n\n return new Response(stream, { headers: getSSEHeaders() });\n } catch (error) {\n console.error('Handler error:', error);\n return new Response(\n JSON.stringify({ error: 'Internal server error' }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n );\n }\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/api/streaming.ts","../../src/api/generateSystemPrompt.ts","../../src/api/createHandler.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * SSE streaming utilities\n */\n\nimport type { StreamEvent } from '../types';\n\n/**\n * Create an SSE encoder for streaming responses\n */\nexport function createSSEEncoder() {\n const encoder = new TextEncoder();\n\n return {\n encode(event: StreamEvent): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify(event)}\\n\\n`);\n },\n\n encodeText(content: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'text', content })}\\n\\n`);\n },\n\n encodeTool(name: string, args: Record<string, unknown>): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'tool', name, args })}\\n\\n`);\n },\n\n encodeDone(): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'done' })}\\n\\n`);\n },\n\n encodeError(message: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'error', message })}\\n\\n`);\n },\n };\n}\n\n/**\n * Create SSE response headers\n */\nexport function getSSEHeaders(): HeadersInit {\n return {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n };\n}\n\n/**\n * Parse SSE events from a ReadableStream\n */\nexport async function* parseSSEStream(\n reader: ReadableStreamDefaultReader<Uint8Array>\n): AsyncGenerator<StreamEvent> {\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n try {\n const data = JSON.parse(line.slice(6)) as StreamEvent;\n yield data;\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n}\n","/**\n * Auto-generate system prompts from site content\n * Makes it easy for any site to get a well-structured AI assistant\n */\n\nexport interface SiteContentItem {\n /** Unique identifier for the item */\n id: string;\n /** Display name */\n name: string;\n /** Category/type (e.g., 'product', 'service', 'team', 'class') */\n category?: string;\n /** Description of the item */\n description?: string;\n /** Additional metadata */\n [key: string]: unknown;\n}\n\nexport interface SiteContent {\n /** Name of the business/site */\n name: string;\n /** Brief description of what the site/business does */\n description?: string;\n /** Type of business (e.g., 'dance studio', 'portfolio', 'e-commerce') */\n type?: string;\n /** Personality/tone for the assistant (e.g., 'friendly', 'professional', 'warm') */\n personality?: string;\n /** Available pages/sections users can navigate to */\n pages?: string[];\n /** Content items (products, services, team members, etc.) */\n items?: SiteContentItem[];\n /** FAQs */\n faqs?: Array<{ question: string; answer: string }>;\n /** Contact information */\n contact?: {\n email?: string;\n phone?: string;\n address?: string;\n hours?: string;\n };\n /** Additional context to include in the prompt */\n additionalContext?: string;\n}\n\n/**\n * Generate a system prompt from site content\n *\n * @example\n * ```ts\n * const prompt = generateSystemPrompt({\n * name: 'Acme Dance Studio',\n * type: 'dance studio',\n * personality: 'warm and encouraging',\n * pages: ['home', 'classes', 'teachers', 'schedule', 'contact'],\n * items: [\n * { id: 'ballet', name: 'Ballet', category: 'class', description: 'Classical ballet for ages 3-adult' },\n * { id: 'jazz', name: 'Jazz', category: 'class', description: 'High-energy jazz for ages 6+' },\n * { id: 'sarah', name: 'Sarah Johnson', category: 'teacher', description: 'Owner, 15 years experience' },\n * ],\n * contact: {\n * email: 'info@acmedance.com',\n * phone: '555-1234',\n * },\n * });\n * ```\n */\nexport function generateSystemPrompt(content: SiteContent): string {\n const {\n name,\n description,\n type,\n personality = 'helpful and friendly',\n pages = [],\n items = [],\n faqs = [],\n contact,\n additionalContext,\n } = content;\n\n const sections: string[] = [];\n\n // Role and personality\n sections.push(`You are the AI assistant for ${name}${type ? `, a ${type}` : ''}.${description ? ` ${description}` : ''}`);\n sections.push(`Your personality is ${personality}. Be conversational but concise.`);\n\n // Group items by category\n const itemsByCategory = items.reduce((acc, item) => {\n const cat = item.category || 'other';\n if (!acc[cat]) acc[cat] = [];\n acc[cat].push(item);\n return acc;\n }, {} as Record<string, SiteContentItem[]>);\n\n // Content sections\n if (Object.keys(itemsByCategory).length > 0) {\n sections.push('\\n## Available Content\\n');\n\n for (const [category, categoryItems] of Object.entries(itemsByCategory)) {\n const categoryTitle = category.charAt(0).toUpperCase() + category.slice(1) + 's';\n sections.push(`### ${categoryTitle}`);\n\n for (const item of categoryItems) {\n let itemLine = `- **${item.name}** (id: \"${item.id}\")`;\n if (item.description) {\n itemLine += `: ${item.description}`;\n }\n // Include any extra fields\n const extraFields = Object.entries(item)\n .filter(([k]) => !['id', 'name', 'category', 'description'].includes(k))\n .map(([k, v]) => `${k}: ${v}`)\n .join(', ');\n if (extraFields) {\n itemLine += ` [${extraFields}]`;\n }\n sections.push(itemLine);\n }\n sections.push('');\n }\n }\n\n // Pages/Navigation\n if (pages.length > 0) {\n sections.push(`## Site Sections\\nUsers can navigate to: ${pages.join(', ')}`);\n }\n\n // FAQs\n if (faqs.length > 0) {\n sections.push('\\n## Frequently Asked Questions');\n for (const faq of faqs) {\n sections.push(`**Q: ${faq.question}**`);\n sections.push(`A: ${faq.answer}\\n`);\n }\n }\n\n // Contact info\n if (contact) {\n sections.push('\\n## Contact Information');\n if (contact.email) sections.push(`- Email: ${contact.email}`);\n if (contact.phone) sections.push(`- Phone: ${contact.phone}`);\n if (contact.address) sections.push(`- Address: ${contact.address}`);\n if (contact.hours) sections.push(`- Hours: ${contact.hours}`);\n }\n\n // Additional context\n if (additionalContext) {\n sections.push(`\\n## Additional Information\\n${additionalContext}`);\n }\n\n // Instructions\n sections.push(`\n## Instructions\n- When users ask about specific items, provide detailed information from the content above\n- Use the available tools to navigate users to relevant pages or show them specific items\n- If you don't know something, say so honestly - don't make up information\n- Keep responses concise but helpful\n- Reference specific items by their id when using tools`);\n\n return sections.join('\\n');\n}\n","/**\n * Universal chat handler using OpenRouter\n * Works with any model: Gemini, GPT-4, Claude, Llama, etc.\n * No SDK required - just standard fetch.\n */\n\nimport type { ToolDefinition } from '../tools/types';\nimport { createSSEEncoder, getSSEHeaders } from './streaming';\nimport { generateSystemPrompt, type SiteContent } from './generateSystemPrompt';\n\ninterface BaseHandlerConfig {\n /** OpenRouter API key (or set OPENROUTER_API_KEY env var) */\n apiKey?: string;\n /** Model to use (e.g., 'google/gemini-2.0-flash-exp:free', 'openai/gpt-4o', 'anthropic/claude-3.5-sonnet') */\n model?: string;\n /** Tool definitions for the AI */\n tools?: ToolDefinition[];\n /** Temperature for response generation (0-1) */\n temperature?: number;\n /** Your site URL (shown in OpenRouter dashboard) */\n siteUrl?: string;\n /** Your app name (shown in OpenRouter dashboard) */\n siteName?: string;\n}\n\ninterface HandlerConfigWithPrompt extends BaseHandlerConfig {\n /** System prompt for the AI (use this OR siteContent, not both) */\n systemPrompt: string;\n siteContent?: never;\n}\n\ninterface HandlerConfigWithContent extends BaseHandlerConfig {\n /** Site content to auto-generate system prompt (use this OR systemPrompt, not both) */\n siteContent: SiteContent;\n systemPrompt?: never;\n}\n\nexport type HandlerConfig = HandlerConfigWithPrompt | HandlerConfigWithContent;\n\ninterface RequestBody {\n messages: Array<{ role: 'user' | 'assistant'; content: string }>;\n}\n\ninterface OpenRouterMessage {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\ninterface OpenRouterTool {\n type: 'function';\n function: {\n name: string;\n description: string;\n parameters: {\n type: 'object';\n properties: Record<string, unknown>;\n required: string[];\n };\n };\n}\n\n/**\n * Convert tool definitions to OpenAI/OpenRouter format\n */\nfunction convertTools(tools: ToolDefinition[]): OpenRouterTool[] {\n return tools.map(tool => ({\n type: 'function' as const,\n function: {\n name: tool.name,\n description: tool.description,\n parameters: {\n type: 'object' as const,\n properties: Object.fromEntries(\n Object.entries(tool.parameters.properties).map(([key, value]) => [\n key,\n {\n type: value.type,\n description: value.description,\n ...(value.enum && { enum: value.enum }),\n },\n ])\n ),\n required: tool.parameters.required,\n },\n },\n }));\n}\n\n/**\n * Create a Next.js API route handler using OpenRouter\n *\n * Works with any model - just change the model string:\n * - 'google/gemini-2.0-flash-exp:free' (free!)\n * - 'openai/gpt-4o'\n * - 'anthropic/claude-3.5-sonnet'\n * - 'meta-llama/llama-3.1-70b-instruct'\n *\n * @example\n * ```ts\n * // app/api/chat/route.ts\n * import { createHandler } from 'ai-site-pilot/api';\n *\n * export const POST = createHandler({\n * model: 'google/gemini-2.0-flash-exp:free',\n * systemPrompt: 'You are a helpful assistant...',\n * tools: myTools,\n * });\n * ```\n */\nexport function createHandler(config: HandlerConfig) {\n const {\n apiKey = process.env.OPENROUTER_API_KEY,\n model = 'google/gemini-2.0-flash-exp:free',\n tools = [],\n temperature = 0.7,\n siteUrl,\n siteName,\n } = config;\n\n // Generate system prompt from siteContent or use provided systemPrompt\n const systemPrompt = 'siteContent' in config && config.siteContent\n ? generateSystemPrompt(config.siteContent)\n : (config as HandlerConfigWithPrompt).systemPrompt;\n\n return async function POST(req: Request): Promise<Response> {\n if (!apiKey) {\n return new Response(\n JSON.stringify({ error: 'OpenRouter API key not configured. Get one at https://openrouter.ai' }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n try {\n const body = (await req.json()) as RequestBody;\n const { messages } = body;\n\n // Build messages array with system prompt\n const openRouterMessages: OpenRouterMessage[] = [\n { role: 'system', content: systemPrompt },\n ...messages.map((m) => ({\n role: m.role === 'assistant' ? 'assistant' as const : 'user' as const,\n content: m.content,\n })),\n ];\n\n const sse = createSSEEncoder();\n\n const stream = new ReadableStream({\n async start(controller) {\n try {\n const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n 'HTTP-Referer': siteUrl || '',\n 'X-Title': siteName || '',\n },\n body: JSON.stringify({\n model,\n messages: openRouterMessages,\n temperature,\n stream: true,\n ...(tools.length > 0 && { tools: convertTools(tools) }),\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n console.error('OpenRouter error:', error);\n controller.enqueue(sse.encodeError('Failed to get AI response'));\n controller.enqueue(sse.encodeDone());\n controller.close();\n return;\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n controller.enqueue(sse.encodeError('No response stream'));\n controller.enqueue(sse.encodeDone());\n controller.close();\n return;\n }\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n // Accumulate tool calls across stream chunks\n // OpenRouter streams tool calls in pieces: name first, then arguments in chunks\n const pendingToolCalls: Map<number, { name: string; arguments: string }> = new Map();\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n const data = line.slice(6);\n if (data === '[DONE]') continue;\n\n try {\n const parsed = JSON.parse(data);\n const delta = parsed.choices?.[0]?.delta;\n\n // Handle text content\n if (delta?.content) {\n controller.enqueue(sse.encodeText(delta.content));\n }\n\n // Handle tool calls - accumulate across chunks\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n const index = toolCall.index ?? 0;\n\n // Get or create pending tool call\n let pending = pendingToolCalls.get(index);\n if (!pending) {\n pending = { name: '', arguments: '' };\n pendingToolCalls.set(index, pending);\n }\n\n // Accumulate name (usually comes in first chunk)\n if (toolCall.function?.name) {\n pending.name = toolCall.function.name;\n }\n\n // Accumulate arguments (streamed in chunks)\n if (toolCall.function?.arguments) {\n pending.arguments += toolCall.function.arguments;\n }\n }\n }\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n\n // Emit all accumulated tool calls now that streaming is complete\n for (const [, toolCall] of pendingToolCalls) {\n if (toolCall.name && toolCall.arguments) {\n try {\n const args = JSON.parse(toolCall.arguments);\n controller.enqueue(sse.encodeTool(toolCall.name, args));\n } catch (e) {\n console.error('Failed to parse tool arguments:', toolCall.arguments, e);\n }\n }\n }\n\n controller.enqueue(sse.encodeDone());\n controller.close();\n } catch (error) {\n console.error('Streaming error:', error);\n controller.enqueue(sse.encodeError('An error occurred during streaming'));\n controller.close();\n }\n },\n });\n\n return new Response(stream, { headers: getSSEHeaders() });\n } catch (error) {\n console.error('Handler error:', error);\n return new Response(\n JSON.stringify({ error: 'Internal server error' }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n );\n }\n };\n}\n"]}
@@ -57,6 +57,81 @@ async function* parseSSEStream(reader) {
57
57
  }
58
58
  }
59
59
 
60
+ // src/api/generateSystemPrompt.ts
61
+ function generateSystemPrompt(content) {
62
+ const {
63
+ name,
64
+ description,
65
+ type,
66
+ personality = "helpful and friendly",
67
+ pages = [],
68
+ items = [],
69
+ faqs = [],
70
+ contact,
71
+ additionalContext
72
+ } = content;
73
+ const sections = [];
74
+ sections.push(`You are the AI assistant for ${name}${type ? `, a ${type}` : ""}.${description ? ` ${description}` : ""}`);
75
+ sections.push(`Your personality is ${personality}. Be conversational but concise.`);
76
+ const itemsByCategory = items.reduce((acc, item) => {
77
+ const cat = item.category || "other";
78
+ if (!acc[cat]) acc[cat] = [];
79
+ acc[cat].push(item);
80
+ return acc;
81
+ }, {});
82
+ if (Object.keys(itemsByCategory).length > 0) {
83
+ sections.push("\n## Available Content\n");
84
+ for (const [category, categoryItems] of Object.entries(itemsByCategory)) {
85
+ const categoryTitle = category.charAt(0).toUpperCase() + category.slice(1) + "s";
86
+ sections.push(`### ${categoryTitle}`);
87
+ for (const item of categoryItems) {
88
+ let itemLine = `- **${item.name}** (id: "${item.id}")`;
89
+ if (item.description) {
90
+ itemLine += `: ${item.description}`;
91
+ }
92
+ const extraFields = Object.entries(item).filter(([k]) => !["id", "name", "category", "description"].includes(k)).map(([k, v]) => `${k}: ${v}`).join(", ");
93
+ if (extraFields) {
94
+ itemLine += ` [${extraFields}]`;
95
+ }
96
+ sections.push(itemLine);
97
+ }
98
+ sections.push("");
99
+ }
100
+ }
101
+ if (pages.length > 0) {
102
+ sections.push(`## Site Sections
103
+ Users can navigate to: ${pages.join(", ")}`);
104
+ }
105
+ if (faqs.length > 0) {
106
+ sections.push("\n## Frequently Asked Questions");
107
+ for (const faq of faqs) {
108
+ sections.push(`**Q: ${faq.question}**`);
109
+ sections.push(`A: ${faq.answer}
110
+ `);
111
+ }
112
+ }
113
+ if (contact) {
114
+ sections.push("\n## Contact Information");
115
+ if (contact.email) sections.push(`- Email: ${contact.email}`);
116
+ if (contact.phone) sections.push(`- Phone: ${contact.phone}`);
117
+ if (contact.address) sections.push(`- Address: ${contact.address}`);
118
+ if (contact.hours) sections.push(`- Hours: ${contact.hours}`);
119
+ }
120
+ if (additionalContext) {
121
+ sections.push(`
122
+ ## Additional Information
123
+ ${additionalContext}`);
124
+ }
125
+ sections.push(`
126
+ ## Instructions
127
+ - When users ask about specific items, provide detailed information from the content above
128
+ - Use the available tools to navigate users to relevant pages or show them specific items
129
+ - If you don't know something, say so honestly - don't make up information
130
+ - Keep responses concise but helpful
131
+ - Reference specific items by their id when using tools`);
132
+ return sections.join("\n");
133
+ }
134
+
60
135
  // src/api/createHandler.ts
61
136
  function convertTools(tools) {
62
137
  return tools.map((tool) => ({
@@ -85,12 +160,12 @@ function createHandler(config) {
85
160
  const {
86
161
  apiKey = process.env.OPENROUTER_API_KEY,
87
162
  model = "google/gemini-2.0-flash-exp:free",
88
- systemPrompt,
89
163
  tools = [],
90
164
  temperature = 0.7,
91
165
  siteUrl,
92
166
  siteName
93
167
  } = config;
168
+ const systemPrompt = "siteContent" in config && config.siteContent ? generateSystemPrompt(config.siteContent) : config.systemPrompt;
94
169
  return async function POST(req) {
95
170
  if (!apiKey) {
96
171
  return new Response(
@@ -213,6 +288,6 @@ function createHandler(config) {
213
288
  };
214
289
  }
215
290
 
216
- export { createHandler, createSSEEncoder, getSSEHeaders, parseSSEStream };
291
+ export { createHandler, createSSEEncoder, generateSystemPrompt, getSSEHeaders, parseSSEStream };
217
292
  //# sourceMappingURL=index.mjs.map
218
293
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/api/streaming.ts","../../src/api/createHandler.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * SSE streaming utilities\n */\n\nimport type { StreamEvent } from '../types';\n\n/**\n * Create an SSE encoder for streaming responses\n */\nexport function createSSEEncoder() {\n const encoder = new TextEncoder();\n\n return {\n encode(event: StreamEvent): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify(event)}\\n\\n`);\n },\n\n encodeText(content: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'text', content })}\\n\\n`);\n },\n\n encodeTool(name: string, args: Record<string, unknown>): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'tool', name, args })}\\n\\n`);\n },\n\n encodeDone(): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'done' })}\\n\\n`);\n },\n\n encodeError(message: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'error', message })}\\n\\n`);\n },\n };\n}\n\n/**\n * Create SSE response headers\n */\nexport function getSSEHeaders(): HeadersInit {\n return {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n };\n}\n\n/**\n * Parse SSE events from a ReadableStream\n */\nexport async function* parseSSEStream(\n reader: ReadableStreamDefaultReader<Uint8Array>\n): AsyncGenerator<StreamEvent> {\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n try {\n const data = JSON.parse(line.slice(6)) as StreamEvent;\n yield data;\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n}\n","/**\n * Universal chat handler using OpenRouter\n * Works with any model: Gemini, GPT-4, Claude, Llama, etc.\n * No SDK required - just standard fetch.\n */\n\nimport type { ToolDefinition } from '../tools/types';\nimport { createSSEEncoder, getSSEHeaders } from './streaming';\n\nexport interface HandlerConfig {\n /** OpenRouter API key (or set OPENROUTER_API_KEY env var) */\n apiKey?: string;\n /** Model to use (e.g., 'google/gemini-2.0-flash-exp:free', 'openai/gpt-4o', 'anthropic/claude-3.5-sonnet') */\n model?: string;\n /** System prompt for the AI */\n systemPrompt: string;\n /** Tool definitions for the AI */\n tools?: ToolDefinition[];\n /** Temperature for response generation (0-1) */\n temperature?: number;\n /** Your site URL (shown in OpenRouter dashboard) */\n siteUrl?: string;\n /** Your app name (shown in OpenRouter dashboard) */\n siteName?: string;\n}\n\ninterface RequestBody {\n messages: Array<{ role: 'user' | 'assistant'; content: string }>;\n}\n\ninterface OpenRouterMessage {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\ninterface OpenRouterTool {\n type: 'function';\n function: {\n name: string;\n description: string;\n parameters: {\n type: 'object';\n properties: Record<string, unknown>;\n required: string[];\n };\n };\n}\n\n/**\n * Convert tool definitions to OpenAI/OpenRouter format\n */\nfunction convertTools(tools: ToolDefinition[]): OpenRouterTool[] {\n return tools.map(tool => ({\n type: 'function' as const,\n function: {\n name: tool.name,\n description: tool.description,\n parameters: {\n type: 'object' as const,\n properties: Object.fromEntries(\n Object.entries(tool.parameters.properties).map(([key, value]) => [\n key,\n {\n type: value.type,\n description: value.description,\n ...(value.enum && { enum: value.enum }),\n },\n ])\n ),\n required: tool.parameters.required,\n },\n },\n }));\n}\n\n/**\n * Create a Next.js API route handler using OpenRouter\n *\n * Works with any model - just change the model string:\n * - 'google/gemini-2.0-flash-exp:free' (free!)\n * - 'openai/gpt-4o'\n * - 'anthropic/claude-3.5-sonnet'\n * - 'meta-llama/llama-3.1-70b-instruct'\n *\n * @example\n * ```ts\n * // app/api/chat/route.ts\n * import { createHandler } from 'ai-site-pilot/api';\n *\n * export const POST = createHandler({\n * model: 'google/gemini-2.0-flash-exp:free',\n * systemPrompt: 'You are a helpful assistant...',\n * tools: myTools,\n * });\n * ```\n */\nexport function createHandler(config: HandlerConfig) {\n const {\n apiKey = process.env.OPENROUTER_API_KEY,\n model = 'google/gemini-2.0-flash-exp:free',\n systemPrompt,\n tools = [],\n temperature = 0.7,\n siteUrl,\n siteName,\n } = config;\n\n return async function POST(req: Request): Promise<Response> {\n if (!apiKey) {\n return new Response(\n JSON.stringify({ error: 'OpenRouter API key not configured. Get one at https://openrouter.ai' }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n try {\n const body = (await req.json()) as RequestBody;\n const { messages } = body;\n\n // Build messages array with system prompt\n const openRouterMessages: OpenRouterMessage[] = [\n { role: 'system', content: systemPrompt },\n ...messages.map((m) => ({\n role: m.role === 'assistant' ? 'assistant' as const : 'user' as const,\n content: m.content,\n })),\n ];\n\n const sse = createSSEEncoder();\n\n const stream = new ReadableStream({\n async start(controller) {\n try {\n const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n 'HTTP-Referer': siteUrl || '',\n 'X-Title': siteName || '',\n },\n body: JSON.stringify({\n model,\n messages: openRouterMessages,\n temperature,\n stream: true,\n ...(tools.length > 0 && { tools: convertTools(tools) }),\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n console.error('OpenRouter error:', error);\n controller.enqueue(sse.encodeError('Failed to get AI response'));\n controller.enqueue(sse.encodeDone());\n controller.close();\n return;\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n controller.enqueue(sse.encodeError('No response stream'));\n controller.enqueue(sse.encodeDone());\n controller.close();\n return;\n }\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n // Accumulate tool calls across stream chunks\n // OpenRouter streams tool calls in pieces: name first, then arguments in chunks\n const pendingToolCalls: Map<number, { name: string; arguments: string }> = new Map();\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n const data = line.slice(6);\n if (data === '[DONE]') continue;\n\n try {\n const parsed = JSON.parse(data);\n const delta = parsed.choices?.[0]?.delta;\n\n // Handle text content\n if (delta?.content) {\n controller.enqueue(sse.encodeText(delta.content));\n }\n\n // Handle tool calls - accumulate across chunks\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n const index = toolCall.index ?? 0;\n\n // Get or create pending tool call\n let pending = pendingToolCalls.get(index);\n if (!pending) {\n pending = { name: '', arguments: '' };\n pendingToolCalls.set(index, pending);\n }\n\n // Accumulate name (usually comes in first chunk)\n if (toolCall.function?.name) {\n pending.name = toolCall.function.name;\n }\n\n // Accumulate arguments (streamed in chunks)\n if (toolCall.function?.arguments) {\n pending.arguments += toolCall.function.arguments;\n }\n }\n }\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n\n // Emit all accumulated tool calls now that streaming is complete\n for (const [, toolCall] of pendingToolCalls) {\n if (toolCall.name && toolCall.arguments) {\n try {\n const args = JSON.parse(toolCall.arguments);\n controller.enqueue(sse.encodeTool(toolCall.name, args));\n } catch (e) {\n console.error('Failed to parse tool arguments:', toolCall.arguments, e);\n }\n }\n }\n\n controller.enqueue(sse.encodeDone());\n controller.close();\n } catch (error) {\n console.error('Streaming error:', error);\n controller.enqueue(sse.encodeError('An error occurred during streaming'));\n controller.close();\n }\n },\n });\n\n return new Response(stream, { headers: getSSEHeaders() });\n } catch (error) {\n console.error('Handler error:', error);\n return new Response(\n JSON.stringify({ error: 'Internal server error' }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n );\n }\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/api/streaming.ts","../../src/api/generateSystemPrompt.ts","../../src/api/createHandler.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * SSE streaming utilities\n */\n\nimport type { StreamEvent } from '../types';\n\n/**\n * Create an SSE encoder for streaming responses\n */\nexport function createSSEEncoder() {\n const encoder = new TextEncoder();\n\n return {\n encode(event: StreamEvent): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify(event)}\\n\\n`);\n },\n\n encodeText(content: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'text', content })}\\n\\n`);\n },\n\n encodeTool(name: string, args: Record<string, unknown>): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'tool', name, args })}\\n\\n`);\n },\n\n encodeDone(): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'done' })}\\n\\n`);\n },\n\n encodeError(message: string): Uint8Array {\n return encoder.encode(`data: ${JSON.stringify({ type: 'error', message })}\\n\\n`);\n },\n };\n}\n\n/**\n * Create SSE response headers\n */\nexport function getSSEHeaders(): HeadersInit {\n return {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n };\n}\n\n/**\n * Parse SSE events from a ReadableStream\n */\nexport async function* parseSSEStream(\n reader: ReadableStreamDefaultReader<Uint8Array>\n): AsyncGenerator<StreamEvent> {\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n try {\n const data = JSON.parse(line.slice(6)) as StreamEvent;\n yield data;\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n}\n","/**\n * Auto-generate system prompts from site content\n * Makes it easy for any site to get a well-structured AI assistant\n */\n\nexport interface SiteContentItem {\n /** Unique identifier for the item */\n id: string;\n /** Display name */\n name: string;\n /** Category/type (e.g., 'product', 'service', 'team', 'class') */\n category?: string;\n /** Description of the item */\n description?: string;\n /** Additional metadata */\n [key: string]: unknown;\n}\n\nexport interface SiteContent {\n /** Name of the business/site */\n name: string;\n /** Brief description of what the site/business does */\n description?: string;\n /** Type of business (e.g., 'dance studio', 'portfolio', 'e-commerce') */\n type?: string;\n /** Personality/tone for the assistant (e.g., 'friendly', 'professional', 'warm') */\n personality?: string;\n /** Available pages/sections users can navigate to */\n pages?: string[];\n /** Content items (products, services, team members, etc.) */\n items?: SiteContentItem[];\n /** FAQs */\n faqs?: Array<{ question: string; answer: string }>;\n /** Contact information */\n contact?: {\n email?: string;\n phone?: string;\n address?: string;\n hours?: string;\n };\n /** Additional context to include in the prompt */\n additionalContext?: string;\n}\n\n/**\n * Generate a system prompt from site content\n *\n * @example\n * ```ts\n * const prompt = generateSystemPrompt({\n * name: 'Acme Dance Studio',\n * type: 'dance studio',\n * personality: 'warm and encouraging',\n * pages: ['home', 'classes', 'teachers', 'schedule', 'contact'],\n * items: [\n * { id: 'ballet', name: 'Ballet', category: 'class', description: 'Classical ballet for ages 3-adult' },\n * { id: 'jazz', name: 'Jazz', category: 'class', description: 'High-energy jazz for ages 6+' },\n * { id: 'sarah', name: 'Sarah Johnson', category: 'teacher', description: 'Owner, 15 years experience' },\n * ],\n * contact: {\n * email: 'info@acmedance.com',\n * phone: '555-1234',\n * },\n * });\n * ```\n */\nexport function generateSystemPrompt(content: SiteContent): string {\n const {\n name,\n description,\n type,\n personality = 'helpful and friendly',\n pages = [],\n items = [],\n faqs = [],\n contact,\n additionalContext,\n } = content;\n\n const sections: string[] = [];\n\n // Role and personality\n sections.push(`You are the AI assistant for ${name}${type ? `, a ${type}` : ''}.${description ? ` ${description}` : ''}`);\n sections.push(`Your personality is ${personality}. Be conversational but concise.`);\n\n // Group items by category\n const itemsByCategory = items.reduce((acc, item) => {\n const cat = item.category || 'other';\n if (!acc[cat]) acc[cat] = [];\n acc[cat].push(item);\n return acc;\n }, {} as Record<string, SiteContentItem[]>);\n\n // Content sections\n if (Object.keys(itemsByCategory).length > 0) {\n sections.push('\\n## Available Content\\n');\n\n for (const [category, categoryItems] of Object.entries(itemsByCategory)) {\n const categoryTitle = category.charAt(0).toUpperCase() + category.slice(1) + 's';\n sections.push(`### ${categoryTitle}`);\n\n for (const item of categoryItems) {\n let itemLine = `- **${item.name}** (id: \"${item.id}\")`;\n if (item.description) {\n itemLine += `: ${item.description}`;\n }\n // Include any extra fields\n const extraFields = Object.entries(item)\n .filter(([k]) => !['id', 'name', 'category', 'description'].includes(k))\n .map(([k, v]) => `${k}: ${v}`)\n .join(', ');\n if (extraFields) {\n itemLine += ` [${extraFields}]`;\n }\n sections.push(itemLine);\n }\n sections.push('');\n }\n }\n\n // Pages/Navigation\n if (pages.length > 0) {\n sections.push(`## Site Sections\\nUsers can navigate to: ${pages.join(', ')}`);\n }\n\n // FAQs\n if (faqs.length > 0) {\n sections.push('\\n## Frequently Asked Questions');\n for (const faq of faqs) {\n sections.push(`**Q: ${faq.question}**`);\n sections.push(`A: ${faq.answer}\\n`);\n }\n }\n\n // Contact info\n if (contact) {\n sections.push('\\n## Contact Information');\n if (contact.email) sections.push(`- Email: ${contact.email}`);\n if (contact.phone) sections.push(`- Phone: ${contact.phone}`);\n if (contact.address) sections.push(`- Address: ${contact.address}`);\n if (contact.hours) sections.push(`- Hours: ${contact.hours}`);\n }\n\n // Additional context\n if (additionalContext) {\n sections.push(`\\n## Additional Information\\n${additionalContext}`);\n }\n\n // Instructions\n sections.push(`\n## Instructions\n- When users ask about specific items, provide detailed information from the content above\n- Use the available tools to navigate users to relevant pages or show them specific items\n- If you don't know something, say so honestly - don't make up information\n- Keep responses concise but helpful\n- Reference specific items by their id when using tools`);\n\n return sections.join('\\n');\n}\n","/**\n * Universal chat handler using OpenRouter\n * Works with any model: Gemini, GPT-4, Claude, Llama, etc.\n * No SDK required - just standard fetch.\n */\n\nimport type { ToolDefinition } from '../tools/types';\nimport { createSSEEncoder, getSSEHeaders } from './streaming';\nimport { generateSystemPrompt, type SiteContent } from './generateSystemPrompt';\n\ninterface BaseHandlerConfig {\n /** OpenRouter API key (or set OPENROUTER_API_KEY env var) */\n apiKey?: string;\n /** Model to use (e.g., 'google/gemini-2.0-flash-exp:free', 'openai/gpt-4o', 'anthropic/claude-3.5-sonnet') */\n model?: string;\n /** Tool definitions for the AI */\n tools?: ToolDefinition[];\n /** Temperature for response generation (0-1) */\n temperature?: number;\n /** Your site URL (shown in OpenRouter dashboard) */\n siteUrl?: string;\n /** Your app name (shown in OpenRouter dashboard) */\n siteName?: string;\n}\n\ninterface HandlerConfigWithPrompt extends BaseHandlerConfig {\n /** System prompt for the AI (use this OR siteContent, not both) */\n systemPrompt: string;\n siteContent?: never;\n}\n\ninterface HandlerConfigWithContent extends BaseHandlerConfig {\n /** Site content to auto-generate system prompt (use this OR systemPrompt, not both) */\n siteContent: SiteContent;\n systemPrompt?: never;\n}\n\nexport type HandlerConfig = HandlerConfigWithPrompt | HandlerConfigWithContent;\n\ninterface RequestBody {\n messages: Array<{ role: 'user' | 'assistant'; content: string }>;\n}\n\ninterface OpenRouterMessage {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\ninterface OpenRouterTool {\n type: 'function';\n function: {\n name: string;\n description: string;\n parameters: {\n type: 'object';\n properties: Record<string, unknown>;\n required: string[];\n };\n };\n}\n\n/**\n * Convert tool definitions to OpenAI/OpenRouter format\n */\nfunction convertTools(tools: ToolDefinition[]): OpenRouterTool[] {\n return tools.map(tool => ({\n type: 'function' as const,\n function: {\n name: tool.name,\n description: tool.description,\n parameters: {\n type: 'object' as const,\n properties: Object.fromEntries(\n Object.entries(tool.parameters.properties).map(([key, value]) => [\n key,\n {\n type: value.type,\n description: value.description,\n ...(value.enum && { enum: value.enum }),\n },\n ])\n ),\n required: tool.parameters.required,\n },\n },\n }));\n}\n\n/**\n * Create a Next.js API route handler using OpenRouter\n *\n * Works with any model - just change the model string:\n * - 'google/gemini-2.0-flash-exp:free' (free!)\n * - 'openai/gpt-4o'\n * - 'anthropic/claude-3.5-sonnet'\n * - 'meta-llama/llama-3.1-70b-instruct'\n *\n * @example\n * ```ts\n * // app/api/chat/route.ts\n * import { createHandler } from 'ai-site-pilot/api';\n *\n * export const POST = createHandler({\n * model: 'google/gemini-2.0-flash-exp:free',\n * systemPrompt: 'You are a helpful assistant...',\n * tools: myTools,\n * });\n * ```\n */\nexport function createHandler(config: HandlerConfig) {\n const {\n apiKey = process.env.OPENROUTER_API_KEY,\n model = 'google/gemini-2.0-flash-exp:free',\n tools = [],\n temperature = 0.7,\n siteUrl,\n siteName,\n } = config;\n\n // Generate system prompt from siteContent or use provided systemPrompt\n const systemPrompt = 'siteContent' in config && config.siteContent\n ? generateSystemPrompt(config.siteContent)\n : (config as HandlerConfigWithPrompt).systemPrompt;\n\n return async function POST(req: Request): Promise<Response> {\n if (!apiKey) {\n return new Response(\n JSON.stringify({ error: 'OpenRouter API key not configured. Get one at https://openrouter.ai' }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n try {\n const body = (await req.json()) as RequestBody;\n const { messages } = body;\n\n // Build messages array with system prompt\n const openRouterMessages: OpenRouterMessage[] = [\n { role: 'system', content: systemPrompt },\n ...messages.map((m) => ({\n role: m.role === 'assistant' ? 'assistant' as const : 'user' as const,\n content: m.content,\n })),\n ];\n\n const sse = createSSEEncoder();\n\n const stream = new ReadableStream({\n async start(controller) {\n try {\n const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n 'HTTP-Referer': siteUrl || '',\n 'X-Title': siteName || '',\n },\n body: JSON.stringify({\n model,\n messages: openRouterMessages,\n temperature,\n stream: true,\n ...(tools.length > 0 && { tools: convertTools(tools) }),\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n console.error('OpenRouter error:', error);\n controller.enqueue(sse.encodeError('Failed to get AI response'));\n controller.enqueue(sse.encodeDone());\n controller.close();\n return;\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n controller.enqueue(sse.encodeError('No response stream'));\n controller.enqueue(sse.encodeDone());\n controller.close();\n return;\n }\n\n const decoder = new TextDecoder();\n let buffer = '';\n\n // Accumulate tool calls across stream chunks\n // OpenRouter streams tool calls in pieces: name first, then arguments in chunks\n const pendingToolCalls: Map<number, { name: string; arguments: string }> = new Map();\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n const data = line.slice(6);\n if (data === '[DONE]') continue;\n\n try {\n const parsed = JSON.parse(data);\n const delta = parsed.choices?.[0]?.delta;\n\n // Handle text content\n if (delta?.content) {\n controller.enqueue(sse.encodeText(delta.content));\n }\n\n // Handle tool calls - accumulate across chunks\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n const index = toolCall.index ?? 0;\n\n // Get or create pending tool call\n let pending = pendingToolCalls.get(index);\n if (!pending) {\n pending = { name: '', arguments: '' };\n pendingToolCalls.set(index, pending);\n }\n\n // Accumulate name (usually comes in first chunk)\n if (toolCall.function?.name) {\n pending.name = toolCall.function.name;\n }\n\n // Accumulate arguments (streamed in chunks)\n if (toolCall.function?.arguments) {\n pending.arguments += toolCall.function.arguments;\n }\n }\n }\n } catch {\n // Skip malformed JSON\n }\n }\n }\n }\n\n // Emit all accumulated tool calls now that streaming is complete\n for (const [, toolCall] of pendingToolCalls) {\n if (toolCall.name && toolCall.arguments) {\n try {\n const args = JSON.parse(toolCall.arguments);\n controller.enqueue(sse.encodeTool(toolCall.name, args));\n } catch (e) {\n console.error('Failed to parse tool arguments:', toolCall.arguments, e);\n }\n }\n }\n\n controller.enqueue(sse.encodeDone());\n controller.close();\n } catch (error) {\n console.error('Streaming error:', error);\n controller.enqueue(sse.encodeError('An error occurred during streaming'));\n controller.close();\n }\n },\n });\n\n return new Response(stream, { headers: getSSEHeaders() });\n } catch (error) {\n console.error('Handler error:', error);\n return new Response(\n JSON.stringify({ error: 'Internal server error' }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n );\n }\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-site-pilot",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "AI chat widget that can control and navigate your website. Works with any AI model via OpenRouter.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",