@opengeni/react 3.8.0-canary.1 → 4.0.1-canary.1
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 +10 -24
- package/dist/artifacts.js +7 -7
- package/dist/{browser-viewer-SS46LUAC.js → browser-viewer-NTD6UIPR.js} +3 -3
- package/dist/{chunk-VEPNNDNG.js → chunk-24M4YBCL.js} +4 -4
- package/dist/{chunk-ELIYWBZR.js → chunk-BV5A4OTE.js} +4 -4
- package/dist/{chunk-7W6PVVTK.js → chunk-CQG7YUJB.js} +2582 -1037
- package/dist/chunk-CQG7YUJB.js.map +1 -0
- package/dist/{chunk-BXJOQFCD.js → chunk-HUMIRRYI.js} +84 -221
- package/dist/chunk-HUMIRRYI.js.map +1 -0
- package/dist/{chunk-7BWFU6H4.js → chunk-Z7WBDB4E.js} +395 -287
- package/dist/chunk-Z7WBDB4E.js.map +1 -0
- package/dist/components/human-input-form.d.ts +3 -1
- package/dist/components/human-input-surface.d.ts +3 -2
- package/dist/components/markdown-table-layout.d.ts +9 -0
- package/dist/components/session-conversation.d.ts +2 -0
- package/dist/composer.js +2 -3
- package/dist/{computer-viewer-FWEZISW4.js → computer-viewer-7TEZC6Y2.js} +3 -3
- package/dist/hooks/use-codex-accounts.d.ts +11 -1
- package/dist/hooks/use-session-events.d.ts +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +44 -34
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +3 -3
- package/dist/markdown-table-layout-3Z3AWBY7.js +71 -0
- package/dist/markdown-table-layout-3Z3AWBY7.js.map +1 -0
- package/dist/older-history.d.ts +3 -2
- package/dist/{platform-activity-row-BJHUHXAT.js → platform-activity-row-27LKAWZD.js} +2 -2
- package/dist/platform-activity-row-27LKAWZD.js.map +1 -0
- package/dist/realtime.js +3 -3
- package/dist/session-ui.js +6 -9
- package/dist/session.js +1 -1
- package/dist/timeline/types.d.ts +3 -0
- package/package.json +2 -6
- package/src/components/human-input-form.tsx +185 -53
- package/src/components/human-input-surface.tsx +3 -0
- package/src/components/markdown-table-layout.ts +85 -0
- package/src/components/markdown.tsx +20 -2
- package/src/components/message-timeline.tsx +47 -7
- package/src/components/session-commands-panel.tsx +7 -1
- package/src/components/session-conversation.tsx +4 -1
- package/src/hooks/use-codex-accounts.ts +32 -5
- package/src/hooks/use-session-events.ts +16 -277
- package/src/index.ts +0 -1
- package/src/older-history.ts +22 -4
- package/src/timeline/platform-activity-row.tsx +4 -1
- package/src/timeline/projection.ts +99 -9
- package/src/timeline/types.ts +3 -0
- package/dist/chat.d.ts +0 -37
- package/dist/chat.js +0 -315
- package/dist/chat.js.map +0 -1
- package/dist/chunk-7BWFU6H4.js.map +0 -1
- package/dist/chunk-7W6PVVTK.js.map +0 -1
- package/dist/chunk-BXJOQFCD.js.map +0 -1
- package/dist/chunk-HQV2I5HV.js +0 -124
- package/dist/chunk-HQV2I5HV.js.map +0 -1
- package/dist/chunk-YAIPFO5D.js +0 -1416
- package/dist/chunk-YAIPFO5D.js.map +0 -1
- package/dist/platform-activity-row-BJHUHXAT.js.map +0 -1
- package/src/chat.tsx +0 -393
- /package/dist/{browser-viewer-SS46LUAC.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
- /package/dist/{chunk-VEPNNDNG.js.map → chunk-24M4YBCL.js.map} +0 -0
- /package/dist/{chunk-ELIYWBZR.js.map → chunk-BV5A4OTE.js.map} +0 -0
- /package/dist/{computer-viewer-FWEZISW4.js.map → computer-viewer-7TEZC6Y2.js.map} +0 -0
package/src/chat.tsx
DELETED
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CHAT_CONVERSATION_HEADER,
|
|
3
|
-
CHAT_FORMAT_HEADER,
|
|
4
|
-
parseChatChunkStream,
|
|
5
|
-
type ChatPending,
|
|
6
|
-
} from "@opengeni/sdk/chat";
|
|
7
|
-
import {
|
|
8
|
-
useCallback,
|
|
9
|
-
useEffect,
|
|
10
|
-
useRef,
|
|
11
|
-
useState,
|
|
12
|
-
type CSSProperties,
|
|
13
|
-
type FormEvent,
|
|
14
|
-
type ReactNode,
|
|
15
|
-
} from "react";
|
|
16
|
-
import { HumanInputForm, type HumanInputFormProps } from "./components/human-input-form";
|
|
17
|
-
import { Markdown } from "./components/markdown";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Chat component that talks only to the host's `createChatHandler` endpoint:
|
|
21
|
-
* it restores the conversation with `GET` on mount, POSTs `{ message }`
|
|
22
|
-
* (native format), renders the streamed reply, shows a card for a pending
|
|
23
|
-
* approval or human-input request, and answers it through
|
|
24
|
-
* `${handlerUrl}/respond`. No provider or OpenGeni credentials in the browser.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
export type OpenGeniChatMessage = {
|
|
28
|
-
id: string;
|
|
29
|
-
role: "user" | "assistant";
|
|
30
|
-
text: string;
|
|
31
|
-
streaming: boolean;
|
|
32
|
-
/** Tool names the agent used while producing this reply. */
|
|
33
|
-
tools: string[];
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export type OpenGeniChatProps = {
|
|
37
|
-
/** The host endpoint served by `createChatHandler`. */
|
|
38
|
-
handlerUrl: string;
|
|
39
|
-
/** Sent as the `x-opengeni-conversation` header so the host's `resolve` can pick it up. */
|
|
40
|
-
conversation?: string | undefined;
|
|
41
|
-
/**
|
|
42
|
-
* Host authentication headers, resolved on each host render. Changed values
|
|
43
|
-
* reset the chat. Re-render the host when a callback's credentials change.
|
|
44
|
-
*/
|
|
45
|
-
headers?: Record<string, string> | (() => Record<string, string>) | undefined;
|
|
46
|
-
/**
|
|
47
|
-
* Stable authenticated user/tenant identity. Change this on sign-in, sign-out
|
|
48
|
-
* or tenant switches when authentication uses cookies or otherwise changes
|
|
49
|
-
* without changing the headers. This resets local state; it is not sent.
|
|
50
|
-
*/
|
|
51
|
-
authKey?: string | undefined;
|
|
52
|
-
placeholder?: string | undefined;
|
|
53
|
-
className?: string | undefined;
|
|
54
|
-
renderMessage?: ((message: OpenGeniChatMessage) => ReactNode) | undefined;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const styles = {
|
|
58
|
-
root: {
|
|
59
|
-
display: "flex",
|
|
60
|
-
flexDirection: "column",
|
|
61
|
-
gap: "0.75rem",
|
|
62
|
-
minHeight: 0,
|
|
63
|
-
color: "var(--og-color-fg)",
|
|
64
|
-
fontFamily: "var(--og-font-sans)",
|
|
65
|
-
fontSize: "var(--og-font-size-base)",
|
|
66
|
-
},
|
|
67
|
-
list: { display: "flex", flexDirection: "column", gap: "0.5rem", overflowY: "auto" },
|
|
68
|
-
user: {
|
|
69
|
-
alignSelf: "flex-end",
|
|
70
|
-
maxWidth: "80%",
|
|
71
|
-
whiteSpace: "pre-wrap",
|
|
72
|
-
padding: "0.5rem 0.75rem",
|
|
73
|
-
borderRadius: "var(--og-radius-lg)",
|
|
74
|
-
background: "var(--og-color-accent-soft)",
|
|
75
|
-
},
|
|
76
|
-
assistant: { alignSelf: "stretch", padding: "0.25rem 0" },
|
|
77
|
-
meta: { color: "var(--og-color-fg-muted)", fontSize: "var(--og-font-size-xs)" },
|
|
78
|
-
card: {
|
|
79
|
-
display: "flex",
|
|
80
|
-
flexDirection: "column",
|
|
81
|
-
gap: "0.5rem",
|
|
82
|
-
padding: "0.75rem",
|
|
83
|
-
border: "1px solid var(--og-color-border)",
|
|
84
|
-
borderRadius: "var(--og-radius-md)",
|
|
85
|
-
background: "var(--og-color-surface-2)",
|
|
86
|
-
},
|
|
87
|
-
row: { display: "flex", gap: "0.5rem", alignItems: "flex-end" },
|
|
88
|
-
input: {
|
|
89
|
-
flex: 1,
|
|
90
|
-
minHeight: "2.5rem",
|
|
91
|
-
padding: "0.5rem 0.75rem",
|
|
92
|
-
border: "1px solid var(--og-color-border)",
|
|
93
|
-
borderRadius: "var(--og-radius-md)",
|
|
94
|
-
background: "var(--og-color-surface-1)",
|
|
95
|
-
color: "inherit",
|
|
96
|
-
font: "inherit",
|
|
97
|
-
resize: "vertical",
|
|
98
|
-
},
|
|
99
|
-
button: {
|
|
100
|
-
padding: "0.5rem 0.9rem",
|
|
101
|
-
border: "1px solid var(--og-color-border-strong)",
|
|
102
|
-
borderRadius: "var(--og-radius-md)",
|
|
103
|
-
background: "var(--og-color-accent)",
|
|
104
|
-
color: "var(--og-color-accent-fg)",
|
|
105
|
-
font: "inherit",
|
|
106
|
-
cursor: "pointer",
|
|
107
|
-
},
|
|
108
|
-
secondary: {
|
|
109
|
-
padding: "0.5rem 0.9rem",
|
|
110
|
-
border: "1px solid var(--og-color-border)",
|
|
111
|
-
borderRadius: "var(--og-radius-md)",
|
|
112
|
-
background: "var(--og-color-surface-1)",
|
|
113
|
-
color: "inherit",
|
|
114
|
-
font: "inherit",
|
|
115
|
-
cursor: "pointer",
|
|
116
|
-
},
|
|
117
|
-
error: { color: "var(--og-color-danger)", fontSize: "var(--og-font-size-sm)" },
|
|
118
|
-
} satisfies Record<string, CSSProperties>;
|
|
119
|
-
|
|
120
|
-
function uid(): string {
|
|
121
|
-
return typeof crypto !== "undefined" && "randomUUID" in crypto
|
|
122
|
-
? crypto.randomUUID()
|
|
123
|
-
: Math.random().toString(36).slice(2);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
type HostHeaders = Record<string, string>;
|
|
127
|
-
|
|
128
|
-
function requestHeaders(conversation: string | undefined, headers: HostHeaders) {
|
|
129
|
-
return {
|
|
130
|
-
[CHAT_FORMAT_HEADER]: "native",
|
|
131
|
-
...(conversation ? { [CHAT_CONVERSATION_HEADER]: conversation } : {}),
|
|
132
|
-
...headers,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Messages from the handler's `GET` history response; anything unexpected is dropped. */
|
|
137
|
-
function restoredMessages(payload: unknown): OpenGeniChatMessage[] {
|
|
138
|
-
const list = (payload as { messages?: unknown } | null)?.messages;
|
|
139
|
-
if (!Array.isArray(list)) return [];
|
|
140
|
-
const restored: OpenGeniChatMessage[] = [];
|
|
141
|
-
for (const entry of list) {
|
|
142
|
-
const record = (entry ?? {}) as { role?: unknown; text?: unknown };
|
|
143
|
-
if ((record.role !== "user" && record.role !== "assistant") || typeof record.text !== "string")
|
|
144
|
-
continue;
|
|
145
|
-
restored.push({ id: uid(), role: record.role, text: record.text, streaming: false, tools: [] });
|
|
146
|
-
}
|
|
147
|
-
return restored;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function OpenGeniChat(props: OpenGeniChatProps) {
|
|
151
|
-
// Normalize actual HTTP values so an inline object/callback or header casing
|
|
152
|
-
// change does not erase a draft. Resolve callbacks once per host render: GET
|
|
153
|
-
// and POST must use the same authentication snapshot as the displayed state.
|
|
154
|
-
const headers = Object.fromEntries(
|
|
155
|
-
[...new Headers(typeof props.headers === "function" ? props.headers() : props.headers)]
|
|
156
|
-
.map(([name, value]) => [name.toLowerCase(), value] as const)
|
|
157
|
-
.sort(([left], [right]) => left.localeCompare(right)),
|
|
158
|
-
);
|
|
159
|
-
const identity = JSON.stringify([
|
|
160
|
-
props.handlerUrl,
|
|
161
|
-
props.conversation ?? null,
|
|
162
|
-
props.authKey ?? null,
|
|
163
|
-
headers,
|
|
164
|
-
]);
|
|
165
|
-
// A keyed boundary drops private state in the same commit, before effects
|
|
166
|
-
// run. Unmount cleanup aborts old history and streams; late completions can
|
|
167
|
-
// only address the unmounted instance, never the new user's conversation.
|
|
168
|
-
return <ChatConversation key={identity} {...props} headers={headers} />;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function ChatConversation({
|
|
172
|
-
handlerUrl,
|
|
173
|
-
conversation,
|
|
174
|
-
headers,
|
|
175
|
-
placeholder = "Message",
|
|
176
|
-
className,
|
|
177
|
-
renderMessage,
|
|
178
|
-
}: Omit<OpenGeniChatProps, "headers"> & { headers: HostHeaders }) {
|
|
179
|
-
const [messages, setMessages] = useState<OpenGeniChatMessage[]>([]);
|
|
180
|
-
const [pendingRequests, setPendingRequests] = useState<ChatPending[]>([]);
|
|
181
|
-
const pending = pendingRequests[0] ?? null;
|
|
182
|
-
const [restoring, setRestoring] = useState(true);
|
|
183
|
-
const [draft, setDraft] = useState("");
|
|
184
|
-
const [busy, setBusy] = useState(false);
|
|
185
|
-
const [error, setError] = useState<string | null>(null);
|
|
186
|
-
const abortRef = useRef<AbortController | null>(null);
|
|
187
|
-
const headersRef = useRef<HostHeaders>(headers);
|
|
188
|
-
headersRef.current = headers;
|
|
189
|
-
|
|
190
|
-
// Restore the conversation on mount and whenever it changes. The composer
|
|
191
|
-
// waits for restoration so a late snapshot cannot resurrect a decided card.
|
|
192
|
-
useEffect(() => {
|
|
193
|
-
const controller = new AbortController();
|
|
194
|
-
abortRef.current?.abort();
|
|
195
|
-
setBusy(false);
|
|
196
|
-
setRestoring(true);
|
|
197
|
-
setError(null);
|
|
198
|
-
setMessages([]);
|
|
199
|
-
setPendingRequests([]);
|
|
200
|
-
void (async () => {
|
|
201
|
-
try {
|
|
202
|
-
const response = await fetch(handlerUrl, {
|
|
203
|
-
method: "GET",
|
|
204
|
-
headers: requestHeaders(conversation, headersRef.current),
|
|
205
|
-
signal: controller.signal,
|
|
206
|
-
});
|
|
207
|
-
if (!response.ok) return;
|
|
208
|
-
const payload = (await response.json()) as { pending?: ChatPending[] };
|
|
209
|
-
if (controller.signal.aborted) return;
|
|
210
|
-
setMessages(restoredMessages(payload));
|
|
211
|
-
setPendingRequests(Array.isArray(payload.pending) ? payload.pending : []);
|
|
212
|
-
} catch {
|
|
213
|
-
// Hosts without history may still accept sends.
|
|
214
|
-
} finally {
|
|
215
|
-
if (!controller.signal.aborted) setRestoring(false);
|
|
216
|
-
}
|
|
217
|
-
})();
|
|
218
|
-
return () => {
|
|
219
|
-
controller.abort();
|
|
220
|
-
abortRef.current?.abort();
|
|
221
|
-
};
|
|
222
|
-
}, [handlerUrl, conversation]);
|
|
223
|
-
|
|
224
|
-
const update = useCallback(
|
|
225
|
-
(id: string, patch: (message: OpenGeniChatMessage) => OpenGeniChatMessage) =>
|
|
226
|
-
setMessages((prev) => prev.map((message) => (message.id === id ? patch(message) : message))),
|
|
227
|
-
[],
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
const consume = useCallback(
|
|
231
|
-
async (url: string, body: unknown, assistantId: string) => {
|
|
232
|
-
abortRef.current?.abort();
|
|
233
|
-
const controller = new AbortController();
|
|
234
|
-
abortRef.current = controller;
|
|
235
|
-
setBusy(true);
|
|
236
|
-
setError(null);
|
|
237
|
-
try {
|
|
238
|
-
const response = await fetch(url, {
|
|
239
|
-
method: "POST",
|
|
240
|
-
headers: { "Content-Type": "application/json", ...requestHeaders(conversation, headers) },
|
|
241
|
-
body: JSON.stringify(body),
|
|
242
|
-
signal: controller.signal,
|
|
243
|
-
});
|
|
244
|
-
if (!response.ok || !response.body) {
|
|
245
|
-
throw new Error(`Chat request failed (${response.status}).`);
|
|
246
|
-
}
|
|
247
|
-
for await (const chunk of parseChatChunkStream(response.body)) {
|
|
248
|
-
if (controller.signal.aborted) return;
|
|
249
|
-
if (chunk.type === "text") {
|
|
250
|
-
update(assistantId, (m) => ({ ...m, text: m.text + chunk.text }));
|
|
251
|
-
} else if (chunk.type === "tool" && chunk.status === "started") {
|
|
252
|
-
update(assistantId, (m) => ({ ...m, tools: [...m.tools, chunk.name] }));
|
|
253
|
-
} else if (chunk.type === "pending") {
|
|
254
|
-
setPendingRequests([chunk.pending]);
|
|
255
|
-
} else if (chunk.type === "done") {
|
|
256
|
-
setPendingRequests(chunk.reply.pending ? [chunk.reply.pending] : []);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
} catch (caught) {
|
|
260
|
-
if (
|
|
261
|
-
!controller.signal.aborted &&
|
|
262
|
-
!(caught instanceof Error && caught.name === "AbortError")
|
|
263
|
-
) {
|
|
264
|
-
setError(caught instanceof Error ? caught.message : "Chat request failed.");
|
|
265
|
-
}
|
|
266
|
-
} finally {
|
|
267
|
-
if (!controller.signal.aborted) {
|
|
268
|
-
update(assistantId, (m) => ({ ...m, streaming: false }));
|
|
269
|
-
setBusy(false);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
[conversation, headers, update],
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
const submit = (event: FormEvent) => {
|
|
277
|
-
event.preventDefault();
|
|
278
|
-
const text = draft.trim();
|
|
279
|
-
if (!text || busy || restoring) return;
|
|
280
|
-
setDraft("");
|
|
281
|
-
const assistantId = uid();
|
|
282
|
-
setMessages((prev) => [
|
|
283
|
-
...prev,
|
|
284
|
-
{ id: uid(), role: "user", text, streaming: false, tools: [] },
|
|
285
|
-
{ id: assistantId, role: "assistant", text: "", streaming: true, tools: [] },
|
|
286
|
-
]);
|
|
287
|
-
void consume(handlerUrl, { message: text }, assistantId);
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
const respond = (input: Record<string, unknown>) => {
|
|
291
|
-
if (!pending || busy || restoring) return;
|
|
292
|
-
const last = [...messages].reverse().find((m) => m.role === "assistant");
|
|
293
|
-
const assistantId = last?.id ?? uid();
|
|
294
|
-
if (last) update(assistantId, (m) => ({ ...m, streaming: true }));
|
|
295
|
-
else
|
|
296
|
-
setMessages((prev) => [
|
|
297
|
-
...prev,
|
|
298
|
-
{ id: assistantId, role: "assistant", text: "", streaming: true, tools: [] },
|
|
299
|
-
]);
|
|
300
|
-
void consume(
|
|
301
|
-
`${handlerUrl.replace(/\/+$/, "")}/respond`,
|
|
302
|
-
{ requestId: pending.requestId, ...input },
|
|
303
|
-
assistantId,
|
|
304
|
-
);
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
return (
|
|
308
|
-
<div className={`og-root og-chat${className ? ` ${className}` : ""}`} style={styles.root}>
|
|
309
|
-
<div className="og-chat-messages" style={styles.list}>
|
|
310
|
-
{messages.map((message) =>
|
|
311
|
-
renderMessage ? (
|
|
312
|
-
<div key={message.id}>{renderMessage(message)}</div>
|
|
313
|
-
) : message.role === "user" ? (
|
|
314
|
-
<div key={message.id} className="og-chat-user" style={styles.user}>
|
|
315
|
-
{message.text}
|
|
316
|
-
</div>
|
|
317
|
-
) : (
|
|
318
|
-
<div key={message.id} className="og-chat-assistant" style={styles.assistant}>
|
|
319
|
-
{message.tools.length > 0 ? (
|
|
320
|
-
<div style={styles.meta}>Used {message.tools.join(", ")}</div>
|
|
321
|
-
) : null}
|
|
322
|
-
{message.text ? (
|
|
323
|
-
<Markdown streaming={message.streaming}>{message.text}</Markdown>
|
|
324
|
-
) : message.streaming ? (
|
|
325
|
-
<div style={styles.meta}>Thinking</div>
|
|
326
|
-
) : null}
|
|
327
|
-
</div>
|
|
328
|
-
),
|
|
329
|
-
)}
|
|
330
|
-
</div>
|
|
331
|
-
{pending ? (
|
|
332
|
-
<div className="og-chat-pending" style={styles.card}>
|
|
333
|
-
{pending.kind === "approval" ? (
|
|
334
|
-
<>
|
|
335
|
-
<div>The agent wants to run {pending.name ?? "a tool"}.</div>
|
|
336
|
-
<div style={styles.row}>
|
|
337
|
-
<button
|
|
338
|
-
type="button"
|
|
339
|
-
style={styles.button}
|
|
340
|
-
onClick={() => respond({ decision: "approve" })}
|
|
341
|
-
>
|
|
342
|
-
Approve
|
|
343
|
-
</button>
|
|
344
|
-
<button
|
|
345
|
-
type="button"
|
|
346
|
-
style={styles.secondary}
|
|
347
|
-
onClick={() => respond({ decision: "reject" })}
|
|
348
|
-
>
|
|
349
|
-
Reject
|
|
350
|
-
</button>
|
|
351
|
-
</div>
|
|
352
|
-
</>
|
|
353
|
-
) : (
|
|
354
|
-
<HumanInputForm
|
|
355
|
-
request={pending.payload as HumanInputFormProps["request"]}
|
|
356
|
-
submitting={busy || restoring}
|
|
357
|
-
error={error}
|
|
358
|
-
onSubmit={(response) =>
|
|
359
|
-
respond(
|
|
360
|
-
response.outcome === "answered" ? { answers: response.answers } : { skip: true },
|
|
361
|
-
)
|
|
362
|
-
}
|
|
363
|
-
/>
|
|
364
|
-
)}
|
|
365
|
-
</div>
|
|
366
|
-
) : null}
|
|
367
|
-
{error ? (
|
|
368
|
-
<div className="og-chat-error" style={styles.error}>
|
|
369
|
-
{error}
|
|
370
|
-
</div>
|
|
371
|
-
) : null}
|
|
372
|
-
<form className="og-chat-composer" style={styles.row} onSubmit={submit}>
|
|
373
|
-
<textarea
|
|
374
|
-
style={styles.input}
|
|
375
|
-
value={draft}
|
|
376
|
-
placeholder={placeholder}
|
|
377
|
-
rows={1}
|
|
378
|
-
onChange={(e) => setDraft(e.target.value)}
|
|
379
|
-
onInput={(e) => setDraft(e.currentTarget.value)}
|
|
380
|
-
onKeyDown={(e) => {
|
|
381
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
382
|
-
e.preventDefault();
|
|
383
|
-
e.currentTarget.form?.requestSubmit();
|
|
384
|
-
}
|
|
385
|
-
}}
|
|
386
|
-
/>
|
|
387
|
-
<button type="submit" style={styles.button} disabled={busy || restoring || !draft.trim()}>
|
|
388
|
-
Send
|
|
389
|
-
</button>
|
|
390
|
-
</form>
|
|
391
|
-
</div>
|
|
392
|
-
);
|
|
393
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|