ai-site-pilot 0.2.3 → 0.4.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 +93 -189
- package/dist/api/index.d.mts +23 -29
- package/dist/api/index.d.ts +23 -29
- package/dist/api/index.js +110 -45
- package/dist/api/index.js.map +1 -1
- package/dist/api/index.mjs +110 -45
- package/dist/api/index.mjs.map +1 -1
- package/dist/styles.css +11 -0
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -5,36 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
AI chat widget that can **control and navigate your website**. Unlike typical chatbots that just answer questions, Site Pilot can take actions—scroll to sections, open modals, filter content, and more.
|
|
7
7
|
|
|
8
|
+
Works with any AI model (Gemini, GPT-4, Claude, Llama) via [OpenRouter](https://openrouter.ai).
|
|
9
|
+
|
|
8
10
|
## Features
|
|
9
11
|
|
|
10
12
|
- 🎯 **Tool System** - Define custom actions the AI can take on your site
|
|
11
|
-
- 🌊 **Streaming** - Real-time streaming responses
|
|
13
|
+
- 🌊 **Streaming** - Real-time streaming responses
|
|
14
|
+
- 🤖 **Any Model** - GPT-4, Claude, Gemini, Llama - just change one string
|
|
12
15
|
- 🎤 **Speech** - Voice input and text-to-speech output
|
|
13
16
|
- 🎨 **Themeable** - CSS variables for easy customization
|
|
14
17
|
- 📱 **Responsive** - Works on all screen sizes
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
## How It Works
|
|
18
|
-
|
|
19
|
-
The package provides the chat UI and streaming infrastructure. **You teach the AI about your specific site** through:
|
|
20
|
-
|
|
21
|
-
1. **System Prompt** - Tell the AI what sections, data, and features exist on your site
|
|
22
|
-
2. **Tool Definitions** - Define what actions the AI can take (filter, navigate, open modals)
|
|
23
|
-
3. **Client Handlers** - Write the code that actually executes those actions
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
User: "Show me your mobile apps"
|
|
27
|
-
↓
|
|
28
|
-
AI understands request
|
|
29
|
-
↓
|
|
30
|
-
AI calls filter_by_category("Mobile") tool
|
|
31
|
-
↓
|
|
32
|
-
Your handler receives the call
|
|
33
|
-
↓
|
|
34
|
-
Your code filters the UI
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
The AI doesn't automatically know your site structure—you teach it via the system prompt. See the [complete example](#teaching-the-ai-your-site) below.
|
|
18
|
+
- 🆓 **Free Tier** - Gemini 2.0 Flash is free on OpenRouter
|
|
38
19
|
|
|
39
20
|
## Installation
|
|
40
21
|
|
|
@@ -44,13 +25,16 @@ npm install ai-site-pilot
|
|
|
44
25
|
|
|
45
26
|
## Quick Start
|
|
46
27
|
|
|
47
|
-
### 1.
|
|
28
|
+
### 1. Get an OpenRouter API Key
|
|
29
|
+
|
|
30
|
+
Sign up at [openrouter.ai](https://openrouter.ai) and get your API key.
|
|
31
|
+
|
|
32
|
+
### 2. Create the API Route
|
|
48
33
|
|
|
49
34
|
```typescript
|
|
50
35
|
// app/api/chat/route.ts
|
|
51
|
-
import {
|
|
36
|
+
import { createHandler } from 'ai-site-pilot/api';
|
|
52
37
|
import { defineTool } from 'ai-site-pilot/tools';
|
|
53
|
-
import { google } from '@ai-sdk/google';
|
|
54
38
|
|
|
55
39
|
const navigateTool = defineTool({
|
|
56
40
|
name: 'navigate',
|
|
@@ -68,15 +52,15 @@ const navigateTool = defineTool({
|
|
|
68
52
|
},
|
|
69
53
|
});
|
|
70
54
|
|
|
71
|
-
export const POST =
|
|
72
|
-
model: google
|
|
55
|
+
export const POST = createHandler({
|
|
56
|
+
model: 'google/gemini-2.0-flash-exp:free', // Free! Or use any model
|
|
73
57
|
systemPrompt: `You are a helpful assistant for our website.
|
|
74
58
|
You can navigate users to different sections using the navigate tool.`,
|
|
75
59
|
tools: [navigateTool],
|
|
76
60
|
});
|
|
77
61
|
```
|
|
78
62
|
|
|
79
|
-
###
|
|
63
|
+
### 3. Add the Component
|
|
80
64
|
|
|
81
65
|
```tsx
|
|
82
66
|
// app/layout.tsx or components/ChatWidget.tsx
|
|
@@ -105,72 +89,71 @@ export function ChatWidget() {
|
|
|
105
89
|
}
|
|
106
90
|
```
|
|
107
91
|
|
|
108
|
-
|
|
92
|
+
### 4. Add Environment Variable
|
|
109
93
|
|
|
110
|
-
|
|
94
|
+
```bash
|
|
95
|
+
# .env.local
|
|
96
|
+
OPENROUTER_API_KEY=sk-or-...
|
|
97
|
+
```
|
|
111
98
|
|
|
112
|
-
|
|
99
|
+
That's it!
|
|
113
100
|
|
|
114
|
-
|
|
115
|
-
|------|------|---------|-------------|
|
|
116
|
-
| `apiEndpoint` | `string` | required | API endpoint for chat |
|
|
117
|
-
| `theme` | `SitePilotTheme` | `{}` | Theme configuration |
|
|
118
|
-
| `suggestions` | `Suggestion[]` | `[]` | Suggestion prompts |
|
|
119
|
-
| `features` | `SitePilotFeatures` | `{}` | Feature toggles |
|
|
120
|
-
| `onToolCall` | `(name, args) => void` | - | Tool call handler |
|
|
121
|
-
| `defaultOpen` | `boolean` | `false` | Initial open state |
|
|
122
|
-
| `placeholder` | `string` | `'Type a message...'` | Input placeholder |
|
|
123
|
-
| `welcomeMessage` | `string` | `'Hi! I'm here to help...'` | Welcome message |
|
|
101
|
+
## Available Models
|
|
124
102
|
|
|
125
|
-
|
|
103
|
+
Change the `model` string to use any model:
|
|
126
104
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
accentColor?: string; // Primary accent '#f59e0b'
|
|
134
|
-
accentColorDark?: string; // Gradient end '#d97706'
|
|
135
|
-
backgroundColor?: string; // Panel background '#0F0720'
|
|
136
|
-
textColor?: string; // Primary text '#ffffff'
|
|
137
|
-
textMutedColor?: string; // Secondary text '#a1a1aa'
|
|
138
|
-
borderColor?: string; // Border 'rgba(255,255,255,0.1)'
|
|
139
|
-
userMessageBg?: string; // User message bubble
|
|
140
|
-
assistantMessageBg?: string; // Assistant message bubble
|
|
141
|
-
}
|
|
142
|
-
```
|
|
105
|
+
| Model | ID | Notes |
|
|
106
|
+
|-------|-----|-------|
|
|
107
|
+
| Gemini 2.0 Flash | `google/gemini-2.0-flash-exp:free` | **Free!** |
|
|
108
|
+
| GPT-4o | `openai/gpt-4o` | Best overall |
|
|
109
|
+
| Claude 3.5 Sonnet | `anthropic/claude-3.5-sonnet` | Best for coding |
|
|
110
|
+
| Llama 3.1 70B | `meta-llama/llama-3.1-70b-instruct` | Open source |
|
|
143
111
|
|
|
144
|
-
|
|
112
|
+
See all models at [openrouter.ai/models](https://openrouter.ai/models)
|
|
145
113
|
|
|
146
|
-
|
|
147
|
-
interface SitePilotFeatures {
|
|
148
|
-
speech?: boolean; // Voice input (default: true)
|
|
149
|
-
tts?: boolean; // Text-to-speech (default: true)
|
|
150
|
-
fullscreen?: boolean; // Fullscreen mode (default: true)
|
|
151
|
-
suggestions?: boolean; // Show suggestions (default: true)
|
|
152
|
-
}
|
|
153
|
-
```
|
|
114
|
+
## API Reference
|
|
154
115
|
|
|
155
|
-
### `
|
|
116
|
+
### `createHandler()`
|
|
156
117
|
|
|
157
|
-
|
|
118
|
+
Creates a Next.js API route handler.
|
|
158
119
|
|
|
159
120
|
```typescript
|
|
160
|
-
import {
|
|
121
|
+
import { createHandler } from 'ai-site-pilot/api';
|
|
161
122
|
|
|
162
|
-
export const POST =
|
|
163
|
-
|
|
123
|
+
export const POST = createHandler({
|
|
124
|
+
// Required
|
|
164
125
|
systemPrompt: 'You are a helpful assistant...',
|
|
126
|
+
|
|
127
|
+
// Optional
|
|
128
|
+
apiKey: process.env.OPENROUTER_API_KEY, // Uses env var by default
|
|
129
|
+
model: 'google/gemini-2.0-flash-exp:free', // Default
|
|
165
130
|
tools: [myTool1, myTool2],
|
|
166
131
|
temperature: 0.7,
|
|
167
|
-
|
|
132
|
+
siteUrl: 'https://mysite.com', // Shown in OpenRouter dashboard
|
|
133
|
+
siteName: 'My Site',
|
|
168
134
|
});
|
|
169
135
|
```
|
|
170
136
|
|
|
137
|
+
### `<SitePilot />`
|
|
138
|
+
|
|
139
|
+
Main chat widget component.
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<SitePilot
|
|
143
|
+
apiEndpoint="/api/chat"
|
|
144
|
+
suggestions={[{ text: 'Help me', icon: '❓' }]}
|
|
145
|
+
onToolCall={(name, args) => { /* handle tool calls */ }}
|
|
146
|
+
theme={{ accentColor: '#f59e0b' }}
|
|
147
|
+
features={{ speech: true, tts: true }}
|
|
148
|
+
welcomeMessage="Hi! How can I help?"
|
|
149
|
+
placeholder="Type a message..."
|
|
150
|
+
defaultOpen={false}
|
|
151
|
+
/>
|
|
152
|
+
```
|
|
153
|
+
|
|
171
154
|
### `defineTool()`
|
|
172
155
|
|
|
173
|
-
Helper for defining tools
|
|
156
|
+
Helper for defining tools.
|
|
174
157
|
|
|
175
158
|
```typescript
|
|
176
159
|
import { defineTool } from 'ai-site-pilot/tools';
|
|
@@ -186,143 +169,64 @@ const searchTool = defineTool({
|
|
|
186
169
|
},
|
|
187
170
|
required: ['query'],
|
|
188
171
|
},
|
|
189
|
-
handler: async ({ query, category }) => {
|
|
190
|
-
// Client-side handler (optional)
|
|
191
|
-
const results = await searchProducts(query, category);
|
|
192
|
-
displayResults(results);
|
|
193
|
-
},
|
|
194
172
|
});
|
|
195
173
|
```
|
|
196
174
|
|
|
197
|
-
|
|
175
|
+
## Custom API Implementation
|
|
198
176
|
|
|
199
|
-
|
|
177
|
+
If you need to use a different AI provider, implement this SSE format:
|
|
200
178
|
|
|
201
|
-
```typescript
|
|
202
|
-
import { useChat } from 'ai-site-pilot/hooks';
|
|
203
|
-
|
|
204
|
-
function MyCustomChat() {
|
|
205
|
-
const {
|
|
206
|
-
messages,
|
|
207
|
-
input,
|
|
208
|
-
setInput,
|
|
209
|
-
isLoading,
|
|
210
|
-
sendMessage,
|
|
211
|
-
clearMessages,
|
|
212
|
-
} = useChat({
|
|
213
|
-
apiEndpoint: '/api/chat',
|
|
214
|
-
onToolCall: (name, args) => {
|
|
215
|
-
// Handle tool calls
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
return (
|
|
220
|
-
// Your custom UI
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
179
|
```
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
Override these variables to customize the appearance:
|
|
230
|
-
|
|
231
|
-
```css
|
|
232
|
-
.pilot-container {
|
|
233
|
-
--pilot-accent-h: 38; /* Hue */
|
|
234
|
-
--pilot-accent-s: 92%; /* Saturation */
|
|
235
|
-
--pilot-accent-l: 50%; /* Lightness */
|
|
236
|
-
--pilot-bg: #0F0720; /* Background */
|
|
237
|
-
--pilot-text: #ffffff; /* Text color */
|
|
238
|
-
--pilot-text-muted: #a1a1aa; /* Muted text */
|
|
239
|
-
--pilot-border: rgba(255, 255, 255, 0.1);
|
|
240
|
-
--pilot-radius: 24px;
|
|
241
|
-
}
|
|
180
|
+
data: {"type":"text","content":"Hello, "}
|
|
181
|
+
data: {"type":"text","content":"how can I help?"}
|
|
182
|
+
data: {"type":"tool","name":"navigate","args":{"section":"products"}}
|
|
183
|
+
data: {"type":"done"}
|
|
242
184
|
```
|
|
243
185
|
|
|
244
|
-
|
|
186
|
+
Use the built-in SSE helpers:
|
|
245
187
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
```javascript
|
|
249
|
-
// tailwind.config.js
|
|
250
|
-
module.exports = {
|
|
251
|
-
theme: {
|
|
252
|
-
extend: {
|
|
253
|
-
colors: {
|
|
254
|
-
pilot: {
|
|
255
|
-
accent: 'hsl(var(--pilot-accent-h), var(--pilot-accent-s), var(--pilot-accent-l))',
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
},
|
|
260
|
-
};
|
|
261
|
-
```
|
|
188
|
+
```typescript
|
|
189
|
+
import { createSSEEncoder, getSSEHeaders } from 'ai-site-pilot/api';
|
|
262
190
|
|
|
263
|
-
|
|
191
|
+
export async function POST(req: Request) {
|
|
192
|
+
const sse = createSSEEncoder();
|
|
264
193
|
|
|
265
|
-
|
|
194
|
+
const stream = new ReadableStream({
|
|
195
|
+
async start(controller) {
|
|
196
|
+
controller.enqueue(sse.encodeText('Hello!'));
|
|
197
|
+
controller.enqueue(sse.encodeTool('navigate', { section: 'products' }));
|
|
198
|
+
controller.enqueue(sse.encodeDone());
|
|
199
|
+
controller.close();
|
|
200
|
+
},
|
|
201
|
+
});
|
|
266
202
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
defineTool({
|
|
270
|
-
name: 'search_products',
|
|
271
|
-
description: 'Search product catalog',
|
|
272
|
-
parameters: { /* ... */ },
|
|
273
|
-
}),
|
|
274
|
-
defineTool({
|
|
275
|
-
name: 'add_to_cart',
|
|
276
|
-
description: 'Add item to shopping cart',
|
|
277
|
-
parameters: { /* ... */ },
|
|
278
|
-
}),
|
|
279
|
-
defineTool({
|
|
280
|
-
name: 'show_category',
|
|
281
|
-
description: 'Filter products by category',
|
|
282
|
-
parameters: { /* ... */ },
|
|
283
|
-
}),
|
|
284
|
-
];
|
|
203
|
+
return new Response(stream, { headers: getSSEHeaders() });
|
|
204
|
+
}
|
|
285
205
|
```
|
|
286
206
|
|
|
287
|
-
|
|
207
|
+
## Handling Tool-Only Responses
|
|
208
|
+
|
|
209
|
+
When the AI calls tools without text, customize the fallback:
|
|
288
210
|
|
|
289
211
|
```typescript
|
|
290
|
-
|
|
291
|
-
defineTool({
|
|
292
|
-
name: 'search_docs',
|
|
293
|
-
description: 'Search documentation',
|
|
294
|
-
parameters: { /* ... */ },
|
|
295
|
-
}),
|
|
296
|
-
defineTool({
|
|
297
|
-
name: 'navigate_to_page',
|
|
298
|
-
description: 'Go to a documentation page',
|
|
299
|
-
parameters: { /* ... */ },
|
|
300
|
-
}),
|
|
301
|
-
];
|
|
302
|
-
```
|
|
212
|
+
import { SitePilot, createFallbackMessageGenerator } from 'ai-site-pilot';
|
|
303
213
|
|
|
304
|
-
|
|
214
|
+
const generateFallback = createFallbackMessageGenerator({
|
|
215
|
+
navigate: (args) => `Scrolled to **${args.section}**.`,
|
|
216
|
+
filter: (args) => `Showing **${args.category}** items.`,
|
|
217
|
+
});
|
|
305
218
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
description: 'Open project details modal',
|
|
311
|
-
parameters: { /* ... */ },
|
|
312
|
-
}),
|
|
313
|
-
defineTool({
|
|
314
|
-
name: 'filter_by_category',
|
|
315
|
-
description: 'Filter projects by category',
|
|
316
|
-
parameters: { /* ... */ },
|
|
317
|
-
}),
|
|
318
|
-
];
|
|
219
|
+
<SitePilot
|
|
220
|
+
apiEndpoint="/api/chat"
|
|
221
|
+
generateFallbackMessage={generateFallback}
|
|
222
|
+
/>
|
|
319
223
|
```
|
|
320
224
|
|
|
321
225
|
## Requirements
|
|
322
226
|
|
|
323
227
|
- React 18+ or React 19
|
|
324
228
|
- Next.js 13+ (for API routes)
|
|
325
|
-
-
|
|
229
|
+
- OpenRouter API key (free at [openrouter.ai](https://openrouter.ai))
|
|
326
230
|
|
|
327
231
|
## License
|
|
328
232
|
|
package/dist/api/index.d.mts
CHANGED
|
@@ -1,56 +1,50 @@
|
|
|
1
|
-
import { LanguageModel } from 'ai';
|
|
2
1
|
import { T as ToolDefinition } from '../types--7jDyUM6.mjs';
|
|
3
2
|
import { S as StreamEvent } from '../types-K00dDlBC.mjs';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
5
|
+
* Universal chat handler using OpenRouter
|
|
6
|
+
* Works with any model: Gemini, GPT-4, Claude, Llama, etc.
|
|
7
|
+
* No SDK required - just standard fetch.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
interface
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
interface HandlerConfig {
|
|
11
|
+
/** OpenRouter API key (or set OPENROUTER_API_KEY env var) */
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
/** Model to use (e.g., 'google/gemini-2.0-flash-exp:free', 'openai/gpt-4o', 'anthropic/claude-3.5-sonnet') */
|
|
14
|
+
model?: string;
|
|
12
15
|
/** System prompt for the AI */
|
|
13
16
|
systemPrompt: string;
|
|
14
17
|
/** Tool definitions for the AI */
|
|
15
18
|
tools?: ToolDefinition[];
|
|
16
19
|
/** Temperature for response generation (0-1) */
|
|
17
20
|
temperature?: number;
|
|
18
|
-
/**
|
|
19
|
-
|
|
21
|
+
/** Your site URL (shown in OpenRouter dashboard) */
|
|
22
|
+
siteUrl?: string;
|
|
23
|
+
/** Your app name (shown in OpenRouter dashboard) */
|
|
24
|
+
siteName?: string;
|
|
20
25
|
}
|
|
21
26
|
/**
|
|
22
|
-
* Create a Next.js API route handler
|
|
27
|
+
* Create a Next.js API route handler using OpenRouter
|
|
23
28
|
*
|
|
24
|
-
* Works with any
|
|
25
|
-
* -
|
|
26
|
-
* -
|
|
27
|
-
* -
|
|
28
|
-
* -
|
|
29
|
+
* Works with any model - just change the model string:
|
|
30
|
+
* - 'google/gemini-2.0-flash-exp:free' (free!)
|
|
31
|
+
* - 'openai/gpt-4o'
|
|
32
|
+
* - 'anthropic/claude-3.5-sonnet'
|
|
33
|
+
* - 'meta-llama/llama-3.1-70b-instruct'
|
|
29
34
|
*
|
|
30
35
|
* @example
|
|
31
36
|
* ```ts
|
|
32
37
|
* // app/api/chat/route.ts
|
|
33
|
-
* import {
|
|
34
|
-
* import { google } from '@ai-sdk/google';
|
|
38
|
+
* import { createHandler } from 'ai-site-pilot/api';
|
|
35
39
|
*
|
|
36
|
-
* export const POST =
|
|
37
|
-
* model: google
|
|
40
|
+
* export const POST = createHandler({
|
|
41
|
+
* model: 'google/gemini-2.0-flash-exp:free',
|
|
38
42
|
* systemPrompt: 'You are a helpful assistant...',
|
|
39
43
|
* tools: myTools,
|
|
40
44
|
* });
|
|
41
45
|
* ```
|
|
42
|
-
*
|
|
43
|
-
* @example Using OpenAI
|
|
44
|
-
* ```ts
|
|
45
|
-
* import { openai } from '@ai-sdk/openai';
|
|
46
|
-
*
|
|
47
|
-
* export const POST = createChatHandler({
|
|
48
|
-
* model: openai('gpt-4o'),
|
|
49
|
-
* systemPrompt: 'You are a helpful assistant...',
|
|
50
|
-
* });
|
|
51
|
-
* ```
|
|
52
46
|
*/
|
|
53
|
-
declare function
|
|
47
|
+
declare function createHandler(config: HandlerConfig): (req: Request) => Promise<Response>;
|
|
54
48
|
|
|
55
49
|
/**
|
|
56
50
|
* SSE streaming utilities
|
|
@@ -75,4 +69,4 @@ declare function getSSEHeaders(): HeadersInit;
|
|
|
75
69
|
*/
|
|
76
70
|
declare function parseSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>): AsyncGenerator<StreamEvent>;
|
|
77
71
|
|
|
78
|
-
export { type
|
|
72
|
+
export { type HandlerConfig, createHandler, createSSEEncoder, getSSEHeaders, parseSSEStream };
|
package/dist/api/index.d.ts
CHANGED
|
@@ -1,56 +1,50 @@
|
|
|
1
|
-
import { LanguageModel } from 'ai';
|
|
2
1
|
import { T as ToolDefinition } from '../types--7jDyUM6.js';
|
|
3
2
|
import { S as StreamEvent } from '../types-K00dDlBC.js';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
5
|
+
* Universal chat handler using OpenRouter
|
|
6
|
+
* Works with any model: Gemini, GPT-4, Claude, Llama, etc.
|
|
7
|
+
* No SDK required - just standard fetch.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
interface
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
interface HandlerConfig {
|
|
11
|
+
/** OpenRouter API key (or set OPENROUTER_API_KEY env var) */
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
/** Model to use (e.g., 'google/gemini-2.0-flash-exp:free', 'openai/gpt-4o', 'anthropic/claude-3.5-sonnet') */
|
|
14
|
+
model?: string;
|
|
12
15
|
/** System prompt for the AI */
|
|
13
16
|
systemPrompt: string;
|
|
14
17
|
/** Tool definitions for the AI */
|
|
15
18
|
tools?: ToolDefinition[];
|
|
16
19
|
/** Temperature for response generation (0-1) */
|
|
17
20
|
temperature?: number;
|
|
18
|
-
/**
|
|
19
|
-
|
|
21
|
+
/** Your site URL (shown in OpenRouter dashboard) */
|
|
22
|
+
siteUrl?: string;
|
|
23
|
+
/** Your app name (shown in OpenRouter dashboard) */
|
|
24
|
+
siteName?: string;
|
|
20
25
|
}
|
|
21
26
|
/**
|
|
22
|
-
* Create a Next.js API route handler
|
|
27
|
+
* Create a Next.js API route handler using OpenRouter
|
|
23
28
|
*
|
|
24
|
-
* Works with any
|
|
25
|
-
* -
|
|
26
|
-
* -
|
|
27
|
-
* -
|
|
28
|
-
* -
|
|
29
|
+
* Works with any model - just change the model string:
|
|
30
|
+
* - 'google/gemini-2.0-flash-exp:free' (free!)
|
|
31
|
+
* - 'openai/gpt-4o'
|
|
32
|
+
* - 'anthropic/claude-3.5-sonnet'
|
|
33
|
+
* - 'meta-llama/llama-3.1-70b-instruct'
|
|
29
34
|
*
|
|
30
35
|
* @example
|
|
31
36
|
* ```ts
|
|
32
37
|
* // app/api/chat/route.ts
|
|
33
|
-
* import {
|
|
34
|
-
* import { google } from '@ai-sdk/google';
|
|
38
|
+
* import { createHandler } from 'ai-site-pilot/api';
|
|
35
39
|
*
|
|
36
|
-
* export const POST =
|
|
37
|
-
* model: google
|
|
40
|
+
* export const POST = createHandler({
|
|
41
|
+
* model: 'google/gemini-2.0-flash-exp:free',
|
|
38
42
|
* systemPrompt: 'You are a helpful assistant...',
|
|
39
43
|
* tools: myTools,
|
|
40
44
|
* });
|
|
41
45
|
* ```
|
|
42
|
-
*
|
|
43
|
-
* @example Using OpenAI
|
|
44
|
-
* ```ts
|
|
45
|
-
* import { openai } from '@ai-sdk/openai';
|
|
46
|
-
*
|
|
47
|
-
* export const POST = createChatHandler({
|
|
48
|
-
* model: openai('gpt-4o'),
|
|
49
|
-
* systemPrompt: 'You are a helpful assistant...',
|
|
50
|
-
* });
|
|
51
|
-
* ```
|
|
52
46
|
*/
|
|
53
|
-
declare function
|
|
47
|
+
declare function createHandler(config: HandlerConfig): (req: Request) => Promise<Response>;
|
|
54
48
|
|
|
55
49
|
/**
|
|
56
50
|
* SSE streaming utilities
|
|
@@ -75,4 +69,4 @@ declare function getSSEHeaders(): HeadersInit;
|
|
|
75
69
|
*/
|
|
76
70
|
declare function parseSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>): AsyncGenerator<StreamEvent>;
|
|
77
71
|
|
|
78
|
-
export { type
|
|
72
|
+
export { type HandlerConfig, createHandler, createSSEEncoder, getSSEHeaders, parseSSEStream };
|
package/dist/api/index.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var ai = require('ai');
|
|
4
|
-
|
|
5
|
-
// src/api/createChatHandler.ts
|
|
6
|
-
|
|
7
3
|
// src/api/streaming.ts
|
|
8
4
|
function createSSEEncoder() {
|
|
9
5
|
const encoder = new TextEncoder();
|
|
@@ -63,54 +59,125 @@ async function* parseSSEStream(reader) {
|
|
|
63
59
|
}
|
|
64
60
|
}
|
|
65
61
|
|
|
66
|
-
// src/api/
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
// src/api/createHandler.ts
|
|
63
|
+
function convertTools(tools) {
|
|
64
|
+
return tools.map((tool) => ({
|
|
65
|
+
type: "function",
|
|
66
|
+
function: {
|
|
67
|
+
name: tool.name,
|
|
71
68
|
description: tool.description,
|
|
72
69
|
parameters: {
|
|
73
70
|
type: "object",
|
|
74
|
-
properties:
|
|
71
|
+
properties: Object.fromEntries(
|
|
72
|
+
Object.entries(tool.parameters.properties).map(([key, value]) => [
|
|
73
|
+
key,
|
|
74
|
+
{
|
|
75
|
+
type: value.type,
|
|
76
|
+
description: value.description,
|
|
77
|
+
...value.enum && { enum: value.enum }
|
|
78
|
+
}
|
|
79
|
+
])
|
|
80
|
+
),
|
|
75
81
|
required: tool.parameters.required
|
|
76
82
|
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return result;
|
|
83
|
+
}
|
|
84
|
+
}));
|
|
80
85
|
}
|
|
81
|
-
function
|
|
82
|
-
const {
|
|
86
|
+
function createHandler(config) {
|
|
87
|
+
const {
|
|
88
|
+
apiKey = process.env.OPENROUTER_API_KEY,
|
|
89
|
+
model = "google/gemini-2.0-flash-exp:free",
|
|
90
|
+
systemPrompt,
|
|
91
|
+
tools = [],
|
|
92
|
+
temperature = 0.7,
|
|
93
|
+
siteUrl,
|
|
94
|
+
siteName
|
|
95
|
+
} = config;
|
|
83
96
|
return async function POST(req) {
|
|
97
|
+
if (!apiKey) {
|
|
98
|
+
return new Response(
|
|
99
|
+
JSON.stringify({ error: "OpenRouter API key not configured. Get one at https://openrouter.ai" }),
|
|
100
|
+
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
101
|
+
);
|
|
102
|
+
}
|
|
84
103
|
try {
|
|
85
104
|
const body = await req.json();
|
|
86
105
|
const { messages } = body;
|
|
87
|
-
const
|
|
88
|
-
role:
|
|
89
|
-
|
|
90
|
-
|
|
106
|
+
const openRouterMessages = [
|
|
107
|
+
{ role: "system", content: systemPrompt },
|
|
108
|
+
...messages.map((m) => ({
|
|
109
|
+
role: m.role === "assistant" ? "assistant" : "user",
|
|
110
|
+
content: m.content
|
|
111
|
+
}))
|
|
112
|
+
];
|
|
91
113
|
const sse = createSSEEncoder();
|
|
92
114
|
const stream = new ReadableStream({
|
|
93
115
|
async start(controller) {
|
|
94
116
|
try {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: {
|
|
120
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
121
|
+
"Content-Type": "application/json",
|
|
122
|
+
"HTTP-Referer": siteUrl || "",
|
|
123
|
+
"X-Title": siteName || ""
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
model,
|
|
127
|
+
messages: openRouterMessages,
|
|
128
|
+
temperature,
|
|
129
|
+
stream: true,
|
|
130
|
+
...tools.length > 0 && { tools: convertTools(tools) }
|
|
131
|
+
})
|
|
102
132
|
});
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
const error = await response.text();
|
|
135
|
+
console.error("OpenRouter error:", error);
|
|
136
|
+
controller.enqueue(sse.encodeError("Failed to get AI response"));
|
|
137
|
+
controller.enqueue(sse.encodeDone());
|
|
138
|
+
controller.close();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const reader = response.body?.getReader();
|
|
142
|
+
if (!reader) {
|
|
143
|
+
controller.enqueue(sse.encodeError("No response stream"));
|
|
144
|
+
controller.enqueue(sse.encodeDone());
|
|
145
|
+
controller.close();
|
|
146
|
+
return;
|
|
107
147
|
}
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
148
|
+
const decoder = new TextDecoder();
|
|
149
|
+
let buffer = "";
|
|
150
|
+
while (true) {
|
|
151
|
+
const { done, value } = await reader.read();
|
|
152
|
+
if (done) break;
|
|
153
|
+
buffer += decoder.decode(value, { stream: true });
|
|
154
|
+
const lines = buffer.split("\n");
|
|
155
|
+
buffer = lines.pop() || "";
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
if (line.startsWith("data: ")) {
|
|
158
|
+
const data = line.slice(6);
|
|
159
|
+
if (data === "[DONE]") continue;
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(data);
|
|
162
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
163
|
+
if (delta?.content) {
|
|
164
|
+
controller.enqueue(sse.encodeText(delta.content));
|
|
165
|
+
}
|
|
166
|
+
if (delta?.tool_calls) {
|
|
167
|
+
for (const toolCall of delta.tool_calls) {
|
|
168
|
+
if (toolCall.function?.name && toolCall.function?.arguments) {
|
|
169
|
+
try {
|
|
170
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
171
|
+
controller.enqueue(sse.encodeTool(toolCall.function.name, args));
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
114
181
|
}
|
|
115
182
|
controller.enqueue(sse.encodeDone());
|
|
116
183
|
controller.close();
|
|
@@ -121,20 +188,18 @@ function createChatHandler(config) {
|
|
|
121
188
|
}
|
|
122
189
|
}
|
|
123
190
|
});
|
|
124
|
-
return new Response(stream, {
|
|
125
|
-
headers: getSSEHeaders()
|
|
126
|
-
});
|
|
191
|
+
return new Response(stream, { headers: getSSEHeaders() });
|
|
127
192
|
} catch (error) {
|
|
128
|
-
console.error("
|
|
129
|
-
return new Response(
|
|
130
|
-
|
|
131
|
-
headers: { "Content-Type": "application/json" }
|
|
132
|
-
|
|
193
|
+
console.error("Handler error:", error);
|
|
194
|
+
return new Response(
|
|
195
|
+
JSON.stringify({ error: "Internal server error" }),
|
|
196
|
+
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
197
|
+
);
|
|
133
198
|
}
|
|
134
199
|
};
|
|
135
200
|
}
|
|
136
201
|
|
|
137
|
-
exports.
|
|
202
|
+
exports.createHandler = createHandler;
|
|
138
203
|
exports.createSSEEncoder = createSSEEncoder;
|
|
139
204
|
exports.getSSEHeaders = getSSEHeaders;
|
|
140
205
|
exports.parseSSEStream = parseSSEStream;
|
package/dist/api/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/api/streaming.ts","../../src/api/createChatHandler.ts"],"names":["streamText"],"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 * Factory for creating Next.js API route handlers\n */\n\nimport { streamText, type CoreMessage, type LanguageModel } from 'ai';\nimport type { ToolDefinition } from '../tools/types';\nimport { createSSEEncoder, getSSEHeaders } from './streaming';\n\nexport interface ChatHandlerConfig {\n /** The AI model to use (from Vercel AI SDK) */\n model: LanguageModel;\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 /** Maximum tokens in response */\n maxTokens?: number;\n}\n\ninterface RequestBody {\n messages: Array<{ role: 'user' | 'assistant'; content: string }>;\n}\n\n/**\n * Convert tool definitions to Vercel AI SDK format\n */\nfunction convertToolsToAISDK(tools: ToolDefinition[]) {\n const result: Record<string, { description: string; parameters: unknown }> = {};\n\n for (const tool of tools) {\n result[tool.name] = {\n description: tool.description,\n parameters: {\n type: 'object',\n properties: tool.parameters.properties,\n required: tool.parameters.required,\n },\n };\n }\n\n return result;\n}\n\n/**\n * Create a Next.js API route handler for chat\n *\n * Works with any Vercel AI SDK compatible model including:\n * - Google Gemini (@ai-sdk/google)\n * - OpenAI (@ai-sdk/openai)\n * - Anthropic (@ai-sdk/anthropic)\n * - And more...\n *\n * @example\n * ```ts\n * // app/api/chat/route.ts\n * import { createChatHandler } from 'ai-site-pilot/api';\n * import { google } from '@ai-sdk/google';\n *\n * export const POST = createChatHandler({\n * model: google('gemini-2.0-flash'),\n * systemPrompt: 'You are a helpful assistant...',\n * tools: myTools,\n * });\n * ```\n *\n * @example Using OpenAI\n * ```ts\n * import { openai } from '@ai-sdk/openai';\n *\n * export const POST = createChatHandler({\n * model: openai('gpt-4o'),\n * systemPrompt: 'You are a helpful assistant...',\n * });\n * ```\n */\nexport function createChatHandler(config: ChatHandlerConfig) {\n const { model, systemPrompt, tools = [], temperature = 0.7, maxTokens } = config;\n\n return async function POST(req: Request): Promise<Response> {\n try {\n const body = (await req.json()) as RequestBody;\n const { messages } = body;\n\n // Convert messages to CoreMessage format\n const coreMessages: CoreMessage[] = messages.map((m) => ({\n role: m.role,\n content: m.content,\n }));\n\n // Create the SSE encoder\n const sse = createSSEEncoder();\n\n // Create a readable stream for SSE\n const stream = new ReadableStream({\n async start(controller) {\n try {\n const result = streamText({\n model,\n system: systemPrompt,\n messages: coreMessages,\n temperature,\n maxTokens,\n tools: tools.length > 0 ? convertToolsToAISDK(tools) : undefined,\n });\n\n // Stream text chunks\n for await (const chunk of (await result).textStream) {\n if (chunk) {\n controller.enqueue(sse.encodeText(chunk));\n }\n }\n\n // Get tool calls from the result\n const finalResult = await result;\n const toolCalls = (await finalResult.toolCalls) || [];\n\n for (const toolCall of toolCalls) {\n controller.enqueue(\n sse.encodeTool(toolCall.toolName, toolCall.args as Record<string, unknown>)\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, {\n headers: getSSEHeaders(),\n });\n } catch (error) {\n console.error('Chat API error:', error);\n return new Response(JSON.stringify({ error: 'Internal server error' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n };\n}\n"]}
|
|
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 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\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n if (toolCall.function?.name && toolCall.function?.arguments) {\n try {\n const args = JSON.parse(toolCall.function.arguments);\n controller.enqueue(sse.encodeTool(toolCall.function.name, args));\n } catch {\n // Arguments might be streamed in chunks, skip incomplete\n }\n }\n }\n }\n } catch {\n // Skip malformed JSON\n }\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/dist/api/index.mjs
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import { streamText } from 'ai';
|
|
2
|
-
|
|
3
|
-
// src/api/createChatHandler.ts
|
|
4
|
-
|
|
5
1
|
// src/api/streaming.ts
|
|
6
2
|
function createSSEEncoder() {
|
|
7
3
|
const encoder = new TextEncoder();
|
|
@@ -61,54 +57,125 @@ async function* parseSSEStream(reader) {
|
|
|
61
57
|
}
|
|
62
58
|
}
|
|
63
59
|
|
|
64
|
-
// src/api/
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
// src/api/createHandler.ts
|
|
61
|
+
function convertTools(tools) {
|
|
62
|
+
return tools.map((tool) => ({
|
|
63
|
+
type: "function",
|
|
64
|
+
function: {
|
|
65
|
+
name: tool.name,
|
|
69
66
|
description: tool.description,
|
|
70
67
|
parameters: {
|
|
71
68
|
type: "object",
|
|
72
|
-
properties:
|
|
69
|
+
properties: Object.fromEntries(
|
|
70
|
+
Object.entries(tool.parameters.properties).map(([key, value]) => [
|
|
71
|
+
key,
|
|
72
|
+
{
|
|
73
|
+
type: value.type,
|
|
74
|
+
description: value.description,
|
|
75
|
+
...value.enum && { enum: value.enum }
|
|
76
|
+
}
|
|
77
|
+
])
|
|
78
|
+
),
|
|
73
79
|
required: tool.parameters.required
|
|
74
80
|
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return result;
|
|
81
|
+
}
|
|
82
|
+
}));
|
|
78
83
|
}
|
|
79
|
-
function
|
|
80
|
-
const {
|
|
84
|
+
function createHandler(config) {
|
|
85
|
+
const {
|
|
86
|
+
apiKey = process.env.OPENROUTER_API_KEY,
|
|
87
|
+
model = "google/gemini-2.0-flash-exp:free",
|
|
88
|
+
systemPrompt,
|
|
89
|
+
tools = [],
|
|
90
|
+
temperature = 0.7,
|
|
91
|
+
siteUrl,
|
|
92
|
+
siteName
|
|
93
|
+
} = config;
|
|
81
94
|
return async function POST(req) {
|
|
95
|
+
if (!apiKey) {
|
|
96
|
+
return new Response(
|
|
97
|
+
JSON.stringify({ error: "OpenRouter API key not configured. Get one at https://openrouter.ai" }),
|
|
98
|
+
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
99
|
+
);
|
|
100
|
+
}
|
|
82
101
|
try {
|
|
83
102
|
const body = await req.json();
|
|
84
103
|
const { messages } = body;
|
|
85
|
-
const
|
|
86
|
-
role:
|
|
87
|
-
|
|
88
|
-
|
|
104
|
+
const openRouterMessages = [
|
|
105
|
+
{ role: "system", content: systemPrompt },
|
|
106
|
+
...messages.map((m) => ({
|
|
107
|
+
role: m.role === "assistant" ? "assistant" : "user",
|
|
108
|
+
content: m.content
|
|
109
|
+
}))
|
|
110
|
+
];
|
|
89
111
|
const sse = createSSEEncoder();
|
|
90
112
|
const stream = new ReadableStream({
|
|
91
113
|
async start(controller) {
|
|
92
114
|
try {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
"HTTP-Referer": siteUrl || "",
|
|
121
|
+
"X-Title": siteName || ""
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
model,
|
|
125
|
+
messages: openRouterMessages,
|
|
126
|
+
temperature,
|
|
127
|
+
stream: true,
|
|
128
|
+
...tools.length > 0 && { tools: convertTools(tools) }
|
|
129
|
+
})
|
|
100
130
|
});
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
const error = await response.text();
|
|
133
|
+
console.error("OpenRouter error:", error);
|
|
134
|
+
controller.enqueue(sse.encodeError("Failed to get AI response"));
|
|
135
|
+
controller.enqueue(sse.encodeDone());
|
|
136
|
+
controller.close();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const reader = response.body?.getReader();
|
|
140
|
+
if (!reader) {
|
|
141
|
+
controller.enqueue(sse.encodeError("No response stream"));
|
|
142
|
+
controller.enqueue(sse.encodeDone());
|
|
143
|
+
controller.close();
|
|
144
|
+
return;
|
|
105
145
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
);
|
|
146
|
+
const decoder = new TextDecoder();
|
|
147
|
+
let buffer = "";
|
|
148
|
+
while (true) {
|
|
149
|
+
const { done, value } = await reader.read();
|
|
150
|
+
if (done) break;
|
|
151
|
+
buffer += decoder.decode(value, { stream: true });
|
|
152
|
+
const lines = buffer.split("\n");
|
|
153
|
+
buffer = lines.pop() || "";
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
if (line.startsWith("data: ")) {
|
|
156
|
+
const data = line.slice(6);
|
|
157
|
+
if (data === "[DONE]") continue;
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(data);
|
|
160
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
161
|
+
if (delta?.content) {
|
|
162
|
+
controller.enqueue(sse.encodeText(delta.content));
|
|
163
|
+
}
|
|
164
|
+
if (delta?.tool_calls) {
|
|
165
|
+
for (const toolCall of delta.tool_calls) {
|
|
166
|
+
if (toolCall.function?.name && toolCall.function?.arguments) {
|
|
167
|
+
try {
|
|
168
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
169
|
+
controller.enqueue(sse.encodeTool(toolCall.function.name, args));
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
112
179
|
}
|
|
113
180
|
controller.enqueue(sse.encodeDone());
|
|
114
181
|
controller.close();
|
|
@@ -119,19 +186,17 @@ function createChatHandler(config) {
|
|
|
119
186
|
}
|
|
120
187
|
}
|
|
121
188
|
});
|
|
122
|
-
return new Response(stream, {
|
|
123
|
-
headers: getSSEHeaders()
|
|
124
|
-
});
|
|
189
|
+
return new Response(stream, { headers: getSSEHeaders() });
|
|
125
190
|
} catch (error) {
|
|
126
|
-
console.error("
|
|
127
|
-
return new Response(
|
|
128
|
-
|
|
129
|
-
headers: { "Content-Type": "application/json" }
|
|
130
|
-
|
|
191
|
+
console.error("Handler error:", error);
|
|
192
|
+
return new Response(
|
|
193
|
+
JSON.stringify({ error: "Internal server error" }),
|
|
194
|
+
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
195
|
+
);
|
|
131
196
|
}
|
|
132
197
|
};
|
|
133
198
|
}
|
|
134
199
|
|
|
135
|
-
export {
|
|
200
|
+
export { createHandler, createSSEEncoder, getSSEHeaders, parseSSEStream };
|
|
136
201
|
//# sourceMappingURL=index.mjs.map
|
|
137
202
|
//# sourceMappingURL=index.mjs.map
|
package/dist/api/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/api/streaming.ts","../../src/api/createChatHandler.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 * Factory for creating Next.js API route handlers\n */\n\nimport { streamText, type CoreMessage, type LanguageModel } from 'ai';\nimport type { ToolDefinition } from '../tools/types';\nimport { createSSEEncoder, getSSEHeaders } from './streaming';\n\nexport interface ChatHandlerConfig {\n /** The AI model to use (from Vercel AI SDK) */\n model: LanguageModel;\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 /** Maximum tokens in response */\n maxTokens?: number;\n}\n\ninterface RequestBody {\n messages: Array<{ role: 'user' | 'assistant'; content: string }>;\n}\n\n/**\n * Convert tool definitions to Vercel AI SDK format\n */\nfunction convertToolsToAISDK(tools: ToolDefinition[]) {\n const result: Record<string, { description: string; parameters: unknown }> = {};\n\n for (const tool of tools) {\n result[tool.name] = {\n description: tool.description,\n parameters: {\n type: 'object',\n properties: tool.parameters.properties,\n required: tool.parameters.required,\n },\n };\n }\n\n return result;\n}\n\n/**\n * Create a Next.js API route handler for chat\n *\n * Works with any Vercel AI SDK compatible model including:\n * - Google Gemini (@ai-sdk/google)\n * - OpenAI (@ai-sdk/openai)\n * - Anthropic (@ai-sdk/anthropic)\n * - And more...\n *\n * @example\n * ```ts\n * // app/api/chat/route.ts\n * import { createChatHandler } from 'ai-site-pilot/api';\n * import { google } from '@ai-sdk/google';\n *\n * export const POST = createChatHandler({\n * model: google('gemini-2.0-flash'),\n * systemPrompt: 'You are a helpful assistant...',\n * tools: myTools,\n * });\n * ```\n *\n * @example Using OpenAI\n * ```ts\n * import { openai } from '@ai-sdk/openai';\n *\n * export const POST = createChatHandler({\n * model: openai('gpt-4o'),\n * systemPrompt: 'You are a helpful assistant...',\n * });\n * ```\n */\nexport function createChatHandler(config: ChatHandlerConfig) {\n const { model, systemPrompt, tools = [], temperature = 0.7, maxTokens } = config;\n\n return async function POST(req: Request): Promise<Response> {\n try {\n const body = (await req.json()) as RequestBody;\n const { messages } = body;\n\n // Convert messages to CoreMessage format\n const coreMessages: CoreMessage[] = messages.map((m) => ({\n role: m.role,\n content: m.content,\n }));\n\n // Create the SSE encoder\n const sse = createSSEEncoder();\n\n // Create a readable stream for SSE\n const stream = new ReadableStream({\n async start(controller) {\n try {\n const result = streamText({\n model,\n system: systemPrompt,\n messages: coreMessages,\n temperature,\n maxTokens,\n tools: tools.length > 0 ? convertToolsToAISDK(tools) : undefined,\n });\n\n // Stream text chunks\n for await (const chunk of (await result).textStream) {\n if (chunk) {\n controller.enqueue(sse.encodeText(chunk));\n }\n }\n\n // Get tool calls from the result\n const finalResult = await result;\n const toolCalls = (await finalResult.toolCalls) || [];\n\n for (const toolCall of toolCalls) {\n controller.enqueue(\n sse.encodeTool(toolCall.toolName, toolCall.args as Record<string, unknown>)\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, {\n headers: getSSEHeaders(),\n });\n } catch (error) {\n console.error('Chat API error:', error);\n return new Response(JSON.stringify({ error: 'Internal server error' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n };\n}\n"]}
|
|
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 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\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n if (toolCall.function?.name && toolCall.function?.arguments) {\n try {\n const args = JSON.parse(toolCall.function.arguments);\n controller.enqueue(sse.encodeTool(toolCall.function.name, args));\n } catch {\n // Arguments might be streamed in chunks, skip incomplete\n }\n }\n }\n }\n } catch {\n // Skip malformed JSON\n }\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/dist/styles.css
CHANGED
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
.pilot-container {
|
|
16
|
+
/* Container should not interfere with fixed children */
|
|
17
|
+
position: relative;
|
|
18
|
+
z-index: 199;
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
|
|
21
|
+
/* CSS custom properties for theming */
|
|
16
22
|
--pilot-bg: #0F0720;
|
|
17
23
|
--pilot-bg-95: rgba(15, 7, 32, 0.95);
|
|
18
24
|
--pilot-text: #ffffff;
|
|
@@ -31,6 +37,11 @@
|
|
|
31
37
|
--pilot-accent-glow-strong: hsla(var(--pilot-accent-h), var(--pilot-accent-s), var(--pilot-accent-l), 0.3);
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
/* Fixed children need pointer events enabled */
|
|
41
|
+
.pilot-container > * {
|
|
42
|
+
pointer-events: auto;
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
/* Panel */
|
|
35
46
|
.pilot-panel {
|
|
36
47
|
background: var(--pilot-bg-95);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-site-pilot",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AI chat widget that can control and navigate your website.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"chat",
|
|
45
45
|
"chatbot",
|
|
46
46
|
"site-pilot",
|
|
47
|
+
"openrouter",
|
|
47
48
|
"navigation",
|
|
48
49
|
"react",
|
|
49
50
|
"nextjs",
|
|
50
|
-
"vercel-ai-sdk",
|
|
51
51
|
"streaming",
|
|
52
52
|
"tools",
|
|
53
53
|
"function-calling"
|
|
@@ -71,7 +71,6 @@
|
|
|
71
71
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"ai": "^4.0.0",
|
|
75
74
|
"framer-motion": "^11.0.0",
|
|
76
75
|
"lucide-react": "^0.400.0",
|
|
77
76
|
"react-markdown": "^9.0.0"
|