mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 19:51:17 +00:00
120 lines
4.5 KiB
TypeScript
120 lines
4.5 KiB
TypeScript
"use client"
|
|
|
|
import { useRef, useState } from "react"
|
|
import type { AiAccess } from "../demos"
|
|
import type { AiEvent, Booking } from "../schemas"
|
|
|
|
export type ChatPart = { type: "text"; text: string } | { type: "booking"; data: Booking }
|
|
export type ChatMessage = { role: "user" | "assistant"; parts: ChatPart[] }
|
|
|
|
const FALLBACK_ERROR = "Die KI ist gerade nicht erreichbar."
|
|
|
|
export async function readNdjson(response: Response, onEvent: (event: AiEvent) => void) {
|
|
if (!response.ok || !response.body) {
|
|
const data = (await response.json().catch(() => null)) as { error?: string } | null
|
|
throw new Error(data?.error ?? FALLBACK_ERROR)
|
|
}
|
|
|
|
const reader = response.body.getReader()
|
|
const decoder = new TextDecoder()
|
|
let buffer = ""
|
|
for (;;) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
buffer += decoder.decode(value, { stream: true })
|
|
let newline = buffer.indexOf("\n")
|
|
while (newline >= 0) {
|
|
const line = buffer.slice(0, newline).trim()
|
|
buffer = buffer.slice(newline + 1)
|
|
if (line) onEvent(JSON.parse(line) as AiEvent)
|
|
newline = buffer.indexOf("\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
export function messageText(message: ChatMessage) {
|
|
return message.parts.map((part) => (part.type === "text" ? part.text : "")).join("").trim()
|
|
}
|
|
|
|
// Der Verlauf liegt zusätzlich in einer Ref: Sprach-Callbacks des KI-Telefons rufen `send` aus älteren Renders auf
|
|
export function useAiChat({ access, mode, onReply, onError }: {
|
|
access: AiAccess | null
|
|
mode: "chat" | "phone"
|
|
onReply?: (text: string) => void
|
|
onError?: () => void
|
|
}) {
|
|
const [messages, setMessages] = useState<ChatMessage[]>([])
|
|
const [pending, setPending] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const history = useRef<ChatMessage[]>([])
|
|
const busy = useRef(false)
|
|
|
|
function commit(next: ChatMessage[]) {
|
|
history.current = next
|
|
setMessages(next)
|
|
}
|
|
|
|
function updateLast(change: (last: ChatMessage) => ChatMessage) {
|
|
const current = history.current
|
|
commit([...current.slice(0, -1), change(current[current.length - 1])])
|
|
}
|
|
|
|
async function send(text: string) {
|
|
const content = text.trim()
|
|
if (!content || !access || busy.current) return
|
|
busy.current = true
|
|
|
|
const base: ChatMessage[] = [...history.current, { role: "user", parts: [{ type: "text", text: content }] }]
|
|
commit([...base, { role: "assistant", parts: [] }])
|
|
setPending(true)
|
|
setError(null)
|
|
|
|
let reply = ""
|
|
try {
|
|
const response = await fetch("/api/ai/chat", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
access,
|
|
mode,
|
|
messages: base
|
|
.map((message) => ({ role: message.role, content: messageText(message) }))
|
|
.filter((message) => message.content)
|
|
.slice(-30),
|
|
}),
|
|
})
|
|
await readNdjson(response, (event) => {
|
|
if (event.type === "error") throw new Error(event.message)
|
|
if (event.type === "booking") {
|
|
updateLast((last) => ({ ...last, parts: [...last.parts, { type: "booking", data: event.data }] }))
|
|
} else if (event.type === "text") {
|
|
reply += event.text
|
|
updateLast((last) => {
|
|
const parts = [...last.parts]
|
|
const tail = parts[parts.length - 1]
|
|
if (tail?.type === "text") parts[parts.length - 1] = { type: "text", text: tail.text + event.text }
|
|
else parts.push({ type: "text", text: event.text })
|
|
return { ...last, parts }
|
|
})
|
|
}
|
|
})
|
|
busy.current = false
|
|
setPending(false)
|
|
onReply?.(reply.trim())
|
|
} catch (cause) {
|
|
const last = history.current[history.current.length - 1]
|
|
if (last?.role === "assistant" && last.parts.length === 0) commit(history.current.slice(0, -1))
|
|
busy.current = false
|
|
setPending(false)
|
|
setError(cause instanceof Error ? cause.message : FALLBACK_ERROR)
|
|
onError?.()
|
|
}
|
|
}
|
|
|
|
function reset() {
|
|
commit([])
|
|
setError(null)
|
|
}
|
|
|
|
return { messages, pending, error, send, reset }
|
|
}
|