KI-AGENT: FEDEO-Call-Erlebnis ausbauen

This commit is contained in:
2026-05-18 19:24:12 +02:00
parent e4073e01ad
commit 77eabe7e18

View File

@@ -24,6 +24,10 @@ const matrixCallTiles = ref([])
const matrixCallMicEnabled = ref(true)
const matrixCallCameraEnabled = ref(true)
const matrixCallAudioContainer = ref(null)
const matrixCallScreenShareEnabled = ref(false)
const matrixCallStartedAt = ref(null)
const matrixCallDurationNow = ref(Date.now())
const matrixCallAnnouncementSent = ref(false)
const roomCreateForm = ref({
name: "",
key: "",
@@ -39,6 +43,7 @@ const matrixMessageSending = ref(false)
const matrixAutoRefreshActive = ref(false)
const lastUpdated = ref(null)
let matrixRefreshInterval = null
let matrixCallDurationInterval = null
let matrixMessagesRequestActive = false
let matrixMembersRequestActive = false
let matrixLiveKitRoom = null
@@ -75,6 +80,29 @@ const matrixCallTitle = computed(() =>
matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz"
)
const matrixCallStateLabel = computed(() => {
const labels = {
connected: "Verbunden",
connecting: "Verbindet",
reconnecting: "Verbindung wird wiederhergestellt",
disconnected: "Getrennt"
}
return labels[String(matrixCallState.value).toLowerCase()] || matrixCallState.value
})
const matrixCallParticipantCount = computed(() => matrixCallTiles.value.length)
const matrixCallDurationLabel = computed(() => {
if (!matrixCallStartedAt.value) return "00:00"
const durationSeconds = Math.max(0, Math.floor((matrixCallDurationNow.value - matrixCallStartedAt.value) / 1000))
const minutes = String(Math.floor(durationSeconds / 60)).padStart(2, "0")
const seconds = String(durationSeconds % 60).padStart(2, "0")
return `${minutes}:${seconds}`
})
const roomCreateKeyPreview = computed(() =>
normalizeRoomKey(roomCreateForm.value.key || roomCreateForm.value.name)
)
@@ -343,6 +371,11 @@ const syncRoomMembers = async () => {
}
const openMatrixCall = async (mode = "video") => {
if (matrixLiveKitRoom && matrixCallConnected.value) {
matrixCallOpen.value = true
return
}
if (!canStartMatrixCall.value) {
toast.add({
title: "Besprechung kann noch nicht gestartet werden",
@@ -355,6 +388,7 @@ const openMatrixCall = async (mode = "video") => {
matrixCallLoading.value = true
matrixCallError.value = ""
matrixCallOpen.value = true
matrixCallAnnouncementSent.value = false
try {
await leaveMatrixCall()
@@ -363,6 +397,7 @@ const openMatrixCall = async (mode = "video") => {
})
matrixCallSession.value = session
await connectMatrixCall(session, { video: mode === "video" })
await announceMatrixCallStarted(mode)
} catch (error) {
matrixCallError.value = error?.data?.error || error?.message || "Verbindung fehlgeschlagen"
toast.add({
@@ -377,12 +412,20 @@ const openMatrixCall = async (mode = "video") => {
const getParticipantTile = (participant, local = false) => {
const publications = participant.getTrackPublications?.() || []
const videoPublication = publications.find((publication) =>
publication.kind === Track.Kind.Video && publication.track
const screenSharePublication = publications.find((publication) =>
publication.kind === Track.Kind.Video &&
publication.source === Track.Source.ScreenShare &&
publication.track
)
const cameraPublication = publications.find((publication) =>
publication.kind === Track.Kind.Video &&
publication.source !== Track.Source.ScreenShare &&
publication.track
)
const audioPublication = publications.find((publication) =>
publication.kind === Track.Kind.Audio && publication.track
)
const videoPublication = screenSharePublication || cameraPublication
return {
id: local ? "local" : participant.sid || participant.identity,
@@ -390,6 +433,7 @@ const getParticipantTile = (participant, local = false) => {
name: local ? "Du" : participant.name || participant.identity,
local,
speaking: participant.isSpeaking,
screenSharing: Boolean(screenSharePublication?.track),
videoTrack: videoPublication?.track || null,
audioTrack: audioPublication?.track || null,
cameraEnabled: participant.isCameraEnabled,
@@ -428,11 +472,54 @@ const refreshMatrixCallTiles = async () => {
...Array.from(matrixLiveKitRoom.remoteParticipants.values()).map((participant) =>
getParticipantTile(participant)
)
]
].sort((a, b) => Number(b.screenSharing) - Number(a.screenSharing))
await attachMatrixCallMedia()
}
const startMatrixCallDurationTimer = () => {
matrixCallStartedAt.value = Date.now()
matrixCallDurationNow.value = Date.now()
if (matrixCallDurationInterval) {
window.clearInterval(matrixCallDurationInterval)
}
matrixCallDurationInterval = window.setInterval(() => {
matrixCallDurationNow.value = Date.now()
}, 1000)
}
const stopMatrixCallDurationTimer = () => {
if (matrixCallDurationInterval) {
window.clearInterval(matrixCallDurationInterval)
matrixCallDurationInterval = null
}
matrixCallStartedAt.value = null
matrixCallDurationNow.value = Date.now()
}
const announceMatrixCallStarted = async (mode) => {
if (matrixCallAnnouncementSent.value || !activeRoom.value?.exists) return
matrixCallAnnouncementSent.value = true
try {
const message = await $api(`${activeRoomEndpoint.value}/messages`, {
method: "POST",
body: {
text: `${mode === "audio" ? "Audioanruf" : "Videokonferenz"} gestartet.`
}
})
mergeMatrixMessages([message])
await scrollMessagesToBottom()
} catch (error) {
matrixCallAnnouncementSent.value = false
}
}
const connectMatrixCall = async (session, { video = true } = {}) => {
const room = new Room({
adaptiveStream: true,
@@ -452,6 +539,7 @@ const connectMatrixCall = async (session, { video = true } = {}) => {
RoomEvent.TrackMuted,
RoomEvent.TrackUnmuted,
RoomEvent.ActiveSpeakersChanged,
RoomEvent.LocalTrackSubscribed,
]
for (const eventName of refreshEvents) {
@@ -461,11 +549,16 @@ const connectMatrixCall = async (session, { video = true } = {}) => {
room.on(RoomEvent.ConnectionStateChanged, (state) => {
matrixCallState.value = state
matrixCallConnected.value = state === ConnectionState.Connected
if (state === ConnectionState.Connected && !matrixCallStartedAt.value) {
startMatrixCallDurationTimer()
}
})
room.on(RoomEvent.Disconnected, () => {
matrixCallConnected.value = false
matrixCallState.value = ConnectionState.Disconnected
matrixCallScreenShareEnabled.value = false
stopMatrixCallDurationTimer()
})
await room.connect(session.liveKitUrl, session.liveKitToken)
@@ -473,6 +566,10 @@ const connectMatrixCall = async (session, { video = true } = {}) => {
await room.localParticipant.setCameraEnabled(video)
matrixCallMicEnabled.value = true
matrixCallCameraEnabled.value = video
matrixCallScreenShareEnabled.value = false
if (!matrixCallStartedAt.value) {
startMatrixCallDurationTimer()
}
await refreshMatrixCallTiles()
}
@@ -487,6 +584,9 @@ const leaveMatrixCall = async () => {
matrixCallTiles.value = []
matrixCallConnected.value = false
matrixCallState.value = "disconnected"
matrixCallScreenShareEnabled.value = false
matrixCallAnnouncementSent.value = false
stopMatrixCallDurationTimer()
}
const closeMatrixCall = async () => {
@@ -494,6 +594,10 @@ const closeMatrixCall = async () => {
matrixCallOpen.value = false
}
const minimizeMatrixCall = () => {
matrixCallOpen.value = false
}
const setMatrixCallVideoElement = (tileId, element) => {
if (element) {
matrixCallVideoElements.set(tileId, element)
@@ -522,6 +626,22 @@ const toggleMatrixCallCamera = async () => {
await refreshMatrixCallTiles()
}
const toggleMatrixCallScreenShare = async () => {
if (!matrixLiveKitRoom) return
const enabled = !matrixCallScreenShareEnabled.value
try {
await matrixLiveKitRoom.localParticipant.setScreenShareEnabled(enabled)
matrixCallScreenShareEnabled.value = enabled
await refreshMatrixCallTiles()
} catch (error) {
toast.add({
title: "Bildschirmfreigabe konnte nicht geändert werden",
color: "error"
})
}
}
const loadRoomChat = async ({ silent = false, includeMembers = false } = {}) => {
await loadRoomMessages({ silent })
@@ -625,6 +745,7 @@ onMounted(async () => {
onBeforeUnmount(() => {
stopMatrixAutoRefresh()
stopMatrixCallDurationTimer()
leaveMatrixCall()
})
</script>
@@ -929,6 +1050,40 @@ onBeforeUnmount(() => {
</UAlert>
</div>
<div
v-if="matrixLiveKitRoom && matrixCallConnected"
class="flex shrink-0 flex-wrap items-center justify-between gap-3 border-b border-primary/20 bg-primary/10 px-4 py-3"
>
<div class="min-w-0">
<p class="truncate text-sm font-medium text-highlighted">
{{ matrixCallTitle }} läuft in {{ activeRoom.name }}
</p>
<p class="truncate text-xs text-muted">
{{ matrixCallParticipantCount }} Teilnehmer · {{ matrixCallDurationLabel }} · {{ matrixCallStateLabel }}
</p>
</div>
<div class="flex items-center gap-2">
<UButton
icon="i-heroicons-arrow-top-right-on-square"
color="primary"
variant="soft"
size="sm"
@click="matrixCallOpen = true"
>
Öffnen
</UButton>
<UButton
icon="i-heroicons-phone-x-mark"
color="error"
variant="soft"
size="sm"
@click="closeMatrixCall"
>
Auflegen
</UButton>
</div>
</div>
<div
ref="matrixMessagesViewport"
class="min-h-0 flex-1 space-y-3 overflow-y-auto p-4 sm:p-5"
@@ -1133,6 +1288,24 @@ onBeforeUnmount(() => {
>
{{ matrixCallCameraEnabled ? "Kamera an" : "Kamera aus" }}
</UButton>
<UButton
icon="i-heroicons-computer-desktop"
color="neutral"
:variant="matrixCallScreenShareEnabled ? 'soft' : 'outline'"
:disabled="!matrixLiveKitRoom"
@click="toggleMatrixCallScreenShare"
>
{{ matrixCallScreenShareEnabled ? "Freigabe an" : "Freigeben" }}
</UButton>
<UButton
icon="i-heroicons-arrows-pointing-in"
color="neutral"
variant="ghost"
:disabled="!matrixLiveKitRoom"
@click="minimizeMatrixCall"
>
Minimieren
</UButton>
<UButton
icon="i-heroicons-phone-x-mark"
color="error"
@@ -1174,14 +1347,15 @@ onBeforeUnmount(() => {
class="flex h-full flex-col"
>
<div class="flex shrink-0 items-center justify-between border-b border-default bg-default px-4 py-2 text-xs text-muted">
<span>{{ matrixCallSession?.liveKitRoomName || activeRoom.key }}</span>
<span>{{ matrixCallState }}</span>
<span>{{ matrixCallParticipantCount }} Teilnehmer · {{ matrixCallDurationLabel }}</span>
<span>{{ matrixCallStateLabel }}</span>
</div>
<div class="grid min-h-0 flex-1 auto-rows-fr gap-3 overflow-y-auto p-3 sm:grid-cols-2 xl:grid-cols-3">
<div
v-for="tile in matrixCallTiles"
:key="tile.id"
class="relative min-h-52 overflow-hidden rounded-lg bg-inverted text-inverted"
:class="tile.speaking ? 'ring-2 ring-primary' : ''"
>
<video
v-if="tile.videoTrack"
@@ -1200,7 +1374,10 @@ onBeforeUnmount(() => {
</span>
</div>
<div class="absolute inset-x-0 bottom-0 flex items-center justify-between bg-black/55 px-3 py-2 text-sm text-white">
<span class="truncate">{{ tile.name }}</span>
<span class="truncate">
{{ tile.name }}
<span v-if="tile.screenSharing" class="ml-1 text-xs opacity-80">teilt Bildschirm</span>
</span>
<div class="flex items-center gap-2">
<UIcon
:name="tile.microphoneEnabled ? 'i-heroicons-microphone' : 'i-heroicons-microphone'"