@smooai/smooth-operator 0.1.0 → 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.
Files changed (81) hide show
  1. package/README.md +18 -4
  2. package/dist/react/components/SmoothChat.d.ts +35 -0
  3. package/dist/react/components/SmoothChat.d.ts.map +1 -0
  4. package/dist/react/components/SmoothChat.js +26 -0
  5. package/dist/react/components/SmoothChat.js.map +1 -0
  6. package/dist/react/components/parts.d.ts +30 -0
  7. package/dist/react/components/parts.d.ts.map +1 -0
  8. package/dist/react/components/parts.js +67 -0
  9. package/dist/react/components/parts.js.map +1 -0
  10. package/dist/react/index.d.ts +27 -0
  11. package/dist/react/index.d.ts.map +1 -0
  12. package/dist/react/index.js +26 -0
  13. package/dist/react/index.js.map +1 -0
  14. package/dist/react/provider.d.ts +27 -0
  15. package/dist/react/provider.d.ts.map +1 -0
  16. package/dist/react/provider.js +25 -0
  17. package/dist/react/provider.js.map +1 -0
  18. package/dist/react/response.d.ts +19 -0
  19. package/dist/react/response.d.ts.map +1 -0
  20. package/dist/react/response.js +53 -0
  21. package/dist/react/response.js.map +1 -0
  22. package/dist/react/theme.d.ts +64 -0
  23. package/dist/react/theme.d.ts.map +1 -0
  24. package/dist/react/theme.js +38 -0
  25. package/dist/react/theme.js.map +1 -0
  26. package/dist/react/types.d.ts +24 -0
  27. package/dist/react/types.d.ts.map +1 -0
  28. package/dist/react/types.js +2 -0
  29. package/dist/react/types.js.map +1 -0
  30. package/dist/react/use-conversation.d.ts +59 -0
  31. package/dist/react/use-conversation.d.ts.map +1 -0
  32. package/dist/react/use-conversation.js +154 -0
  33. package/dist/react/use-conversation.js.map +1 -0
  34. package/dist/widget/chat-widget.iife.js +1301 -0
  35. package/dist/widget/chat-widget.iife.js.map +1 -0
  36. package/dist/widget/config.d.ts +73 -0
  37. package/dist/widget/config.d.ts.map +1 -0
  38. package/dist/widget/config.js +30 -0
  39. package/dist/widget/config.js.map +1 -0
  40. package/dist/widget/conversation.d.ts +50 -0
  41. package/dist/widget/conversation.d.ts.map +1 -0
  42. package/dist/widget/conversation.js +173 -0
  43. package/dist/widget/conversation.js.map +1 -0
  44. package/dist/widget/element.d.ts +94 -0
  45. package/dist/widget/element.d.ts.map +1 -0
  46. package/dist/widget/element.js +377 -0
  47. package/dist/widget/element.js.map +1 -0
  48. package/dist/widget/index.d.ts +25 -0
  49. package/dist/widget/index.d.ts.map +1 -0
  50. package/dist/widget/index.js +24 -0
  51. package/dist/widget/index.js.map +1 -0
  52. package/dist/widget/logo.d.ts +9 -0
  53. package/dist/widget/logo.d.ts.map +1 -0
  54. package/dist/widget/logo.js +10 -0
  55. package/dist/widget/logo.js.map +1 -0
  56. package/dist/widget/standalone.d.ts +25 -0
  57. package/dist/widget/standalone.d.ts.map +1 -0
  58. package/dist/widget/standalone.js +15 -0
  59. package/dist/widget/standalone.js.map +1 -0
  60. package/dist/widget/styles.d.ts +12 -0
  61. package/dist/widget/styles.d.ts.map +1 -0
  62. package/dist/widget/styles.js +262 -0
  63. package/dist/widget/styles.js.map +1 -0
  64. package/package.json +44 -6
  65. package/src/react/components/SmoothChat.tsx +57 -0
  66. package/src/react/components/parts.tsx +141 -0
  67. package/src/react/index.ts +30 -0
  68. package/src/react/provider.tsx +40 -0
  69. package/src/react/response.ts +57 -0
  70. package/src/react/styles.css +231 -0
  71. package/src/react/theme.ts +92 -0
  72. package/src/react/types.ts +27 -0
  73. package/src/react/use-conversation.ts +203 -0
  74. package/src/widget/config.ts +102 -0
  75. package/src/widget/conversation.ts +212 -0
  76. package/src/widget/element.ts +420 -0
  77. package/src/widget/index.ts +37 -0
  78. package/src/widget/logo.ts +9 -0
  79. package/src/widget/smooth-logo.svg +32 -0
  80. package/src/widget/standalone.ts +33 -0
  81. package/src/widget/styles.ts +265 -0
@@ -0,0 +1,15 @@
1
+ import { defineChatWidget, mountChatWidget, mountFullPageChat, SmoothAgentChatElement } from './element.js';
2
+ defineChatWidget();
3
+ export { defineChatWidget, mountChatWidget, mountFullPageChat, SmoothAgentChatElement };
4
+ /** Convenience alias matching the global API surface (`SmoothAgentChat.mount`). */
5
+ export function mount(config, target) {
6
+ return mountChatWidget(config, target);
7
+ }
8
+ /**
9
+ * Full-page convenience alias (`SmoothAgentChat.mountFullPage`): mounts the chat
10
+ * in `mode: "fullpage"` so it fills its container/viewport with no launcher.
11
+ */
12
+ export function mountFullPage(config, target) {
13
+ return mountFullPageChat(config, target);
14
+ }
15
+ //# sourceMappingURL=standalone.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"standalone.js","sourceRoot":"","sources":["../../src/widget/standalone.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE5G,gBAAgB,EAAE,CAAC;AAEnB,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,CAAC;AAExF,mFAAmF;AACnF,MAAM,UAAU,KAAK,CAAC,MAAwB,EAAE,MAAoB;IAChE,OAAO,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAC3C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,MAAsC,EAAE,MAAoB;IACtF,OAAO,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { ChatWidgetMode, ChatWidgetTheme } from './config.js';
2
+ /**
3
+ * Render the widget's scoped stylesheet. All theme values are injected as CSS
4
+ * custom properties on `:host` so they can be overridden per-instance and so the
5
+ * styles below stay static. Kept deliberately framework-light — no Tailwind, no
6
+ * runtime CSS-in-JS; just a string the web component drops into its shadow root.
7
+ *
8
+ * `mode` switches the host positioning + panel sizing between the floating
9
+ * popover (default) and the full-page layout (fills its container/viewport).
10
+ */
11
+ export declare function buildStyles(theme: Required<ChatWidgetTheme>, mode?: ChatWidgetMode): string;
12
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/widget/styles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnE;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,EAAE,IAAI,GAAE,cAA0B,GAAG,MAAM,CA6PtG"}
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Render the widget's scoped stylesheet. All theme values are injected as CSS
3
+ * custom properties on `:host` so they can be overridden per-instance and so the
4
+ * styles below stay static. Kept deliberately framework-light — no Tailwind, no
5
+ * runtime CSS-in-JS; just a string the web component drops into its shadow root.
6
+ *
7
+ * `mode` switches the host positioning + panel sizing between the floating
8
+ * popover (default) and the full-page layout (fills its container/viewport).
9
+ */
10
+ export function buildStyles(theme, mode = 'popover') {
11
+ return `
12
+ :host {
13
+ --sac-text: ${theme.text};
14
+ --sac-bg: ${theme.background};
15
+ --sac-primary: ${theme.primary};
16
+ --sac-primary-text: ${theme.primaryText};
17
+ --sac-assistant-bubble: ${theme.assistantBubble};
18
+ --sac-assistant-bubble-text: ${theme.assistantBubbleText};
19
+ --sac-user-bubble: ${theme.userBubble};
20
+ --sac-user-bubble-text: ${theme.userBubbleText};
21
+ --sac-border: ${theme.border};
22
+
23
+ ${mode === 'fullpage'
24
+ ? `/* Full-page: fill the host's box (the element should be sized by its
25
+ container, or it falls back to filling the viewport). */
26
+ display: block;
27
+ position: relative;
28
+ width: 100%;
29
+ height: 100%;
30
+ min-height: 100vh;`
31
+ : `/* Popover: float in the bottom-right corner. */
32
+ position: fixed;
33
+ bottom: 20px;
34
+ right: 20px;
35
+ z-index: 2147483000;`}
36
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
37
+ }
38
+
39
+ * { box-sizing: border-box; }
40
+
41
+ .launcher {
42
+ width: 56px;
43
+ height: 56px;
44
+ border-radius: 50%;
45
+ border: none;
46
+ cursor: pointer;
47
+ background: var(--sac-primary);
48
+ color: var(--sac-primary-text);
49
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ font-size: 24px;
54
+ transition: transform 0.15s ease;
55
+ }
56
+ .launcher:hover { transform: scale(1.05); }
57
+
58
+ .panel {
59
+ width: 360px;
60
+ max-width: calc(100vw - 40px);
61
+ height: 520px;
62
+ max-height: calc(100vh - 40px);
63
+ display: flex;
64
+ flex-direction: column;
65
+ background: var(--sac-bg);
66
+ color: var(--sac-text);
67
+ border: 1px solid var(--sac-border);
68
+ border-radius: 14px;
69
+ overflow: hidden;
70
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
71
+ }
72
+
73
+ /* Full-page: the panel becomes the whole surface — no floating box, no shadow,
74
+ no rounded corners; it fills the host. */
75
+ .panel.fullpage {
76
+ width: 100%;
77
+ height: 100%;
78
+ min-height: 100vh;
79
+ max-width: none;
80
+ max-height: none;
81
+ border: none;
82
+ border-radius: 0;
83
+ box-shadow: none;
84
+ }
85
+
86
+ .header {
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: space-between;
90
+ padding: 12px 14px;
91
+ background: var(--sac-primary);
92
+ color: var(--sac-primary-text);
93
+ }
94
+ .header .brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
95
+ .header .logo { height: 24px; width: auto; display: block; }
96
+ .header .title { font-weight: 600; font-size: 15px; }
97
+ .header .status { font-size: 11px; opacity: 0.85; }
98
+ .header .powered {
99
+ font-size: 10px;
100
+ opacity: 0.7;
101
+ letter-spacing: 0.02em;
102
+ }
103
+ .header .close {
104
+ background: transparent;
105
+ border: none;
106
+ color: inherit;
107
+ cursor: pointer;
108
+ font-size: 18px;
109
+ line-height: 1;
110
+ padding: 4px;
111
+ }
112
+
113
+ /* Full-page header: taller, logo-led, centered max-width content row. */
114
+ .panel.fullpage .header { padding: 14px 20px; }
115
+ .panel.fullpage .logo { height: 30px; }
116
+
117
+ .messages {
118
+ flex: 1;
119
+ overflow-y: auto;
120
+ padding: 14px;
121
+ display: flex;
122
+ flex-direction: column;
123
+ gap: 10px;
124
+ }
125
+
126
+ .bubble {
127
+ max-width: 80%;
128
+ padding: 9px 12px;
129
+ border-radius: 12px;
130
+ font-size: 14px;
131
+ line-height: 1.4;
132
+ white-space: pre-wrap;
133
+ word-break: break-word;
134
+ }
135
+ .bubble.assistant {
136
+ align-self: flex-start;
137
+ background: var(--sac-assistant-bubble);
138
+ color: var(--sac-assistant-bubble-text);
139
+ border-bottom-left-radius: 4px;
140
+ }
141
+ .bubble.user {
142
+ align-self: flex-end;
143
+ background: var(--sac-user-bubble);
144
+ color: var(--sac-user-bubble-text);
145
+ border-bottom-right-radius: 4px;
146
+ }
147
+ .bubble.greeting { opacity: 0.85; font-style: italic; }
148
+
149
+ /* Full-page: center the conversation in a readable column and let bubbles
150
+ breathe a little wider. */
151
+ .panel.fullpage .messages {
152
+ padding: 24px 20px;
153
+ align-items: stretch;
154
+ }
155
+ .panel.fullpage .messages > * {
156
+ width: 100%;
157
+ max-width: 760px;
158
+ margin-left: auto;
159
+ margin-right: auto;
160
+ }
161
+ .panel.fullpage .bubble { max-width: 100%; }
162
+ .panel.fullpage .bubble.user { align-self: flex-end; max-width: 80%; margin-right: auto; }
163
+ .panel.fullpage .bubble.assistant { align-self: flex-start; max-width: 100%; }
164
+
165
+ /* Sources panel — rendered under an assistant bubble whose terminal
166
+ eventual_response carried citations. */
167
+ .sources {
168
+ align-self: flex-start;
169
+ max-width: 80%;
170
+ margin-top: -4px;
171
+ font-size: 12.5px;
172
+ color: var(--sac-text);
173
+ }
174
+ .panel.fullpage .sources { max-width: 100%; }
175
+ .sources details { background: transparent; }
176
+ .sources summary {
177
+ cursor: pointer;
178
+ font-weight: 600;
179
+ opacity: 0.85;
180
+ list-style: none;
181
+ user-select: none;
182
+ padding: 2px 0;
183
+ }
184
+ .sources summary::-webkit-details-marker { display: none; }
185
+ .sources summary::before {
186
+ content: '▸';
187
+ display: inline-block;
188
+ margin-right: 6px;
189
+ transition: transform 0.15s ease;
190
+ }
191
+ .sources details[open] summary::before { transform: rotate(90deg); }
192
+ .sources ol {
193
+ margin: 6px 0 0;
194
+ padding-left: 0;
195
+ list-style: none;
196
+ display: flex;
197
+ flex-direction: column;
198
+ gap: 8px;
199
+ }
200
+ .sources li {
201
+ border-left: 2px solid var(--sac-primary);
202
+ padding-left: 10px;
203
+ }
204
+ .sources .src-title {
205
+ color: var(--sac-primary);
206
+ text-decoration: none;
207
+ font-weight: 600;
208
+ word-break: break-word;
209
+ }
210
+ .sources a.src-title:hover { text-decoration: underline; }
211
+ .sources span.src-title { color: var(--sac-text); opacity: 0.95; }
212
+ .sources .src-snippet {
213
+ display: block;
214
+ margin-top: 2px;
215
+ opacity: 0.7;
216
+ line-height: 1.4;
217
+ white-space: normal;
218
+ }
219
+
220
+ .cursor::after {
221
+ content: '▋';
222
+ margin-left: 1px;
223
+ animation: sac-blink 1s steps(2, start) infinite;
224
+ }
225
+ @keyframes sac-blink { to { visibility: hidden; } }
226
+
227
+ .composer {
228
+ display: flex;
229
+ gap: 8px;
230
+ padding: 10px;
231
+ border-top: 1px solid var(--sac-border);
232
+ }
233
+ .composer textarea {
234
+ flex: 1;
235
+ resize: none;
236
+ border: 1px solid var(--sac-border);
237
+ border-radius: 8px;
238
+ padding: 8px 10px;
239
+ font-family: inherit;
240
+ font-size: 14px;
241
+ background: transparent;
242
+ color: var(--sac-text);
243
+ max-height: 96px;
244
+ line-height: 1.4;
245
+ }
246
+ .composer textarea:focus { outline: 1px solid var(--sac-primary); }
247
+ .composer button {
248
+ border: none;
249
+ border-radius: 8px;
250
+ padding: 0 14px;
251
+ cursor: pointer;
252
+ background: var(--sac-primary);
253
+ color: var(--sac-primary-text);
254
+ font-weight: 600;
255
+ font-size: 14px;
256
+ }
257
+ .composer button:disabled { opacity: 0.5; cursor: default; }
258
+
259
+ .hidden { display: none !important; }
260
+ `;
261
+ }
262
+ //# sourceMappingURL=styles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.js","sourceRoot":"","sources":["../../src/widget/styles.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,KAAgC,EAAE,OAAuB,SAAS;IAC1F,OAAO;;kBAEO,KAAK,CAAC,IAAI;gBACZ,KAAK,CAAC,UAAU;qBACX,KAAK,CAAC,OAAO;0BACR,KAAK,CAAC,WAAW;8BACb,KAAK,CAAC,eAAe;mCAChB,KAAK,CAAC,mBAAmB;yBACnC,KAAK,CAAC,UAAU;8BACX,KAAK,CAAC,cAAc;oBAC9B,KAAK,CAAC,MAAM;;MAGxB,IAAI,KAAK,UAAU;QACf,CAAC,CAAC;;;;;;uBAMS;QACX,CAAC,CAAC;;;;yBAKV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiOH,CAAC;AACF,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@smooai/smooth-operator",
3
- "version": "0.1.0",
4
- "description": "TypeScript protocol types and native WebSocket client for the smooth-operator protocol. Generated from the language-neutral JSON Schemas in spec/.",
3
+ "version": "0.2.0",
4
+ "description": "TypeScript SDK for the smooth-operator WebSocket protocol: the native client (`.`), React bindings (`./react`), and the embeddable web-component chat widget (`./widget`). Generated from the language-neutral JSON Schemas in spec/.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "engines": {
@@ -18,7 +18,17 @@
18
18
  "./validate": {
19
19
  "types": "./dist/validate.d.ts",
20
20
  "import": "./dist/validate.js"
21
- }
21
+ },
22
+ "./react": {
23
+ "types": "./dist/react/index.d.ts",
24
+ "import": "./dist/react/index.js"
25
+ },
26
+ "./react/styles.css": "./src/react/styles.css",
27
+ "./widget": {
28
+ "types": "./dist/widget/index.d.ts",
29
+ "import": "./dist/widget/index.js"
30
+ },
31
+ "./widget/standalone": "./dist/widget/chat-widget.iife.js"
22
32
  },
23
33
  "publishConfig": {
24
34
  "access": "public"
@@ -36,7 +46,12 @@
36
46
  "client",
37
47
  "ai",
38
48
  "agent",
39
- "streaming"
49
+ "streaming",
50
+ "react",
51
+ "chat",
52
+ "widget",
53
+ "web-component",
54
+ "rag"
40
55
  ],
41
56
  "files": [
42
57
  "dist",
@@ -46,20 +61,43 @@
46
61
  "ajv": "^8.17.1",
47
62
  "ajv-formats": "^3.0.1"
48
63
  },
64
+ "peerDependencies": {
65
+ "react": ">=18",
66
+ "react-dom": ">=18"
67
+ },
68
+ "peerDependenciesMeta": {
69
+ "react": {
70
+ "optional": true
71
+ },
72
+ "react-dom": {
73
+ "optional": true
74
+ }
75
+ },
49
76
  "devDependencies": {
50
77
  "@apidevtools/json-schema-ref-parser": "^11.7.3",
78
+ "@playwright/test": "^1.49.0",
79
+ "@testing-library/dom": "^10.4.0",
80
+ "@testing-library/react": "^16.1.0",
51
81
  "@types/node": "^25.9.2",
82
+ "@types/react": "^19.0.0",
83
+ "@types/react-dom": "^19.0.0",
84
+ "jsdom": "^25.0.1",
52
85
  "json-schema-to-typescript": "^15.0.4",
86
+ "react": "^19.0.0",
87
+ "react-dom": "^19.0.0",
88
+ "tsdown": "^0.22.0",
53
89
  "tsx": "^4.19.2",
54
90
  "typescript": "^5.7.3",
55
91
  "vitest": "^2.1.8"
56
92
  },
57
93
  "scripts": {
58
94
  "generate": "tsx scripts/generate.ts",
59
- "build": "tsc -p tsconfig.json",
95
+ "build": "tsc -p tsconfig.json && tsdown",
60
96
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
97
+ "typecheck:e2e": "tsc --noEmit -p tsconfig.e2e.json",
61
98
  "test": "vitest run",
62
99
  "test:watch": "vitest",
63
- "test:e2e": "vitest run --config vitest.e2e.config.ts"
100
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
101
+ "test:e2e:widget": "playwright test --project=chromium"
64
102
  }
65
103
  }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * <SmoothChat> — the opinionated, drop-in chat surface.
3
+ *
4
+ * It wires `useConversation` to the presentational parts and applies the
5
+ * `.smooth-chat` root class whose styling is 100% CSS-variable-driven. Theme it
6
+ * any of three ways (see `theme.ts`): override `--smooth-*` in your CSS/Tailwind,
7
+ * pass a `theme` prop (sets the variables inline), or restyle the
8
+ * `smooth-chat__*` classes outright.
9
+ *
10
+ * Need full control? Skip this component and build your own view over
11
+ * `useConversation` + the exported parts. This is just the batteries-included path.
12
+ *
13
+ * Remember to import the stylesheet once in your app:
14
+ * import '@smooai/smooth-operator/react/styles.css';
15
+ */
16
+ import { themeToStyle, type ChatTheme } from '../theme.js';
17
+ import { useConversation, type UseConversationOptions } from '../use-conversation.js';
18
+ import { Composer, ConnectionStatusLabel, MessageList } from './parts.js';
19
+
20
+ export interface SmoothChatProps extends UseConversationOptions {
21
+ /** Header label for the agent. Default "Assistant". */
22
+ agentName?: string;
23
+ /** Greeting rendered before any messages. */
24
+ greeting?: string;
25
+ /** Placeholder for the input. */
26
+ placeholder?: string;
27
+ /** Per-instance theme overrides (sets `--smooth-*` inline; wins over stylesheet rules). */
28
+ theme?: ChatTheme;
29
+ /** Show the small connection-status label in the header. Default `true`. */
30
+ showStatus?: boolean;
31
+ /** Extra class names on the `.smooth-chat` root (e.g. Tailwind layout utilities). */
32
+ className?: string;
33
+ /** Layout: `panel` (bordered card) or `fullpage` (fills its container). Default `panel`. */
34
+ layout?: 'panel' | 'fullpage';
35
+ }
36
+
37
+ export function SmoothChat(props: SmoothChatProps) {
38
+ const { agentName = 'Assistant', greeting, placeholder, theme, showStatus = true, className, layout = 'panel', ...conversationOptions } = props;
39
+
40
+ const { status, messages, send } = useConversation(conversationOptions);
41
+ const busy = messages.some((m) => m.streaming);
42
+
43
+ return (
44
+ <div
45
+ className={['smooth-chat', `smooth-chat--${layout}`, className].filter(Boolean).join(' ')}
46
+ style={themeToStyle(theme)}
47
+ data-status={status}
48
+ >
49
+ <header className="smooth-chat__header">
50
+ <span className="smooth-chat__title">{agentName}</span>
51
+ {showStatus ? <ConnectionStatusLabel status={status} /> : null}
52
+ </header>
53
+ <MessageList messages={messages} greeting={greeting} />
54
+ <Composer onSend={(text) => void send(text)} disabled={status === 'connecting' || busy} placeholder={placeholder} />
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Presentational building blocks.
3
+ *
4
+ * Each part is a thin, styling-light view over the data `useConversation`
5
+ * produces. They render semantic markup with `smooth-chat__*` class names whose
6
+ * styling lives entirely in `styles.css` and is driven by `--smooth-*` CSS
7
+ * variables — so retheming is CSS, never prop-drilling. Every part also forwards
8
+ * `className` so you can layer Tailwind utilities on top, and you can ignore
9
+ * these entirely and render the hook's state yourself.
10
+ */
11
+ import { useState, type FormEvent, type KeyboardEvent } from 'react';
12
+ import { safeHttpUrl } from '../response.js';
13
+ import type { ChatMessage, Citation, ConnectionStatus } from '../types.js';
14
+
15
+ function cx(...parts: (string | false | undefined)[]): string {
16
+ return parts.filter(Boolean).join(' ');
17
+ }
18
+
19
+ /** A list of grounding sources rendered under an assistant bubble. */
20
+ export function Citations({ citations, className }: { citations: Citation[]; className?: string }) {
21
+ if (!citations.length) return null;
22
+ return (
23
+ <div className={cx('smooth-chat__sources', className)}>
24
+ <details>
25
+ <summary>
26
+ {citations.length} source{citations.length === 1 ? '' : 's'}
27
+ </summary>
28
+ <ol>
29
+ {citations.map((c) => {
30
+ const href = safeHttpUrl(c.url);
31
+ return (
32
+ <li key={c.id || c.title}>
33
+ {href ? (
34
+ <a className="smooth-chat__source-title" href={href} target="_blank" rel="noreferrer noopener">
35
+ {c.title}
36
+ </a>
37
+ ) : (
38
+ <span className="smooth-chat__source-title">{c.title}</span>
39
+ )}
40
+ {c.snippet ? <span className="smooth-chat__source-snippet">{c.snippet}</span> : null}
41
+ </li>
42
+ );
43
+ })}
44
+ </ol>
45
+ </details>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ /** A single message bubble (+ its citations + streaming cursor). */
51
+ export function MessageBubble({ message, className }: { message: ChatMessage; className?: string }) {
52
+ return (
53
+ <>
54
+ <div className={cx('smooth-chat__bubble', `smooth-chat__bubble--${message.role}`, message.streaming && 'smooth-chat__bubble--streaming', className)}>
55
+ {message.text}
56
+ {message.streaming ? <span className="smooth-chat__cursor" aria-hidden="true" /> : null}
57
+ </div>
58
+ {message.role === 'assistant' && message.citations?.length ? <Citations citations={message.citations} /> : null}
59
+ </>
60
+ );
61
+ }
62
+
63
+ /** The scrollable message column. Renders an optional greeting before any messages. */
64
+ export function MessageList({ messages, greeting, className }: { messages: ChatMessage[]; greeting?: string; className?: string }) {
65
+ return (
66
+ <div className={cx('smooth-chat__messages', className)} role="log" aria-live="polite">
67
+ {messages.length === 0 && greeting ? <div className="smooth-chat__bubble smooth-chat__bubble--assistant smooth-chat__greeting">{greeting}</div> : null}
68
+ {messages.map((m) => (
69
+ <MessageBubble key={m.id} message={m} />
70
+ ))}
71
+ </div>
72
+ );
73
+ }
74
+
75
+ /** The input row. Calls `onSend` on submit / Enter (Shift+Enter inserts a newline). */
76
+ export function Composer({
77
+ onSend,
78
+ disabled,
79
+ placeholder = 'Type a message…',
80
+ className,
81
+ }: {
82
+ onSend: (text: string) => void;
83
+ disabled?: boolean;
84
+ placeholder?: string;
85
+ className?: string;
86
+ }) {
87
+ const [value, setValue] = useState('');
88
+
89
+ const submit = () => {
90
+ const text = value.trim();
91
+ if (!text || disabled) return;
92
+ setValue('');
93
+ onSend(text);
94
+ };
95
+
96
+ const onSubmit = (e: FormEvent) => {
97
+ e.preventDefault();
98
+ submit();
99
+ };
100
+
101
+ const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
102
+ if (e.key === 'Enter' && !e.shiftKey) {
103
+ e.preventDefault();
104
+ submit();
105
+ }
106
+ };
107
+
108
+ return (
109
+ <form className={cx('smooth-chat__composer', className)} onSubmit={onSubmit}>
110
+ <textarea
111
+ className="smooth-chat__input"
112
+ value={value}
113
+ placeholder={placeholder}
114
+ onChange={(e) => setValue(e.target.value)}
115
+ onKeyDown={onKeyDown}
116
+ rows={1}
117
+ aria-label="Message"
118
+ />
119
+ <button className="smooth-chat__send" type="submit" disabled={disabled || value.trim().length === 0}>
120
+ Send
121
+ </button>
122
+ </form>
123
+ );
124
+ }
125
+
126
+ const STATUS_LABEL: Record<ConnectionStatus, string> = {
127
+ idle: 'Idle',
128
+ connecting: 'Connecting…',
129
+ ready: 'Online',
130
+ error: 'Connection error',
131
+ closed: 'Disconnected',
132
+ };
133
+
134
+ /** A small connection-status label. */
135
+ export function ConnectionStatusLabel({ status, className }: { status: ConnectionStatus; className?: string }) {
136
+ return (
137
+ <span className={cx('smooth-chat__status', `smooth-chat__status--${status}`, className)} aria-live="polite">
138
+ {STATUS_LABEL[status]}
139
+ </span>
140
+ );
141
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @smooai/smooth-operator/react — React bindings for the smooth-operator
3
+ * WebSocket protocol.
4
+ *
5
+ * Two layers, use whichever you need:
6
+ *
7
+ * • Headless — `useConversation` (+ `SmoothOperatorProvider`) own the whole
8
+ * connect → session → stream lifecycle and expose only state + actions. Build
9
+ * any UI you like on top; this package imposes zero styling.
10
+ *
11
+ * • Batteries-included — `<SmoothChat>` and the `parts` components render a
12
+ * complete, accessible chat surface themed entirely by `--smooth-*` CSS
13
+ * variables. Import the stylesheet once:
14
+ * import '@smooai/smooth-operator/react/styles.css';
15
+ *
16
+ * The protocol client itself lives in `@smooai/smooth-operator`; re-exported here
17
+ * for convenience so you can construct/share a client without a second import.
18
+ */
19
+ export { SmoothAgentClient, MessageTurn, ProtocolError, TurnTimeoutError, type SmoothAgentClientOptions } from '../index.js';
20
+
21
+ export { useConversation, type UseConversationOptions, type UseConversationResult } from './use-conversation.js';
22
+ export { SmoothOperatorProvider, useSmoothOperator, type SmoothOperatorProviderProps, type SmoothOperatorContextValue } from './provider.js';
23
+
24
+ export { themeToStyle, type ChatTheme } from './theme.js';
25
+ export { safeHttpUrl, extractCitations, extractFinalText } from './response.js';
26
+
27
+ export { SmoothChat, type SmoothChatProps } from './components/SmoothChat.js';
28
+ export { MessageList, MessageBubble, Citations, Composer, ConnectionStatusLabel } from './components/parts.js';
29
+
30
+ export type { ChatMessage, ConnectionStatus, Role, Citation } from './types.js';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * SmoothOperatorProvider — optional context that owns a single shared
3
+ * {@link SmoothAgentClient} for a subtree.
4
+ *
5
+ * You don't need this to use the hooks (`useConversation` can take a `url`
6
+ * directly), but a provider is handy when several components share one WS
7
+ * connection, or when you want to construct the client yourself (custom
8
+ * transport, auth-token refresh, etc.) and hand it down.
9
+ */
10
+ import { SmoothAgentClient } from '../client.js';
11
+ import { createContext, createElement, useContext, useMemo, type ReactNode } from 'react';
12
+
13
+ export interface SmoothOperatorContextValue {
14
+ client: SmoothAgentClient | null;
15
+ /** The base WS URL, if the provider was given one (so hooks can construct per-session clients). */
16
+ url: string | null;
17
+ }
18
+
19
+ const SmoothOperatorContext = createContext<SmoothOperatorContextValue | null>(null);
20
+
21
+ export interface SmoothOperatorProviderProps {
22
+ /** A pre-constructed client to share. Takes precedence over `url`. */
23
+ client?: SmoothAgentClient;
24
+ /** A WS URL the provider memoizes a client from, if no `client` is passed. */
25
+ url?: string;
26
+ children: ReactNode;
27
+ }
28
+
29
+ export function SmoothOperatorProvider({ client, url, children }: SmoothOperatorProviderProps) {
30
+ const value = useMemo<SmoothOperatorContextValue>(() => {
31
+ if (client) return { client, url: url ?? null };
32
+ return { client: null, url: url ?? null };
33
+ }, [client, url]);
34
+ return createElement(SmoothOperatorContext.Provider, { value }, children);
35
+ }
36
+
37
+ /** Read the nearest provider value, or `null` if there is no provider. */
38
+ export function useSmoothOperator(): SmoothOperatorContextValue | null {
39
+ return useContext(SmoothOperatorContext);
40
+ }