refactorings
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Chat } from "./chat"
|
import { WerkChatSession } from "@kontextwerk/web-sdk"
|
||||||
import InputRow from "./InputRow.svelte"
|
import InputRow from "./InputRow.svelte"
|
||||||
import Messages from "./Messages.svelte"
|
import Messages from "./Messages.svelte"
|
||||||
function generateUniqueId() {
|
function generateUniqueId() {
|
||||||
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
|
||||||
}
|
}
|
||||||
let chat = $derived(
|
let chat = $derived(
|
||||||
new Chat(generateUniqueId(), {
|
new WerkChatSession(generateUniqueId(), {
|
||||||
url: "https://2schat-server.robins-spielwiese.de/api/v1/chatbot/stream",
|
url: "https://2schat-server.robins-spielwiese.de/api/v1/chatbot/stream",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { mdiSendVariantOutline } from "@mdi/js"
|
import { mdiSendVariantOutline } from "@mdi/js"
|
||||||
import Icon from "../widgets/Icon.svelte"
|
import Icon from "../widgets/Icon.svelte"
|
||||||
import type { Chat } from "./chat"
|
import type { WerkChatSession } from "@kontextwerk/web-sdk"
|
||||||
let { chat }: { chat: Chat } = $props()
|
let { chat }: { chat: WerkChatSession } = $props()
|
||||||
let value = $state("")
|
let value = $state("")
|
||||||
let textareaEl: HTMLTextAreaElement = $state(null)
|
let textareaEl: HTMLTextAreaElement = $state(null)
|
||||||
function autoResize() {
|
function autoResize() {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Chat } from "./chat"
|
import type { WerkChatSession } from "@kontextwerk/web-sdk"
|
||||||
let {
|
let {
|
||||||
chat,
|
chat,
|
||||||
}: {
|
}: {
|
||||||
chat: Chat
|
chat: WerkChatSession
|
||||||
} = $props()
|
} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,175 +1,2 @@
|
|||||||
import { fetchEventSource } from "@microsoft/fetch-event-source"
|
export { WerkChatSession as Chat } from "@kontextwerk/web-sdk"
|
||||||
|
export type { ChatMessage, ChatOptions } from "@kontextwerk/web-sdk"
|
||||||
export type Role = "user" | "assistant" | "system"
|
|
||||||
|
|
||||||
export interface ChatMessage {
|
|
||||||
role: Role
|
|
||||||
content: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatRequestPayload {
|
|
||||||
user_id: string
|
|
||||||
message: string
|
|
||||||
session_id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerEvent =
|
|
||||||
| { type: "token"; delta: string }
|
|
||||||
| { type: "heartbeat" }
|
|
||||||
| { type: "error"; message: string }
|
|
||||||
| { type: "final"; state: unknown }
|
|
||||||
|
|
||||||
// Optionale Konfiguration für Headers, URL usw.
|
|
||||||
export interface ChatOptions {
|
|
||||||
/** Vollständige URL, z. B. "https://api.example.com/stream" */
|
|
||||||
url: string
|
|
||||||
/** Zusätzliche Request-Header (z. B. Auth) */
|
|
||||||
headers?: Record<string, string>
|
|
||||||
/** Fallback-Assistant-Fehlermeldung für die UI */
|
|
||||||
fallbackErrorText?: string
|
|
||||||
/** Offen lassen, wenn Tab im Hintergrund – sinnvoll für Safari */
|
|
||||||
openWhenHidden?: boolean
|
|
||||||
/** User-ID, wenn du sie nicht pro Aufruf übergibst */
|
|
||||||
userId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Chat {
|
|
||||||
public messages: ChatMessage[] = []
|
|
||||||
public isStreaming = false
|
|
||||||
public lastFinalState: unknown = null
|
|
||||||
|
|
||||||
private sessionId: string
|
|
||||||
private opts: ChatOptions
|
|
||||||
private controller: AbortController | null = null
|
|
||||||
|
|
||||||
constructor(sessionId: string, opts: ChatOptions) {
|
|
||||||
if (!sessionId) throw new Error("session_id ist erforderlich")
|
|
||||||
if (!opts?.url) throw new Error("ChatOptions.url ist erforderlich")
|
|
||||||
this.sessionId = sessionId
|
|
||||||
this.opts = {
|
|
||||||
fallbackErrorText: "Es ist ein Fehler aufgetreten.",
|
|
||||||
openWhenHidden: true,
|
|
||||||
...opts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public abortActiveStream() {
|
|
||||||
this.controller?.abort()
|
|
||||||
this.controller = null
|
|
||||||
this.isStreaming = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sendet eine Benutzer-Nachricht und streamt die Assistent-Antwort in `messages`.
|
|
||||||
* @param message Text der Benutzer-Nachricht
|
|
||||||
* @param userId optionaler Override der User-ID (falls nicht in opts.userId)
|
|
||||||
*/
|
|
||||||
public async generateResponse(message: string, userId?: string): Promise<void> {
|
|
||||||
const text = (message ?? "").trim()
|
|
||||||
if (!text) return
|
|
||||||
if (this.isStreaming) this.abortActiveStream()
|
|
||||||
this.messages = [...this.messages, { role: "user", content: text }]
|
|
||||||
const assistantIndex = this.messages.length
|
|
||||||
this.messages = [...this.messages, { role: "assistant", content: "" }]
|
|
||||||
|
|
||||||
const payload: ChatRequestPayload = {
|
|
||||||
user_id: userId ?? this.opts.userId ?? "anonymous",
|
|
||||||
message: text,
|
|
||||||
session_id: this.sessionId,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.controller = new AbortController()
|
|
||||||
this.isStreaming = true
|
|
||||||
this.lastFinalState = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fetchEventSource(this.opts.url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...(this.opts.headers ?? {}),
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
signal: this.controller.signal,
|
|
||||||
openWhenHidden: this.opts.openWhenHidden,
|
|
||||||
|
|
||||||
onopen: async (res) => {
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
},
|
|
||||||
|
|
||||||
onmessage: (ev) => {
|
|
||||||
if (!ev.data) return
|
|
||||||
let parsed: ServerEvent | null = null
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(ev.data) as ServerEvent
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (parsed.type) {
|
|
||||||
case "token": {
|
|
||||||
const delta = parsed.delta ?? ""
|
|
||||||
const updated = {
|
|
||||||
...this.messages[assistantIndex],
|
|
||||||
content: (this.messages[assistantIndex]?.content ?? "") + delta,
|
|
||||||
}
|
|
||||||
this.messages = [
|
|
||||||
...this.messages.slice(0, assistantIndex),
|
|
||||||
updated,
|
|
||||||
...this.messages.slice(assistantIndex + 1),
|
|
||||||
]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "heartbeat": {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "error": {
|
|
||||||
const msg = parsed.message || this.opts.fallbackErrorText!
|
|
||||||
const updated = {
|
|
||||||
...this.messages[assistantIndex],
|
|
||||||
content: (this.messages[assistantIndex]?.content || "") + `\n\n⚠️ ${msg}`,
|
|
||||||
}
|
|
||||||
this.messages = [
|
|
||||||
...this.messages.slice(0, assistantIndex),
|
|
||||||
updated,
|
|
||||||
...this.messages.slice(assistantIndex + 1),
|
|
||||||
]
|
|
||||||
this.abortActiveStream()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "final": {
|
|
||||||
this.lastFinalState = parsed.state
|
|
||||||
this.abortActiveStream()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onerror: (err) => {
|
|
||||||
const updated = {
|
|
||||||
...this.messages[assistantIndex],
|
|
||||||
content:
|
|
||||||
(this.messages[assistantIndex]?.content || "") +
|
|
||||||
`\n\n⚠️ ${this.opts.fallbackErrorText} (${String(err)})`,
|
|
||||||
}
|
|
||||||
this.messages = [
|
|
||||||
...this.messages.slice(0, assistantIndex),
|
|
||||||
updated,
|
|
||||||
...this.messages.slice(assistantIndex + 1),
|
|
||||||
]
|
|
||||||
throw err
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
this.isStreaming = false
|
|
||||||
this.controller = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { mdiBookAccountOutline, mdiCreation, mdiFaceAgent, mdiHours24 } from "@mdi/js"
|
import { mdiBookAccountOutline, mdiCreation, mdiFaceAgent, mdiHours24 } from "@mdi/js"
|
||||||
import ProductCategoryFrame from "../widgets/ProductCategoryFrame.svelte"
|
import ProductCategoryFrame from "../widgets/ProductCategoryFrame.svelte"
|
||||||
import CrinkledSection from "../CrinkledSection.svelte"
|
import CrinkledSection from "../CrinkledSection.svelte"
|
||||||
import { createVoicebotPreviewController } from "./voicebotPreviewController"
|
import { createVoicebotPreviewController } from "../voicebotDemo/voicebotPreviewController"
|
||||||
|
|
||||||
const voiceProperties: Array<{ title: string; icon: string; color: string }> = [
|
const voiceProperties: Array<{ title: string; icon: string; color: string }> = [
|
||||||
{
|
{
|
||||||
@@ -149,12 +149,6 @@
|
|||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.connected {
|
|
||||||
}
|
|
||||||
|
|
||||||
&.errored {
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: 2px solid var(--primary-200);
|
outline: 2px solid var(--primary-200);
|
||||||
outline-offset: 4px;
|
outline-offset: 4px;
|
||||||
|
|||||||
@@ -1,207 +1,2 @@
|
|||||||
import { derived, get, writable, type Readable } from "svelte/store"
|
export { createVoicebotPreviewController } from "../voicebotDemo/voicebotPreviewController"
|
||||||
import {
|
export type { VoiceStatus } from "../voicebotDemo/voicebotPreviewController"
|
||||||
ConnectorLifecycleEvents,
|
|
||||||
createVoiceConnector,
|
|
||||||
WS_URL,
|
|
||||||
} from "../voicebotDemo/helper"
|
|
||||||
import type { WerkRealtimeConnector } from "../voicebotDemo/helper"
|
|
||||||
|
|
||||||
export type VoiceStatus = "idle" | "connecting" | "connected" | "error"
|
|
||||||
|
|
||||||
interface VoicebotPreviewController {
|
|
||||||
status: Readable<VoiceStatus>
|
|
||||||
errorMessage: Readable<string>
|
|
||||||
statusHint: Readable<string>
|
|
||||||
setup: () => void
|
|
||||||
teardown: () => void
|
|
||||||
start: () => Promise<void>
|
|
||||||
stop: (silent?: boolean) => Promise<void>
|
|
||||||
toggle: () => Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
const isBrowser = typeof window !== "undefined"
|
|
||||||
|
|
||||||
const extractErrorMessage = (err: unknown, fallback: string) => {
|
|
||||||
if (err instanceof DOMException) {
|
|
||||||
if (err.name === "NotAllowedError") return "Zugriff auf das Mikrofon wurde verweigert."
|
|
||||||
if (err.name === "NotFoundError") return "Kein Mikrofon gefunden oder verfügbar."
|
|
||||||
if (err.name === "NotReadableError") return "Auf das Mikrofon konnte nicht zugegriffen werden."
|
|
||||||
if (err.name === "SecurityError") return "Der Browser blockiert den Zugriff – bitte die Seite über HTTPS öffnen."
|
|
||||||
}
|
|
||||||
if (err instanceof Error && err.message) return err.message
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createVoicebotPreviewController = (): VoicebotPreviewController => {
|
|
||||||
const statusStore = writable<VoiceStatus>("idle")
|
|
||||||
const errorStore = writable("")
|
|
||||||
|
|
||||||
const statusHint = derived([statusStore, errorStore], ([$status, $error]) => {
|
|
||||||
switch ($status) {
|
|
||||||
case "idle":
|
|
||||||
return "Tippen, um die Voice-Demo zu starten"
|
|
||||||
case "connecting":
|
|
||||||
return "Verbindung wird aufgebaut …"
|
|
||||||
case "connected":
|
|
||||||
return "Live – sprechen Sie jetzt"
|
|
||||||
case "error":
|
|
||||||
return $error || "Verbindung fehlgeschlagen"
|
|
||||||
default:
|
|
||||||
return "Voice-Demo"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let connector: WerkRealtimeConnector | null = null
|
|
||||||
let detachHandlers: Array<() => void> = []
|
|
||||||
let startPromise: Promise<void> | null = null
|
|
||||||
let stopPromise: Promise<void> | null = null
|
|
||||||
let closing = false
|
|
||||||
const handleRealtimeEvent = (rawType: string, msg: any) => {
|
|
||||||
const type = rawType
|
|
||||||
if (!type) return
|
|
||||||
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
}
|
|
||||||
const ensureConnector = () => {
|
|
||||||
if (!isBrowser || connector) return
|
|
||||||
|
|
||||||
const instance = createVoiceConnector()
|
|
||||||
|
|
||||||
const detachConnecting = instance.onLifecycle(ConnectorLifecycleEvents.CONNECTING, () => {
|
|
||||||
statusStore.set("connecting")
|
|
||||||
errorStore.set("")
|
|
||||||
})
|
|
||||||
const detachConnected = instance.onLifecycle(ConnectorLifecycleEvents.CONNECTED, () => {
|
|
||||||
statusStore.set("connected")
|
|
||||||
closing = false
|
|
||||||
})
|
|
||||||
const detachDisconnected = instance.onLifecycle(ConnectorLifecycleEvents.DISCONNECTED, () => {
|
|
||||||
if (closing) {
|
|
||||||
closing = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (get(statusStore) !== "error") {
|
|
||||||
statusStore.set("idle")
|
|
||||||
errorStore.set("")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const detachError = instance.onLifecycle(ConnectorLifecycleEvents.ERROR, (evt) => {
|
|
||||||
const message =
|
|
||||||
typeof evt?.message === "string" && evt.message.trim().length
|
|
||||||
? evt.message
|
|
||||||
: "Verbindung fehlgeschlagen"
|
|
||||||
errorStore.set(`${message} (${WS_URL})`)
|
|
||||||
statusStore.set("error")
|
|
||||||
closing = false
|
|
||||||
})
|
|
||||||
|
|
||||||
detachHandlers = [detachConnecting, detachConnected, detachDisconnected, detachError]
|
|
||||||
connector = instance
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanupConnector = () => {
|
|
||||||
detachHandlers.forEach((fn) => fn())
|
|
||||||
detachHandlers = []
|
|
||||||
const instance = connector
|
|
||||||
connector = null
|
|
||||||
if (instance) {
|
|
||||||
void instance.stop()
|
|
||||||
}
|
|
||||||
startPromise = null
|
|
||||||
stopPromise = null
|
|
||||||
closing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const stop = async (silent = false) => {
|
|
||||||
if (!connector) return
|
|
||||||
if (stopPromise) {
|
|
||||||
await stopPromise
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
closing = true
|
|
||||||
if (!silent && get(statusStore) !== "error") {
|
|
||||||
statusStore.set("idle")
|
|
||||||
errorStore.set("")
|
|
||||||
}
|
|
||||||
|
|
||||||
stopPromise = connector.stop()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await stopPromise
|
|
||||||
} catch (err) {
|
|
||||||
console.error("VoicebotPreview stop error", err)
|
|
||||||
if (!silent) {
|
|
||||||
errorStore.set(extractErrorMessage(err, "Verbindung konnte nicht beendet werden."))
|
|
||||||
statusStore.set("error")
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
stopPromise = null
|
|
||||||
closing = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = async () => {
|
|
||||||
if (!isBrowser) {
|
|
||||||
statusStore.set("error")
|
|
||||||
errorStore.set("Die Sprach-Demo steht nur im Browser zur Verfügung.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureConnector()
|
|
||||||
if (!connector || startPromise) return
|
|
||||||
|
|
||||||
await stop(true)
|
|
||||||
|
|
||||||
statusStore.set("connecting")
|
|
||||||
errorStore.set("")
|
|
||||||
|
|
||||||
startPromise = (async () => {
|
|
||||||
await connector!.start()
|
|
||||||
connector!.setInputMuted(false)
|
|
||||||
connector!.setOutputMuted(false)
|
|
||||||
})()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await startPromise
|
|
||||||
} catch (err) {
|
|
||||||
console.error("VoicebotPreview start error", err)
|
|
||||||
errorStore.set(extractErrorMessage(err, "Verbindung konnte nicht aufgebaut werden."))
|
|
||||||
statusStore.set("error")
|
|
||||||
closing = false
|
|
||||||
} finally {
|
|
||||||
startPromise = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggle = async () => {
|
|
||||||
const current = get(statusStore)
|
|
||||||
if (current === "connecting") return
|
|
||||||
if (current === "connected") {
|
|
||||||
await stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await start()
|
|
||||||
}
|
|
||||||
|
|
||||||
const setup = () => {
|
|
||||||
ensureConnector()
|
|
||||||
}
|
|
||||||
|
|
||||||
const teardown = () => {
|
|
||||||
void stop(true)
|
|
||||||
cleanupConnector()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: { subscribe: statusStore.subscribe },
|
|
||||||
errorMessage: { subscribe: errorStore.subscribe },
|
|
||||||
statusHint,
|
|
||||||
setup,
|
|
||||||
teardown,
|
|
||||||
start,
|
|
||||||
stop,
|
|
||||||
toggle,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { derived, get, writable, type Readable } from "svelte/store"
|
||||||
|
import { ConnectorLifecycleEvents, createVoiceConnector, WS_URL } from "./helper"
|
||||||
|
import type { WerkRealtimeConnector } from "./helper"
|
||||||
|
|
||||||
|
export type VoiceStatus = "idle" | "connecting" | "connected" | "error"
|
||||||
|
|
||||||
|
interface VoicebotPreviewController {
|
||||||
|
status: Readable<VoiceStatus>
|
||||||
|
errorMessage: Readable<string>
|
||||||
|
statusHint: Readable<string>
|
||||||
|
setup: () => void
|
||||||
|
teardown: () => void
|
||||||
|
start: () => Promise<void>
|
||||||
|
stop: (silent?: boolean) => Promise<void>
|
||||||
|
toggle: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBrowser = typeof window !== "undefined"
|
||||||
|
|
||||||
|
const extractErrorMessage = (err: unknown, fallback: string) => {
|
||||||
|
if (err instanceof DOMException) {
|
||||||
|
if (err.name === "NotAllowedError") return "Zugriff auf das Mikrofon wurde verweigert."
|
||||||
|
if (err.name === "NotFoundError") return "Kein Mikrofon gefunden oder verfügbar."
|
||||||
|
if (err.name === "NotReadableError") return "Auf das Mikrofon konnte nicht zugegriffen werden."
|
||||||
|
if (err.name === "SecurityError") {
|
||||||
|
return "Der Browser blockiert den Zugriff – bitte die Seite über HTTPS öffnen."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (err instanceof Error && err.message) return err.message
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createVoicebotPreviewController = (): VoicebotPreviewController => {
|
||||||
|
const statusStore = writable<VoiceStatus>("idle")
|
||||||
|
const errorStore = writable("")
|
||||||
|
|
||||||
|
const statusHint = derived([statusStore, errorStore], ([$status, $error]) => {
|
||||||
|
switch ($status) {
|
||||||
|
case "idle":
|
||||||
|
return "Tippen, um die Voice-Demo zu starten"
|
||||||
|
case "connecting":
|
||||||
|
return "Verbindung wird aufgebaut …"
|
||||||
|
case "connected":
|
||||||
|
return "Live – sprechen Sie jetzt"
|
||||||
|
case "error":
|
||||||
|
return $error || "Verbindung fehlgeschlagen"
|
||||||
|
default:
|
||||||
|
return "Voice-Demo"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let connector: WerkRealtimeConnector | null = null
|
||||||
|
let detachHandlers: Array<() => void> = []
|
||||||
|
let startPromise: Promise<void> | null = null
|
||||||
|
let stopPromise: Promise<void> | null = null
|
||||||
|
let closing = false
|
||||||
|
|
||||||
|
const ensureConnector = () => {
|
||||||
|
if (!isBrowser || connector) return
|
||||||
|
|
||||||
|
const instance = createVoiceConnector()
|
||||||
|
|
||||||
|
const detachConnecting = instance.onLifecycle(ConnectorLifecycleEvents.CONNECTING, () => {
|
||||||
|
statusStore.set("connecting")
|
||||||
|
errorStore.set("")
|
||||||
|
})
|
||||||
|
const detachConnected = instance.onLifecycle(ConnectorLifecycleEvents.CONNECTED, () => {
|
||||||
|
statusStore.set("connected")
|
||||||
|
closing = false
|
||||||
|
})
|
||||||
|
const detachDisconnected = instance.onLifecycle(ConnectorLifecycleEvents.DISCONNECTED, () => {
|
||||||
|
if (closing) {
|
||||||
|
closing = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (get(statusStore) !== "error") {
|
||||||
|
statusStore.set("idle")
|
||||||
|
errorStore.set("")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const detachError = instance.onLifecycle(ConnectorLifecycleEvents.ERROR, (evt) => {
|
||||||
|
const message =
|
||||||
|
typeof evt?.message === "string" && evt.message.trim().length
|
||||||
|
? evt.message
|
||||||
|
: "Verbindung fehlgeschlagen"
|
||||||
|
errorStore.set(`${message} (${WS_URL})`)
|
||||||
|
statusStore.set("error")
|
||||||
|
closing = false
|
||||||
|
})
|
||||||
|
|
||||||
|
detachHandlers = [detachConnecting, detachConnected, detachDisconnected, detachError]
|
||||||
|
connector = instance
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanupConnector = () => {
|
||||||
|
detachHandlers.forEach((fn) => fn())
|
||||||
|
detachHandlers = []
|
||||||
|
const instance = connector
|
||||||
|
connector = null
|
||||||
|
if (instance) {
|
||||||
|
void instance.stop()
|
||||||
|
}
|
||||||
|
startPromise = null
|
||||||
|
stopPromise = null
|
||||||
|
closing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const stop = async (silent = false) => {
|
||||||
|
if (!connector) return
|
||||||
|
if (stopPromise) {
|
||||||
|
await stopPromise
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
closing = true
|
||||||
|
if (!silent && get(statusStore) !== "error") {
|
||||||
|
statusStore.set("idle")
|
||||||
|
errorStore.set("")
|
||||||
|
}
|
||||||
|
|
||||||
|
stopPromise = connector.stop()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stopPromise
|
||||||
|
} catch (err) {
|
||||||
|
console.error("VoicebotPreview stop error", err)
|
||||||
|
if (!silent) {
|
||||||
|
errorStore.set(extractErrorMessage(err, "Verbindung konnte nicht beendet werden."))
|
||||||
|
statusStore.set("error")
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
stopPromise = null
|
||||||
|
closing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = async () => {
|
||||||
|
if (!isBrowser) {
|
||||||
|
statusStore.set("error")
|
||||||
|
errorStore.set("Die Sprach-Demo steht nur im Browser zur Verfügung.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureConnector()
|
||||||
|
if (!connector || startPromise) return
|
||||||
|
|
||||||
|
await stop(true)
|
||||||
|
|
||||||
|
statusStore.set("connecting")
|
||||||
|
errorStore.set("")
|
||||||
|
|
||||||
|
startPromise = (async () => {
|
||||||
|
await connector!.start()
|
||||||
|
connector!.setInputMuted(false)
|
||||||
|
connector!.setOutputMuted(false)
|
||||||
|
})()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await startPromise
|
||||||
|
} catch (err) {
|
||||||
|
console.error("VoicebotPreview start error", err)
|
||||||
|
errorStore.set(extractErrorMessage(err, "Verbindung konnte nicht aufgebaut werden."))
|
||||||
|
statusStore.set("error")
|
||||||
|
closing = false
|
||||||
|
} finally {
|
||||||
|
startPromise = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggle = async () => {
|
||||||
|
const current = get(statusStore)
|
||||||
|
if (current === "connecting") return
|
||||||
|
if (current === "connected") {
|
||||||
|
await stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await start()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setup = () => {
|
||||||
|
ensureConnector()
|
||||||
|
}
|
||||||
|
|
||||||
|
const teardown = () => {
|
||||||
|
void stop(true)
|
||||||
|
cleanupConnector()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: { subscribe: statusStore.subscribe },
|
||||||
|
errorMessage: { subscribe: errorStore.subscribe },
|
||||||
|
statusHint,
|
||||||
|
setup,
|
||||||
|
teardown,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
toggle,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"upload:sourcemaps": "scripts/upload-sourcemaps.sh"
|
"upload:sourcemaps": "scripts/upload-sourcemaps.sh"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.27.2",
|
"@babel/cli": "^7.28.3",
|
||||||
"@babel/core": "^7.27.1",
|
"@babel/core": "^7.27.1",
|
||||||
"@babel/plugin-transform-async-to-generator": "^7.27.1",
|
"@babel/plugin-transform-async-to-generator": "^7.27.1",
|
||||||
"@babel/preset-env": "^7.27.2",
|
"@babel/preset-env": "^7.27.2",
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kontextwerk/web-sdk": "0.1.0",
|
"@kontextwerk/web-sdk": "0.1.1",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@microsoft/fetch-event-source": "^2.0.1",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
"@okrad/svelte-progressbar": "^2.2.0",
|
"@okrad/svelte-progressbar": "^2.2.0",
|
||||||
|
|||||||
14
yarn.lock
14
yarn.lock
@@ -5,7 +5,7 @@ __metadata:
|
|||||||
version: 6
|
version: 6
|
||||||
cacheKey: 8
|
cacheKey: 8
|
||||||
|
|
||||||
"@babel/cli@npm:^7.27.2":
|
"@babel/cli@npm:^7.28.3":
|
||||||
version: 7.28.3
|
version: 7.28.3
|
||||||
resolution: "@babel/cli@npm:7.28.3"
|
resolution: "@babel/cli@npm:7.28.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1779,12 +1779,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@kontextwerk/web-sdk@npm:0.1.0":
|
"@kontextwerk/web-sdk@npm:0.1.1":
|
||||||
version: 0.1.0
|
version: 0.1.1
|
||||||
resolution: "@kontextwerk/web-sdk@npm:0.1.0"
|
resolution: "@kontextwerk/web-sdk@npm:0.1.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: ^3.23.8
|
zod: ^3.23.8
|
||||||
checksum: b99d6a71584c1db40ab2eb83f5b269bca6a70c4eec4c644eed0873e909c08fbae9600556599b7012ff13fb6150086687612c59c98c2f03699ad8559302090f83
|
checksum: 8b7aa7608f2f71efc6c299088eb4f9f89fcd18b49edad194cd8e19e9ac6617c762f092ad286d3974f781acfc5137a7154f350e673c030f6cfe92f54fb2a9898b
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -6092,11 +6092,11 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "renz-shop-2020@workspace:."
|
resolution: "renz-shop-2020@workspace:."
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/cli": ^7.27.2
|
"@babel/cli": ^7.28.3
|
||||||
"@babel/core": ^7.27.1
|
"@babel/core": ^7.27.1
|
||||||
"@babel/plugin-transform-async-to-generator": ^7.27.1
|
"@babel/plugin-transform-async-to-generator": ^7.27.1
|
||||||
"@babel/preset-env": ^7.27.2
|
"@babel/preset-env": ^7.27.2
|
||||||
"@kontextwerk/web-sdk": 0.1.0
|
"@kontextwerk/web-sdk": 0.1.1
|
||||||
"@mdi/js": ^7.4.47
|
"@mdi/js": ^7.4.47
|
||||||
"@microsoft/fetch-event-source": ^2.0.1
|
"@microsoft/fetch-event-source": ^2.0.1
|
||||||
"@okrad/svelte-progressbar": ^2.2.0
|
"@okrad/svelte-progressbar": ^2.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user