@kieksme/csp-plugin-chat 0.11.0 → 0.12.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/README.md +3 -1
- package/dist/browser.js +492 -113
- package/dist/{chunk-W5MUDSVD.js → chunk-DZ3YQB4V.js} +1 -1
- package/dist/server.d.ts +20 -0
- package/dist/server.js +128 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ For local provider testing, set `CSP_DEMO=true` and `CSP_CHAT_DEMO=false`: porta
|
|
|
8
8
|
|
|
9
9
|
Langsame lokale Modelle können mit der Runtime-Variable `CSP_CHAT_TIMEOUT_MS` mehr Antwortzeit erhalten (Standard `60000`, Bereich `1000`–`300000` ms).
|
|
10
10
|
|
|
11
|
-
Send a message with Cmd + Enter on macOS, Ctrl + Enter on other systems, or the send button. The hint displays the system-specific shortcut. Plain Enter inserts a line break. A subtle shortcut hint appears
|
|
11
|
+
Send a message with Cmd + Enter on macOS, Ctrl + Enter on other systems, or the send button. The hint displays the system-specific shortcut. Plain Enter inserts a line break. A subtle shortcut hint appears below the text field; blank messages and shortcuts during an active reply do not send.
|
|
12
12
|
|
|
13
13
|
The input starts with one text row and automatically grows with wrapped text or line breaks. It shrinks when text is removed or sent and adjusts when its width changes.
|
|
14
14
|
|
|
@@ -29,3 +29,5 @@ WebMCP tool: `chat_ask` streams one answer from `POST /api/v1/chat` and returns
|
|
|
29
29
|
Users can mark a chat **Problem gelöst** and start a new chat afterward. With a support transport this is an authenticated, persisted owner action, interrupting generation and updating the channel card; without a transport it applies to the current browser chat only. A resolved conversation is distinct from an ordinary closure.
|
|
30
30
|
|
|
31
31
|
The assistant asks one short clarifying question for vague requests (for example “Hallo, meine Email”) before offering tickets. It uses supplied help/status facts and reserves ticket links for escalation or an explicit ticket request. Model behaviour requires a real-provider acceptance test.
|
|
32
|
+
|
|
33
|
+
Long histories follow new messages while the visitor reads at the bottom; scrolling up preserves the reading position and offers **Neue Nachrichten ↓**. Real OpenAI providers expose editable dictation through the server-only transcription API. With a support transport, **Ich möchte mit einer echten Person sprechen** pauses the bot immediately and queues a persistent, directed support request. See [chat behaviour and APIs](../../docs/chat.md) and [runtime options](../../docs/configuration.md#spracheingabe-und-supportanfragen).
|
package/dist/browser.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cspPlugin
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-DZ3YQB4V.js";
|
|
4
4
|
|
|
5
5
|
// src/tools.ts
|
|
6
6
|
import {
|
|
@@ -167,10 +167,10 @@ var chatTools = [
|
|
|
167
167
|
];
|
|
168
168
|
|
|
169
169
|
// src/browser.tsx
|
|
170
|
-
import { useEffect as
|
|
170
|
+
import { useEffect as useEffect4, useLayoutEffect as useLayoutEffect2, useRef as useRef4, useState as useState4 } from "react";
|
|
171
171
|
|
|
172
172
|
// src/support-chat.tsx
|
|
173
|
-
import { useEffect, useRef, useState } from "react";
|
|
173
|
+
import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
174
174
|
|
|
175
175
|
// src/support-session.ts
|
|
176
176
|
async function supportSession(ctx) {
|
|
@@ -297,14 +297,327 @@ function anonymousSession(ctx) {
|
|
|
297
297
|
return current;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
// src/chat-controls.tsx
|
|
301
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
302
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
303
|
+
function sendShortcut(event, disabled) {
|
|
304
|
+
if (event.key !== "Enter" || !(event.ctrlKey || event.metaKey) || event.nativeEvent.isComposing || event.repeat)
|
|
305
|
+
return;
|
|
306
|
+
event.preventDefault();
|
|
307
|
+
if (!disabled) event.currentTarget.form?.requestSubmit();
|
|
308
|
+
}
|
|
309
|
+
function Shortcut({ id }) {
|
|
310
|
+
const [mac, setMac] = useState(false);
|
|
311
|
+
useEffect(() => setMac(/Mac|iPhone|iPad/.test(navigator.platform)), []);
|
|
312
|
+
return /* @__PURE__ */ jsxs("span", { id, className: "block text-[0.6875rem] text-muted mt-2", children: [
|
|
313
|
+
mac ? "\u2318" : "Strg",
|
|
314
|
+
" Enter zum Senden"
|
|
315
|
+
] });
|
|
316
|
+
}
|
|
317
|
+
function useChatScroll(content, conversation) {
|
|
318
|
+
const history = useRef(null), pinned = useRef(true);
|
|
319
|
+
const [unread, setUnread] = useState(false);
|
|
320
|
+
const follow = () => {
|
|
321
|
+
pinned.current = true;
|
|
322
|
+
setUnread(false);
|
|
323
|
+
const element = history.current;
|
|
324
|
+
if (element) element.scrollTop = element.scrollHeight;
|
|
325
|
+
};
|
|
326
|
+
useLayoutEffect(follow, [conversation]);
|
|
327
|
+
useLayoutEffect(() => {
|
|
328
|
+
if (pinned.current) follow();
|
|
329
|
+
else setUnread(true);
|
|
330
|
+
}, [content]);
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
const element = history.current;
|
|
333
|
+
if (!element) return;
|
|
334
|
+
const observer = new ResizeObserver(() => {
|
|
335
|
+
if (pinned.current) follow();
|
|
336
|
+
});
|
|
337
|
+
observer.observe(element);
|
|
338
|
+
return () => observer.disconnect();
|
|
339
|
+
}, []);
|
|
340
|
+
return {
|
|
341
|
+
history,
|
|
342
|
+
follow,
|
|
343
|
+
unread,
|
|
344
|
+
onScroll: () => {
|
|
345
|
+
const element = history.current;
|
|
346
|
+
if (!element) return;
|
|
347
|
+
pinned.current = element.scrollHeight - element.scrollTop - element.clientHeight < 80;
|
|
348
|
+
if (pinned.current) setUnread(false);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function NewMessages({
|
|
353
|
+
visible,
|
|
354
|
+
follow
|
|
355
|
+
}) {
|
|
356
|
+
return visible ? /* @__PURE__ */ jsx(
|
|
357
|
+
"button",
|
|
358
|
+
{
|
|
359
|
+
type: "button",
|
|
360
|
+
className: "border border-line rounded-card px-3 py-2 mb-3 text-[0.8125rem]",
|
|
361
|
+
onClick: follow,
|
|
362
|
+
children: "Neue Nachrichten \u2193"
|
|
363
|
+
}
|
|
364
|
+
) : null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/voice-input.tsx
|
|
368
|
+
import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
369
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
370
|
+
function VoiceInput({
|
|
371
|
+
ctx,
|
|
372
|
+
value,
|
|
373
|
+
setValue,
|
|
374
|
+
disabled,
|
|
375
|
+
token,
|
|
376
|
+
onBusy
|
|
377
|
+
}) {
|
|
378
|
+
const [enabled, setEnabled] = useState2(false), [state, setState] = useState2("idle"), [error, setError] = useState2("");
|
|
379
|
+
const recorder = useRef2(null), stream = useRef2(null), timer = useRef2(null), request = useRef2(null);
|
|
380
|
+
const alive = useRef2(true), cancelled = useRef2(false), draft = useRef2(value);
|
|
381
|
+
const operation = useRef2(0);
|
|
382
|
+
draft.current = value;
|
|
383
|
+
const release = () => {
|
|
384
|
+
if (timer.current) clearTimeout(timer.current);
|
|
385
|
+
stream.current?.getTracks().forEach((track) => track.stop());
|
|
386
|
+
stream.current = null;
|
|
387
|
+
};
|
|
388
|
+
const idle = () => {
|
|
389
|
+
if (alive.current) {
|
|
390
|
+
setState("idle");
|
|
391
|
+
onBusy(false);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
useEffect2(() => {
|
|
395
|
+
alive.current = true;
|
|
396
|
+
const controller = new AbortController();
|
|
397
|
+
if (typeof MediaRecorder !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function") {
|
|
398
|
+
void (ctx.request ?? fetch)(
|
|
399
|
+
ctx.config.apiUrl + "/api/v1/chat/capabilities",
|
|
400
|
+
{ signal: controller.signal }
|
|
401
|
+
).then((response) => response.ok ? response.json() : null).then((result) => {
|
|
402
|
+
if (alive.current) setEnabled(result?.transcription === true);
|
|
403
|
+
}).catch(() => {
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return () => {
|
|
407
|
+
operation.current++;
|
|
408
|
+
alive.current = false;
|
|
409
|
+
cancelled.current = true;
|
|
410
|
+
controller.abort();
|
|
411
|
+
request.current?.abort();
|
|
412
|
+
if (recorder.current?.state === "recording") recorder.current.stop();
|
|
413
|
+
release();
|
|
414
|
+
};
|
|
415
|
+
}, [ctx.config.apiUrl]);
|
|
416
|
+
useEffect2(() => {
|
|
417
|
+
if (disabled && state !== "idle") {
|
|
418
|
+
operation.current++;
|
|
419
|
+
cancelled.current = true;
|
|
420
|
+
request.current?.abort();
|
|
421
|
+
if (recorder.current?.state === "recording") recorder.current.stop();
|
|
422
|
+
release();
|
|
423
|
+
idle();
|
|
424
|
+
}
|
|
425
|
+
}, [disabled]);
|
|
426
|
+
async function start() {
|
|
427
|
+
if (state !== "idle" || disabled) return;
|
|
428
|
+
const attempt = ++operation.current;
|
|
429
|
+
setState("starting");
|
|
430
|
+
onBusy(true);
|
|
431
|
+
setError("");
|
|
432
|
+
cancelled.current = false;
|
|
433
|
+
try {
|
|
434
|
+
const media = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
435
|
+
if (!alive.current || cancelled.current || operation.current !== attempt) {
|
|
436
|
+
media.getTracks().forEach((track) => track.stop());
|
|
437
|
+
if (operation.current === attempt) idle();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
stream.current = media;
|
|
441
|
+
const mime = [
|
|
442
|
+
"audio/webm;codecs=opus",
|
|
443
|
+
"audio/mp4",
|
|
444
|
+
"audio/ogg;codecs=opus"
|
|
445
|
+
].find((type) => MediaRecorder.isTypeSupported(type));
|
|
446
|
+
if (!mime)
|
|
447
|
+
throw new Error(
|
|
448
|
+
"Dieser Browser unterst\xFCtzt keine passende Audioaufnahme."
|
|
449
|
+
);
|
|
450
|
+
const capture = new MediaRecorder(media, { mimeType: mime });
|
|
451
|
+
recorder.current = capture;
|
|
452
|
+
const chunks = [];
|
|
453
|
+
let bytes = 0;
|
|
454
|
+
capture.ondataavailable = (event) => {
|
|
455
|
+
if (event.data.size) {
|
|
456
|
+
bytes += event.data.size;
|
|
457
|
+
chunks.push(event.data);
|
|
458
|
+
if (bytes > 10 * 1024 * 1024) {
|
|
459
|
+
cancelled.current = true;
|
|
460
|
+
if (capture.state === "recording") capture.stop();
|
|
461
|
+
release();
|
|
462
|
+
if (alive.current)
|
|
463
|
+
setError("Die Aufnahme ist zu gro\xDF. Bitte k\xFCrzer diktieren.");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
capture.onerror = () => {
|
|
468
|
+
if (operation.current !== attempt) return;
|
|
469
|
+
cancelled.current = true;
|
|
470
|
+
release();
|
|
471
|
+
if (alive.current)
|
|
472
|
+
setError("Aufnahme fehlgeschlagen. Ihr Entwurf bleibt erhalten.");
|
|
473
|
+
idle();
|
|
474
|
+
};
|
|
475
|
+
capture.onstop = () => {
|
|
476
|
+
if (operation.current !== attempt) return;
|
|
477
|
+
release();
|
|
478
|
+
if (cancelled.current || !alive.current) {
|
|
479
|
+
idle();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
void transcribe(new Blob(chunks, { type: mime }));
|
|
483
|
+
};
|
|
484
|
+
capture.start(1e3);
|
|
485
|
+
setState("recording");
|
|
486
|
+
timer.current = setTimeout(() => {
|
|
487
|
+
if (capture.state === "recording") capture.stop();
|
|
488
|
+
}, 6e4);
|
|
489
|
+
} catch (failure) {
|
|
490
|
+
if (operation.current !== attempt) return;
|
|
491
|
+
release();
|
|
492
|
+
if (alive.current)
|
|
493
|
+
setError(
|
|
494
|
+
failure instanceof DOMException && failure.name === "NotAllowedError" ? "Bitte erlauben Sie den Mikrofonzugriff f\xFCr die Spracheingabe." : failure instanceof Error ? failure.message : "Mikrofon nicht verf\xFCgbar."
|
|
495
|
+
);
|
|
496
|
+
idle();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function transcribe(audio) {
|
|
500
|
+
const attempt = operation.current;
|
|
501
|
+
setState("transcribing");
|
|
502
|
+
const controller = new AbortController();
|
|
503
|
+
request.current = controller;
|
|
504
|
+
try {
|
|
505
|
+
const bearer = await token?.();
|
|
506
|
+
if (controller.signal.aborted) return;
|
|
507
|
+
const response = await (ctx.request ?? fetch)(
|
|
508
|
+
ctx.config.apiUrl + "/api/v1/chat/transcriptions",
|
|
509
|
+
{
|
|
510
|
+
method: "POST",
|
|
511
|
+
body: audio,
|
|
512
|
+
credentials: "same-origin",
|
|
513
|
+
signal: controller.signal,
|
|
514
|
+
headers: {
|
|
515
|
+
"Content-Type": audio.type,
|
|
516
|
+
...bearer ? { Authorization: `Bearer ${bearer}` } : {}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
);
|
|
520
|
+
const result = await response.json();
|
|
521
|
+
if (!response.ok)
|
|
522
|
+
throw new Error(
|
|
523
|
+
result.error ?? "Spracheingabe derzeit nicht verf\xFCgbar."
|
|
524
|
+
);
|
|
525
|
+
if (typeof result.text !== "string" || !result.text.trim())
|
|
526
|
+
throw new Error("Keine Sprache erkannt.");
|
|
527
|
+
if (draft.current.length + result.text.length + (draft.current ? 1 : 0) > 4e3)
|
|
528
|
+
throw new Error(
|
|
529
|
+
"Der Entwurf w\xE4re zu lang. Bitte k\xFCrzer diktieren. Ihr Text bleibt erhalten."
|
|
530
|
+
);
|
|
531
|
+
if (alive.current && !controller.signal.aborted && operation.current === attempt)
|
|
532
|
+
setValue((previous) => previous + (previous ? "\n" : "") + result.text);
|
|
533
|
+
} catch (failure) {
|
|
534
|
+
if (alive.current && !controller.signal.aborted)
|
|
535
|
+
setError(
|
|
536
|
+
failure instanceof Error ? failure.message : "Spracheingabe fehlgeschlagen. Ihr Entwurf bleibt erhalten."
|
|
537
|
+
);
|
|
538
|
+
} finally {
|
|
539
|
+
if (operation.current === attempt) {
|
|
540
|
+
request.current = null;
|
|
541
|
+
idle();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (!enabled) return null;
|
|
546
|
+
return /* @__PURE__ */ jsxs2("div", { className: "mt-3 text-[0.8125rem]", children: [
|
|
547
|
+
state === "idle" ? /* @__PURE__ */ jsxs2(
|
|
548
|
+
"button",
|
|
549
|
+
{
|
|
550
|
+
type: "button",
|
|
551
|
+
className: "border border-line rounded-card px-3 py-2",
|
|
552
|
+
disabled,
|
|
553
|
+
onClick: () => void start(),
|
|
554
|
+
children: [
|
|
555
|
+
/* @__PURE__ */ jsxs2(
|
|
556
|
+
"svg",
|
|
557
|
+
{
|
|
558
|
+
"aria-hidden": "true",
|
|
559
|
+
viewBox: "0 0 24 24",
|
|
560
|
+
width: "16",
|
|
561
|
+
height: "16",
|
|
562
|
+
className: "inline mr-2",
|
|
563
|
+
fill: "none",
|
|
564
|
+
stroke: "currentColor",
|
|
565
|
+
strokeWidth: "2",
|
|
566
|
+
children: [
|
|
567
|
+
/* @__PURE__ */ jsx2("rect", { x: "9", y: "2", width: "6", height: "12", rx: "3" }),
|
|
568
|
+
/* @__PURE__ */ jsx2("path", { d: "M5 10v2a7 7 0 0 0 14 0v-2M12 19v3M8 22h8" })
|
|
569
|
+
]
|
|
570
|
+
}
|
|
571
|
+
),
|
|
572
|
+
"Nachricht diktieren"
|
|
573
|
+
]
|
|
574
|
+
}
|
|
575
|
+
) : /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
576
|
+
/* @__PURE__ */ jsx2("span", { role: "status", children: state === "recording" ? "\u25CF Aufnahme l\xE4uft \xB7 maximal 60 Sekunden" : state === "starting" ? "Mikrofon wird gestartet \u2026" : "Sprache wird in Text umgewandelt \u2026" }),
|
|
577
|
+
state === "recording" && /* @__PURE__ */ jsx2(
|
|
578
|
+
"button",
|
|
579
|
+
{
|
|
580
|
+
type: "button",
|
|
581
|
+
className: "border border-line rounded-card px-3 py-2 ml-2",
|
|
582
|
+
onClick: () => recorder.current?.stop(),
|
|
583
|
+
children: "Aufnahme stoppen"
|
|
584
|
+
}
|
|
585
|
+
),
|
|
586
|
+
/* @__PURE__ */ jsx2(
|
|
587
|
+
"button",
|
|
588
|
+
{
|
|
589
|
+
type: "button",
|
|
590
|
+
className: "underline ml-2",
|
|
591
|
+
onClick: () => {
|
|
592
|
+
operation.current++;
|
|
593
|
+
cancelled.current = true;
|
|
594
|
+
request.current?.abort();
|
|
595
|
+
if (recorder.current?.state === "recording")
|
|
596
|
+
recorder.current.stop();
|
|
597
|
+
release();
|
|
598
|
+
idle();
|
|
599
|
+
},
|
|
600
|
+
children: "Abbrechen"
|
|
601
|
+
}
|
|
602
|
+
)
|
|
603
|
+
] }),
|
|
604
|
+
/* @__PURE__ */ jsx2("p", { className: "text-[0.6875rem] text-muted mt-2", children: "Die Aufnahme wird zur Texterkennung an OpenAI gesendet. Pr\xFCfen Sie den Text vor dem Senden." }),
|
|
605
|
+
error && /* @__PURE__ */ jsx2("p", { role: "alert", className: "text-danger dark:text-warning mt-2", children: error })
|
|
606
|
+
] });
|
|
607
|
+
}
|
|
608
|
+
|
|
300
609
|
// src/support-chat.tsx
|
|
301
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
610
|
+
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
302
611
|
function SupportChat(ctx) {
|
|
303
|
-
const [current, setCurrent] =
|
|
304
|
-
const
|
|
612
|
+
const [current, setCurrent] = useState3(null), [input, setInput] = useState3(""), [error, setError] = useState3(""), [busy, setBusy] = useState3(false), [ready, setReady] = useState3(false), [auth, setAuth] = useState3(null);
|
|
613
|
+
const [voiceBusy, setVoiceBusy] = useState3(false);
|
|
614
|
+
const scroll = useChatScroll(current?.messages, current?.id);
|
|
615
|
+
const sending = useRef3(false);
|
|
616
|
+
const blocked = busy || voiceBusy || !ready || current?.status === "closed" || !!current?.messages.some((m) => !m.complete);
|
|
617
|
+
const seq = useRef3(0), requestRef = useRef3(null);
|
|
305
618
|
const anonymous = ctx.config.chatSupport?.auth === "anonymous";
|
|
306
619
|
const demo = ctx.config.chatSupport?.mode === "demo";
|
|
307
|
-
|
|
620
|
+
useEffect3(() => {
|
|
308
621
|
let alive = true;
|
|
309
622
|
const controller = new AbortController();
|
|
310
623
|
async function init() {
|
|
@@ -358,7 +671,7 @@ function SupportChat(ctx) {
|
|
|
358
671
|
controller.abort();
|
|
359
672
|
};
|
|
360
673
|
}, [ctx.config.apiUrl]);
|
|
361
|
-
|
|
674
|
+
useEffect3(() => {
|
|
362
675
|
if (!current?.id || !ready) return;
|
|
363
676
|
let alive = true;
|
|
364
677
|
const controller = new AbortController();
|
|
@@ -418,14 +731,17 @@ function SupportChat(ctx) {
|
|
|
418
731
|
}, [current?.id, ready]);
|
|
419
732
|
async function newConversation() {
|
|
420
733
|
const c = await (await requestRef.current("", { method: "POST" })).json();
|
|
734
|
+
scroll.follow();
|
|
421
735
|
seq.current = c.seq;
|
|
422
736
|
setCurrent(c);
|
|
423
737
|
return c;
|
|
424
738
|
}
|
|
425
|
-
const pending =
|
|
739
|
+
const pending = useRef3(null);
|
|
426
740
|
async function submit(e) {
|
|
427
741
|
e.preventDefault();
|
|
428
|
-
if (!input.trim() ||
|
|
742
|
+
if (!input.trim() || blocked || sending.current) return;
|
|
743
|
+
sending.current = true;
|
|
744
|
+
scroll.follow();
|
|
429
745
|
setBusy(true);
|
|
430
746
|
setError("");
|
|
431
747
|
try {
|
|
@@ -439,10 +755,38 @@ function SupportChat(ctx) {
|
|
|
439
755
|
pending.current = null;
|
|
440
756
|
setInput("");
|
|
441
757
|
const refreshed = await (await requestRef.current(`/${c.id}`)).json();
|
|
442
|
-
|
|
758
|
+
applySnapshot(refreshed);
|
|
443
759
|
} catch (e2) {
|
|
444
760
|
setError(e2 instanceof Error ? e2.message : "Nachricht nicht gesendet.");
|
|
445
761
|
} finally {
|
|
762
|
+
sending.current = false;
|
|
763
|
+
setBusy(false);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
function applySnapshot(result) {
|
|
767
|
+
setCurrent(
|
|
768
|
+
(previous) => previous?.id === result.id && previous.seq > result.seq ? previous : result
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
async function requestHuman() {
|
|
772
|
+
if (busy || !ready || sending.current) return;
|
|
773
|
+
sending.current = true;
|
|
774
|
+
setBusy(true);
|
|
775
|
+
setError("");
|
|
776
|
+
try {
|
|
777
|
+
const c = current ?? await newConversation();
|
|
778
|
+
const result = await (await requestRef.current(`/${c.id}/request-support`, {
|
|
779
|
+
method: "POST",
|
|
780
|
+
body: "{}"
|
|
781
|
+
})).json();
|
|
782
|
+
applySnapshot(result);
|
|
783
|
+
scroll.follow();
|
|
784
|
+
} catch (error2) {
|
|
785
|
+
setError(
|
|
786
|
+
error2 instanceof Error ? error2.message : "Support konnte nicht angefragt werden."
|
|
787
|
+
);
|
|
788
|
+
} finally {
|
|
789
|
+
sending.current = false;
|
|
446
790
|
setBusy(false);
|
|
447
791
|
}
|
|
448
792
|
}
|
|
@@ -455,7 +799,7 @@ function SupportChat(ctx) {
|
|
|
455
799
|
method: "POST",
|
|
456
800
|
body: "{}"
|
|
457
801
|
})).json();
|
|
458
|
-
|
|
802
|
+
applySnapshot(result);
|
|
459
803
|
pending.current = null;
|
|
460
804
|
setInput("");
|
|
461
805
|
} catch (e) {
|
|
@@ -463,6 +807,7 @@ function SupportChat(ctx) {
|
|
|
463
807
|
e instanceof Error ? e.message : "Problem konnte nicht als gel\xF6st markiert werden."
|
|
464
808
|
);
|
|
465
809
|
} finally {
|
|
810
|
+
sending.current = false;
|
|
466
811
|
setBusy(false);
|
|
467
812
|
}
|
|
468
813
|
}
|
|
@@ -479,26 +824,28 @@ function SupportChat(ctx) {
|
|
|
479
824
|
setError(e instanceof Error ? e.message : "Aktion fehlgeschlagen.");
|
|
480
825
|
}
|
|
481
826
|
}
|
|
482
|
-
return /* @__PURE__ */
|
|
483
|
-
/* @__PURE__ */
|
|
484
|
-
/* @__PURE__ */
|
|
485
|
-
current?.status === "support" ? `${current.support?.name} \xB7 Support` : current?.status === "closed" ? current.resolution ? "Problem gel\xF6st" : "Gespr\xE4ch abgeschlossen" : "Digitaler Assistent",
|
|
827
|
+
return /* @__PURE__ */ jsxs3("div", { className: "chat-panel support-chat text-ink bg-card border border-line p-6 rounded-card compact:p-4", children: [
|
|
828
|
+
/* @__PURE__ */ jsx3("p", { className: "text-[0.75rem] text-ink mb-3", children: demo ? "Synthetische Support-Demo. Keine \xDCbertragung an einen Support-Channel." : `Ihre Nachrichten und die Antworten werden an den Support in ${ctx.config.chatSupport?.mode === "slack" ? "Slack" : ctx.config.chatSupport?.mode === "telegram" ? "Telegram" : "Microsoft Teams"} weitergegeben.` }),
|
|
829
|
+
/* @__PURE__ */ jsxs3("p", { role: "status", className: "text-[0.8125rem] mb-3", children: [
|
|
830
|
+
current?.status === "support" ? `${current.support?.name} \xB7 Support` : current?.status === "waiting" ? current.supportRequest?.notification === "sent" ? "Support-Team informiert \xB7 KI pausiert" : current.supportRequest?.notification === "failed" ? "Zustellung verz\xF6gert \xB7 erneuter Versuch l\xE4uft \xB7 KI pausiert" : "Pers\xF6nlicher Support angefragt \xB7 KI pausiert" : current?.status === "closed" ? current.resolution ? "Problem gel\xF6st" : "Gespr\xE4ch abgeschlossen" : "Digitaler Assistent",
|
|
486
831
|
auth && ready && !demo ? anonymous ? ` \xB7 ${auth.name}` : ` \xB7 Angemeldet als ${auth.name}` : ""
|
|
487
832
|
] }),
|
|
488
|
-
/* @__PURE__ */
|
|
833
|
+
/* @__PURE__ */ jsx3(
|
|
489
834
|
"div",
|
|
490
835
|
{
|
|
491
|
-
|
|
836
|
+
ref: scroll.history,
|
|
837
|
+
onScroll: scroll.onScroll,
|
|
838
|
+
className: "chat-history grid gap-3 max-h-[min(400px,45dvh)] overflow-auto overscroll-contain mb-4",
|
|
492
839
|
"aria-live": "polite",
|
|
493
840
|
"aria-label": "Gespr\xE4chsverlauf",
|
|
494
|
-
children: current?.messages.map((m) => /* @__PURE__ */
|
|
841
|
+
children: current?.messages.map((m) => /* @__PURE__ */ jsxs3(
|
|
495
842
|
"div",
|
|
496
843
|
{
|
|
497
844
|
className: `message ${m.kind === "user" ? "user bg-soft p-3 rounded-card" : "assistant border-l-2 border-accent pl-4"} text-[0.8125rem] whitespace-pre-wrap [overflow-wrap:anywhere]`,
|
|
498
845
|
children: [
|
|
499
|
-
/* @__PURE__ */
|
|
846
|
+
/* @__PURE__ */ jsx3("span", { className: "block text-[0.6875rem] text-ink mb-1", children: m.kind === "user" ? "Sie" : m.kind === "support" ? `${m.name} \xB7 Support` : m.kind === "bot" ? m.responder?.role === "on-duty" && m.responder.name ? `Digitaler Assistent von ${m.responder.name}` : "Digitaler Assistent des Service-Teams" : "Gespr\xE4ch" }),
|
|
500
847
|
m.kind === "bot" ? answerParts(m.content, m.sources ?? [], !m.complete).map(
|
|
501
|
-
(part, i) => part.href ? /* @__PURE__ */
|
|
848
|
+
(part, i) => part.href ? /* @__PURE__ */ jsx3(
|
|
502
849
|
"a",
|
|
503
850
|
{
|
|
504
851
|
href: part.href,
|
|
@@ -510,14 +857,15 @@ function SupportChat(ctx) {
|
|
|
510
857
|
i
|
|
511
858
|
) : part.text
|
|
512
859
|
) : m.content,
|
|
513
|
-
!m.complete && !m.content && /* @__PURE__ */
|
|
860
|
+
!m.complete && !m.content && /* @__PURE__ */ jsx3("span", { children: "Antwort wird erstellt \u2026" })
|
|
514
861
|
]
|
|
515
862
|
},
|
|
516
863
|
m.id
|
|
517
864
|
))
|
|
518
865
|
}
|
|
519
866
|
),
|
|
520
|
-
|
|
867
|
+
/* @__PURE__ */ jsx3(NewMessages, { visible: scroll.unread, follow: scroll.follow }),
|
|
868
|
+
!ready && /* @__PURE__ */ jsx3(
|
|
521
869
|
"button",
|
|
522
870
|
{
|
|
523
871
|
type: "button",
|
|
@@ -531,32 +879,59 @@ function SupportChat(ctx) {
|
|
|
531
879
|
children: anonymous ? "Chat neu verbinden" : ctx.config.chatSupport?.auth === "session" ? "Portal-Anmeldung erneuern" : "Mit Microsoft anmelden"
|
|
532
880
|
}
|
|
533
881
|
),
|
|
534
|
-
ready && /* @__PURE__ */
|
|
535
|
-
/* @__PURE__ */
|
|
536
|
-
|
|
537
|
-
|
|
882
|
+
ready && /* @__PURE__ */ jsxs3("div", { children: [
|
|
883
|
+
/* @__PURE__ */ jsxs3("form", { onSubmit: submit, className: "chat-form flex gap-2.5 items-end", children: [
|
|
884
|
+
/* @__PURE__ */ jsx3("label", { htmlFor: "support-chat-input", className: "sr-only", children: "Ihre Nachricht" }),
|
|
885
|
+
/* @__PURE__ */ jsx3(
|
|
886
|
+
"textarea",
|
|
887
|
+
{
|
|
888
|
+
id: "support-chat-input",
|
|
889
|
+
value: input,
|
|
890
|
+
rows: 2,
|
|
891
|
+
maxLength: 4e3,
|
|
892
|
+
disabled: current?.status === "closed",
|
|
893
|
+
className: "flex-1 min-w-0 max-h-[180px] overflow-auto resize-none border border-line rounded-card bg-card p-3",
|
|
894
|
+
onChange: (e) => setInput(e.target.value),
|
|
895
|
+
onKeyDown: (e) => sendShortcut(e, blocked || !input.trim()),
|
|
896
|
+
"aria-keyshortcuts": "Control+Enter Meta+Enter",
|
|
897
|
+
"aria-describedby": "support-chat-shortcut",
|
|
898
|
+
placeholder: "Wie k\xF6nnen wir Ihnen helfen?"
|
|
899
|
+
}
|
|
900
|
+
),
|
|
901
|
+
/* @__PURE__ */ jsx3(
|
|
902
|
+
"button",
|
|
903
|
+
{
|
|
904
|
+
type: "submit",
|
|
905
|
+
className: "bg-accent text-accent-ink rounded-card p-3",
|
|
906
|
+
disabled: blocked || !input.trim(),
|
|
907
|
+
children: "Senden"
|
|
908
|
+
}
|
|
909
|
+
)
|
|
910
|
+
] }),
|
|
911
|
+
/* @__PURE__ */ jsx3(Shortcut, { id: "support-chat-shortcut" }),
|
|
912
|
+
/* @__PURE__ */ jsx3(
|
|
913
|
+
VoiceInput,
|
|
538
914
|
{
|
|
539
|
-
|
|
915
|
+
ctx,
|
|
540
916
|
value: input,
|
|
541
|
-
|
|
542
|
-
maxLength: 4e3,
|
|
917
|
+
setValue: setInput,
|
|
543
918
|
disabled: current?.status === "closed",
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
placeholder: "Wie k\xF6nnen wir Ihnen helfen?"
|
|
547
|
-
}
|
|
548
|
-
),
|
|
549
|
-
/* @__PURE__ */ jsx(
|
|
550
|
-
"button",
|
|
551
|
-
{
|
|
552
|
-
type: "submit",
|
|
553
|
-
className: "bg-accent text-accent-ink rounded-card p-3",
|
|
554
|
-
disabled: busy || !input.trim() || current?.status === "closed" || !!current?.messages.some((m) => !m.complete),
|
|
555
|
-
children: "Senden"
|
|
919
|
+
token: auth?.token,
|
|
920
|
+
onBusy: setVoiceBusy
|
|
556
921
|
}
|
|
557
922
|
)
|
|
558
923
|
] }),
|
|
559
|
-
ready && current
|
|
924
|
+
ready && (!current || current.status === "bot") && /* @__PURE__ */ jsx3(
|
|
925
|
+
"button",
|
|
926
|
+
{
|
|
927
|
+
type: "button",
|
|
928
|
+
className: "border border-line rounded-card px-4 py-2 mt-3",
|
|
929
|
+
disabled: busy,
|
|
930
|
+
onClick: () => void requestHuman(),
|
|
931
|
+
children: "Ich m\xF6chte mit einer echten Person sprechen"
|
|
932
|
+
}
|
|
933
|
+
),
|
|
934
|
+
ready && current && current.status !== "closed" && current.messages.some((m) => m.kind === "user") && /* @__PURE__ */ jsx3(
|
|
560
935
|
"button",
|
|
561
936
|
{
|
|
562
937
|
type: "button",
|
|
@@ -566,7 +941,7 @@ function SupportChat(ctx) {
|
|
|
566
941
|
children: "Problem gel\xF6st"
|
|
567
942
|
}
|
|
568
943
|
),
|
|
569
|
-
ready && current?.status === "closed" && /* @__PURE__ */
|
|
944
|
+
ready && current?.status === "closed" && /* @__PURE__ */ jsx3(
|
|
570
945
|
"button",
|
|
571
946
|
{
|
|
572
947
|
type: "button",
|
|
@@ -575,12 +950,12 @@ function SupportChat(ctx) {
|
|
|
575
950
|
children: "Neuen Chat starten"
|
|
576
951
|
}
|
|
577
952
|
),
|
|
578
|
-
demo && current && /* @__PURE__ */
|
|
953
|
+
demo && current && /* @__PURE__ */ jsx3(
|
|
579
954
|
"div",
|
|
580
955
|
{
|
|
581
956
|
className: "flex flex-wrap gap-3 mt-3 text-[0.75rem]",
|
|
582
957
|
"aria-label": "Support-Demo-Steuerung",
|
|
583
|
-
children: current.status === "bot" ? /* @__PURE__ */
|
|
958
|
+
children: current.status === "bot" || current.status === "waiting" ? /* @__PURE__ */ jsx3(
|
|
584
959
|
"button",
|
|
585
960
|
{
|
|
586
961
|
type: "button",
|
|
@@ -588,8 +963,8 @@ function SupportChat(ctx) {
|
|
|
588
963
|
onClick: () => void simulate("take"),
|
|
589
964
|
children: "Lena \xFCbernimmt"
|
|
590
965
|
}
|
|
591
|
-
) : current.status === "support" ? /* @__PURE__ */
|
|
592
|
-
/* @__PURE__ */
|
|
966
|
+
) : current.status === "support" ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
967
|
+
/* @__PURE__ */ jsx3(
|
|
593
968
|
"button",
|
|
594
969
|
{
|
|
595
970
|
type: "button",
|
|
@@ -598,7 +973,7 @@ function SupportChat(ctx) {
|
|
|
598
973
|
children: "Lena antwortet"
|
|
599
974
|
}
|
|
600
975
|
),
|
|
601
|
-
/* @__PURE__ */
|
|
976
|
+
/* @__PURE__ */ jsx3(
|
|
602
977
|
"button",
|
|
603
978
|
{
|
|
604
979
|
type: "button",
|
|
@@ -607,7 +982,7 @@ function SupportChat(ctx) {
|
|
|
607
982
|
children: "Bot freigeben"
|
|
608
983
|
}
|
|
609
984
|
),
|
|
610
|
-
/* @__PURE__ */
|
|
985
|
+
/* @__PURE__ */ jsx3(
|
|
611
986
|
"button",
|
|
612
987
|
{
|
|
613
988
|
type: "button",
|
|
@@ -619,7 +994,7 @@ function SupportChat(ctx) {
|
|
|
619
994
|
] }) : null
|
|
620
995
|
}
|
|
621
996
|
),
|
|
622
|
-
error && !demo && auth && ready && /* @__PURE__ */
|
|
997
|
+
error && !demo && auth && ready && /* @__PURE__ */ jsx3(
|
|
623
998
|
"button",
|
|
624
999
|
{
|
|
625
1000
|
type: "button",
|
|
@@ -632,27 +1007,27 @@ function SupportChat(ctx) {
|
|
|
632
1007
|
children: anonymous ? "Chat neu verbinden" : ctx.config.chatSupport?.auth === "session" ? "Portal-Anmeldung erneuern" : "Microsoft-Anmeldung erneuern"
|
|
633
1008
|
}
|
|
634
1009
|
),
|
|
635
|
-
error && /* @__PURE__ */
|
|
1010
|
+
error && /* @__PURE__ */ jsx3("p", { role: "alert", className: "text-danger text-[0.75rem] mt-3", children: error })
|
|
636
1011
|
] });
|
|
637
1012
|
}
|
|
638
1013
|
|
|
639
1014
|
// src/browser.tsx
|
|
640
|
-
import { jsx as
|
|
1015
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
641
1016
|
function resizeInput(field) {
|
|
642
1017
|
if (!field) return;
|
|
643
1018
|
field.style.height = "auto";
|
|
644
1019
|
field.style.height = `${field.scrollHeight + field.offsetHeight - field.clientHeight}px`;
|
|
645
1020
|
}
|
|
646
1021
|
function Chat(ctx) {
|
|
647
|
-
return ctx.config.chatSupport ? /* @__PURE__ */
|
|
1022
|
+
return ctx.config.chatSupport ? /* @__PURE__ */ jsx4(SupportChat, { ...ctx }) : /* @__PURE__ */ jsx4(LegacyChat, { ...ctx });
|
|
648
1023
|
}
|
|
649
1024
|
function LegacyChat(ctx) {
|
|
650
|
-
const [messages, setMessages] =
|
|
651
|
-
const [input, setInput] =
|
|
652
|
-
const [resolved, setResolved] =
|
|
653
|
-
const inputField =
|
|
654
|
-
|
|
655
|
-
|
|
1025
|
+
const [messages, setMessages] = useState4([]);
|
|
1026
|
+
const [input, setInput] = useState4("");
|
|
1027
|
+
const [resolved, setResolved] = useState4(false);
|
|
1028
|
+
const inputField = useRef4(null);
|
|
1029
|
+
useLayoutEffect2(() => resizeInput(inputField.current), [input]);
|
|
1030
|
+
useEffect4(() => {
|
|
656
1031
|
const field = inputField.current;
|
|
657
1032
|
if (!field) return;
|
|
658
1033
|
let width = field.clientWidth;
|
|
@@ -665,15 +1040,19 @@ function LegacyChat(ctx) {
|
|
|
665
1040
|
observer.observe(field);
|
|
666
1041
|
return () => observer.disconnect();
|
|
667
1042
|
}, []);
|
|
668
|
-
const [busy, setBusy] =
|
|
669
|
-
const [
|
|
670
|
-
|
|
671
|
-
const
|
|
672
|
-
const
|
|
673
|
-
|
|
1043
|
+
const [busy, setBusy] = useState4(false);
|
|
1044
|
+
const [voiceBusy, setVoiceBusy] = useState4(false);
|
|
1045
|
+
const scroll = useChatScroll(messages);
|
|
1046
|
+
const sending = useRef4(false);
|
|
1047
|
+
const [error, setError] = useState4("");
|
|
1048
|
+
const abort = useRef4(null);
|
|
1049
|
+
useEffect4(() => () => abort.current?.abort(), []);
|
|
674
1050
|
async function submit(e) {
|
|
675
1051
|
e.preventDefault();
|
|
676
|
-
if (!input.trim() || busy || resolved)
|
|
1052
|
+
if (!input.trim() || busy || voiceBusy || resolved || sending.current)
|
|
1053
|
+
return;
|
|
1054
|
+
sending.current = true;
|
|
1055
|
+
scroll.follow();
|
|
677
1056
|
const history = [
|
|
678
1057
|
...messages.map(({ role, content }) => ({ role, content })),
|
|
679
1058
|
{ role: "user", content: input.trim() }
|
|
@@ -759,45 +1138,49 @@ function LegacyChat(ctx) {
|
|
|
759
1138
|
setError(err instanceof Error ? err.message : "Chat nicht verf\xFCgbar.");
|
|
760
1139
|
} finally {
|
|
761
1140
|
setBusy(false);
|
|
1141
|
+
sending.current = false;
|
|
762
1142
|
abort.current = null;
|
|
763
1143
|
}
|
|
764
1144
|
}
|
|
765
|
-
return /* @__PURE__ */
|
|
1145
|
+
return /* @__PURE__ */ jsxs4(
|
|
766
1146
|
"div",
|
|
767
1147
|
{
|
|
768
1148
|
className: ctx.surface === "hero" ? "chat-layout block" : "chat-layout grid grid-cols-[1fr_1.7fr] gap-13.75 compact:grid-cols-[1fr] compact:gap-3.75",
|
|
769
1149
|
children: [
|
|
770
|
-
ctx.surface !== "hero" && /* @__PURE__ */
|
|
771
|
-
/* @__PURE__ */
|
|
772
|
-
/* @__PURE__ */
|
|
773
|
-
/* @__PURE__ */
|
|
1150
|
+
ctx.surface !== "hero" && /* @__PURE__ */ jsxs4("div", { className: "chat-intro [&_p]:text-[0.875rem] [&_p]:text-muted [&_p]:leading-[1.8] compact:max-w-[500px]", children: [
|
|
1151
|
+
/* @__PURE__ */ jsx4("h3", { children: "Eine Frage reicht." }),
|
|
1152
|
+
/* @__PURE__ */ jsx4("p", { children: "Unser Assistent kennt die Hilfe-Inhalte, das Team und die aktuellen Meldungen. F\xFCr dringende St\xF6rungen bleibt die Hotline der direkte Weg." }),
|
|
1153
|
+
/* @__PURE__ */ jsx4("p", { className: "data-state font-mono text-[0.625rem] text-muted m-[calc(var(--spacing)_*_3)_0_0] [&.warning]:text-warning", children: "\xD6ffentlicher Chat \xB7 Verlauf nur in dieser Sitzung" })
|
|
774
1154
|
] }),
|
|
775
|
-
/* @__PURE__ */
|
|
776
|
-
/* @__PURE__ */
|
|
1155
|
+
/* @__PURE__ */ jsxs4("div", { className: "chat-panel text-ink compact:p-4 bg-card border border-line p-6 rounded-card", children: [
|
|
1156
|
+
/* @__PURE__ */ jsxs4(
|
|
777
1157
|
"div",
|
|
778
1158
|
{
|
|
779
|
-
|
|
1159
|
+
ref: scroll.history,
|
|
1160
|
+
onScroll: scroll.onScroll,
|
|
1161
|
+
className: "chat-history empty:hidden grid gap-3.75 max-h-[min(400px,45dvh)] overflow-auto overscroll-contain mb-4",
|
|
1162
|
+
"aria-label": "Gespr\xE4chsverlauf",
|
|
780
1163
|
"aria-live": "polite",
|
|
781
1164
|
"aria-busy": busy,
|
|
782
1165
|
children: [
|
|
783
|
-
!messages.length && ctx.surface !== "hero" && /* @__PURE__ */
|
|
784
|
-
messages.map((m, i) => /* @__PURE__ */
|
|
1166
|
+
!messages.length && ctx.surface !== "hero" && /* @__PURE__ */ jsx4("p", { className: "message assistant text-[0.8125rem] leading-[1.8] whitespace-pre-wrap [overflow-wrap:anywhere] [&.user]:bg-soft [&.user]:pt-3 [&.user]:pb-3 [&.user]:pl-4 [&.user]:pr-4 [&.user]:rounded-card [&.assistant]:[border-left:2px_solid_var(--accent)] [&.assistant]:pt-[0] [&.assistant]:pb-[0] [&.assistant]:pl-4 [&.assistant]:pr-4", children: "Wie kann ich Ihnen helfen?" }),
|
|
1167
|
+
messages.map((m, i) => /* @__PURE__ */ jsxs4(
|
|
785
1168
|
"div",
|
|
786
1169
|
{
|
|
787
1170
|
className: "message text-[0.8125rem] leading-[1.8] whitespace-pre-wrap [overflow-wrap:anywhere] [&.user]:bg-soft [&.user]:pt-3 [&.user]:pb-3 [&.user]:pl-4 [&.user]:pr-4 [&.user]:rounded-card [&.assistant]:[border-left:2px_solid_var(--accent)] [&.assistant]:pt-[0] [&.assistant]:pb-[0] [&.assistant]:pl-4 [&.assistant]:pr-4 " + m.role,
|
|
788
1171
|
children: [
|
|
789
|
-
m.role === "user" ? /* @__PURE__ */
|
|
1172
|
+
m.role === "user" ? /* @__PURE__ */ jsx4("span", { className: "sr-only", children: "Sie: " }) : /* @__PURE__ */ jsx4("span", { className: "block text-[0.6875rem] text-muted mb-1", children: m.responder?.role === "on-duty" && m.responder.name ? `Digitaler Assistent von ${m.responder.name}` : "Digitaler Assistent des Service-Teams" }),
|
|
790
1173
|
m.role === "user" ? m.content : m.content ? answerParts(m.content, m.sources ?? [], !m.complete).map(
|
|
791
|
-
(part, index) => part.href ? /* @__PURE__ */
|
|
1174
|
+
(part, index) => part.href ? /* @__PURE__ */ jsx4("a", { href: part.href, children: part.text }, index) : /* @__PURE__ */ jsx4("span", { children: part.text }, index)
|
|
792
1175
|
) : "Antwort wird vorbereitet \u2026",
|
|
793
|
-
m.role === "assistant" && !!m.sources?.length && /* @__PURE__ */
|
|
1176
|
+
m.role === "assistant" && !!m.sources?.length && /* @__PURE__ */ jsxs4("div", { className: "sources text-[0.6875rem] text-muted mt-3 mb-1 [&_a]:mr-3", children: [
|
|
794
1177
|
"Referenzquellen:",
|
|
795
1178
|
" ",
|
|
796
1179
|
m.sources.map(
|
|
797
|
-
(source) => sourceHref(source.href) ? /* @__PURE__ */
|
|
1180
|
+
(source) => sourceHref(source.href) ? /* @__PURE__ */ jsxs4("a", { href: sourceHref(source.href), children: [
|
|
798
1181
|
source.title,
|
|
799
1182
|
source.stale ? " (veraltet)" : ""
|
|
800
|
-
] }, source.id) : /* @__PURE__ */
|
|
1183
|
+
] }, source.id) : /* @__PURE__ */ jsxs4("span", { children: [
|
|
801
1184
|
source.title,
|
|
802
1185
|
" "
|
|
803
1186
|
] }, source.id)
|
|
@@ -810,56 +1193,41 @@ function LegacyChat(ctx) {
|
|
|
810
1193
|
]
|
|
811
1194
|
}
|
|
812
1195
|
),
|
|
813
|
-
/* @__PURE__ */
|
|
1196
|
+
/* @__PURE__ */ jsx4(NewMessages, { visible: scroll.unread, follow: scroll.follow }),
|
|
1197
|
+
/* @__PURE__ */ jsxs4(
|
|
814
1198
|
"form",
|
|
815
1199
|
{
|
|
816
1200
|
className: "chat-form flex gap-2.5 items-end [&_textarea]:resize-none [&_textarea]:min-h-[50px] [&_textarea]:[flex:1] [&_textarea]:min-w-0 [&_button]:bg-accent [&_button]:text-accent-ink [&_button]:[border:0] [&_button]:w-[50px] [&_button]:h-[50px] [&_button]:rounded-card [&_button]:text-[1.25rem] [&_button:disabled]:opacity-[0.5]",
|
|
817
1201
|
onSubmit: submit,
|
|
818
1202
|
children: [
|
|
819
|
-
/* @__PURE__ */
|
|
820
|
-
/* @__PURE__ */
|
|
821
|
-
/* @__PURE__ */
|
|
1203
|
+
/* @__PURE__ */ jsx4("label", { className: "sr-only", htmlFor: "chat-input", children: "Ihre Frage" }),
|
|
1204
|
+
/* @__PURE__ */ jsxs4("div", { className: "relative flex-1 min-w-0", children: [
|
|
1205
|
+
/* @__PURE__ */ jsx4(
|
|
822
1206
|
"textarea",
|
|
823
1207
|
{
|
|
824
1208
|
id: "chat-input",
|
|
825
1209
|
ref: inputField,
|
|
826
1210
|
rows: 1,
|
|
827
1211
|
autoFocus: true,
|
|
828
|
-
className: "search bg-card border border-line rounded-card pt-3.5 pb-7 px-4.5 text-ink w-full block overflow-
|
|
1212
|
+
className: "search bg-card border border-line rounded-card pt-3.5 pb-7 px-4.5 text-ink w-full block max-h-[180px] overflow-auto",
|
|
829
1213
|
value: input,
|
|
830
1214
|
disabled: resolved,
|
|
831
1215
|
maxLength: 4e3,
|
|
832
1216
|
onChange: (e) => setInput(e.target.value),
|
|
833
|
-
onKeyDown: (e) =>
|
|
834
|
-
if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && !e.nativeEvent.isComposing && !e.repeat) {
|
|
835
|
-
e.preventDefault();
|
|
836
|
-
if (!busy && input.trim())
|
|
837
|
-
e.currentTarget.form?.requestSubmit();
|
|
838
|
-
}
|
|
839
|
-
},
|
|
1217
|
+
onKeyDown: (e) => sendShortcut(e, busy || voiceBusy || resolved || !input.trim()),
|
|
840
1218
|
"aria-keyshortcuts": "Control+Enter Meta+Enter",
|
|
841
1219
|
"aria-describedby": "chat-shortcut",
|
|
842
1220
|
placeholder: "Ihre Frage an unser Team \u2026",
|
|
843
1221
|
required: true
|
|
844
1222
|
}
|
|
845
1223
|
),
|
|
846
|
-
/* @__PURE__ */
|
|
847
|
-
"span",
|
|
848
|
-
{
|
|
849
|
-
id: "chat-shortcut",
|
|
850
|
-
className: "absolute right-4.5 bottom-2 text-[0.625rem] text-muted pointer-events-none",
|
|
851
|
-
children: [
|
|
852
|
-
isMac ? "\u2318" : "Strg",
|
|
853
|
-
" + Enter zum Senden"
|
|
854
|
-
]
|
|
855
|
-
}
|
|
856
|
-
)
|
|
1224
|
+
/* @__PURE__ */ jsx4(Shortcut, { id: "chat-shortcut" })
|
|
857
1225
|
] }),
|
|
858
|
-
/* @__PURE__ */
|
|
1226
|
+
/* @__PURE__ */ jsx4(
|
|
859
1227
|
"button",
|
|
860
1228
|
{
|
|
861
1229
|
type: "submit",
|
|
862
|
-
disabled: busy || resolved || !input.trim(),
|
|
1230
|
+
disabled: busy || voiceBusy || resolved || !input.trim(),
|
|
863
1231
|
"aria-label": "Frage senden",
|
|
864
1232
|
children: "\u2191"
|
|
865
1233
|
}
|
|
@@ -867,8 +1235,18 @@ function LegacyChat(ctx) {
|
|
|
867
1235
|
]
|
|
868
1236
|
}
|
|
869
1237
|
),
|
|
870
|
-
|
|
871
|
-
|
|
1238
|
+
/* @__PURE__ */ jsx4(
|
|
1239
|
+
VoiceInput,
|
|
1240
|
+
{
|
|
1241
|
+
ctx,
|
|
1242
|
+
value: input,
|
|
1243
|
+
setValue: setInput,
|
|
1244
|
+
disabled: resolved,
|
|
1245
|
+
onBusy: setVoiceBusy
|
|
1246
|
+
}
|
|
1247
|
+
),
|
|
1248
|
+
ctx.surface === "hero" && /* @__PURE__ */ jsx4("p", { className: "data-state font-mono text-[0.625rem] text-muted mt-3 mb-0", children: "Digitaler Assistent \xB7 Ihre Frage wird automatisch beantwortet. F\xFCr pers\xF6nlichen Kontakt nutzen Sie die Hotline." }),
|
|
1249
|
+
messages.length > 0 && !resolved && /* @__PURE__ */ jsx4(
|
|
872
1250
|
"button",
|
|
873
1251
|
{
|
|
874
1252
|
type: "button",
|
|
@@ -891,15 +1269,16 @@ function LegacyChat(ctx) {
|
|
|
891
1269
|
children: "Problem gel\xF6st"
|
|
892
1270
|
}
|
|
893
1271
|
),
|
|
894
|
-
resolved && /* @__PURE__ */
|
|
895
|
-
/* @__PURE__ */
|
|
896
|
-
/* @__PURE__ */
|
|
1272
|
+
resolved && /* @__PURE__ */ jsxs4("div", { className: "mt-3", children: [
|
|
1273
|
+
/* @__PURE__ */ jsx4("p", { role: "status", children: "Problem gel\xF6st" }),
|
|
1274
|
+
/* @__PURE__ */ jsx4(
|
|
897
1275
|
"button",
|
|
898
1276
|
{
|
|
899
1277
|
type: "button",
|
|
900
1278
|
className: "underline mt-2",
|
|
901
1279
|
disabled: busy,
|
|
902
1280
|
onClick: () => {
|
|
1281
|
+
scroll.follow();
|
|
903
1282
|
setMessages([]);
|
|
904
1283
|
setResolved(false);
|
|
905
1284
|
setInput("");
|
|
@@ -909,7 +1288,7 @@ function LegacyChat(ctx) {
|
|
|
909
1288
|
}
|
|
910
1289
|
)
|
|
911
1290
|
] }),
|
|
912
|
-
error && /* @__PURE__ */
|
|
1291
|
+
error && /* @__PURE__ */ jsx4(
|
|
913
1292
|
"p",
|
|
914
1293
|
{
|
|
915
1294
|
className: "chat-error text-danger dark:text-warning text-[0.75rem]",
|
package/dist/server.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
|
|
|
7
7
|
CSP_CHAT_DEMO: z.ZodOptional<z.ZodEnum<["true", "false"]>>;
|
|
8
8
|
CSP_CHAT_PROVIDER: z.ZodDefault<z.ZodEnum<["openai", "azure", "ollama"]>>;
|
|
9
9
|
CSP_CHAT_MODEL: z.ZodOptional<z.ZodString>;
|
|
10
|
+
CSP_CHAT_TRANSCRIPTION_ENABLED: z.ZodDefault<z.ZodEnum<["true", "false"]>>;
|
|
11
|
+
CSP_CHAT_TRANSCRIPTION_MODEL: z.ZodDefault<z.ZodString>;
|
|
10
12
|
CSP_CHAT_OPENAI_API_KEY: z.ZodOptional<z.ZodString>;
|
|
11
13
|
CSP_CHAT_OPENAI_URL: z.ZodOptional<z.ZodString>;
|
|
12
14
|
CSP_CHAT_AZURE_API_KEY: z.ZodOptional<z.ZodString>;
|
|
@@ -19,6 +21,8 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
|
|
|
19
21
|
CSP_CHAT_CONTEXT_CHARS: z.ZodDefault<z.ZodNumber>;
|
|
20
22
|
}, "strip", z.ZodTypeAny, {
|
|
21
23
|
CSP_CHAT_PROVIDER: "openai" | "azure" | "ollama";
|
|
24
|
+
CSP_CHAT_TRANSCRIPTION_ENABLED: "true" | "false";
|
|
25
|
+
CSP_CHAT_TRANSCRIPTION_MODEL: string;
|
|
22
26
|
CSP_CHAT_RATE_LIMIT: number;
|
|
23
27
|
CSP_CHAT_CONCURRENCY: number;
|
|
24
28
|
CSP_CHAT_MAX_TOKENS: number;
|
|
@@ -37,6 +41,8 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
|
|
|
37
41
|
CSP_CHAT_DEMO?: "true" | "false" | undefined;
|
|
38
42
|
CSP_CHAT_PROVIDER?: "openai" | "azure" | "ollama" | undefined;
|
|
39
43
|
CSP_CHAT_MODEL?: string | undefined;
|
|
44
|
+
CSP_CHAT_TRANSCRIPTION_ENABLED?: "true" | "false" | undefined;
|
|
45
|
+
CSP_CHAT_TRANSCRIPTION_MODEL?: string | undefined;
|
|
40
46
|
CSP_CHAT_OPENAI_API_KEY?: string | undefined;
|
|
41
47
|
CSP_CHAT_OPENAI_URL?: string | undefined;
|
|
42
48
|
CSP_CHAT_AZURE_API_KEY?: string | undefined;
|
|
@@ -49,6 +55,8 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
|
|
|
49
55
|
CSP_CHAT_CONTEXT_CHARS?: number | undefined;
|
|
50
56
|
}>, {
|
|
51
57
|
CSP_CHAT_PROVIDER: "openai" | "azure" | "ollama";
|
|
58
|
+
CSP_CHAT_TRANSCRIPTION_ENABLED: "true" | "false";
|
|
59
|
+
CSP_CHAT_TRANSCRIPTION_MODEL: string;
|
|
52
60
|
CSP_CHAT_RATE_LIMIT: number;
|
|
53
61
|
CSP_CHAT_CONCURRENCY: number;
|
|
54
62
|
CSP_CHAT_MAX_TOKENS: number;
|
|
@@ -67,6 +75,8 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
|
|
|
67
75
|
CSP_CHAT_DEMO?: "true" | "false" | undefined;
|
|
68
76
|
CSP_CHAT_PROVIDER?: "openai" | "azure" | "ollama" | undefined;
|
|
69
77
|
CSP_CHAT_MODEL?: string | undefined;
|
|
78
|
+
CSP_CHAT_TRANSCRIPTION_ENABLED?: "true" | "false" | undefined;
|
|
79
|
+
CSP_CHAT_TRANSCRIPTION_MODEL?: string | undefined;
|
|
70
80
|
CSP_CHAT_OPENAI_API_KEY?: string | undefined;
|
|
71
81
|
CSP_CHAT_OPENAI_URL?: string | undefined;
|
|
72
82
|
CSP_CHAT_AZURE_API_KEY?: string | undefined;
|
|
@@ -88,6 +98,8 @@ declare const _default: {
|
|
|
88
98
|
CSP_CHAT_DEMO: z.ZodOptional<z.ZodEnum<["true", "false"]>>;
|
|
89
99
|
CSP_CHAT_PROVIDER: z.ZodDefault<z.ZodEnum<["openai", "azure", "ollama"]>>;
|
|
90
100
|
CSP_CHAT_MODEL: z.ZodOptional<z.ZodString>;
|
|
101
|
+
CSP_CHAT_TRANSCRIPTION_ENABLED: z.ZodDefault<z.ZodEnum<["true", "false"]>>;
|
|
102
|
+
CSP_CHAT_TRANSCRIPTION_MODEL: z.ZodDefault<z.ZodString>;
|
|
91
103
|
CSP_CHAT_OPENAI_API_KEY: z.ZodOptional<z.ZodString>;
|
|
92
104
|
CSP_CHAT_OPENAI_URL: z.ZodOptional<z.ZodString>;
|
|
93
105
|
CSP_CHAT_AZURE_API_KEY: z.ZodOptional<z.ZodString>;
|
|
@@ -100,6 +112,8 @@ declare const _default: {
|
|
|
100
112
|
CSP_CHAT_CONTEXT_CHARS: z.ZodDefault<z.ZodNumber>;
|
|
101
113
|
}, "strip", z.ZodTypeAny, {
|
|
102
114
|
CSP_CHAT_PROVIDER: "openai" | "azure" | "ollama";
|
|
115
|
+
CSP_CHAT_TRANSCRIPTION_ENABLED: "true" | "false";
|
|
116
|
+
CSP_CHAT_TRANSCRIPTION_MODEL: string;
|
|
103
117
|
CSP_CHAT_RATE_LIMIT: number;
|
|
104
118
|
CSP_CHAT_CONCURRENCY: number;
|
|
105
119
|
CSP_CHAT_MAX_TOKENS: number;
|
|
@@ -118,6 +132,8 @@ declare const _default: {
|
|
|
118
132
|
CSP_CHAT_DEMO?: "true" | "false" | undefined;
|
|
119
133
|
CSP_CHAT_PROVIDER?: "openai" | "azure" | "ollama" | undefined;
|
|
120
134
|
CSP_CHAT_MODEL?: string | undefined;
|
|
135
|
+
CSP_CHAT_TRANSCRIPTION_ENABLED?: "true" | "false" | undefined;
|
|
136
|
+
CSP_CHAT_TRANSCRIPTION_MODEL?: string | undefined;
|
|
121
137
|
CSP_CHAT_OPENAI_API_KEY?: string | undefined;
|
|
122
138
|
CSP_CHAT_OPENAI_URL?: string | undefined;
|
|
123
139
|
CSP_CHAT_AZURE_API_KEY?: string | undefined;
|
|
@@ -130,6 +146,8 @@ declare const _default: {
|
|
|
130
146
|
CSP_CHAT_CONTEXT_CHARS?: number | undefined;
|
|
131
147
|
}>, {
|
|
132
148
|
CSP_CHAT_PROVIDER: "openai" | "azure" | "ollama";
|
|
149
|
+
CSP_CHAT_TRANSCRIPTION_ENABLED: "true" | "false";
|
|
150
|
+
CSP_CHAT_TRANSCRIPTION_MODEL: string;
|
|
133
151
|
CSP_CHAT_RATE_LIMIT: number;
|
|
134
152
|
CSP_CHAT_CONCURRENCY: number;
|
|
135
153
|
CSP_CHAT_MAX_TOKENS: number;
|
|
@@ -148,6 +166,8 @@ declare const _default: {
|
|
|
148
166
|
CSP_CHAT_DEMO?: "true" | "false" | undefined;
|
|
149
167
|
CSP_CHAT_PROVIDER?: "openai" | "azure" | "ollama" | undefined;
|
|
150
168
|
CSP_CHAT_MODEL?: string | undefined;
|
|
169
|
+
CSP_CHAT_TRANSCRIPTION_ENABLED?: "true" | "false" | undefined;
|
|
170
|
+
CSP_CHAT_TRANSCRIPTION_MODEL?: string | undefined;
|
|
151
171
|
CSP_CHAT_OPENAI_API_KEY?: string | undefined;
|
|
152
172
|
CSP_CHAT_OPENAI_URL?: string | undefined;
|
|
153
173
|
CSP_CHAT_AZURE_API_KEY?: string | undefined;
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cspPlugin
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-DZ3YQB4V.js";
|
|
4
4
|
|
|
5
5
|
// src/server.ts
|
|
6
6
|
import { z as z2 } from "zod";
|
|
@@ -194,12 +194,138 @@ Bieten Sie bei erforderlicher Eskalation belegte pers\xF6nliche Kontaktm\xF6glic
|
|
|
194
194
|
Nur vorhandene Quellen-IDs zitieren, keine URLs oder technischen Ticketbezeichnungen erfinden. Kein Markdown-Link, kein HTML. Keine Aktionen ausf\xFChren, keine Ticketanlage oder pers\xF6nliche Pr\xFCfung behaupten. Notf\xE4lle: Hotline ${JSON.stringify(phone)}. Automatische Antwort, kein menschlicher Live-Chat.`;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// src/transcription.ts
|
|
198
|
+
import OpenAI2, { toFile } from "openai";
|
|
199
|
+
var limit = 10 * 1024 * 1024;
|
|
200
|
+
var formats = {
|
|
201
|
+
"audio/webm": "webm",
|
|
202
|
+
"audio/mp4": "mp4",
|
|
203
|
+
"video/mp4": "mp4",
|
|
204
|
+
"audio/ogg": "ogg",
|
|
205
|
+
"audio/wav": "wav",
|
|
206
|
+
"audio/mpeg": "mp3"
|
|
207
|
+
};
|
|
208
|
+
function validAudio(bytes, format) {
|
|
209
|
+
if (format === "webm")
|
|
210
|
+
return bytes.subarray(0, 4).equals(Buffer.from([26, 69, 223, 163]));
|
|
211
|
+
if (format === "mp4") return bytes.subarray(4, 8).toString() === "ftyp";
|
|
212
|
+
if (format === "ogg") return bytes.subarray(0, 4).toString() === "OggS";
|
|
213
|
+
if (format === "wav")
|
|
214
|
+
return bytes.subarray(0, 4).toString() === "RIFF" && bytes.subarray(8, 12).toString() === "WAVE";
|
|
215
|
+
return bytes.subarray(0, 3).toString() === "ID3" || bytes[0] === 255 && (bytes[1] & 224) === 224;
|
|
216
|
+
}
|
|
217
|
+
function setupTranscription(ctx, config, demo) {
|
|
218
|
+
const enabled = !demo && config.CSP_CHAT_TRANSCRIPTION_ENABLED === "true" && config.CSP_CHAT_PROVIDER === "openai" && Boolean(config.CSP_CHAT_OPENAI_API_KEY);
|
|
219
|
+
ctx.app.get("/api/v1/chat/capabilities", async () => ({
|
|
220
|
+
transcription: enabled,
|
|
221
|
+
maxRecordingSeconds: 60,
|
|
222
|
+
maxAudioBytes: limit
|
|
223
|
+
}));
|
|
224
|
+
const client = enabled ? new OpenAI2({
|
|
225
|
+
apiKey: config.CSP_CHAT_OPENAI_API_KEY,
|
|
226
|
+
baseURL: config.CSP_CHAT_OPENAI_URL,
|
|
227
|
+
fetch: ctx.fetch,
|
|
228
|
+
maxRetries: 0
|
|
229
|
+
}) : void 0;
|
|
230
|
+
const windows = /* @__PURE__ */ new Map();
|
|
231
|
+
let active = 0;
|
|
232
|
+
const reservations = /* @__PURE__ */ new WeakSet();
|
|
233
|
+
const release = (req) => {
|
|
234
|
+
if (reservations.delete(req)) active--;
|
|
235
|
+
};
|
|
236
|
+
const controllers = /* @__PURE__ */ new Set();
|
|
237
|
+
ctx.app.addHook("preClose", async () => {
|
|
238
|
+
for (const controller of controllers) controller.abort();
|
|
239
|
+
});
|
|
240
|
+
ctx.app.register(async (scope) => {
|
|
241
|
+
scope.addHook("onResponse", async (req) => release(req));
|
|
242
|
+
scope.addHook("onRequestAbort", async (req) => release(req));
|
|
243
|
+
scope.addContentTypeParser(
|
|
244
|
+
Object.keys(formats),
|
|
245
|
+
{ parseAs: "buffer", bodyLimit: limit },
|
|
246
|
+
(_req, body, done) => done(null, body)
|
|
247
|
+
);
|
|
248
|
+
scope.post(
|
|
249
|
+
"/api/v1/chat/transcriptions",
|
|
250
|
+
{
|
|
251
|
+
bodyLimit: limit,
|
|
252
|
+
onRequest: async (req, reply) => {
|
|
253
|
+
if (!enabled)
|
|
254
|
+
return reply.code(404).send({ error: "Spracheingabe ist nicht verf\xFCgbar." });
|
|
255
|
+
const identity = ctx.config.chatSupport ? await ctx.authenticateChat?.(req.headers.authorization) : void 0;
|
|
256
|
+
if (ctx.config.chatSupport && !identity)
|
|
257
|
+
return reply.code(401).send({ error: "Chat-Anmeldung erforderlich." });
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
for (const [key, value] of windows)
|
|
260
|
+
if (value.until <= now) windows.delete(key);
|
|
261
|
+
const keys = [
|
|
262
|
+
"ip:" + req.ip,
|
|
263
|
+
...identity ? ["user:" + identity.tenantId + ":" + identity.userId] : []
|
|
264
|
+
];
|
|
265
|
+
if (active >= config.CSP_CHAT_CONCURRENCY || windows.size >= 1e4 || keys.some(
|
|
266
|
+
(key) => (windows.get(key)?.count ?? 0) >= config.CSP_CHAT_RATE_LIMIT
|
|
267
|
+
))
|
|
268
|
+
return reply.code(429).header("Retry-After", "60").send({ error: "Bitte warten Sie eine Minute." });
|
|
269
|
+
active++;
|
|
270
|
+
reservations.add(req);
|
|
271
|
+
for (const key of keys) {
|
|
272
|
+
const value = windows.get(key) ?? { count: 0, until: now + 6e4 };
|
|
273
|
+
value.count++;
|
|
274
|
+
windows.set(key, value);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
async (req, reply) => {
|
|
279
|
+
const mime = req.headers["content-type"]?.split(";")[0].trim() ?? "", format = formats[mime], bytes = req.body;
|
|
280
|
+
if (!format || !Buffer.isBuffer(bytes) || !bytes.length || !validAudio(bytes, format))
|
|
281
|
+
return reply.code(400).send({ error: "Ung\xFCltige Audioaufnahme." });
|
|
282
|
+
const controller = new AbortController();
|
|
283
|
+
controllers.add(controller);
|
|
284
|
+
const timeout = setTimeout(
|
|
285
|
+
() => controller.abort(),
|
|
286
|
+
config.CSP_CHAT_TIMEOUT_MS
|
|
287
|
+
), cancel = () => controller.abort();
|
|
288
|
+
reply.raw.on("close", cancel);
|
|
289
|
+
try {
|
|
290
|
+
const result = await client.audio.transcriptions.create(
|
|
291
|
+
{
|
|
292
|
+
file: await toFile(bytes, "recording." + format, { type: mime }),
|
|
293
|
+
model: config.CSP_CHAT_TRANSCRIPTION_MODEL,
|
|
294
|
+
response_format: "json"
|
|
295
|
+
},
|
|
296
|
+
{ signal: controller.signal }
|
|
297
|
+
);
|
|
298
|
+
if (typeof result.text !== "string" || !result.text.trim())
|
|
299
|
+
return reply.code(422).send({
|
|
300
|
+
error: "Keine Sprache erkannt. Bitte versuchen Sie es erneut."
|
|
301
|
+
});
|
|
302
|
+
if (result.text.length > 4e3)
|
|
303
|
+
return reply.code(422).send({
|
|
304
|
+
error: "Die Aufnahme enth\xE4lt zu viel Text. Bitte k\xFCrzer diktieren."
|
|
305
|
+
});
|
|
306
|
+
return { text: result.text.trim() };
|
|
307
|
+
} catch {
|
|
308
|
+
return reply.code(controller.signal.aborted ? 504 : 503).send({
|
|
309
|
+
error: "Spracheingabe derzeit nicht verf\xFCgbar. Ihr Entwurf bleibt erhalten."
|
|
310
|
+
});
|
|
311
|
+
} finally {
|
|
312
|
+
clearTimeout(timeout);
|
|
313
|
+
reply.raw.off("close", cancel);
|
|
314
|
+
controllers.delete(controller);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
197
321
|
// src/server.ts
|
|
198
322
|
var configSchema = z2.object({
|
|
199
323
|
CSP_DEMO: z2.string().optional(),
|
|
200
324
|
CSP_CHAT_DEMO: z2.enum(["true", "false"]).optional(),
|
|
201
325
|
CSP_CHAT_PROVIDER: z2.enum(["openai", "azure", "ollama"]).default("ollama"),
|
|
202
326
|
CSP_CHAT_MODEL: z2.string().optional(),
|
|
327
|
+
CSP_CHAT_TRANSCRIPTION_ENABLED: z2.enum(["true", "false"]).default("true"),
|
|
328
|
+
CSP_CHAT_TRANSCRIPTION_MODEL: z2.string().min(1).max(100).default("gpt-4o-mini-transcribe"),
|
|
203
329
|
CSP_CHAT_OPENAI_API_KEY: z2.string().optional(),
|
|
204
330
|
CSP_CHAT_OPENAI_URL: z2.string().url().optional(),
|
|
205
331
|
CSP_CHAT_AZURE_API_KEY: z2.string().optional(),
|
|
@@ -286,6 +412,7 @@ var server_default = {
|
|
|
286
412
|
setup(ctx) {
|
|
287
413
|
const c = configSchema.parse(ctx.env);
|
|
288
414
|
const demo = c.CSP_CHAT_DEMO === "true" || c.CSP_CHAT_DEMO !== "false" && ctx.demo;
|
|
415
|
+
setupTranscription(ctx, c, demo);
|
|
289
416
|
const provider = demo ? void 0 : createProvider(
|
|
290
417
|
{
|
|
291
418
|
...ctx.env,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kieksme/csp-plugin-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@azure/msal-browser": "^5.25.0",
|
|
26
|
-
"@kieksme/csp-sdk": "^0.
|
|
26
|
+
"@kieksme/csp-sdk": "^0.12.0",
|
|
27
27
|
"openai": "^6.3.0",
|
|
28
28
|
"zod": "^3.25.76"
|
|
29
29
|
},
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"cspPlugin": {
|
|
37
37
|
"id": "chat",
|
|
38
|
-
"sdkVersion": "^0.
|
|
38
|
+
"sdkVersion": "^0.12.0",
|
|
39
39
|
"browser": "./browser",
|
|
40
40
|
"server": "./server"
|
|
41
41
|
},
|