dopus 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 +286 -2
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +977 -0
- package/dist/index.d.ts +154 -0
- package/dist/index.js +278 -0
- package/dist/version-DOEBWDfi.js +31 -0
- package/package.json +75 -4
- package/templates/prompts/app.ts +18 -0
- package/templates/prompts/presets/customer-support.ts +13 -0
- package/templates/prompts/presets/general.ts +12 -0
- package/templates/prompts/presets/internal-ops.ts +13 -0
- package/templates/ui/agent-chat.module.css +255 -0
- package/templates/ui/agent-chat.tsx +294 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/* Styles for agent-chat.tsx. Copied by `dopus init`; yours to edit. Theme with the --dopus-* variables. */
|
|
2
|
+
.chat {
|
|
3
|
+
--dopus-bg: #ffffff;
|
|
4
|
+
--dopus-fg: #18181b;
|
|
5
|
+
--dopus-muted: #71717a;
|
|
6
|
+
--dopus-border: #e4e4e7;
|
|
7
|
+
--dopus-surface: #f4f4f5;
|
|
8
|
+
--dopus-accent: #18181b;
|
|
9
|
+
--dopus-accent-fg: #ffffff;
|
|
10
|
+
--dopus-danger: #b91c1c;
|
|
11
|
+
--dopus-radius: 12px;
|
|
12
|
+
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
height: 100%;
|
|
16
|
+
min-height: 320px;
|
|
17
|
+
background: var(--dopus-bg);
|
|
18
|
+
color: var(--dopus-fg);
|
|
19
|
+
border: 1px solid var(--dopus-border);
|
|
20
|
+
border-radius: var(--dopus-radius);
|
|
21
|
+
font:
|
|
22
|
+
14px / 1.5 system-ui,
|
|
23
|
+
-apple-system,
|
|
24
|
+
"Segoe UI",
|
|
25
|
+
sans-serif;
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@media (prefers-color-scheme: dark) {
|
|
30
|
+
.chat {
|
|
31
|
+
--dopus-bg: #18181b;
|
|
32
|
+
--dopus-fg: #f4f4f5;
|
|
33
|
+
--dopus-muted: #a1a1aa;
|
|
34
|
+
--dopus-border: #3f3f46;
|
|
35
|
+
--dopus-surface: #27272a;
|
|
36
|
+
--dopus-accent: #f4f4f5;
|
|
37
|
+
--dopus-accent-fg: #18181b;
|
|
38
|
+
--dopus-danger: #f87171;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.header {
|
|
43
|
+
padding: 12px 16px;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
border-bottom: 1px solid var(--dopus-border);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.messages {
|
|
49
|
+
flex: 1;
|
|
50
|
+
overflow-y: auto;
|
|
51
|
+
padding: 16px;
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
gap: 12px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.message {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
gap: 6px;
|
|
61
|
+
max-width: 85%;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.message[data-role="user"] {
|
|
65
|
+
align-self: flex-end;
|
|
66
|
+
align-items: flex-end;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.text {
|
|
70
|
+
white-space: pre-wrap;
|
|
71
|
+
overflow-wrap: anywhere;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.message[data-role="user"] .text {
|
|
75
|
+
background: var(--dopus-accent);
|
|
76
|
+
color: var(--dopus-accent-fg);
|
|
77
|
+
padding: 8px 12px;
|
|
78
|
+
border-radius: var(--dopus-radius);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.tool {
|
|
82
|
+
font-size: 13px;
|
|
83
|
+
color: var(--dopus-muted);
|
|
84
|
+
background: var(--dopus-surface);
|
|
85
|
+
border-radius: 8px;
|
|
86
|
+
padding: 6px 10px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.tool summary {
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.tool[data-state="error"] {
|
|
94
|
+
color: var(--dopus-danger);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.approval {
|
|
98
|
+
margin: 0;
|
|
99
|
+
min-inline-size: 0;
|
|
100
|
+
border: 1px solid var(--dopus-border);
|
|
101
|
+
border-radius: var(--dopus-radius);
|
|
102
|
+
padding: 12px;
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
gap: 8px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.approvalTitle {
|
|
109
|
+
float: left;
|
|
110
|
+
width: 100%;
|
|
111
|
+
padding: 0;
|
|
112
|
+
font-weight: 600;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.approvalActions {
|
|
116
|
+
display: flex;
|
|
117
|
+
gap: 8px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.json {
|
|
121
|
+
margin: 0;
|
|
122
|
+
font:
|
|
123
|
+
12px / 1.4 ui-monospace,
|
|
124
|
+
SFMono-Regular,
|
|
125
|
+
Menlo,
|
|
126
|
+
monospace;
|
|
127
|
+
white-space: pre-wrap;
|
|
128
|
+
overflow-wrap: anywhere;
|
|
129
|
+
max-height: 200px;
|
|
130
|
+
overflow: auto;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.thinking {
|
|
134
|
+
color: var(--dopus-muted);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.error {
|
|
138
|
+
color: var(--dopus-danger);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.suggestions {
|
|
142
|
+
display: flex;
|
|
143
|
+
flex-wrap: wrap;
|
|
144
|
+
gap: 8px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.suggestion {
|
|
148
|
+
border: 1px solid var(--dopus-border);
|
|
149
|
+
background: transparent;
|
|
150
|
+
color: inherit;
|
|
151
|
+
border-radius: 999px;
|
|
152
|
+
padding: 6px 12px;
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
font: inherit;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.form {
|
|
158
|
+
display: flex;
|
|
159
|
+
gap: 8px;
|
|
160
|
+
padding: 12px;
|
|
161
|
+
border-top: 1px solid var(--dopus-border);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.input {
|
|
165
|
+
flex: 1;
|
|
166
|
+
resize: none;
|
|
167
|
+
border: 1px solid var(--dopus-border);
|
|
168
|
+
border-radius: 8px;
|
|
169
|
+
padding: 8px 10px;
|
|
170
|
+
font: inherit;
|
|
171
|
+
background: var(--dopus-bg);
|
|
172
|
+
color: inherit;
|
|
173
|
+
max-height: 160px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.primary,
|
|
177
|
+
.secondary {
|
|
178
|
+
border-radius: 8px;
|
|
179
|
+
padding: 8px 14px;
|
|
180
|
+
font: inherit;
|
|
181
|
+
cursor: pointer;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.primary {
|
|
185
|
+
background: var(--dopus-accent);
|
|
186
|
+
color: var(--dopus-accent-fg);
|
|
187
|
+
border: 1px solid var(--dopus-accent);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.primary:disabled {
|
|
191
|
+
opacity: 0.4;
|
|
192
|
+
cursor: default;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.secondary {
|
|
196
|
+
background: transparent;
|
|
197
|
+
color: inherit;
|
|
198
|
+
border: 1px solid var(--dopus-border);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.link {
|
|
202
|
+
background: none;
|
|
203
|
+
border: 0;
|
|
204
|
+
padding: 0;
|
|
205
|
+
color: inherit;
|
|
206
|
+
text-decoration: underline;
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
font: inherit;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.spinner {
|
|
212
|
+
display: inline-block;
|
|
213
|
+
width: 10px;
|
|
214
|
+
height: 10px;
|
|
215
|
+
border: 2px solid currentColor;
|
|
216
|
+
border-right-color: transparent;
|
|
217
|
+
border-radius: 50%;
|
|
218
|
+
animation: dopus-spin 0.8s linear infinite;
|
|
219
|
+
vertical-align: -1px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@keyframes dopus-spin {
|
|
223
|
+
to {
|
|
224
|
+
transform: rotate(360deg);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.widget {
|
|
229
|
+
position: fixed;
|
|
230
|
+
right: 20px;
|
|
231
|
+
bottom: 20px;
|
|
232
|
+
z-index: 50;
|
|
233
|
+
display: flex;
|
|
234
|
+
flex-direction: column;
|
|
235
|
+
align-items: flex-end;
|
|
236
|
+
gap: 12px;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.panel {
|
|
240
|
+
width: min(400px, calc(100vw - 40px));
|
|
241
|
+
height: min(600px, calc(100vh - 120px));
|
|
242
|
+
box-shadow: 0 12px 40px rgb(0 0 0 / 0.18);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.launcher {
|
|
246
|
+
width: 52px;
|
|
247
|
+
height: 52px;
|
|
248
|
+
border-radius: 50%;
|
|
249
|
+
border: 0;
|
|
250
|
+
background: #18181b;
|
|
251
|
+
color: #ffffff;
|
|
252
|
+
font-size: 22px;
|
|
253
|
+
cursor: pointer;
|
|
254
|
+
box-shadow: 0 6px 20px rgb(0 0 0 / 0.2);
|
|
255
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chat UI for the dopus agent. Copied by `dopus init`; this file is yours to edit.
|
|
5
|
+
* Talks to POST /api/agent with the AI SDK `useChat` protocol. Styles: ./agent-chat.module.css
|
|
6
|
+
*/
|
|
7
|
+
import { useChat } from '@ai-sdk/react'
|
|
8
|
+
import {
|
|
9
|
+
DefaultChatTransport,
|
|
10
|
+
getToolName,
|
|
11
|
+
isToolUIPart,
|
|
12
|
+
lastAssistantMessageIsCompleteWithApprovalResponses,
|
|
13
|
+
type UIMessage,
|
|
14
|
+
} from 'ai'
|
|
15
|
+
import { type FormEvent, type KeyboardEvent, useEffect, useRef, useState } from 'react'
|
|
16
|
+
import styles from './agent-chat.module.css'
|
|
17
|
+
|
|
18
|
+
const DEFAULT_LABELS = {
|
|
19
|
+
title: 'Assistant',
|
|
20
|
+
placeholder: 'Ask anything…',
|
|
21
|
+
send: 'Send',
|
|
22
|
+
stop: 'Stop',
|
|
23
|
+
retry: 'Retry',
|
|
24
|
+
error: 'Something went wrong.',
|
|
25
|
+
thinking: 'Thinking…',
|
|
26
|
+
running: 'Running',
|
|
27
|
+
needsApproval: 'Needs your approval',
|
|
28
|
+
approve: 'Approve',
|
|
29
|
+
decline: 'Decline',
|
|
30
|
+
approved: 'Approved',
|
|
31
|
+
declined: 'Declined',
|
|
32
|
+
failed: 'Failed',
|
|
33
|
+
details: 'Details',
|
|
34
|
+
open: 'Open assistant',
|
|
35
|
+
close: 'Close assistant',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type AgentChatLabels = typeof DEFAULT_LABELS
|
|
39
|
+
|
|
40
|
+
export interface AgentChatProps {
|
|
41
|
+
/** Route created by `dopus init`. */
|
|
42
|
+
api?: string
|
|
43
|
+
labels?: Partial<AgentChatLabels>
|
|
44
|
+
/** Example prompts shown before the first message. */
|
|
45
|
+
suggestions?: string[]
|
|
46
|
+
className?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type ToolPart = Extract<UIMessage['parts'][number], { toolCallId: string }>
|
|
50
|
+
|
|
51
|
+
/** `cancelOrder` → `Cancel order` */
|
|
52
|
+
function humanize(name: string) {
|
|
53
|
+
const words = name
|
|
54
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
55
|
+
.replace(/[_-]+/g, ' ')
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
return words.charAt(0).toUpperCase() + words.slice(1)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Json({ value }: { value: unknown }) {
|
|
61
|
+
return <pre className={styles.json}>{JSON.stringify(value, null, 2)}</pre>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ToolCall({
|
|
65
|
+
part,
|
|
66
|
+
labels,
|
|
67
|
+
onRespond,
|
|
68
|
+
}: {
|
|
69
|
+
part: ToolPart
|
|
70
|
+
labels: AgentChatLabels
|
|
71
|
+
onRespond: (approvalId: string, approved: boolean) => void
|
|
72
|
+
}) {
|
|
73
|
+
const name = humanize(getToolName(part))
|
|
74
|
+
|
|
75
|
+
switch (part.state) {
|
|
76
|
+
case 'input-streaming':
|
|
77
|
+
case 'input-available':
|
|
78
|
+
return (
|
|
79
|
+
<div className={styles.tool} data-state="running">
|
|
80
|
+
<span className={styles.spinner} aria-hidden /> {labels.running}: {name}
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
case 'approval-requested':
|
|
84
|
+
return (
|
|
85
|
+
<fieldset className={styles.approval}>
|
|
86
|
+
<legend className={styles.approvalTitle}>
|
|
87
|
+
{name} · {labels.needsApproval}
|
|
88
|
+
</legend>
|
|
89
|
+
<Json value={part.input} />
|
|
90
|
+
<div className={styles.approvalActions}>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
className={styles.primary}
|
|
94
|
+
onClick={() => onRespond(part.approval.id, true)}
|
|
95
|
+
>
|
|
96
|
+
{labels.approve}
|
|
97
|
+
</button>
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
className={styles.secondary}
|
|
101
|
+
onClick={() => onRespond(part.approval.id, false)}
|
|
102
|
+
>
|
|
103
|
+
{labels.decline}
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
</fieldset>
|
|
107
|
+
)
|
|
108
|
+
case 'approval-responded':
|
|
109
|
+
return (
|
|
110
|
+
<div className={styles.tool} data-state={part.approval.approved ? 'running' : 'declined'}>
|
|
111
|
+
{part.approval.approved ? `${labels.approved}: ${name}` : `${labels.declined}: ${name}`}
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
case 'output-available':
|
|
115
|
+
return (
|
|
116
|
+
<details className={styles.tool} data-state="done">
|
|
117
|
+
<summary>✓ {name}</summary>
|
|
118
|
+
<Json value={part.output} />
|
|
119
|
+
</details>
|
|
120
|
+
)
|
|
121
|
+
case 'output-error':
|
|
122
|
+
return (
|
|
123
|
+
<div className={styles.tool} data-state="error">
|
|
124
|
+
✗ {name} · {labels.failed}
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
case 'output-denied':
|
|
128
|
+
return (
|
|
129
|
+
<div className={styles.tool} data-state="declined">
|
|
130
|
+
{labels.declined}: {name}
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
default:
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function Message({
|
|
139
|
+
message,
|
|
140
|
+
labels,
|
|
141
|
+
onRespond,
|
|
142
|
+
}: {
|
|
143
|
+
message: UIMessage
|
|
144
|
+
labels: AgentChatLabels
|
|
145
|
+
onRespond: (approvalId: string, approved: boolean) => void
|
|
146
|
+
}) {
|
|
147
|
+
return (
|
|
148
|
+
<div className={styles.message} data-role={message.role}>
|
|
149
|
+
{message.parts.map((part, index) => {
|
|
150
|
+
const key = `${message.id}-${index}`
|
|
151
|
+
if (part.type === 'text') {
|
|
152
|
+
return part.text ? (
|
|
153
|
+
<div key={key} className={styles.text}>
|
|
154
|
+
{part.text}
|
|
155
|
+
</div>
|
|
156
|
+
) : null
|
|
157
|
+
}
|
|
158
|
+
if (isToolUIPart(part)) {
|
|
159
|
+
return <ToolCall key={key} part={part} labels={labels} onRespond={onRespond} />
|
|
160
|
+
}
|
|
161
|
+
return null
|
|
162
|
+
})}
|
|
163
|
+
</div>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function AgentChat({
|
|
168
|
+
api = '/api/agent',
|
|
169
|
+
labels: labelOverrides,
|
|
170
|
+
suggestions = [],
|
|
171
|
+
className,
|
|
172
|
+
}: AgentChatProps) {
|
|
173
|
+
const labels = { ...DEFAULT_LABELS, ...labelOverrides }
|
|
174
|
+
const [transport] = useState(
|
|
175
|
+
() =>
|
|
176
|
+
new DefaultChatTransport({
|
|
177
|
+
api,
|
|
178
|
+
// Untrusted hints for the prompt; the server only accepts `locale` and `page`.
|
|
179
|
+
body: () => ({ context: { page: window.location.pathname, locale: navigator.language } }),
|
|
180
|
+
}),
|
|
181
|
+
)
|
|
182
|
+
const { messages, sendMessage, status, stop, error, regenerate, addToolApprovalResponse } =
|
|
183
|
+
useChat({
|
|
184
|
+
transport,
|
|
185
|
+
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
|
|
186
|
+
})
|
|
187
|
+
const [input, setInput] = useState('')
|
|
188
|
+
const listRef = useRef<HTMLDivElement>(null)
|
|
189
|
+
const busy = status === 'submitted' || status === 'streaming'
|
|
190
|
+
|
|
191
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: scroll whenever messages change
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: 'smooth' })
|
|
194
|
+
}, [messages])
|
|
195
|
+
|
|
196
|
+
function send(text: string) {
|
|
197
|
+
const trimmed = text.trim()
|
|
198
|
+
if (!trimmed || busy) return
|
|
199
|
+
sendMessage({ text: trimmed })
|
|
200
|
+
setInput('')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function onSubmit(event: FormEvent) {
|
|
204
|
+
event.preventDefault()
|
|
205
|
+
send(input)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function onKeyDown(event: KeyboardEvent<HTMLTextAreaElement>) {
|
|
209
|
+
// Enter sends, Shift+Enter adds a line. Ignore Enter while an IME (Korean, Japanese…) is composing.
|
|
210
|
+
if (event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing) {
|
|
211
|
+
event.preventDefault()
|
|
212
|
+
send(input)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const respond = (id: string, approved: boolean) => addToolApprovalResponse({ id, approved })
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<section
|
|
220
|
+
className={[styles.chat, className].filter(Boolean).join(' ')}
|
|
221
|
+
aria-label={labels.title}
|
|
222
|
+
>
|
|
223
|
+
<header className={styles.header}>{labels.title}</header>
|
|
224
|
+
<div ref={listRef} className={styles.messages} aria-live="polite">
|
|
225
|
+
{messages.length === 0 && suggestions.length > 0 && (
|
|
226
|
+
<div className={styles.suggestions}>
|
|
227
|
+
{suggestions.map((text) => (
|
|
228
|
+
<button
|
|
229
|
+
key={text}
|
|
230
|
+
type="button"
|
|
231
|
+
className={styles.suggestion}
|
|
232
|
+
onClick={() => send(text)}
|
|
233
|
+
>
|
|
234
|
+
{text}
|
|
235
|
+
</button>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
{messages.map((message) => (
|
|
240
|
+
<Message key={message.id} message={message} labels={labels} onRespond={respond} />
|
|
241
|
+
))}
|
|
242
|
+
{status === 'submitted' && <div className={styles.thinking}>{labels.thinking}</div>}
|
|
243
|
+
{error && (
|
|
244
|
+
<div className={styles.error} role="alert">
|
|
245
|
+
{labels.error}{' '}
|
|
246
|
+
<button type="button" className={styles.link} onClick={() => regenerate()}>
|
|
247
|
+
{labels.retry}
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
<form className={styles.form} onSubmit={onSubmit}>
|
|
253
|
+
<textarea
|
|
254
|
+
className={styles.input}
|
|
255
|
+
value={input}
|
|
256
|
+
onChange={(event) => setInput(event.target.value)}
|
|
257
|
+
onKeyDown={onKeyDown}
|
|
258
|
+
placeholder={labels.placeholder}
|
|
259
|
+
rows={1}
|
|
260
|
+
aria-label={labels.placeholder}
|
|
261
|
+
/>
|
|
262
|
+
{busy ? (
|
|
263
|
+
<button type="button" className={styles.secondary} onClick={() => stop()}>
|
|
264
|
+
{labels.stop}
|
|
265
|
+
</button>
|
|
266
|
+
) : (
|
|
267
|
+
<button type="submit" className={styles.primary} disabled={!input.trim()}>
|
|
268
|
+
{labels.send}
|
|
269
|
+
</button>
|
|
270
|
+
)}
|
|
271
|
+
</form>
|
|
272
|
+
</section>
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Floating button that opens the chat in a panel. Put it once in your root layout. */
|
|
277
|
+
export function AgentWidget(props: AgentChatProps) {
|
|
278
|
+
const [open, setOpen] = useState(false)
|
|
279
|
+
const labels = { ...DEFAULT_LABELS, ...props.labels }
|
|
280
|
+
return (
|
|
281
|
+
<div className={styles.widget}>
|
|
282
|
+
{open && <AgentChat {...props} className={styles.panel} />}
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
className={styles.launcher}
|
|
286
|
+
aria-expanded={open}
|
|
287
|
+
aria-label={open ? labels.close : labels.open}
|
|
288
|
+
onClick={() => setOpen((value) => !value)}
|
|
289
|
+
>
|
|
290
|
+
{open ? '×' : '✦'}
|
|
291
|
+
</button>
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
}
|