Files
creator/frontend/src/composables/useChat.js
2026-06-14 17:57:40 +02:00

90 lines
2.9 KiB
JavaScript

import { ref, nextTick } from 'vue'
// (Fast) am unteren Rand? Schwelle fängt Sub-Pixel und kleine Abstände ab.
export function istUnten(el, schwelle = 60) {
return el.scrollHeight - el.scrollTop - el.clientHeight < schwelle
}
// Gemeinsame Chat-Mechanik: senden, abbrechen (Run-Counter), scrollen, Fokus.
// performRequest(messages) → Promise<{ reply, … }>; send() gibt die Antwort
// zurück, damit der Aufrufer Extras (z. B. changes) auswerten kann.
export function useChat(performRequest) {
const messages = ref([])
const input = ref('')
const loading = ref(false)
const messagesEl = ref(null) // Template-Ref: Nachrichten-Container
const inputEl = ref(null) // Template-Ref: Textarea
const stick = ref(true) // an den Boden „gepinnt" — nur dann auto-scrollen
let run = 0 // laufende Anfrage identifizieren; Abbruch ignoriert ihr Ergebnis
// @scroll-Handler: pinnt nur, wenn der Nutzer (fast) unten ist.
function onScroll() {
if (messagesEl.value) stick.value = istUnten(messagesEl.value)
}
async function scrollToBottom() {
await nextTick()
if (messagesEl.value && stick.value) messagesEl.value.scrollTop = messagesEl.value.scrollHeight
}
function autoGrow(max = 140) {
const el = inputEl.value
if (!el) return
el.style.height = 'auto'
el.style.height = Math.min(el.scrollHeight, max) + 'px'
}
function cancel() {
run++
loading.value = false
messages.value.push({ role: 'assistant', content: 'Abgebrochen.' })
}
function reset() {
run++
loading.value = false
messages.value = []
input.value = ''
}
async function send() {
if (loading.value) { // zweiter Klick = abbrechen
cancel()
return null
}
const text = input.value.trim()
if (!text) return null
stick.value = true // eigenes Senden = ans Ende; Hochscrollen während des Wartens setzt es wieder false
const current = ++run
messages.value.push({ role: 'user', content: text })
input.value = ''
nextTick(() => autoGrow())
loading.value = true
scrollToBottom()
try {
const res = await performRequest(messages.value)
if (current !== run) return null
// Prüfung liefert `frage` (+ getrenntes `feedback`); andere Chats `reply`.
messages.value.push({
role: 'assistant',
content: res.frage ?? res.reply ?? '…',
feedback: res.feedback ?? null,
bewertung: res.bewertung ?? null,
})
return res
} catch {
if (current !== run) return null
messages.value.push({ role: 'assistant', content: 'Fehler bei der Anfrage.' })
return null
} finally {
if (current === run) {
loading.value = false
scrollToBottom()
nextTick(() => inputEl.value?.focus())
}
}
}
return { messages, input, loading, messagesEl, inputEl, stick, onScroll, send, cancel, reset, scrollToBottom, autoGrow }
}