KI-AGENT: Chaträume im Frontend verwalten

This commit is contained in:
2026-05-18 18:23:14 +02:00
parent 6e14f48770
commit bb54a8779e

View File

@@ -10,8 +10,15 @@ const matrixMessages = ref([])
const matrixMembers = ref([])
const matrixMessageDraft = ref("")
const matrixMessagesViewport = ref(null)
const roomCreateOpen = ref(false)
const roomCreateForm = ref({
name: "",
key: "",
topic: ""
})
const loading = ref(false)
const roomProvisioning = ref(false)
const roomCreating = ref(false)
const matrixMessagesLoading = ref(false)
const matrixMembersLoading = ref(false)
const matrixMessageSending = ref(false)
@@ -40,6 +47,10 @@ const activeRoomEndpoint = computed(() =>
`/api/communication/matrix/rooms/${encodeURIComponent(activeRoomKey.value)}`
)
const roomCreateKeyPreview = computed(() =>
normalizeRoomKey(roomCreateForm.value.key || roomCreateForm.value.name)
)
const rooms = computed(() => [
...matrixRooms.value.map((room) => ({
...room,
@@ -61,6 +72,22 @@ const rooms = computed(() => [
}
])
const normalizeRoomKey = (value) => {
const normalized = String(value || "")
.toLowerCase()
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/ä/g, "a")
.replace(/ö/g, "o")
.replace(/ü/g, "u")
.replace(/ß/g, "ss")
.replace(/[^a-z0-9._=-]+/g, "_")
.replace(/_+/g, "_")
.replace(/^[._=-]+|[._=-]+$/g, "")
return normalized || "raum"
}
const setActiveRoom = async (room) => {
if (room.disabled || room.key === activeRoomKey.value) return
@@ -70,6 +97,13 @@ const setActiveRoom = async (room) => {
await loadRoomChat({ includeMembers: true })
}
const upsertRoom = (room) => {
const roomWasKnown = matrixRooms.value.some((item) => item.key === room.key)
matrixRooms.value = roomWasKnown
? matrixRooms.value.map((item) => item.key === room.key ? { ...item, ...room } : item)
: [...matrixRooms.value, room]
}
const mergeMatrixMessages = (incomingMessages) => {
const byId = new Map()
@@ -131,10 +165,7 @@ const provisionActiveRoom = async () => {
method: "POST"
})
const roomWasKnown = matrixRooms.value.some((room) => room.key === res.key)
matrixRooms.value = roomWasKnown
? matrixRooms.value.map((room) => room.key === res.key ? { ...room, ...res, exists: true } : room)
: [...matrixRooms.value, { ...res, exists: true }]
upsertRoom({ ...res, exists: true })
toast.add({
title: res.alreadyExisted ? "Chatraum ist bereit" : "Chatraum erstellt",
@@ -152,6 +183,58 @@ const provisionActiveRoom = async () => {
}
}
const createRoom = async () => {
const name = roomCreateForm.value.name.trim()
const key = roomCreateKeyPreview.value
const topic = roomCreateForm.value.topic.trim()
if (!name) {
toast.add({
title: "Bitte gib einen Raumnamen ein",
color: "warning"
})
return
}
roomCreating.value = true
try {
const room = await $api("/api/communication/matrix/rooms", {
method: "POST",
body: {
key,
name,
topic: topic || undefined,
type: "room"
}
})
upsertRoom({ ...room, exists: true })
activeRoomKey.value = room.key
matrixMessages.value = []
matrixMembers.value = []
roomCreateForm.value = {
name: "",
key: "",
topic: ""
}
roomCreateOpen.value = false
toast.add({
title: room.alreadyExisted ? "Chatraum ist bereit" : "Chatraum erstellt",
color: "success"
})
await loadRoomChat({ includeMembers: true })
} catch (error) {
toast.add({
title: "Chatraum konnte nicht erstellt werden",
color: "error"
})
} finally {
roomCreating.value = false
}
}
const loadRoomMessages = async ({ silent = false } = {}) => {
if (!canUseMatrixChat.value || !activeRoom.value?.exists) return
if (matrixMessagesRequestActive) return
@@ -333,13 +416,74 @@ onBeforeUnmount(stopMatrixAutoRefresh)
</div>
</div>
<div class="border-b border-default p-3">
<UButton
icon="i-heroicons-plus"
color="primary"
variant="soft"
block
:disabled="!canUseMatrixChat"
@click="roomCreateOpen = !roomCreateOpen"
>
Raum erstellen
</UButton>
<form
v-if="roomCreateOpen"
class="mt-3 space-y-3 rounded-md border border-default bg-muted p-3"
@submit.prevent="createRoom"
>
<UInput
v-model="roomCreateForm.name"
placeholder="Raumname"
:disabled="roomCreating"
/>
<UInput
v-model="roomCreateForm.key"
placeholder="Raum-Key optional"
:disabled="roomCreating"
/>
<UTextarea
v-model="roomCreateForm.topic"
placeholder="Thema optional"
:rows="2"
:disabled="roomCreating"
/>
<p class="truncate text-xs text-muted">
Key: {{ roomCreateKeyPreview }}
</p>
<div class="flex justify-end gap-2">
<UButton
type="button"
color="neutral"
variant="ghost"
:disabled="roomCreating"
@click="roomCreateOpen = false"
>
Abbrechen
</UButton>
<UButton
type="submit"
icon="i-heroicons-check"
:loading="roomCreating"
:disabled="!roomCreateForm.name.trim()"
>
Erstellen
</UButton>
</div>
</form>
</div>
<div class="flex-1 overflow-y-auto p-3">
<button
v-for="room in rooms"
:key="room.key"
type="button"
class="mb-1 flex w-full items-center gap-3 rounded-md px-3 py-2 text-left transition"
:class="room.disabled ? 'cursor-not-allowed opacity-50' : 'bg-muted text-highlighted'"
:class="[
room.disabled ? 'cursor-not-allowed opacity-50' : 'text-highlighted hover:bg-muted',
room.key === activeRoomKey ? 'bg-muted ring-1 ring-primary/20' : ''
]"
:disabled="room.disabled"
@click="setActiveRoom(room)"
>
@@ -358,6 +502,14 @@ onBeforeUnmount(stopMatrixAutoRefresh)
>
aktiv
</UBadge>
<UBadge
v-else-if="!room.disabled"
color="neutral"
variant="soft"
size="xs"
>
offen
</UBadge>
</button>
</div>
@@ -404,6 +556,14 @@ onBeforeUnmount(stopMatrixAutoRefresh)
color="neutral"
variant="outline"
/>
<UButton
class="lg:hidden"
icon="i-heroicons-plus"
color="primary"
variant="soft"
:disabled="!canUseMatrixChat"
@click="roomCreateOpen = !roomCreateOpen"
/>
<UButton
icon="i-heroicons-arrow-path"
color="neutral"
@@ -417,6 +577,67 @@ onBeforeUnmount(stopMatrixAutoRefresh)
</div>
</header>
<div class="border-b border-default bg-default p-3 lg:hidden">
<div class="flex gap-2 overflow-x-auto pb-1">
<button
v-for="room in rooms"
:key="room.key"
type="button"
class="shrink-0 rounded-md border border-default px-3 py-2 text-left text-sm"
:class="room.key === activeRoomKey ? 'bg-muted text-highlighted' : 'text-muted'"
:disabled="room.disabled"
@click="setActiveRoom(room)"
>
{{ room.name }}
</button>
</div>
<form
v-if="roomCreateOpen"
class="mt-3 grid gap-2 rounded-md border border-default bg-muted p-3"
@submit.prevent="createRoom"
>
<UInput
v-model="roomCreateForm.name"
placeholder="Raumname"
:disabled="roomCreating"
/>
<UInput
v-model="roomCreateForm.key"
placeholder="Raum-Key optional"
:disabled="roomCreating"
/>
<UTextarea
v-model="roomCreateForm.topic"
placeholder="Thema optional"
:rows="2"
:disabled="roomCreating"
/>
<p class="truncate text-xs text-muted">
Key: {{ roomCreateKeyPreview }}
</p>
<div class="flex justify-end gap-2">
<UButton
type="button"
color="neutral"
variant="ghost"
:disabled="roomCreating"
@click="roomCreateOpen = false"
>
Abbrechen
</UButton>
<UButton
type="submit"
icon="i-heroicons-check"
:loading="roomCreating"
:disabled="!roomCreateForm.name.trim()"
>
Erstellen
</UButton>
</div>
</form>
</div>
<div
v-if="status && (!status.reachable || !status.provisioningConfigured)"
class="border-b border-default bg-default px-4 py-3"
@@ -438,16 +659,16 @@ onBeforeUnmount(stopMatrixAutoRefresh)
icon="i-heroicons-chat-bubble-left-right"
color="primary"
variant="soft"
title="Allgemeiner Chat ist noch nicht eingerichtet"
description="Lege den mandantenweiten Startraum an, um FEDEO als Chatplattform zu nutzen."
:title="`${activeRoom.name} ist noch nicht in Matrix angelegt`"
description="Die Raum-Metadaten sind in FEDEO vorhanden. Lege jetzt den Matrix-Raum dazu an."
>
<template #actions>
<UButton
icon="i-heroicons-plus"
:loading="roomProvisioning"
@click="provisionActiveRoom"
@click="provisionActiveRoom"
>
Chat erstellen
Matrix-Raum erstellen
</UButton>
</template>
</UAlert>
@@ -461,7 +682,7 @@ onBeforeUnmount(stopMatrixAutoRefresh)
v-if="!matrixMessages.length && !matrixMessagesLoading"
class="flex h-full min-h-64 items-center justify-center text-sm text-muted"
>
Noch keine Nachrichten im allgemeinen Chat.
Noch keine Nachrichten in diesem Chat.
</div>
<div