@trossino/chat-widget 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +179 -2
- package/dist/index.cjs +484 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +79 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +462 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +249 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +45 -0
- package/dist/server.d.ts +45 -0
- package/dist/server.js +246 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.css +29 -0
- package/package.json +90 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tommaso Rossino
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,180 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @trossino/chat-widget
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Batteries-included React chat widget, plus an optional framework-agnostic server handler for Anthropic and OpenAI.
|
|
4
|
+
|
|
5
|
+
- Drop-in UI: floating bottom-right panel, fully styled inline (no Tailwind, no CSS file required).
|
|
6
|
+
- Zero runtime dependencies: native `fetch`, Web `Request`/`Response`.
|
|
7
|
+
- JSON or SSE streaming responses, detected automatically from `Content-Type`.
|
|
8
|
+
- Works with Next.js App Router, Vercel, Cloudflare Workers, Deno, Bun, or any Web-standard runtime.
|
|
9
|
+
- Accessible: labelled controls, `role="log"` live region, Enter to send, Shift+Enter for newline, Esc to close.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i @trossino/chat-widget
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Peer dependencies: `react` and `react-dom` 18 or 19.
|
|
18
|
+
|
|
19
|
+
## Client only (your own endpoint)
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { ChatWidget } from '@trossino/chat-widget';
|
|
23
|
+
|
|
24
|
+
export default function App() {
|
|
25
|
+
return <ChatWidget endpoint="/api/my-chat" title="Support" welcomeMessage="Hi! How can I help?" />;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Your endpoint receives `POST { messages: Array<{ role: 'user' | 'assistant', content: string }> }` and must answer with either:
|
|
30
|
+
|
|
31
|
+
- JSON: `{ "reply": "..." }`
|
|
32
|
+
- SSE (`Content-Type: text/event-stream`): `data: {"delta":"..."}` events, ending with `data: [DONE]`. Errors: `event: error` + `data: {"error":"..."}`.
|
|
33
|
+
|
|
34
|
+
Want your own UI? Use the hook directly:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { useChat } from '@trossino/chat-widget';
|
|
38
|
+
|
|
39
|
+
const { messages, loading, error, send, stop, reset } = useChat({ endpoint: '/api/chat' });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Batteries included (Next.js App Router)
|
|
43
|
+
|
|
44
|
+
`app/api/chat/route.ts`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { createChatHandler } from '@trossino/chat-widget/server';
|
|
48
|
+
|
|
49
|
+
export const POST = createChatHandler({
|
|
50
|
+
provider: 'anthropic', // or 'openai'
|
|
51
|
+
apiKey: process.env.ANTHROPIC_API_KEY ?? '',
|
|
52
|
+
systemPrompt: 'You are a concise, friendly assistant.',
|
|
53
|
+
maxTokens: 1024,
|
|
54
|
+
stream: true,
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`app/page.tsx`:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { ChatWidget } from '@trossino/chat-widget';
|
|
62
|
+
|
|
63
|
+
export default function Page() {
|
|
64
|
+
return <ChatWidget endpoint="/api/chat" />;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The client bundle ships with `"use client"`, so it can be rendered from Server Components. See [`examples/nextjs`](./examples/nextjs) for a full app.
|
|
69
|
+
|
|
70
|
+
The handler is a plain `(req: Request) => Promise<Response>`, so it also works as a Cloudflare Worker (`export default { fetch: handler }`) or with `Deno.serve(handler)`.
|
|
71
|
+
|
|
72
|
+
## `<ChatWidget>` props
|
|
73
|
+
|
|
74
|
+
| Prop | Type | Default | Description |
|
|
75
|
+
| ---------------- | ------------------------ | -------------------- | ------------------------------------------------------------------------ |
|
|
76
|
+
| `endpoint` | `string` | — | URL receiving `POST { messages }`. Required. |
|
|
77
|
+
| `headers` | `Record<string, string>` | — | Extra request headers (e.g. a session token). **Never a provider key.** |
|
|
78
|
+
| `placeholder` | `string` | `"Ask me anything…"` | Input placeholder. |
|
|
79
|
+
| `title` | `string` | `"Chat with me"` | Panel title. |
|
|
80
|
+
| `welcomeMessage` | `string` | — | Shown when the conversation is empty. Display-only, not sent to the API. |
|
|
81
|
+
| `onError` | `(error: Error) => void` | — | Called on network, HTTP, or stream errors. |
|
|
82
|
+
| `defaultOpen` | `boolean` | `false` | Open the panel on mount. |
|
|
83
|
+
| `variant` | `'light' \| 'dark'` | `'light'` | Built-in palette. See [Theming](#theming). |
|
|
84
|
+
| `className` | `string` | — | Extra class on the root element (it always has `tr-chat-widget`). |
|
|
85
|
+
| `style` | `ChatWidgetStyle` | — | Inline styles merged onto the root. Accepts `--cw-*` properties. |
|
|
86
|
+
| `toggleIcon` | `ReactNode` | chat bubble icon | Custom launcher icon, shown while the panel is closed. |
|
|
87
|
+
|
|
88
|
+
## Theming
|
|
89
|
+
|
|
90
|
+
Every color, radius, shadow, and font is read from a CSS custom property on the root element, with a built-in fallback. Set the properties from a class (via `className`) or inline (via `style`) and the widget picks them up. Anything you set wins over `variant`; anything you don't set falls back to the variant's value.
|
|
91
|
+
|
|
92
|
+
| Property | Light default | Dark default | Used for |
|
|
93
|
+
| ----------------------- | ------------------------------------------------------------------- | ------------------------ | ------------------------------------------- |
|
|
94
|
+
| `--cw-font` | `system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif` | same | Font family |
|
|
95
|
+
| `--cw-text` | `#111827` | `#e5e0db` | Base text |
|
|
96
|
+
| `--cw-muted` | `#6b7280` | `#8a8580` | "Typing…" indicator |
|
|
97
|
+
| `--cw-panel-bg` | `#ffffff` | `#0c0c0c` | Panel and input bar background |
|
|
98
|
+
| `--cw-list-bg` | `#f9fafb` | `#0a0a0a` | Message list background |
|
|
99
|
+
| `--cw-border` | `#e5e7eb` | `rgba(255,255,255,0.08)` | Panel, assistant bubble, input bar borders |
|
|
100
|
+
| `--cw-accent` | `#2563eb` | same | Launcher, header, user bubble, send, focus |
|
|
101
|
+
| `--cw-accent-contrast` | `#ffffff` | same | Text/icons on accent |
|
|
102
|
+
| `--cw-bubble-bg` | `#ffffff` | `rgba(255,255,255,0.04)` | Assistant bubble background |
|
|
103
|
+
| `--cw-bubble-text` | `#111827` | `#e5e0db` | Assistant bubble text |
|
|
104
|
+
| `--cw-input-bg` | `#ffffff` | `rgba(255,255,255,0.03)` | Textarea background |
|
|
105
|
+
| `--cw-input-border` | `#d1d5db` | `rgba(255,255,255,0.08)` | Textarea border |
|
|
106
|
+
| `--cw-error-bg` | `#fef2f2` | `rgba(185,28,28,0.15)` | Error banner background |
|
|
107
|
+
| `--cw-error-text` | `#b91c1c` | `#fca5a5` | Error banner text |
|
|
108
|
+
| `--cw-shadow` | `0 12px 40px rgba(0,0,0,0.18)` | same | Panel shadow |
|
|
109
|
+
| `--cw-toggle-shadow` | `0 6px 20px rgba(0,0,0,0.2)` | same | Launcher shadow |
|
|
110
|
+
| `--cw-radius` | `12px` | same | Panel and bubbles |
|
|
111
|
+
| `--cw-radius-sm` | `8px` | same | Input, send button, error banner |
|
|
112
|
+
| `--cw-header-bg` | `var(--cw-accent)` | same | Header background |
|
|
113
|
+
| `--cw-header-text` | `var(--cw-accent-contrast)` | same | Header title and close button |
|
|
114
|
+
| `--cw-header-border` | `none` | same | Header `border-bottom` shorthand |
|
|
115
|
+
| `--cw-title-font` | `inherit` | same | Header title font family |
|
|
116
|
+
|
|
117
|
+
Quick accent change, inline:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
<ChatWidget endpoint="/api/chat" style={{ '--cw-accent': '#c8553d' }} />
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Dark theme example
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
<ChatWidget endpoint="/api/chat" variant="dark" className="site-chat" />
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```css
|
|
130
|
+
/* Tune on top of the dark variant: only what differs needs setting. */
|
|
131
|
+
.site-chat {
|
|
132
|
+
--cw-accent: #c8553d;
|
|
133
|
+
--cw-font: "DM Sans", ui-sans-serif, system-ui, sans-serif;
|
|
134
|
+
--cw-radius: 10px;
|
|
135
|
+
--cw-radius-sm: 6px;
|
|
136
|
+
--cw-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
|
137
|
+
|
|
138
|
+
/* Editorial header: no accent fill, just a hairline. */
|
|
139
|
+
--cw-header-bg: transparent;
|
|
140
|
+
--cw-header-text: #e5e0db;
|
|
141
|
+
--cw-header-border: 1px solid rgba(255, 255, 255, 0.08);
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For deeper changes, every element also has a stable class (`tr-chat-panel`, `tr-chat-header`, `tr-chat-msg-user`, `tr-chat-input`, …) you can target with your own CSS, using `!important` where it needs to beat the inline styles.
|
|
146
|
+
|
|
147
|
+
## `createChatHandler` options
|
|
148
|
+
|
|
149
|
+
| Option | Type | Default |
|
|
150
|
+
| ------------------ | ------------------------- | ------------------------------------------------------- |
|
|
151
|
+
| `provider` | `'anthropic' \| 'openai'` | — (required) |
|
|
152
|
+
| `apiKey` | `string` | — (required; responds `401` if empty) |
|
|
153
|
+
| `model` | `string` | `claude-sonnet-4-5` (anthropic), `gpt-4o-mini` (openai) |
|
|
154
|
+
| `systemPrompt` | `string` | — |
|
|
155
|
+
| `maxTokens` | `number` | `1024` |
|
|
156
|
+
| `stream` | `boolean` | `false` (`true` responds with SSE) |
|
|
157
|
+
| `maxMessages` | `number` | `50` |
|
|
158
|
+
| `maxMessageLength` | `number` | `8000` characters |
|
|
159
|
+
| `onError` | `(error: unknown) => void`| — (server-side logging hook) |
|
|
160
|
+
|
|
161
|
+
Responses: `200` with `{ reply }` or an SSE stream, `400` for an invalid body, `401` if the API key is missing, `405` for non-POST, `500` with a generic message for any upstream or internal failure. Provider error details are passed to `onError` and never sent to the client.
|
|
162
|
+
|
|
163
|
+
## Optional styles
|
|
164
|
+
|
|
165
|
+
Everything is inline-styled. For extras inline styles can't express (focus rings in `--cw-accent`, hover, entry animation that respects `prefers-reduced-motion`):
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import '@trossino/chat-widget/styles';
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Notes
|
|
172
|
+
|
|
173
|
+
- **Streaming:** with `stream: true` the handler converts the provider's stream into the simple `delta` protocol above. The client renders tokens as they arrive and also accepts plain JSON, so you can switch freely.
|
|
174
|
+
- **Rate limiting is up to you.** The handler has no auth or rate limiting. Anyone who can reach your endpoint spends your tokens. Put it behind your own auth, rate limiter, or edge middleware (e.g. Upstash, Cloudflare rate limiting rules).
|
|
175
|
+
- **API keys:** the widget never handles provider keys. Keep them in server-side env vars. Anything passed via `headers` is visible in the browser.
|
|
176
|
+
- **Privacy:** messages go from the browser to your endpoint, then to the provider you configure. Nothing is stored or logged by this package. Disclose the provider to your users as your privacy policy requires.
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT
|