cencori 1.3.1 → 1.4.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 +25 -0
- package/dist/documents/index.d.mts +94 -0
- package/dist/documents/index.d.ts +94 -0
- package/dist/documents/index.js +88 -0
- package/dist/documents/index.js.map +1 -0
- package/dist/documents/index.mjs +63 -0
- package/dist/documents/index.mjs.map +1 -0
- package/dist/index.d.mts +171 -1
- package/dist/index.d.ts +171 -1
- package/dist/index.js +404 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +401 -0
- package/dist/index.mjs.map +1 -1
- package/dist/memory/index.d.mts +160 -1
- package/dist/memory/index.d.ts +160 -1
- package/dist/memory/index.js +121 -0
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/index.mjs +121 -0
- package/dist/memory/index.mjs.map +1 -1
- package/dist/react/index.d.mts +250 -1
- package/dist/react/index.d.ts +250 -1
- package/dist/react/index.js +729 -51
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +729 -50
- package/dist/react/index.mjs.map +1 -1
- package/dist/vision/index.d.mts +4 -1
- package/dist/vision/index.d.ts +4 -1
- package/dist/vision/index.js +11 -0
- package/dist/vision/index.js.map +1 -1
- package/dist/vision/index.mjs +11 -0
- package/dist/vision/index.mjs.map +1 -1
- package/dist/voice/index.d.mts +130 -0
- package/dist/voice/index.d.ts +130 -0
- package/dist/voice/index.js +141 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/index.mjs +116 -0
- package/dist/voice/index.mjs.map +1 -0
- package/package.json +11 -1
package/dist/react/index.js
CHANGED
|
@@ -20,14 +20,352 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/react/index.ts
|
|
21
21
|
var react_exports = {};
|
|
22
22
|
__export(react_exports, {
|
|
23
|
+
Chat: () => Chat,
|
|
24
|
+
SpeakButton: () => SpeakButton,
|
|
23
25
|
VisionFormatBanner: () => VisionFormatBanner,
|
|
24
|
-
VisionUploader: () => VisionUploader
|
|
26
|
+
VisionUploader: () => VisionUploader,
|
|
27
|
+
VoiceRecorder: () => VoiceRecorder,
|
|
28
|
+
useMemory: () => useMemory,
|
|
29
|
+
useVoiceRecorder: () => useVoiceRecorder
|
|
25
30
|
});
|
|
26
31
|
module.exports = __toCommonJS(react_exports);
|
|
27
32
|
|
|
28
|
-
// src/react/
|
|
33
|
+
// src/react/chat/chat.tsx
|
|
34
|
+
var import_react = require("react");
|
|
29
35
|
var import_lucide_react = require("lucide-react");
|
|
30
36
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
37
|
+
var DEFAULT_BASE_URL = "https://cencori.com";
|
|
38
|
+
function Chat({
|
|
39
|
+
model,
|
|
40
|
+
apiKey,
|
|
41
|
+
memory,
|
|
42
|
+
system,
|
|
43
|
+
initialMessages,
|
|
44
|
+
temperature,
|
|
45
|
+
maxTokens,
|
|
46
|
+
stream = true,
|
|
47
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
48
|
+
placeholder = "Send a message\u2026",
|
|
49
|
+
onMemoryRetrieved,
|
|
50
|
+
onError,
|
|
51
|
+
emptyState,
|
|
52
|
+
className,
|
|
53
|
+
hideMemoryIndicator = false
|
|
54
|
+
}) {
|
|
55
|
+
const [messages, setMessages] = (0, import_react.useState)(initialMessages ?? []);
|
|
56
|
+
const [input, setInput] = (0, import_react.useState)("");
|
|
57
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
58
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
59
|
+
const [recalled, setRecalled] = (0, import_react.useState)(null);
|
|
60
|
+
const scrollRef = (0, import_react.useRef)(null);
|
|
61
|
+
const endpoint = (0, import_react.useMemo)(
|
|
62
|
+
() => `${baseUrl.replace(/\/$/, "")}/api/v1/chat/completions`,
|
|
63
|
+
[baseUrl]
|
|
64
|
+
);
|
|
65
|
+
(0, import_react.useEffect)(() => {
|
|
66
|
+
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" });
|
|
67
|
+
}, [messages, busy]);
|
|
68
|
+
const send = (0, import_react.useCallback)(
|
|
69
|
+
async (text) => {
|
|
70
|
+
const trimmed = text.trim();
|
|
71
|
+
if (!trimmed || busy) return;
|
|
72
|
+
setError(null);
|
|
73
|
+
setBusy(true);
|
|
74
|
+
const userMsg = { role: "user", content: trimmed };
|
|
75
|
+
const history = [...messages, userMsg];
|
|
76
|
+
setMessages(history);
|
|
77
|
+
setInput("");
|
|
78
|
+
const outbound = system ? [{ role: "system", content: system }, ...history] : history;
|
|
79
|
+
const body = {
|
|
80
|
+
model,
|
|
81
|
+
messages: outbound,
|
|
82
|
+
memory,
|
|
83
|
+
temperature,
|
|
84
|
+
max_tokens: maxTokens,
|
|
85
|
+
stream
|
|
86
|
+
};
|
|
87
|
+
const headers = { "Content-Type": "application/json" };
|
|
88
|
+
if (apiKey) headers["CENCORI_API_KEY"] = apiKey;
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch(endpoint, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers,
|
|
93
|
+
body: JSON.stringify(body)
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok || stream && !res.body) {
|
|
96
|
+
const data = await res.json().catch(() => ({}));
|
|
97
|
+
const msg = typeof data.error === "object" ? data.error?.message : data.error;
|
|
98
|
+
throw new Error(msg || `Cencori API error (${res.status})`);
|
|
99
|
+
}
|
|
100
|
+
if (memory && !hideMemoryIndicator) {
|
|
101
|
+
const count = Number(res.headers.get("X-Cencori-Memory-Retrieved") || 0);
|
|
102
|
+
setRecalled(count);
|
|
103
|
+
onMemoryRetrieved?.(count);
|
|
104
|
+
}
|
|
105
|
+
if (stream && res.body) {
|
|
106
|
+
setMessages((prev) => [...prev, { role: "assistant", content: "" }]);
|
|
107
|
+
const reader = res.body.getReader();
|
|
108
|
+
const decoder = new TextDecoder();
|
|
109
|
+
let buffer = "";
|
|
110
|
+
for (; ; ) {
|
|
111
|
+
const { done, value } = await reader.read();
|
|
112
|
+
if (done) break;
|
|
113
|
+
buffer += decoder.decode(value, { stream: true });
|
|
114
|
+
const lines = buffer.split("\n");
|
|
115
|
+
buffer = lines.pop() ?? "";
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
const t = line.trim();
|
|
118
|
+
if (!t.startsWith("data: ")) continue;
|
|
119
|
+
const payload = t.slice(6);
|
|
120
|
+
if (payload === "[DONE]") break;
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(payload);
|
|
123
|
+
const delta = parsed.choices?.[0]?.delta?.content ?? "";
|
|
124
|
+
if (!delta) continue;
|
|
125
|
+
setMessages((prev) => {
|
|
126
|
+
const next = [...prev];
|
|
127
|
+
const last = next[next.length - 1];
|
|
128
|
+
if (last?.role === "assistant") {
|
|
129
|
+
next[next.length - 1] = {
|
|
130
|
+
...last,
|
|
131
|
+
content: last.content + delta
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return next;
|
|
135
|
+
});
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
const data = await res.json();
|
|
142
|
+
const reply = data.choices?.[0]?.message?.content ?? "";
|
|
143
|
+
setMessages((prev) => [...prev, { role: "assistant", content: reply }]);
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
147
|
+
setError(e.message);
|
|
148
|
+
onError?.(e);
|
|
149
|
+
setMessages(
|
|
150
|
+
(prev) => prev.filter((m, i) => !(i === prev.length - 1 && m.role === "user" && m.content === trimmed))
|
|
151
|
+
);
|
|
152
|
+
setInput(trimmed);
|
|
153
|
+
} finally {
|
|
154
|
+
setBusy(false);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
[
|
|
158
|
+
apiKey,
|
|
159
|
+
busy,
|
|
160
|
+
endpoint,
|
|
161
|
+
hideMemoryIndicator,
|
|
162
|
+
maxTokens,
|
|
163
|
+
memory,
|
|
164
|
+
messages,
|
|
165
|
+
model,
|
|
166
|
+
onError,
|
|
167
|
+
onMemoryRetrieved,
|
|
168
|
+
stream,
|
|
169
|
+
system,
|
|
170
|
+
temperature
|
|
171
|
+
]
|
|
172
|
+
);
|
|
173
|
+
const onSubmit = (e) => {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
void send(input);
|
|
176
|
+
};
|
|
177
|
+
const visible = messages.filter((m) => m.role !== "system");
|
|
178
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
179
|
+
"div",
|
|
180
|
+
{
|
|
181
|
+
className: `flex flex-col h-full min-h-[24rem] rounded-xl border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-950 ${className ?? ""}`,
|
|
182
|
+
children: [
|
|
183
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { ref: scrollRef, className: "flex-1 overflow-y-auto p-4 space-y-3", children: [
|
|
184
|
+
visible.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-full flex items-center justify-center text-sm text-neutral-400", children: emptyState ?? "Start the conversation." }),
|
|
185
|
+
visible.map((m, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
186
|
+
"div",
|
|
187
|
+
{
|
|
188
|
+
className: `flex ${m.role === "user" ? "justify-end" : "justify-start"}`,
|
|
189
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
190
|
+
"div",
|
|
191
|
+
{
|
|
192
|
+
className: `max-w-[85%] whitespace-pre-wrap rounded-2xl px-3.5 py-2 text-sm ${m.role === "user" ? "bg-neutral-900 text-white dark:bg-white dark:text-neutral-900" : "bg-neutral-100 text-neutral-900 dark:bg-neutral-900 dark:text-neutral-100"}`,
|
|
193
|
+
children: m.content || (busy && i === visible.length - 1 ? "\u2026" : "")
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
},
|
|
197
|
+
i
|
|
198
|
+
))
|
|
199
|
+
] }),
|
|
200
|
+
memory && !hideMemoryIndicator && recalled !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-1.5 px-4 py-1.5 text-xs text-neutral-500 border-t border-neutral-100 dark:border-neutral-900", children: [
|
|
201
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Brain, { className: "h-3.5 w-3.5" }),
|
|
202
|
+
recalled > 0 ? `Recalled ${recalled} ${recalled === 1 ? "memory" : "memories"} about this user` : "No prior memories yet \u2014 learning"
|
|
203
|
+
] }),
|
|
204
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "px-4 py-2 text-xs text-red-600 dark:text-red-400", children: error }),
|
|
205
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
206
|
+
"form",
|
|
207
|
+
{
|
|
208
|
+
onSubmit,
|
|
209
|
+
className: "flex items-center gap-2 border-t border-neutral-200 p-3 dark:border-neutral-800",
|
|
210
|
+
children: [
|
|
211
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
212
|
+
"input",
|
|
213
|
+
{
|
|
214
|
+
value: input,
|
|
215
|
+
onChange: (e) => setInput(e.target.value),
|
|
216
|
+
placeholder,
|
|
217
|
+
disabled: busy,
|
|
218
|
+
className: "flex-1 rounded-lg bg-neutral-100 px-3 py-2 text-sm outline-none placeholder:text-neutral-400 disabled:opacity-60 dark:bg-neutral-900"
|
|
219
|
+
}
|
|
220
|
+
),
|
|
221
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
222
|
+
"button",
|
|
223
|
+
{
|
|
224
|
+
type: "submit",
|
|
225
|
+
disabled: busy || !input.trim(),
|
|
226
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded-lg bg-neutral-900 text-white transition disabled:opacity-40 dark:bg-white dark:text-neutral-900",
|
|
227
|
+
"aria-label": "Send",
|
|
228
|
+
children: busy ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Send, { className: "h-4 w-4" })
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
]
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/react/memory/use-memory.ts
|
|
240
|
+
var import_react2 = require("react");
|
|
241
|
+
var DEFAULT_BASE_URL2 = "https://cencori.com";
|
|
242
|
+
function useMemory(options) {
|
|
243
|
+
const {
|
|
244
|
+
userId,
|
|
245
|
+
sessionId,
|
|
246
|
+
scope = "user",
|
|
247
|
+
namespace,
|
|
248
|
+
apiKey,
|
|
249
|
+
baseUrl = DEFAULT_BASE_URL2,
|
|
250
|
+
limit = 50,
|
|
251
|
+
manual = false
|
|
252
|
+
} = options;
|
|
253
|
+
const [memories, setMemories] = (0, import_react2.useState)([]);
|
|
254
|
+
const [loading, setLoading] = (0, import_react2.useState)(!manual);
|
|
255
|
+
const [error, setError] = (0, import_react2.useState)(null);
|
|
256
|
+
const base = (0, import_react2.useMemo)(() => baseUrl.replace(/\/$/, ""), [baseUrl]);
|
|
257
|
+
const headers = (0, import_react2.useMemo)(() => {
|
|
258
|
+
const h = { "Content-Type": "application/json" };
|
|
259
|
+
if (apiKey) h["CENCORI_API_KEY"] = apiKey;
|
|
260
|
+
return h;
|
|
261
|
+
}, [apiKey]);
|
|
262
|
+
const scopeBody = (0, import_react2.useCallback)(
|
|
263
|
+
() => ({ userId, sessionId, scope, namespace }),
|
|
264
|
+
[userId, sessionId, scope, namespace]
|
|
265
|
+
);
|
|
266
|
+
const request = (0, import_react2.useCallback)(
|
|
267
|
+
async (path, init) => {
|
|
268
|
+
const res = await fetch(`${base}${path}`, { ...init, headers });
|
|
269
|
+
if (!res.ok) {
|
|
270
|
+
const data = await res.json().catch(() => ({}));
|
|
271
|
+
throw new Error(data.error?.message || data.message || `Request failed (${res.status})`);
|
|
272
|
+
}
|
|
273
|
+
return res.json();
|
|
274
|
+
},
|
|
275
|
+
[base, headers]
|
|
276
|
+
);
|
|
277
|
+
const refresh = (0, import_react2.useCallback)(async () => {
|
|
278
|
+
if (!userId && !sessionId) return;
|
|
279
|
+
setLoading(true);
|
|
280
|
+
setError(null);
|
|
281
|
+
try {
|
|
282
|
+
const params = new URLSearchParams();
|
|
283
|
+
if (userId) params.set("userId", userId);
|
|
284
|
+
if (sessionId) params.set("sessionId", sessionId);
|
|
285
|
+
params.set("scope", scope);
|
|
286
|
+
if (namespace) params.set("namespace", namespace);
|
|
287
|
+
params.set("limit", String(limit));
|
|
288
|
+
const data = await request(
|
|
289
|
+
`/v1/memory/list?${params.toString()}`,
|
|
290
|
+
{ method: "GET" }
|
|
291
|
+
);
|
|
292
|
+
setMemories(data.memories ?? []);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
295
|
+
} finally {
|
|
296
|
+
setLoading(false);
|
|
297
|
+
}
|
|
298
|
+
}, [userId, sessionId, scope, namespace, limit, request]);
|
|
299
|
+
(0, import_react2.useEffect)(() => {
|
|
300
|
+
if (!manual) void refresh();
|
|
301
|
+
}, [manual, refresh]);
|
|
302
|
+
const write = (0, import_react2.useCallback)(
|
|
303
|
+
async (content, opts) => {
|
|
304
|
+
setError(null);
|
|
305
|
+
try {
|
|
306
|
+
await request("/v1/memory/write", {
|
|
307
|
+
method: "POST",
|
|
308
|
+
body: JSON.stringify({ ...scopeBody(), content, ...opts })
|
|
309
|
+
});
|
|
310
|
+
await refresh();
|
|
311
|
+
} catch (err) {
|
|
312
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
[request, scopeBody, refresh]
|
|
317
|
+
);
|
|
318
|
+
const search = (0, import_react2.useCallback)(
|
|
319
|
+
async (query, topK = 5) => {
|
|
320
|
+
const data = await request("/v1/memory/search", {
|
|
321
|
+
method: "POST",
|
|
322
|
+
body: JSON.stringify({ ...scopeBody(), query, topK })
|
|
323
|
+
});
|
|
324
|
+
return data.results ?? [];
|
|
325
|
+
},
|
|
326
|
+
[request, scopeBody]
|
|
327
|
+
);
|
|
328
|
+
const forget = (0, import_react2.useCallback)(
|
|
329
|
+
async (id) => {
|
|
330
|
+
setError(null);
|
|
331
|
+
setMemories((prev) => prev.filter((m) => m.id !== id));
|
|
332
|
+
try {
|
|
333
|
+
await request(`/v1/memory/${id}`, { method: "DELETE" });
|
|
334
|
+
} catch (err) {
|
|
335
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
336
|
+
await refresh();
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
[request, refresh]
|
|
341
|
+
);
|
|
342
|
+
const exportAll = (0, import_react2.useCallback)(async () => {
|
|
343
|
+
const params = new URLSearchParams();
|
|
344
|
+
if (userId) params.set("userId", userId);
|
|
345
|
+
if (sessionId) params.set("sessionId", sessionId);
|
|
346
|
+
params.set("scope", scope);
|
|
347
|
+
if (namespace) params.set("namespace", namespace);
|
|
348
|
+
params.set("limit", "1000");
|
|
349
|
+
const data = await request(
|
|
350
|
+
`/v1/memory/list?${params.toString()}`,
|
|
351
|
+
{ method: "GET" }
|
|
352
|
+
);
|
|
353
|
+
const blob = new Blob([JSON.stringify(data.memories ?? [], null, 2)], {
|
|
354
|
+
type: "application/json"
|
|
355
|
+
});
|
|
356
|
+
const url = URL.createObjectURL(blob);
|
|
357
|
+
const a = document.createElement("a");
|
|
358
|
+
a.href = url;
|
|
359
|
+
a.download = `cencori-memory-${userId ?? sessionId ?? "export"}.json`;
|
|
360
|
+
a.click();
|
|
361
|
+
URL.revokeObjectURL(url);
|
|
362
|
+
}, [request, userId, sessionId, scope, namespace]);
|
|
363
|
+
return { memories, loading, error, refresh, write, search, forget, exportAll };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/react/vision/vision-format-banner.tsx
|
|
367
|
+
var import_lucide_react2 = require("lucide-react");
|
|
368
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
31
369
|
var UNIVERSAL_FORMATS = ["JPEG", "PNG", "WEBP", "GIF"];
|
|
32
370
|
var PROVIDER_LIMITS = {
|
|
33
371
|
openai: { formats: ["JPEG", "PNG", "WEBP", "GIF"], maxMB: 20, notes: "Non-animated GIFs only." },
|
|
@@ -46,7 +384,7 @@ function VisionFormatBanner({
|
|
|
46
384
|
const info = provider ? PROVIDER_LIMITS[provider] : { formats: UNIVERSAL_FORMATS, maxMB: 5, notes: "These formats work across every supported provider." };
|
|
47
385
|
const heading = title ?? (provider ? `Supported for ${providerLabel(provider)}` : "Supported image formats");
|
|
48
386
|
const base = variant === "info" ? "bg-blue-50 border-blue-200 text-blue-900 dark:bg-blue-950/40 dark:border-blue-900 dark:text-blue-100" : "bg-neutral-50 border-neutral-200 text-neutral-800 dark:bg-neutral-900 dark:border-neutral-800 dark:text-neutral-100";
|
|
49
|
-
return /* @__PURE__ */ (0,
|
|
387
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
50
388
|
"div",
|
|
51
389
|
{
|
|
52
390
|
role: "note",
|
|
@@ -56,23 +394,23 @@ function VisionFormatBanner({
|
|
|
56
394
|
className
|
|
57
395
|
),
|
|
58
396
|
children: [
|
|
59
|
-
/* @__PURE__ */ (0,
|
|
60
|
-
/* @__PURE__ */ (0,
|
|
61
|
-
/* @__PURE__ */ (0,
|
|
62
|
-
/* @__PURE__ */ (0,
|
|
397
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex-shrink-0 pt-0.5", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.Info, { className: "h-5 w-5 opacity-70", "aria-hidden": true }) }),
|
|
398
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col gap-2", children: [
|
|
399
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "font-medium", children: heading }),
|
|
400
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex flex-wrap gap-1.5", children: info.formats.map((f) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
63
401
|
"span",
|
|
64
402
|
{
|
|
65
403
|
className: "inline-flex items-center gap-1 rounded-md border border-current/20 bg-white/50 px-2 py-0.5 text-xs font-medium dark:bg-black/20",
|
|
66
404
|
children: [
|
|
67
|
-
/* @__PURE__ */ (0,
|
|
405
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.ImageIcon, { className: "h-3 w-3 opacity-70", "aria-hidden": true }),
|
|
68
406
|
f
|
|
69
407
|
]
|
|
70
408
|
},
|
|
71
409
|
f
|
|
72
410
|
)) }),
|
|
73
|
-
/* @__PURE__ */ (0,
|
|
411
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "text-xs opacity-80", children: [
|
|
74
412
|
"Up to ",
|
|
75
|
-
/* @__PURE__ */ (0,
|
|
413
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("strong", { children: [
|
|
76
414
|
info.maxMB,
|
|
77
415
|
"MB"
|
|
78
416
|
] }),
|
|
@@ -91,8 +429,8 @@ function providerLabel(p) {
|
|
|
91
429
|
}
|
|
92
430
|
|
|
93
431
|
// src/react/vision/vision-uploader.tsx
|
|
94
|
-
var
|
|
95
|
-
var
|
|
432
|
+
var import_react3 = require("react");
|
|
433
|
+
var import_lucide_react3 = require("lucide-react");
|
|
96
434
|
|
|
97
435
|
// src/react/vision/downscale.ts
|
|
98
436
|
var DOWNSCALABLE_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
@@ -153,7 +491,7 @@ function renameToJpeg(name) {
|
|
|
153
491
|
}
|
|
154
492
|
|
|
155
493
|
// src/react/vision/vision-uploader.tsx
|
|
156
|
-
var
|
|
494
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
157
495
|
var UNIVERSAL_FORMATS2 = ["image/jpeg", "image/png", "image/webp", "image/gif"];
|
|
158
496
|
var PROVIDER_RULES = {
|
|
159
497
|
openai: { formats: UNIVERSAL_FORMATS2, maxBytes: 20 * 1024 * 1024 },
|
|
@@ -188,17 +526,17 @@ function VisionUploader({
|
|
|
188
526
|
headers,
|
|
189
527
|
className
|
|
190
528
|
}) {
|
|
191
|
-
const [file, setFile] = (0,
|
|
192
|
-
const [preview, setPreview] = (0,
|
|
193
|
-
const [error, setError] = (0,
|
|
194
|
-
const [loading, setLoading] = (0,
|
|
195
|
-
const [result, setResult] = (0,
|
|
196
|
-
const [dragging, setDragging] = (0,
|
|
197
|
-
const [notice, setNotice] = (0,
|
|
198
|
-
const inputRef = (0,
|
|
199
|
-
const rules = (0,
|
|
529
|
+
const [file, setFile] = (0, import_react3.useState)(null);
|
|
530
|
+
const [preview, setPreview] = (0, import_react3.useState)(null);
|
|
531
|
+
const [error, setError] = (0, import_react3.useState)(null);
|
|
532
|
+
const [loading, setLoading] = (0, import_react3.useState)(false);
|
|
533
|
+
const [result, setResult] = (0, import_react3.useState)(null);
|
|
534
|
+
const [dragging, setDragging] = (0, import_react3.useState)(false);
|
|
535
|
+
const [notice, setNotice] = (0, import_react3.useState)(null);
|
|
536
|
+
const inputRef = (0, import_react3.useRef)(null);
|
|
537
|
+
const rules = (0, import_react3.useMemo)(() => rulesFor(provider), [provider]);
|
|
200
538
|
const targetUrl = task === "analyze" ? endpoint : `${endpoint.replace(/\/$/, "")}/${task}`;
|
|
201
|
-
const acceptFile = (0,
|
|
539
|
+
const acceptFile = (0, import_react3.useCallback)(async (f) => {
|
|
202
540
|
setResult(null);
|
|
203
541
|
setNotice(null);
|
|
204
542
|
let effective = f;
|
|
@@ -249,13 +587,13 @@ function VisionUploader({
|
|
|
249
587
|
setFile(effective);
|
|
250
588
|
setPreview(URL.createObjectURL(effective));
|
|
251
589
|
}, [rules, provider, autoCompress, onError]);
|
|
252
|
-
const handleDrop = (0,
|
|
590
|
+
const handleDrop = (0, import_react3.useCallback)((e) => {
|
|
253
591
|
e.preventDefault();
|
|
254
592
|
setDragging(false);
|
|
255
593
|
const f = e.dataTransfer.files?.[0];
|
|
256
594
|
if (f) acceptFile(f);
|
|
257
595
|
}, [acceptFile]);
|
|
258
|
-
const handleSubmit = (0,
|
|
596
|
+
const handleSubmit = (0, import_react3.useCallback)(async () => {
|
|
259
597
|
if (!file) return;
|
|
260
598
|
setLoading(true);
|
|
261
599
|
setError(null);
|
|
@@ -292,7 +630,7 @@ function VisionUploader({
|
|
|
292
630
|
setLoading(false);
|
|
293
631
|
}
|
|
294
632
|
}, [file, prompt, model, task, targetUrl, apiKey, headers, onResult, onError]);
|
|
295
|
-
const clear = (0,
|
|
633
|
+
const clear = (0, import_react3.useCallback)(() => {
|
|
296
634
|
setFile(null);
|
|
297
635
|
setPreview(null);
|
|
298
636
|
setResult(null);
|
|
@@ -300,9 +638,9 @@ function VisionUploader({
|
|
|
300
638
|
setNotice(null);
|
|
301
639
|
if (inputRef.current) inputRef.current.value = "";
|
|
302
640
|
}, []);
|
|
303
|
-
return /* @__PURE__ */ (0,
|
|
304
|
-
!hideBanner && /* @__PURE__ */ (0,
|
|
305
|
-
/* @__PURE__ */ (0,
|
|
641
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: joinClasses2("flex flex-col gap-3", className), children: [
|
|
642
|
+
!hideBanner && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(VisionFormatBanner, { provider }),
|
|
643
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
306
644
|
"div",
|
|
307
645
|
{
|
|
308
646
|
onDragOver: (e) => {
|
|
@@ -320,9 +658,9 @@ function VisionUploader({
|
|
|
320
658
|
dragging ? "border-blue-500 bg-blue-50 dark:bg-blue-950/30" : "border-neutral-300 hover:border-neutral-400 dark:border-neutral-700 dark:hover:border-neutral-600"
|
|
321
659
|
),
|
|
322
660
|
children: [
|
|
323
|
-
preview ? /* @__PURE__ */ (0,
|
|
324
|
-
/* @__PURE__ */ (0,
|
|
325
|
-
/* @__PURE__ */ (0,
|
|
661
|
+
preview ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "relative", children: [
|
|
662
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("img", { src: preview, alt: "Preview", className: "max-h-64 rounded-lg object-contain" }),
|
|
663
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
326
664
|
"button",
|
|
327
665
|
{
|
|
328
666
|
type: "button",
|
|
@@ -332,19 +670,19 @@ function VisionUploader({
|
|
|
332
670
|
},
|
|
333
671
|
className: "absolute -right-2 -top-2 rounded-full bg-white p-1 shadow ring-1 ring-neutral-200 hover:bg-neutral-100 dark:bg-neutral-900 dark:ring-neutral-700",
|
|
334
672
|
"aria-label": "Remove image",
|
|
335
|
-
children: /* @__PURE__ */ (0,
|
|
673
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react3.X, { className: "h-4 w-4" })
|
|
336
674
|
}
|
|
337
675
|
)
|
|
338
|
-
] }) : /* @__PURE__ */ (0,
|
|
339
|
-
/* @__PURE__ */ (0,
|
|
340
|
-
/* @__PURE__ */ (0,
|
|
341
|
-
/* @__PURE__ */ (0,
|
|
676
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
677
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react3.Upload, { className: "h-8 w-8 text-neutral-400", "aria-hidden": true }),
|
|
678
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm font-medium", children: "Drop an image or click to upload" }),
|
|
679
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-xs text-neutral-500", children: [
|
|
342
680
|
friendlyFormats(rules.formats),
|
|
343
681
|
" \xB7 up to ",
|
|
344
682
|
formatBytes(rules.maxBytes)
|
|
345
683
|
] })
|
|
346
684
|
] }),
|
|
347
|
-
/* @__PURE__ */ (0,
|
|
685
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
348
686
|
"input",
|
|
349
687
|
{
|
|
350
688
|
ref: inputRef,
|
|
@@ -360,15 +698,15 @@ function VisionUploader({
|
|
|
360
698
|
]
|
|
361
699
|
}
|
|
362
700
|
),
|
|
363
|
-
error && /* @__PURE__ */ (0,
|
|
364
|
-
/* @__PURE__ */ (0,
|
|
365
|
-
/* @__PURE__ */ (0,
|
|
366
|
-
/* @__PURE__ */ (0,
|
|
367
|
-
/* @__PURE__ */ (0,
|
|
701
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "alert", className: "flex items-start gap-2 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-900 dark:border-red-900 dark:bg-red-950/40 dark:text-red-100", children: [
|
|
702
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react3.AlertCircle, { className: "mt-0.5 h-4 w-4 flex-shrink-0", "aria-hidden": true }),
|
|
703
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
|
|
704
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "font-medium", children: "Can't process this image" }),
|
|
705
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs opacity-90", children: error.message })
|
|
368
706
|
] })
|
|
369
707
|
] }),
|
|
370
|
-
notice && !error && /* @__PURE__ */ (0,
|
|
371
|
-
file && !loading && !result && /* @__PURE__ */ (0,
|
|
708
|
+
notice && !error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-neutral-500 dark:text-neutral-400", children: notice }),
|
|
709
|
+
file && !loading && !result && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
372
710
|
"button",
|
|
373
711
|
{
|
|
374
712
|
type: "button",
|
|
@@ -377,19 +715,359 @@ function VisionUploader({
|
|
|
377
715
|
children: "Analyze image"
|
|
378
716
|
}
|
|
379
717
|
),
|
|
380
|
-
loading && /* @__PURE__ */ (0,
|
|
381
|
-
/* @__PURE__ */ (0,
|
|
718
|
+
loading && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "inline-flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-400", children: [
|
|
719
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react3.Loader2, { className: "h-4 w-4 animate-spin", "aria-hidden": true }),
|
|
382
720
|
"Analyzing\u2026"
|
|
383
721
|
] }),
|
|
384
|
-
result !== null && /* @__PURE__ */ (0,
|
|
385
|
-
/* @__PURE__ */ (0,
|
|
386
|
-
/* @__PURE__ */ (0,
|
|
722
|
+
result !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-sm dark:border-neutral-800 dark:bg-neutral-900", children: [
|
|
723
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mb-1 text-xs font-medium uppercase text-neutral-500", children: "Result" }),
|
|
724
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("pre", { className: "overflow-auto whitespace-pre-wrap break-words text-xs", children: typeof result === "string" ? result : JSON.stringify(result, null, 2) })
|
|
725
|
+
] })
|
|
726
|
+
] });
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/react/voice/voice-recorder.tsx
|
|
730
|
+
var import_react5 = require("react");
|
|
731
|
+
var import_lucide_react4 = require("lucide-react");
|
|
732
|
+
|
|
733
|
+
// src/react/voice/use-voice-recorder.ts
|
|
734
|
+
var import_react4 = require("react");
|
|
735
|
+
function pickMimeType() {
|
|
736
|
+
if (typeof MediaRecorder === "undefined") return void 0;
|
|
737
|
+
const candidates = ["audio/webm;codecs=opus", "audio/webm", "audio/mp4", "audio/ogg"];
|
|
738
|
+
return candidates.find((t) => MediaRecorder.isTypeSupported(t));
|
|
739
|
+
}
|
|
740
|
+
function useVoiceRecorder() {
|
|
741
|
+
const [state, setState] = (0, import_react4.useState)("idle");
|
|
742
|
+
const [seconds, setSeconds] = (0, import_react4.useState)(0);
|
|
743
|
+
const [mimeType, setMimeType] = (0, import_react4.useState)(null);
|
|
744
|
+
const [error, setError] = (0, import_react4.useState)(null);
|
|
745
|
+
const [blob, setBlob] = (0, import_react4.useState)(null);
|
|
746
|
+
const recorderRef = (0, import_react4.useRef)(null);
|
|
747
|
+
const streamRef = (0, import_react4.useRef)(null);
|
|
748
|
+
const chunksRef = (0, import_react4.useRef)([]);
|
|
749
|
+
const timerRef = (0, import_react4.useRef)(null);
|
|
750
|
+
const cleanupStream = (0, import_react4.useCallback)(() => {
|
|
751
|
+
if (timerRef.current) {
|
|
752
|
+
clearInterval(timerRef.current);
|
|
753
|
+
timerRef.current = null;
|
|
754
|
+
}
|
|
755
|
+
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
756
|
+
streamRef.current = null;
|
|
757
|
+
}, []);
|
|
758
|
+
(0, import_react4.useEffect)(() => cleanupStream, [cleanupStream]);
|
|
759
|
+
const start = (0, import_react4.useCallback)(async () => {
|
|
760
|
+
setError(null);
|
|
761
|
+
setBlob(null);
|
|
762
|
+
setSeconds(0);
|
|
763
|
+
if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
|
|
764
|
+
setState("error");
|
|
765
|
+
setError("Microphone recording is not supported in this browser.");
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
setState("requesting");
|
|
770
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
771
|
+
streamRef.current = stream;
|
|
772
|
+
const type = pickMimeType();
|
|
773
|
+
const recorder = new MediaRecorder(stream, type ? { mimeType: type } : void 0);
|
|
774
|
+
chunksRef.current = [];
|
|
775
|
+
recorder.ondataavailable = (e) => {
|
|
776
|
+
if (e.data.size > 0) chunksRef.current.push(e.data);
|
|
777
|
+
};
|
|
778
|
+
recorder.start();
|
|
779
|
+
recorderRef.current = recorder;
|
|
780
|
+
setMimeType(recorder.mimeType || type || "audio/webm");
|
|
781
|
+
setState("recording");
|
|
782
|
+
timerRef.current = setInterval(() => setSeconds((s) => s + 1), 1e3);
|
|
783
|
+
} catch (e) {
|
|
784
|
+
cleanupStream();
|
|
785
|
+
setState("error");
|
|
786
|
+
setError(e instanceof Error ? e.message : "Could not access the microphone.");
|
|
787
|
+
}
|
|
788
|
+
}, [cleanupStream]);
|
|
789
|
+
const stop = (0, import_react4.useCallback)(() => {
|
|
790
|
+
const recorder = recorderRef.current;
|
|
791
|
+
if (!recorder || recorder.state === "inactive") {
|
|
792
|
+
return Promise.resolve(null);
|
|
793
|
+
}
|
|
794
|
+
return new Promise((resolve) => {
|
|
795
|
+
recorder.onstop = () => {
|
|
796
|
+
const type = recorder.mimeType || "audio/webm";
|
|
797
|
+
const recorded = new Blob(chunksRef.current, { type });
|
|
798
|
+
cleanupStream();
|
|
799
|
+
recorderRef.current = null;
|
|
800
|
+
setBlob(recorded);
|
|
801
|
+
setState("stopped");
|
|
802
|
+
resolve(recorded);
|
|
803
|
+
};
|
|
804
|
+
recorder.stop();
|
|
805
|
+
});
|
|
806
|
+
}, [cleanupStream]);
|
|
807
|
+
const reset = (0, import_react4.useCallback)(() => {
|
|
808
|
+
cleanupStream();
|
|
809
|
+
recorderRef.current = null;
|
|
810
|
+
chunksRef.current = [];
|
|
811
|
+
setBlob(null);
|
|
812
|
+
setSeconds(0);
|
|
813
|
+
setError(null);
|
|
814
|
+
setState("idle");
|
|
815
|
+
}, [cleanupStream]);
|
|
816
|
+
return { state, seconds, mimeType, error, blob, start, stop, reset };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/react/voice/voice-recorder.tsx
|
|
820
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
821
|
+
function joinClasses3(...parts) {
|
|
822
|
+
return parts.filter(Boolean).join(" ");
|
|
823
|
+
}
|
|
824
|
+
function formatClock(total) {
|
|
825
|
+
const m = Math.floor(total / 60);
|
|
826
|
+
const s = total % 60;
|
|
827
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
828
|
+
}
|
|
829
|
+
function VoiceRecorder({
|
|
830
|
+
endpoint = "/api/ai/audio/transcriptions",
|
|
831
|
+
apiKey,
|
|
832
|
+
model = "whisper-1",
|
|
833
|
+
language,
|
|
834
|
+
diarize = false,
|
|
835
|
+
onTranscript,
|
|
836
|
+
onError,
|
|
837
|
+
headers,
|
|
838
|
+
className
|
|
839
|
+
}) {
|
|
840
|
+
const rec = useVoiceRecorder();
|
|
841
|
+
const [uploading, setUploading] = (0, import_react5.useState)(false);
|
|
842
|
+
const [transcript, setTranscript] = (0, import_react5.useState)(null);
|
|
843
|
+
const [error, setError] = (0, import_react5.useState)(null);
|
|
844
|
+
const transcribe = (0, import_react5.useCallback)(async (blob) => {
|
|
845
|
+
setUploading(true);
|
|
846
|
+
setError(null);
|
|
847
|
+
try {
|
|
848
|
+
const form = new FormData();
|
|
849
|
+
const ext = (blob.type.split("/")[1] || "webm").split(";")[0];
|
|
850
|
+
form.append("file", blob, `recording.${ext}`);
|
|
851
|
+
form.append("model", model);
|
|
852
|
+
form.append("response_format", diarize ? "verbose_json" : "json");
|
|
853
|
+
if (language) form.append("language", language);
|
|
854
|
+
if (diarize) form.append("diarize", "true");
|
|
855
|
+
const res = await fetch(endpoint, {
|
|
856
|
+
method: "POST",
|
|
857
|
+
headers: { ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}, ...headers },
|
|
858
|
+
body: form
|
|
859
|
+
});
|
|
860
|
+
const data = await res.json().catch(() => ({}));
|
|
861
|
+
if (!res.ok) {
|
|
862
|
+
const err = {
|
|
863
|
+
code: typeof data.error === "string" ? data.error : "request_failed",
|
|
864
|
+
message: typeof data.message === "string" ? data.message : `Transcription failed (${res.status})`
|
|
865
|
+
};
|
|
866
|
+
setError(err);
|
|
867
|
+
onError?.(err);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const full = data;
|
|
871
|
+
setTranscript(full);
|
|
872
|
+
onTranscript?.(full.text ?? "", full);
|
|
873
|
+
} catch (e) {
|
|
874
|
+
const err = { code: "network_error", message: e instanceof Error ? e.message : "Network error" };
|
|
875
|
+
setError(err);
|
|
876
|
+
onError?.(err);
|
|
877
|
+
} finally {
|
|
878
|
+
setUploading(false);
|
|
879
|
+
}
|
|
880
|
+
}, [endpoint, apiKey, model, language, diarize, headers, onTranscript, onError]);
|
|
881
|
+
const toggle = (0, import_react5.useCallback)(async () => {
|
|
882
|
+
if (rec.state === "recording") {
|
|
883
|
+
const blob = await rec.stop();
|
|
884
|
+
if (blob) await transcribe(blob);
|
|
885
|
+
} else {
|
|
886
|
+
setTranscript(null);
|
|
887
|
+
await rec.start();
|
|
888
|
+
}
|
|
889
|
+
}, [rec, transcribe]);
|
|
890
|
+
const recording = rec.state === "recording";
|
|
891
|
+
const busy = rec.state === "requesting" || uploading;
|
|
892
|
+
const activeError = error || (rec.error ? { code: "mic_error", message: rec.error } : null);
|
|
893
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: joinClasses3("cencori-voice-recorder", className), style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
|
|
894
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [
|
|
895
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
896
|
+
"button",
|
|
897
|
+
{
|
|
898
|
+
type: "button",
|
|
899
|
+
onClick: toggle,
|
|
900
|
+
disabled: busy,
|
|
901
|
+
"aria-label": recording ? "Stop recording" : "Start recording",
|
|
902
|
+
className: joinClasses3("cencori-voice-btn", recording && "is-recording"),
|
|
903
|
+
style: {
|
|
904
|
+
display: "inline-flex",
|
|
905
|
+
alignItems: "center",
|
|
906
|
+
justifyContent: "center",
|
|
907
|
+
width: 56,
|
|
908
|
+
height: 56,
|
|
909
|
+
borderRadius: "9999px",
|
|
910
|
+
border: "none",
|
|
911
|
+
cursor: busy ? "default" : "pointer",
|
|
912
|
+
background: recording ? "#dc2626" : "#111827",
|
|
913
|
+
color: "#fff",
|
|
914
|
+
opacity: busy ? 0.6 : 1,
|
|
915
|
+
transition: "background 0.15s"
|
|
916
|
+
},
|
|
917
|
+
children: busy ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react4.Loader2, { size: 22, className: "animate-spin" }) : recording ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react4.Square, { size: 20 }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react4.Mic, { size: 22 })
|
|
918
|
+
}
|
|
919
|
+
),
|
|
920
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", flexDirection: "column" }, children: [
|
|
921
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontVariantNumeric: "tabular-nums", fontWeight: 600 }, children: recording ? formatClock(rec.seconds) : uploading ? "Transcribing\u2026" : "Tap to record" }),
|
|
922
|
+
recording && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { fontSize: 12, color: "#dc2626", display: "inline-flex", alignItems: "center", gap: 6 }, children: [
|
|
923
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { width: 8, height: 8, borderRadius: "9999px", background: "#dc2626", display: "inline-block" } }),
|
|
924
|
+
"recording"
|
|
925
|
+
] })
|
|
926
|
+
] })
|
|
927
|
+
] }),
|
|
928
|
+
activeError && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { role: "alert", style: { display: "flex", gap: 8, alignItems: "flex-start", color: "#b91c1c", fontSize: 13 }, children: [
|
|
929
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react4.AlertCircle, { size: 16, style: { flexShrink: 0, marginTop: 2 } }),
|
|
930
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: activeError.message })
|
|
931
|
+
] }),
|
|
932
|
+
transcript && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "cencori-voice-result", style: { fontSize: 14, lineHeight: 1.5 }, children: transcript.segments && transcript.segments.some((s) => s.speaker) ? transcript.segments.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { style: { margin: "4px 0" }, children: [
|
|
933
|
+
s.speaker && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("strong", { style: { marginRight: 6 }, children: [
|
|
934
|
+
s.speaker,
|
|
935
|
+
":"
|
|
936
|
+
] }),
|
|
937
|
+
s.text
|
|
938
|
+
] }, i)) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { margin: 0 }, children: transcript.text }) })
|
|
939
|
+
] });
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/react/voice/speak-button.tsx
|
|
943
|
+
var import_react6 = require("react");
|
|
944
|
+
var import_lucide_react5 = require("lucide-react");
|
|
945
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
946
|
+
function joinClasses4(...parts) {
|
|
947
|
+
return parts.filter(Boolean).join(" ");
|
|
948
|
+
}
|
|
949
|
+
function SpeakButton({
|
|
950
|
+
text,
|
|
951
|
+
endpoint = "/api/ai/audio/speech",
|
|
952
|
+
apiKey,
|
|
953
|
+
model = "tts-1",
|
|
954
|
+
voice,
|
|
955
|
+
language,
|
|
956
|
+
label = "Listen",
|
|
957
|
+
headers,
|
|
958
|
+
onError,
|
|
959
|
+
className
|
|
960
|
+
}) {
|
|
961
|
+
const [state, setState] = (0, import_react6.useState)("idle");
|
|
962
|
+
const [error, setError] = (0, import_react6.useState)(null);
|
|
963
|
+
const audioRef = (0, import_react6.useRef)(null);
|
|
964
|
+
const urlRef = (0, import_react6.useRef)(null);
|
|
965
|
+
const cleanup = (0, import_react6.useCallback)(() => {
|
|
966
|
+
if (audioRef.current) {
|
|
967
|
+
audioRef.current.pause();
|
|
968
|
+
audioRef.current = null;
|
|
969
|
+
}
|
|
970
|
+
if (urlRef.current) {
|
|
971
|
+
URL.revokeObjectURL(urlRef.current);
|
|
972
|
+
urlRef.current = null;
|
|
973
|
+
}
|
|
974
|
+
}, []);
|
|
975
|
+
(0, import_react6.useEffect)(() => cleanup, [cleanup]);
|
|
976
|
+
const play = (0, import_react6.useCallback)(async () => {
|
|
977
|
+
if (state === "playing") {
|
|
978
|
+
cleanup();
|
|
979
|
+
setState("idle");
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (!text.trim()) return;
|
|
983
|
+
setState("loading");
|
|
984
|
+
setError(null);
|
|
985
|
+
try {
|
|
986
|
+
const res = await fetch(endpoint, {
|
|
987
|
+
method: "POST",
|
|
988
|
+
headers: {
|
|
989
|
+
"Content-Type": "application/json",
|
|
990
|
+
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
991
|
+
...headers
|
|
992
|
+
},
|
|
993
|
+
body: JSON.stringify({ input: text, model, voice, language })
|
|
994
|
+
});
|
|
995
|
+
if (!res.ok) {
|
|
996
|
+
const data = await res.json().catch(() => ({}));
|
|
997
|
+
const err = {
|
|
998
|
+
code: typeof data.error === "string" ? data.error : "request_failed",
|
|
999
|
+
message: typeof data.message === "string" ? data.message : `Speech failed (${res.status})`
|
|
1000
|
+
};
|
|
1001
|
+
setError(err);
|
|
1002
|
+
onError?.(err);
|
|
1003
|
+
setState("idle");
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const blob = await res.blob();
|
|
1007
|
+
cleanup();
|
|
1008
|
+
const url = URL.createObjectURL(blob);
|
|
1009
|
+
urlRef.current = url;
|
|
1010
|
+
const audio = new Audio(url);
|
|
1011
|
+
audioRef.current = audio;
|
|
1012
|
+
audio.onended = () => {
|
|
1013
|
+
setState("idle");
|
|
1014
|
+
cleanup();
|
|
1015
|
+
};
|
|
1016
|
+
audio.onerror = () => {
|
|
1017
|
+
setState("idle");
|
|
1018
|
+
cleanup();
|
|
1019
|
+
};
|
|
1020
|
+
await audio.play();
|
|
1021
|
+
setState("playing");
|
|
1022
|
+
} catch (e) {
|
|
1023
|
+
const err = { code: "network_error", message: e instanceof Error ? e.message : "Network error" };
|
|
1024
|
+
setError(err);
|
|
1025
|
+
onError?.(err);
|
|
1026
|
+
setState("idle");
|
|
1027
|
+
}
|
|
1028
|
+
}, [state, text, endpoint, apiKey, model, voice, language, headers, onError, cleanup]);
|
|
1029
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { display: "inline-flex", flexDirection: "column", gap: 4 }, children: [
|
|
1030
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
1031
|
+
"button",
|
|
1032
|
+
{
|
|
1033
|
+
type: "button",
|
|
1034
|
+
onClick: play,
|
|
1035
|
+
disabled: state === "loading" || !text.trim(),
|
|
1036
|
+
"aria-label": label ?? (state === "playing" ? "Pause" : "Listen"),
|
|
1037
|
+
className: joinClasses4("cencori-speak-btn", className),
|
|
1038
|
+
style: {
|
|
1039
|
+
display: "inline-flex",
|
|
1040
|
+
alignItems: "center",
|
|
1041
|
+
gap: 6,
|
|
1042
|
+
padding: label ? "6px 12px" : 8,
|
|
1043
|
+
borderRadius: 8,
|
|
1044
|
+
border: "1px solid #e5e7eb",
|
|
1045
|
+
background: "#fff",
|
|
1046
|
+
color: "#111827",
|
|
1047
|
+
cursor: state === "loading" ? "default" : "pointer",
|
|
1048
|
+
fontSize: 14,
|
|
1049
|
+
fontWeight: 500
|
|
1050
|
+
},
|
|
1051
|
+
children: [
|
|
1052
|
+
state === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react5.Loader2, { size: 16, className: "animate-spin" }) : state === "playing" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react5.Pause, { size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react5.Volume2, { size: 16 }),
|
|
1053
|
+
label && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: label })
|
|
1054
|
+
]
|
|
1055
|
+
}
|
|
1056
|
+
),
|
|
1057
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { role: "alert", style: { display: "inline-flex", gap: 4, alignItems: "center", color: "#b91c1c", fontSize: 12 }, children: [
|
|
1058
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react5.AlertCircle, { size: 13 }),
|
|
1059
|
+
error.message
|
|
387
1060
|
] })
|
|
388
1061
|
] });
|
|
389
1062
|
}
|
|
390
1063
|
// Annotate the CommonJS export names for ESM import in node:
|
|
391
1064
|
0 && (module.exports = {
|
|
1065
|
+
Chat,
|
|
1066
|
+
SpeakButton,
|
|
392
1067
|
VisionFormatBanner,
|
|
393
|
-
VisionUploader
|
|
1068
|
+
VisionUploader,
|
|
1069
|
+
VoiceRecorder,
|
|
1070
|
+
useMemory,
|
|
1071
|
+
useVoiceRecorder
|
|
394
1072
|
});
|
|
395
1073
|
//# sourceMappingURL=index.js.map
|