From 20bb2e6e0533b93f8f3bbc07377afe99b84339fa Mon Sep 17 00:00:00 2001 From: lukasIO Date: Wed, 5 Feb 2025 15:54:14 +0100 Subject: [PATCH 01/21] Use stream APIs for chat --- packages/core/src/components/chat.ts | 281 ++++++++++++++-------- packages/react/src/hooks/useByteStream.ts | 14 ++ packages/react/src/hooks/useChat.ts | 2 +- packages/react/src/hooks/useTextStream.ts | 14 ++ 4 files changed, 213 insertions(+), 98 deletions(-) create mode 100644 packages/react/src/hooks/useByteStream.ts create mode 100644 packages/react/src/hooks/useTextStream.ts diff --git a/packages/core/src/components/chat.ts b/packages/core/src/components/chat.ts index 4f8c56d77..5a0b8c79e 100644 --- a/packages/core/src/components/chat.ts +++ b/packages/core/src/components/chat.ts @@ -1,13 +1,8 @@ /* eslint-disable camelcase */ import type { Participant, Room, ChatMessage } from 'livekit-client'; -import { compareVersions, RoomEvent } from 'livekit-client'; -import { BehaviorSubject, Subject, scan, map, takeUntil, merge } from 'rxjs'; -import { - DataTopic, - sendMessage, - setupChatMessageHandler, - setupDataMessageHandler, -} from '../observables/dataChannel'; +import { RoomEvent } from 'livekit-client'; +import { BehaviorSubject, Subject, scan, map, takeUntil, from } from 'rxjs'; +import { DataTopic } from '../observables/dataChannel'; /** @public */ export type { ChatMessage }; @@ -47,76 +42,197 @@ export type ChatOptions = { updateChannelTopic?: string; }; -type RawMessage = { - payload: Uint8Array; - topic: string | undefined; - from: Participant | undefined; -}; +// type RawMessage = { +// payload: Uint8Array; +// topic: string | undefined; +// from: Participant | undefined; +// }; -const encoder = new TextEncoder(); -const decoder = new TextDecoder(); +// const encoder = new TextEncoder(); +// const decoder = new TextDecoder(); -const topicSubjectMap: Map>> = new Map(); +const topicSubjectMap: Map>> = new Map(); -const encode = (message: LegacyReceivedChatMessage) => encoder.encode(JSON.stringify(message)); +// const encode = (message: LegacyReceivedChatMessage) => encoder.encode(JSON.stringify(message)); -const decode = (message: Uint8Array) => - JSON.parse(decoder.decode(message)) as LegacyReceivedChatMessage | ReceivedChatMessage; +// const decode = (message: Uint8Array) => +// JSON.parse(decoder.decode(message)) as LegacyReceivedChatMessage | ReceivedChatMessage; -export function setupChat(room: Room, options?: ChatOptions) { - const onDestroyObservable = new Subject(); +// export function setupChat(room: Room, options?: ChatOptions) { +// const onDestroyObservable = new Subject(); + +// const serverSupportsChatApi = () => +// room.serverInfo?.edition === 1 || +// (!!room.serverInfo?.version && compareVersions(room.serverInfo?.version, '1.17.2') > 0); + +// const { messageDecoder, messageEncoder, channelTopic, updateChannelTopic } = options ?? {}; + +// const topic = channelTopic ?? DataTopic.CHAT; + +// const updateTopic = updateChannelTopic ?? DataTopic.CHAT_UPDATE; + +// let needsSetup = false; +// if (!topicSubjectMap.has(room)) { +// needsSetup = true; +// } +// const topicMap = topicSubjectMap.get(room) ?? new Map>(); +// const messageSubject = topicMap.get(topic) ?? new Subject(); +// topicMap.set(topic, messageSubject); +// topicSubjectMap.set(room, topicMap); + +// if (needsSetup) { +// /** Subscribe to all appropriate messages sent over the wire. */ +// const { messageObservable } = setupDataMessageHandler(room, [topic, updateTopic]); +// messageObservable.pipe(takeUntil(onDestroyObservable)).subscribe(messageSubject); +// } +// const { chatObservable, send: sendChatMessage } = setupChatMessageHandler(room); - const serverSupportsChatApi = () => - room.serverInfo?.edition === 1 || - (!!room.serverInfo?.version && compareVersions(room.serverInfo?.version, '1.17.2') > 0); +// const finalMessageDecoder = messageDecoder ?? decode; - const { messageDecoder, messageEncoder, channelTopic, updateChannelTopic } = options ?? {}; +// /** Build up the message array over time. */ +// const messagesObservable = merge( +// messageSubject.pipe( +// map((msg) => { +// const parsedMessage = finalMessageDecoder(msg.payload); +// const newMessage = { ...parsedMessage, from: msg.from }; +// if (isIgnorableChatMessage(newMessage)) { +// return undefined; +// } +// return newMessage; +// }), +// ), +// chatObservable.pipe( +// map(([msg, participant]) => { +// return { ...msg, from: participant }; +// }), +// ), +// ).pipe( +// scan((acc, value) => { +// // ignore legacy message updates +// if (!value) { +// return acc; +// } +// // handle message updates +// if ( +// 'id' in value && +// acc.find((msg) => msg.from?.identity === value.from?.identity && msg.id === value.id) +// ) { +// const replaceIndex = acc.findIndex((msg) => msg.id === value.id); +// if (replaceIndex > -1) { +// const originalMsg = acc[replaceIndex]; +// acc[replaceIndex] = { +// ...value, +// timestamp: originalMsg.timestamp, +// editTimestamp: value.editTimestamp ?? value.timestamp, +// }; +// } - const topic = channelTopic ?? DataTopic.CHAT; +// return [...acc]; +// } +// return [...acc, value]; +// }, []), +// takeUntil(onDestroyObservable), +// ); + +// const isSending$ = new BehaviorSubject(false); + +// const finalMessageEncoder = messageEncoder ?? encode; + +// const send = async (message: string) => { +// isSending$.next(true); +// try { +// const chatMessage = await sendChatMessage(message); +// const encodedLegacyMsg = finalMessageEncoder({ +// ...chatMessage, +// ignore: serverSupportsChatApi(), +// }); +// await sendMessage(room.localParticipant, encodedLegacyMsg, { +// reliable: true, +// topic, +// }); +// return chatMessage; +// } finally { +// isSending$.next(false); +// } +// }; + +// const update = async (message: string, originalMessageOrId: string | ChatMessage) => { +// const timestamp = Date.now(); +// const originalMessage: ChatMessage = +// typeof originalMessageOrId === 'string' +// ? { id: originalMessageOrId, message: '', timestamp } +// : originalMessageOrId; +// isSending$.next(true); +// try { +// const editedMessage = await room.localParticipant.editChatMessage(message, originalMessage); +// const encodedLegacyMessage = finalMessageEncoder(editedMessage); +// await sendMessage(room.localParticipant, encodedLegacyMessage, { +// topic: updateTopic, +// reliable: true, +// }); +// return editedMessage; +// } finally { +// isSending$.next(false); +// } +// }; + +// function destroy() { +// onDestroyObservable.next(); +// onDestroyObservable.complete(); +// topicSubjectMap.delete(room); +// } +// room.once(RoomEvent.Disconnected, destroy); + +// return { +// messageObservable: messagesObservable, +// isSendingObservable: isSending$, +// send, +// update, +// }; +// } + +// function isIgnorableChatMessage( +// msg: ReceivedChatMessage | LegacyReceivedChatMessage, +// ): msg is ReceivedChatMessage { +// return (msg as LegacyChatMessage).ignore == true; +// } + +export function setupChat(room: Room, options?: ChatOptions) { + const onDestroyObservable = new Subject(); - const updateTopic = updateChannelTopic ?? DataTopic.CHAT_UPDATE; + const topic = options?.channelTopic ?? DataTopic.CHAT; let needsSetup = false; if (!topicSubjectMap.has(room)) { needsSetup = true; } - const topicMap = topicSubjectMap.get(room) ?? new Map>(); - const messageSubject = topicMap.get(topic) ?? new Subject(); + const topicMap = topicSubjectMap.get(room) ?? new Map>(); + const messageSubject = topicMap.get(topic) ?? new Subject(); topicMap.set(topic, messageSubject); topicSubjectMap.set(room, topicMap); if (needsSetup) { - /** Subscribe to all appropriate messages sent over the wire. */ - const { messageObservable } = setupDataMessageHandler(room, [topic, updateTopic]); - messageObservable.pipe(takeUntil(onDestroyObservable)).subscribe(messageSubject); + room.registerTextStreamHandler(topic, async (reader, participantInfo) => { + const { id, timestamp } = reader.info; + const streamObservable = from(reader).pipe( + map((chunk) => { + return { + id, + timestamp, + message: chunk.collected, + from: room.getParticipantByIdentity(participantInfo.identity), + } as ReceivedChatMessage; + }), + ); + streamObservable.subscribe({ + next: (value) => messageSubject.next(value), + }); + }); } - const { chatObservable, send: sendChatMessage } = setupChatMessageHandler(room); - - const finalMessageDecoder = messageDecoder ?? decode; /** Build up the message array over time. */ - const messagesObservable = merge( - messageSubject.pipe( - map((msg) => { - const parsedMessage = finalMessageDecoder(msg.payload); - const newMessage = { ...parsedMessage, from: msg.from }; - if (isIgnorableChatMessage(newMessage)) { - return undefined; - } - return newMessage; - }), - ), - chatObservable.pipe( - map(([msg, participant]) => { - return { ...msg, from: participant }; - }), - ), - ).pipe( - scan((acc, value) => { - // ignore legacy message updates - if (!value) { - return acc; - } + const messagesObservable = messageSubject.pipe( + scan((acc, value) => { // handle message updates if ( 'id' in value && @@ -128,7 +244,6 @@ export function setupChat(room: Room, options?: ChatOptions) { acc[replaceIndex] = { ...value, timestamp: originalMsg.timestamp, - editTimestamp: value.editTimestamp ?? value.timestamp, }; } @@ -141,41 +256,18 @@ export function setupChat(room: Room, options?: ChatOptions) { const isSending$ = new BehaviorSubject(false); - const finalMessageEncoder = messageEncoder ?? encode; - const send = async (message: string) => { isSending$.next(true); try { - const chatMessage = await sendChatMessage(message); - const encodedLegacyMsg = finalMessageEncoder({ - ...chatMessage, - ignore: serverSupportsChatApi(), - }); - await sendMessage(room.localParticipant, encodedLegacyMsg, { - reliable: true, - topic, - }); - return chatMessage; - } finally { - isSending$.next(false); - } - }; - - const update = async (message: string, originalMessageOrId: string | ChatMessage) => { - const timestamp = Date.now(); - const originalMessage: ChatMessage = - typeof originalMessageOrId === 'string' - ? { id: originalMessageOrId, message: '', timestamp } - : originalMessageOrId; - isSending$.next(true); - try { - const editedMessage = await room.localParticipant.editChatMessage(message, originalMessage); - const encodedLegacyMessage = finalMessageEncoder(editedMessage); - await sendMessage(room.localParticipant, encodedLegacyMessage, { - topic: updateTopic, - reliable: true, - }); - return editedMessage; + const info = await room.localParticipant.sendText(message, { topic }); + const chatMsg: ReceivedChatMessage = { + id: info.id, + timestamp: Date.now(), + message, + from: room.localParticipant, + }; + messageSubject.next(chatMsg); + return chatMsg; } finally { isSending$.next(false); } @@ -184,7 +276,9 @@ export function setupChat(room: Room, options?: ChatOptions) { function destroy() { onDestroyObservable.next(); onDestroyObservable.complete(); + messageSubject.complete(); topicSubjectMap.delete(room); + room.unregisterTextStreamHandler(topic); } room.once(RoomEvent.Disconnected, destroy); @@ -192,12 +286,5 @@ export function setupChat(room: Room, options?: ChatOptions) { messageObservable: messagesObservable, isSendingObservable: isSending$, send, - update, }; } - -function isIgnorableChatMessage( - msg: ReceivedChatMessage | LegacyReceivedChatMessage, -): msg is ReceivedChatMessage { - return (msg as LegacyChatMessage).ignore == true; -} diff --git a/packages/react/src/hooks/useByteStream.ts b/packages/react/src/hooks/useByteStream.ts new file mode 100644 index 000000000..0310e88be --- /dev/null +++ b/packages/react/src/hooks/useByteStream.ts @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { useRoomContext } from '../context'; +import type { ByteStreamHandler } from 'livekit-client'; + +export function useByteStream(topic: string, onByteStreamReceived: ByteStreamHandler) { + const room = useRoomContext(); + + React.useEffect(() => { + room.registerByteStreamHandler(topic, onByteStreamReceived); + return () => { + room.unregisterByteStreamHandler(topic); + }; + }, [room, onByteStreamReceived, topic]); +} diff --git a/packages/react/src/hooks/useChat.ts b/packages/react/src/hooks/useChat.ts index c87be44aa..75c99b312 100644 --- a/packages/react/src/hooks/useChat.ts +++ b/packages/react/src/hooks/useChat.ts @@ -26,5 +26,5 @@ export function useChat(options?: ChatOptions) { const isSending = useObservableState(setup.isSendingObservable, false); const chatMessages = useObservableState(setup.messageObservable, []); - return { send: setup.send, update: setup.update, chatMessages, isSending }; + return { send: setup.send, chatMessages, isSending }; } diff --git a/packages/react/src/hooks/useTextStream.ts b/packages/react/src/hooks/useTextStream.ts new file mode 100644 index 000000000..268fe434c --- /dev/null +++ b/packages/react/src/hooks/useTextStream.ts @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { useRoomContext } from '../context'; +import type { TextStreamHandler } from 'livekit-client'; + +export function useTextStream(topic: string, onTextStreamReceived: TextStreamHandler) { + const room = useRoomContext(); + + React.useEffect(() => { + room.registerTextStreamHandler(topic, onTextStreamReceived); + return () => { + room.unregisterTextStreamHandler(topic); + }; + }, [room, onTextStreamReceived, topic]); +} From 3acf80d04cd9fb526c5576131a5bb044c35fc872 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Fri, 7 Feb 2025 14:23:29 +0100 Subject: [PATCH 02/21] add update method --- packages/core/src/components/chat.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/core/src/components/chat.ts b/packages/core/src/components/chat.ts index 5a0b8c79e..cc558e8c1 100644 --- a/packages/core/src/components/chat.ts +++ b/packages/core/src/components/chat.ts @@ -36,9 +36,8 @@ export type ChatOptions = { messageEncoder?: (message: LegacyChatMessage) => Uint8Array; /** @deprecated the new chat API doesn't rely on encoders and decoders anymore and uses a dedicated chat API instead */ messageDecoder?: (message: Uint8Array) => LegacyReceivedChatMessage; - /** @deprecated the new chat API doesn't rely on topics anymore and uses a dedicated chat API instead */ channelTopic?: string; - /** @deprecated the new chat API doesn't rely on topics anymore and uses a dedicated chat API instead */ + /** @deprecated the new chat API doesn't rely on update topics anymore and uses a dedicated chat API instead */ updateChannelTopic?: string; }; @@ -213,7 +212,7 @@ export function setupChat(room: Room, options?: ChatOptions) { if (needsSetup) { room.registerTextStreamHandler(topic, async (reader, participantInfo) => { - const { id, timestamp } = reader.info; + const { id, timestamp, type } = reader.info; const streamObservable = from(reader).pipe( map((chunk) => { return { @@ -221,6 +220,7 @@ export function setupChat(room: Room, options?: ChatOptions) { timestamp, message: chunk.collected, from: room.getParticipantByIdentity(participantInfo.identity), + editTimestamp: type === 'update' ? timestamp : undefined, } as ReceivedChatMessage; }), ); @@ -273,6 +273,25 @@ export function setupChat(room: Room, options?: ChatOptions) { } }; + const update = async (messageId: string, message: string) => { + isSending$.next(true); + try { + const info = await room.localParticipant.updateText(messageId, message); + + const chatMsg: ReceivedChatMessage = { + id: info.id, + timestamp: info.timestamp, + editTimestamp: info.timestamp, + message, + from: room.localParticipant, + }; + messageSubject.next(chatMsg); + return chatMsg; + } finally { + isSending$.next(false); + } + }; + function destroy() { onDestroyObservable.next(); onDestroyObservable.complete(); @@ -286,5 +305,6 @@ export function setupChat(room: Room, options?: ChatOptions) { messageObservable: messagesObservable, isSendingObservable: isSending$, send, + update, }; } From 402fcfe54045f60dcc451711f8eb7bc284243cec Mon Sep 17 00:00:00 2001 From: lukasIO Date: Mon, 10 Feb 2025 16:15:10 +0100 Subject: [PATCH 03/21] update livekit-client --- pnpm-lock.yaml | 42 +++++++++++++++++++++--------------------- pnpm-workspace.yaml | 2 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b4266a18..f4a3e12a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,8 +7,8 @@ settings: catalogs: default: livekit-client: - specifier: ^2.8.1 - version: 2.8.1 + specifier: ^2.9.0 + version: 2.9.0 importers: @@ -58,7 +58,7 @@ importers: version: link:../../packages/styles livekit-client: specifier: 'catalog:' - version: 2.8.1 + version: 2.9.0 react: specifier: ^18.2.0 version: 18.3.1 @@ -122,10 +122,10 @@ importers: version: link:../../packages/styles '@livekit/track-processors': specifier: ^0.3.2 - version: 0.3.2(livekit-client@2.8.1) + version: 0.3.2(livekit-client@2.9.0) livekit-client: specifier: 'catalog:' - version: 2.8.1 + version: 2.9.0 livekit-server-sdk: specifier: ^2.6.1 version: 2.6.1 @@ -165,7 +165,7 @@ importers: version: 1.6.11 livekit-client: specifier: 'catalog:' - version: 2.8.1 + version: 2.9.0 loglevel: specifier: 1.9.1 version: 1.9.1 @@ -214,13 +214,13 @@ importers: version: link:../core '@livekit/krisp-noise-filter': specifier: ^0.2.12 - version: 0.2.12(livekit-client@2.8.1) + version: 0.2.12(livekit-client@2.9.0) clsx: specifier: 2.1.1 version: 2.1.1 livekit-client: specifier: 'catalog:' - version: 2.8.1 + version: 2.9.0 tslib: specifier: ^2.6.2 version: 2.8.1 @@ -1637,12 +1637,12 @@ packages: '@livekit/mutex@1.1.1': resolution: {integrity: sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==} - '@livekit/protocol@1.30.0': - resolution: {integrity: sha512-SDI9ShVKj8N3oOSinr8inaxD3FXgmgoJlqN35uU/Yx1sdoDeQbzAuBFox7bYjM+VhnZ1V22ivIDjAsKr00H+XQ==} - '@livekit/protocol@1.32.0': resolution: {integrity: sha512-GXH59x4Zk5D7I9Bk5si+HiTP6R+I/Ynf1e+5rYRKkskgxdsnBdzCehF09qau55bEoGyVcmCQ4gmekHKetjEC3Q==} + '@livekit/protocol@1.33.0': + resolution: {integrity: sha512-361mBlFgI3nvn8oSQIL38gDUBGbOSwsEOqPgX0c1Jwz75/sD/TTvPeAM4zAz6OrV5Q4vI4Ruswecnyv5SG4oig==} + '@livekit/track-processors@0.3.2': resolution: {integrity: sha512-4JUCzb7yIKoVsTo8J6FTzLZJHcI6DihfX/pGRDg0SOGaxprcDPrt8jaDBBTsnGBSXHeMxl2ugN+xQjdCWzLKEA==} peerDependencies: @@ -5620,8 +5620,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - livekit-client@2.8.1: - resolution: {integrity: sha512-HPv9iHNrnBANI9ucK7CKZspx0sBZK3hjR2EbwaV08+J3RM9+tNGL2ob2n76nxJLEZG7LzdWlLZdbr4fQBP6Hkg==} + livekit-client@2.9.0: + resolution: {integrity: sha512-7YwDXKb1aA/W1fedghc6Vn/ykHdoVHFr5ujOIja+YR4dsqjpWHNjQzZNbrMFu37R5cBiJ/7zK/Zs7r/amHjBMA==} livekit-server-sdk@2.6.1: resolution: {integrity: sha512-j/8TOlahIyWnycNkuSzTv6q+win4JTbDGNH48iMsZDMnJBks9hhC9UwAO4ES42sAorIAxGkrH58hxt4KdTkZaQ==} @@ -9273,25 +9273,25 @@ snapshots: transitivePeerDependencies: - encoding - '@livekit/krisp-noise-filter@0.2.12(livekit-client@2.8.1)': + '@livekit/krisp-noise-filter@0.2.12(livekit-client@2.9.0)': dependencies: - livekit-client: 2.8.1 + livekit-client: 2.9.0 '@livekit/mutex@1.1.1': {} - '@livekit/protocol@1.30.0': + '@livekit/protocol@1.32.0': dependencies: '@bufbuild/protobuf': 1.10.0 - '@livekit/protocol@1.32.0': + '@livekit/protocol@1.33.0': dependencies: '@bufbuild/protobuf': 1.10.0 - '@livekit/track-processors@0.3.2(livekit-client@2.8.1)': + '@livekit/track-processors@0.3.2(livekit-client@2.9.0)': dependencies: '@mediapipe/holistic': 0.5.1675471629 '@mediapipe/tasks-vision': 0.10.9 - livekit-client: 2.8.1 + livekit-client: 2.9.0 '@manypkg/find-root@1.1.0': dependencies: @@ -14673,10 +14673,10 @@ snapshots: lines-and-columns@1.2.4: {} - livekit-client@2.8.1: + livekit-client@2.9.0: dependencies: '@livekit/mutex': 1.1.1 - '@livekit/protocol': 1.30.0 + '@livekit/protocol': 1.33.0 events: 3.3.0 loglevel: 1.9.1 sdp-transform: 2.14.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a471be0c0..a6861f399 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,4 +5,4 @@ packages: - 'tooling/*' catalog: - livekit-client: ^2.8.1 + livekit-client: ^2.9.0 From fe839276be675d9afa1cd7cb9e391b4f5bfda126 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Tue, 11 Feb 2025 11:04:11 +0100 Subject: [PATCH 04/21] update hook names --- ...eByteStream.ts => useReceiveByteStream.ts} | 2 +- ...eTextStream.ts => useReceiveTextStream.ts} | 2 +- pnpm-lock.yaml | 30 +++++++++---------- pnpm-workspace.yaml | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) rename packages/react/src/hooks/{useByteStream.ts => useReceiveByteStream.ts} (79%) rename packages/react/src/hooks/{useTextStream.ts => useReceiveTextStream.ts} (79%) diff --git a/packages/react/src/hooks/useByteStream.ts b/packages/react/src/hooks/useReceiveByteStream.ts similarity index 79% rename from packages/react/src/hooks/useByteStream.ts rename to packages/react/src/hooks/useReceiveByteStream.ts index 0310e88be..424e2cfee 100644 --- a/packages/react/src/hooks/useByteStream.ts +++ b/packages/react/src/hooks/useReceiveByteStream.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import { useRoomContext } from '../context'; import type { ByteStreamHandler } from 'livekit-client'; -export function useByteStream(topic: string, onByteStreamReceived: ByteStreamHandler) { +export function useReceiveByteStream(topic: string, onByteStreamReceived: ByteStreamHandler) { const room = useRoomContext(); React.useEffect(() => { diff --git a/packages/react/src/hooks/useTextStream.ts b/packages/react/src/hooks/useReceiveTextStream.ts similarity index 79% rename from packages/react/src/hooks/useTextStream.ts rename to packages/react/src/hooks/useReceiveTextStream.ts index 268fe434c..938bc4e92 100644 --- a/packages/react/src/hooks/useTextStream.ts +++ b/packages/react/src/hooks/useReceiveTextStream.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import { useRoomContext } from '../context'; import type { TextStreamHandler } from 'livekit-client'; -export function useTextStream(topic: string, onTextStreamReceived: TextStreamHandler) { +export function useReceiveTextStream(topic: string, onTextStreamReceived: TextStreamHandler) { const room = useRoomContext(); React.useEffect(() => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c741de095..717f766f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,8 +7,8 @@ settings: catalogs: default: livekit-client: - specifier: ^2.9.0 - version: 2.9.0 + specifier: ^2.9.1 + version: 2.9.1 importers: @@ -58,7 +58,7 @@ importers: version: link:../../packages/styles livekit-client: specifier: 'catalog:' - version: 2.9.0 + version: 2.9.1 react: specifier: ^18.2.0 version: 18.3.1 @@ -122,10 +122,10 @@ importers: version: link:../../packages/styles '@livekit/track-processors': specifier: ^0.3.2 - version: 0.3.2(livekit-client@2.9.0) + version: 0.3.2(livekit-client@2.9.1) livekit-client: specifier: 'catalog:' - version: 2.9.0 + version: 2.9.1 livekit-server-sdk: specifier: ^2.6.1 version: 2.6.1 @@ -165,7 +165,7 @@ importers: version: 1.6.13 livekit-client: specifier: 'catalog:' - version: 2.9.0 + version: 2.9.1 loglevel: specifier: 1.9.1 version: 1.9.1 @@ -214,13 +214,13 @@ importers: version: link:../core '@livekit/krisp-noise-filter': specifier: ^0.2.12 - version: 0.2.12(livekit-client@2.9.0) + version: 0.2.12(livekit-client@2.9.1) clsx: specifier: 2.1.1 version: 2.1.1 livekit-client: specifier: 'catalog:' - version: 2.9.0 + version: 2.9.1 tslib: specifier: ^2.6.2 version: 2.8.1 @@ -6070,8 +6070,8 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - livekit-client@2.9.0: - resolution: {integrity: sha512-7YwDXKb1aA/W1fedghc6Vn/ykHdoVHFr5ujOIja+YR4dsqjpWHNjQzZNbrMFu37R5cBiJ/7zK/Zs7r/amHjBMA==} + livekit-client@2.9.1: + resolution: {integrity: sha512-MqBxnOmkOB88+n2Whq2pgXVwevfJ+yhERNf0tZf/7RG1ZpCWF+pk7/muRVzZeCxal4fwGOB3Z3MIT0KBJoT0RQ==} livekit-server-sdk@2.6.1: resolution: {integrity: sha512-j/8TOlahIyWnycNkuSzTv6q+win4JTbDGNH48iMsZDMnJBks9hhC9UwAO4ES42sAorIAxGkrH58hxt4KdTkZaQ==} @@ -9905,9 +9905,9 @@ snapshots: transitivePeerDependencies: - encoding - '@livekit/krisp-noise-filter@0.2.12(livekit-client@2.9.0)': + '@livekit/krisp-noise-filter@0.2.12(livekit-client@2.9.1)': dependencies: - livekit-client: 2.9.0 + livekit-client: 2.9.1 '@livekit/mutex@1.1.1': {} @@ -9919,11 +9919,11 @@ snapshots: dependencies: '@bufbuild/protobuf': 1.10.0 - '@livekit/track-processors@0.3.2(livekit-client@2.9.0)': + '@livekit/track-processors@0.3.2(livekit-client@2.9.1)': dependencies: '@mediapipe/holistic': 0.5.1675471629 '@mediapipe/tasks-vision': 0.10.9 - livekit-client: 2.9.0 + livekit-client: 2.9.1 '@manypkg/find-root@1.1.0': dependencies: @@ -15332,7 +15332,7 @@ snapshots: lines-and-columns@1.2.4: {} - livekit-client@2.9.0: + livekit-client@2.9.1: dependencies: '@livekit/mutex': 1.1.1 '@livekit/protocol': 1.33.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a6861f399..7242e9f18 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,4 +5,4 @@ packages: - 'tooling/*' catalog: - livekit-client: ^2.9.0 + livekit-client: ^2.9.1 From b428f04f46abb913e1835b515ca622dbdfe85184 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Tue, 11 Feb 2025 11:49:59 +0100 Subject: [PATCH 05/21] update api --- packages/core/src/components/chat.ts | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/core/src/components/chat.ts b/packages/core/src/components/chat.ts index cc558e8c1..1b3bb60a7 100644 --- a/packages/core/src/components/chat.ts +++ b/packages/core/src/components/chat.ts @@ -212,7 +212,7 @@ export function setupChat(room: Room, options?: ChatOptions) { if (needsSetup) { room.registerTextStreamHandler(topic, async (reader, participantInfo) => { - const { id, timestamp, type } = reader.info; + const { id, timestamp } = reader.info; const streamObservable = from(reader).pipe( map((chunk) => { return { @@ -220,7 +220,7 @@ export function setupChat(room: Room, options?: ChatOptions) { timestamp, message: chunk.collected, from: room.getParticipantByIdentity(participantInfo.identity), - editTimestamp: type === 'update' ? timestamp : undefined, + // editTimestamp: type === 'update' ? timestamp : undefined, } as ReceivedChatMessage; }), ); @@ -276,17 +276,18 @@ export function setupChat(room: Room, options?: ChatOptions) { const update = async (messageId: string, message: string) => { isSending$.next(true); try { - const info = await room.localParticipant.updateText(messageId, message); - - const chatMsg: ReceivedChatMessage = { - id: info.id, - timestamp: info.timestamp, - editTimestamp: info.timestamp, - message, - from: room.localParticipant, - }; - messageSubject.next(chatMsg); - return chatMsg; + throw Error('not implemented'); + // const info = await room.localParticipant.updateText(messageId, message); + + // const chatMsg: ReceivedChatMessage = { + // id: info.id, + // timestamp: info.timestamp, + // editTimestamp: info.timestamp, + // message, + // from: room.localParticipant, + // }; + // messageSubject.next(chatMsg); + // return chatMsg; } finally { isSending$.next(false); } From 3a149cc3995dc7ad935d40bd47722b3f7a776134 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Tue, 11 Feb 2025 11:56:16 +0100 Subject: [PATCH 06/21] merge hackgpt --- eslint.config.mjs | 11 + .../nextjs/public/reduced-mark-on-white.png | Bin 0 -> 1077 bytes examples/openGPT/.eslintrc.json | 3 + examples/openGPT/.gitignore | 40 + examples/openGPT/lib/helper.ts | 6 + examples/openGPT/next.config.cjs | 21 + examples/openGPT/package.json | 35 + examples/openGPT/pages/_app.tsx | 11 + examples/openGPT/pages/api/livekit/token.ts | 51 ++ examples/openGPT/pages/index.tsx | 148 ++++ examples/openGPT/public/favicon.ico | Bin 0 -> 15406 bytes examples/openGPT/public/logo.png | Bin 0 -> 1077 bytes examples/openGPT/styles/globals.css | 139 ++++ examples/openGPT/styles/theme.scss | 66 ++ examples/openGPT/tsconfig.json | 27 + package.json | 1 + packages/core/src/observables/dataChannel.ts | 27 +- packages/core/src/observables/room.ts | 39 +- packages/react/src/components/ChatEntry.tsx | 42 +- .../src/components/controls/RichUserInput.tsx | 109 +++ packages/react/src/components/index.ts | 2 + packages/react/src/hooks/useChatInput.tsx | 62 ++ packages/react/src/hooks/useFileSend.ts | 23 + packages/react/src/prefabs/Chat.tsx | 40 +- .../react/src/prefabs/MediaDeviceMenu.tsx | 5 +- .../src/prefabs/VoiceAssistantControlBar.tsx | 3 +- pnpm-lock.yaml | 711 ++++++++++++++++++ 27 files changed, 1570 insertions(+), 52 deletions(-) create mode 100644 eslint.config.mjs create mode 100644 examples/nextjs/public/reduced-mark-on-white.png create mode 100644 examples/openGPT/.eslintrc.json create mode 100644 examples/openGPT/.gitignore create mode 100644 examples/openGPT/lib/helper.ts create mode 100644 examples/openGPT/next.config.cjs create mode 100644 examples/openGPT/package.json create mode 100644 examples/openGPT/pages/_app.tsx create mode 100644 examples/openGPT/pages/api/livekit/token.ts create mode 100644 examples/openGPT/pages/index.tsx create mode 100644 examples/openGPT/public/favicon.ico create mode 100644 examples/openGPT/public/logo.png create mode 100644 examples/openGPT/styles/globals.css create mode 100644 examples/openGPT/styles/theme.scss create mode 100644 examples/openGPT/tsconfig.json create mode 100644 packages/react/src/components/controls/RichUserInput.tsx create mode 100644 packages/react/src/hooks/useChatInput.tsx create mode 100644 packages/react/src/hooks/useFileSend.ts diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..d9fa4b0f9 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,11 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + + +export default [ + {files: ["**/*.{js,mjs,cjs,ts}"]}, + {languageOptions: { globals: {...globals.browser, ...globals.node} }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; \ No newline at end of file diff --git a/examples/nextjs/public/reduced-mark-on-white.png b/examples/nextjs/public/reduced-mark-on-white.png new file mode 100644 index 0000000000000000000000000000000000000000..b5a4df57eda9df69e60ef7ee05eb7e818942da63 GIT binary patch literal 1077 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K589CU1)O5Y-!wd|}e>`0rLn;{GUO$)_8!qDZ zu=eS6yBH@nN%bJv&d$3n2N$QLZRqZB+`+g1OmNE`N3IE_pP2F-KUlo}{XqZ0AzSWm zJPa3#W45KqmrtL|KJVnqzklD}k%>#eY@sy|IGu2pEvAV zyZdGf!-qH5c2!ocDYcf4I6g=I{$8u|YI7I_)CL(UlUjF)U*2y2yvY4$*sfLm+SNDP zd~H9!7T znHAJjS5?$}e`PbQckli=HNE@B*$*mfcfC2A^We+BPe1be^fz-3!sz(R Zep1?F+Y1)Ev%unj!PC{xWt~$(695N { + // Important: return the modified config + config.module.rules = [ + ...config.module.rules, + { + test: /\.mjs$/, + enforce: 'pre', + use: ['source-map-loader'], + }, + ]; + return config; + }, +}; + +module.exports = nextConfig; diff --git a/examples/openGPT/package.json b/examples/openGPT/package.json new file mode 100644 index 000000000..f26897d24 --- /dev/null +++ b/examples/openGPT/package.json @@ -0,0 +1,35 @@ +{ + "name": "@livekit/open-gpt", + "version": "0.0.1", + "access": "restricted", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@livekit/components-react": "workspace:*", + "@livekit/components-styles": "workspace:*", + "@livekit/krisp-noise-filter": "0.2.12", + "@livekit/track-processors": "^0.3.2", + "@phosphor-icons/react": "^2.1.7", + "livekit-client": "^2.5.7", + "livekit-server-sdk": "^2.6.1", + "next": "^14.2.13", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-markdown": "^9.0.1", + "sass": "^1.54.6" + }, + "devDependencies": { + "@types/node": "^18.19.14", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.18", + "eslint-config-next": "^12.3.4", + "source-map-loader": "^4.0.2", + "typescript": "5.4.2" + } +} diff --git a/examples/openGPT/pages/_app.tsx b/examples/openGPT/pages/_app.tsx new file mode 100644 index 000000000..bd6ce5f80 --- /dev/null +++ b/examples/openGPT/pages/_app.tsx @@ -0,0 +1,11 @@ +import type { AppProps } from 'next/app.js'; +import '@livekit/components-styles'; +import '@livekit/components-styles/prefabs'; +import '../styles/globals.css'; +import '../styles/theme.scss'; + +function MyApp({ Component, pageProps }: AppProps) { + return ; +} + +export default MyApp; diff --git a/examples/openGPT/pages/api/livekit/token.ts b/examples/openGPT/pages/api/livekit/token.ts new file mode 100644 index 000000000..b434a895d --- /dev/null +++ b/examples/openGPT/pages/api/livekit/token.ts @@ -0,0 +1,51 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { AccessToken } from 'livekit-server-sdk'; +import type { AccessTokenOptions, VideoGrant } from 'livekit-server-sdk'; + +const apiKey = process.env.LK_API_KEY; +const apiSecret = process.env.LK_API_SECRET; + +const createToken = async (userInfo: AccessTokenOptions, grant: VideoGrant) => { + const at = new AccessToken(apiKey, apiSecret, userInfo); + at.addGrant(grant); + return await at.toJwt(); +}; + +export default async function handleToken(req: NextApiRequest, res: NextApiResponse) { + try { + const { roomName, identity, name, metadata } = req.query; + + if (typeof identity !== 'string') { + throw Error('provide one (and only one) identity'); + } + if (typeof roomName !== 'string') { + throw Error('provide one (and only one) roomName'); + } + + if (Array.isArray(name)) { + throw Error('provide max one name'); + } + if (Array.isArray(metadata)) { + throw Error('provide max one metadata string'); + } + + // if (!userSession.isAuthenticated) { + // res.status(403).end(); + // return; + // } + const grant: VideoGrant = { + room: roomName, + roomJoin: true, + canPublish: true, + canPublishData: true, + canSubscribe: true, + canUpdateOwnMetadata: true, + }; + const token = await createToken({ identity, name, metadata }, grant); + + res.status(200).json({ identity, accessToken: token }); + } catch (e) { + res.statusMessage = (e as Error).message; + res.status(500).end(); + } +} diff --git a/examples/openGPT/pages/index.tsx b/examples/openGPT/pages/index.tsx new file mode 100644 index 000000000..b0d06e62b --- /dev/null +++ b/examples/openGPT/pages/index.tsx @@ -0,0 +1,148 @@ +'use client'; + +import { + LiveKitRoom, + useToken, + setLogLevel, + useConnectionState, + Chat, + useVoiceAssistant, + BarVisualizer, + VoiceAssistantControlBar, + useRoomContext, + RoomAudioRenderer, +} from '@livekit/components-react'; +import { ChatText, Headphones } from '@phosphor-icons/react'; +import { useKrispNoiseFilter } from '@livekit/components-react/krisp'; + +import type { NextPage } from 'next'; +import { generateRandomUserId } from '../lib/helper'; +import { FormEvent, useEffect, useMemo, useState } from 'react'; +import { ConnectionState, Track, AgentState } from 'livekit-client'; + +function GptUi() { + const connectionState = useConnectionState(); + const room = useRoomContext(); + + const [audioMode, setAudioMode] = useState(false); + const [isSwitchingModes, setIsSwitchingModes] = useState(false); + + const agent = useVoiceAssistant(); + + const krisp = useKrispNoiseFilter(); + + const handleModeSwitch = async () => { + setIsSwitchingModes(true); + try { + let nextMode = !audioMode; + setAudioMode(nextMode); + await room.localParticipant.setMicrophoneEnabled(nextMode); + krisp.setNoiseFilterEnabled(true); + } finally { + setIsSwitchingModes(false); + } + }; + + return ( +
+ {connectionState === ConnectionState.Connected && agent.state !== 'connecting' ? ( + <> +
+ +

+ + hackGPT +

+ {/* */} +
+
+ +
+ + + + +
+
+ + ) : ( +
+
+
+
+
+
+
+
+ )} +
+ ); +} + +const MinimalExample: NextPage = () => { + const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null; + const roomName = useMemo(() => params?.get('room') ?? 'test-room' + generateRandomUserId(), []); + setLogLevel('info', { liveKitClientLogLevel: 'debug' }); + + const tokenOptions = useMemo(() => { + const userId = params?.get('user') ?? generateRandomUserId(); + return { + userInfo: { + identity: userId, + name: userId, + }, + }; + }, []); + + const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, tokenOptions); + + return ( +
+ { + console.error(e); + alert( + 'Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab', + ); + }} + > + + +
+ ); +}; + +export default MinimalExample; diff --git a/examples/openGPT/public/favicon.ico b/examples/openGPT/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8fcb285273d88add3e950383d21efc0da5d3fad1 GIT binary patch literal 15406 zcmeHN&ubGw6dut(pf*Wc^^%B6DU}otMG>Qcw1_`wMZuD&tv&dw2nrrN>sdTCJqUX8 zV4zTf2hTltD)>}0atY|>2NW3#jS=DqjLn>Vv>m$3uv zAnWU6{Fr8s4ly>)7)z&X=fj5?d(Yd(#{OSF#n{&rW5fI&F2W^h?fjtp_4oJx4GH4% ztpd7Rm4#fLd?Jw`88saVem~&T=aYxr_VzaI?Cf}0U0ro#E?)jXcNYH0ebmWAc5Q8q zmX?+ryn6kXvg6O_T>cHA56K3lJY-!vGbi$N_bEAjxNX8QP(FX{6BVYagg%Cjob>u^ zmpt@@u^;lV+k`)iU%6ba^R=1rgFeK7z>n}>A3th2U%UzZIqkmji$h+S6bu@XXVM&( zIFO$j{kU)NHBQLfxAQ#gIES^>>i6gUuzZ@zcRc^eWRi4{b7LZr;`y=$P3{WxA9^=8 zH=VT?`MVJo7Z*L=v94e}21-wUId%ltpw$nZySuxA5Z4uVUBlW88^FVU(LX;wPq|#K z4uvZ(>3Z>-19x8m9)JMfam|Q-=vMVNHvZh%3xRXkp}MC*Ti5CzJ@bHaeBTJ1<$FkQ z0E}eswZ(tv1>XSvH{e&--ZKAj>yMj%k?&rNf6;%$e4P15v?a>?-zfh@sb5@wo7KNp z>o5Eg_a363LI2&ohi2_XZVJplK|1#6n$>Uj-?}w2%ImSmU!%G}ue761!b`Mlj-$it zB#4zTh%%0&?mO;z64pO!e{ZidHh$tAf6Kq+KWq$ykvGo&0|Nup3glWao|Et#mP`)% zVEFuGWIycQ*w~=0tu12|i$x#LgG0w@V)`?c7JhpuKK|+WV&H?Z{jgnEcUE05l}dH? zPu}?A)#^~MgD)5lYrpy(EaGRs@xQ*lPSt9amY0_UaqIRA8a_SaVI(_C7bf2kV#o#N zLldaNp_zw2Jlwqho!7)?3G-NlLF~hi(Ds9O*sjxCm|F48 zRrrGO(8+18Tl@DR|3{CF^4Om9;e4UYzwKG^kiYt|nfGwI_=+TcV7vPc$vvV?6U(h! zxArUI-}0|s{}u5UN&IyC{8jMe`0rLn;{GUO$)_8!qDZ zu=eS6yBH@nN%bJv&d$3n2N$QLZRqZB+`+g1OmNE`N3IE_pP2F-KUlo}{XqZ0AzSWm zJPa3#W45KqmrtL|KJVnqzklD}k%>#eY@sy|IGu2pEvAV zyZdGf!-qH5c2!ocDYcf4I6g=I{$8u|YI7I_)CL(UlUjF)U*2y2yvY4$*sfLm+SNDP zd~H9!7T znHAJjS5?$}e`PbQckli=HNE@B*$*mfcfC2A^We+BPe1be^fz-3!sz(R Zep1?F+Y1)Ev%unj!PC{xWt~$(695N( export function setupChatMessageHandler(room: Room) { const chatObservable = createChatObserver(room); + const textStreamObservable = createTextStreamObserver(room); - const send = async (text: string) => { - const msg = await room.localParticipant.sendChatMessage(text); - return msg; + const send = async (text: string, options: SendTextOptions): Promise => { + const msg = await room.localParticipant.sendChatMessage(text, options); + await room.localParticipant.sendText(text, options); + console.log('attached files', options.attachedFiles); + return { ...msg, from: room.localParticipant, attachedFiles: options.attachedFiles }; }; const edit = async (text: string, originalMsg: ChatMessage) => { @@ -92,5 +97,5 @@ export function setupChatMessageHandler(room: Room) { return msg; }; - return { chatObservable, send, edit }; + return { chatObservable, send, edit, textStreamObservable }; } diff --git a/packages/core/src/observables/room.ts b/packages/core/src/observables/room.ts index 4359b624f..ae403479b 100644 --- a/packages/core/src/observables/room.ts +++ b/packages/core/src/observables/room.ts @@ -1,10 +1,11 @@ import type { Subscriber, Subscription } from 'rxjs'; import { Subject, map, Observable, startWith, finalize, filter, concat } from 'rxjs'; -import type { Participant, TrackPublication } from 'livekit-client'; +import type { ChatMessage, Participant, TrackPublication } from 'livekit-client'; import { LocalParticipant, Room, RoomEvent, Track } from 'livekit-client'; // @ts-ignore some module resolutions (other than 'node') choke on this import type { RoomEventCallbacks } from 'livekit-client/dist/src/room/Room'; import { log } from '../logger'; +import { ReceivedChatMessage } from '../components/chat'; export function observeRoomEvents(room: Room, ...events: RoomEvent[]): Observable { const observable = new Observable((subscribe) => { const onRoomUpdate = () => { @@ -220,6 +221,42 @@ export function createDataObserver(room: Room) { return roomEventSelector(room, RoomEvent.DataReceived); } +export function createTextStreamObserver(room: Room) { + const chatMessages: Map = new Map(); + + const chatMessageSubject = new Subject(); + + room.on(RoomEvent.TextStreamReceived, async (info, stream, participant) => { + if (info.isFinite && info.topic === 'chat') { + handleChatMessage( + { + id: info.messageId, + timestamp: info.timestamp, + message: (await stream.readAll()).join(''), + }, + participant, + ); + } else { + for await (const msg of stream) { + handleChatMessage( + { + id: info.messageId, + timestamp: info.timestamp, + message: [chatMessages.get(info.messageId)?.message ?? '', msg].join(''), + }, + participant, + ); + } + } + }); + + function handleChatMessage(msg: ChatMessage, participant?: Participant) { + chatMessages.set(msg.id, msg); + chatMessageSubject.next({ ...msg, from: participant }); + } + return chatMessageSubject; +} + export function createChatObserver(room: Room) { return roomEventSelector(room, RoomEvent.ChatMessage); } diff --git a/packages/react/src/components/ChatEntry.tsx b/packages/react/src/components/ChatEntry.tsx index 90382a10a..26e0fa67d 100644 --- a/packages/react/src/components/ChatEntry.tsx +++ b/packages/react/src/components/ChatEntry.tsx @@ -47,6 +47,29 @@ export const ChatEntry: ( const time = new Date(entry.timestamp); const locale = navigator ? navigator.language : 'en-US'; + let name = entry.from?.name ?? entry.from?.identity; + if (!name || name === '') { + name = 'hackGPT'; + } + + React.useEffect(() => { + entry.attachedFiles?.forEach((file) => { + const reader = new FileReader(); + reader.onload = function (e) { + const imagePreview = document.querySelector( + `.lk-file-preview#${encodeURI(file.name.replaceAll('.', '-'))}`, + ) as HTMLImageElement; + // Set the preview image source to the file URL + imagePreview.src = e.target!.result; + // Show the image element + imagePreview.style.display = 'block'; + imagePreview.style.maxWidth = '360px'; + }; + // Read the file as a Data URL + reader.readAsDataURL(file); + }); + }, [entry]); + return (
  • {(!hideTimestamp || !hideName || hasBeenEdited) && ( - {!hideName && ( - - {entry.from?.name ?? entry.from?.identity} - - )} + {!hideName && {name}} {(!hideTimestamp || hasBeenEdited) && ( @@ -73,6 +92,19 @@ export const ChatEntry: ( )} {formattedMessage} + {entry.attachedFiles && ( +
    + {entry.attachedFiles.map((file) => ( +
    + +
    + ))} +
    + )}
  • ); }, diff --git a/packages/react/src/components/controls/RichUserInput.tsx b/packages/react/src/components/controls/RichUserInput.tsx new file mode 100644 index 000000000..f92284a20 --- /dev/null +++ b/packages/react/src/components/controls/RichUserInput.tsx @@ -0,0 +1,109 @@ +import * as React from 'react'; +import { useRoomContext } from '../../context'; +import { ChatMessage, SendTextOptions } from 'livekit-client'; + +export interface RichUserInputProps { + send: (text: string, options: SendTextOptions) => Promise; +} + +export function RichUserInput(props: RichUserInputProps) { + const fileRef = React.useRef(null); + const textRef = React.useRef(null); + const room = useRoomContext(); + + const [isSending, setIsSending] = React.useState(false); + const [filesToSend, setFilesToSend] = React.useState>(new Map()); + + const handleFileInput = () => { + if (fileRef.current === null) { + return; + } + const files = fileRef.current.files; + if (files) { + for (let i = 0; i < files?.length; i++) { + const item = files.item(i); + if (item) { + filesToSend.set(item.name, item); + } + } + } + setFilesToSend(new Map(filesToSend)); + }; + + const handleFileDelete = (file: File) => { + filesToSend.delete(file.name); + setFilesToSend(new Map(filesToSend)); + }; + + const handleSubmit = React.useCallback( + async (ev: React.FormEvent) => { + ev.preventDefault(); + if (!textRef.current || !fileRef.current) { + return; + } + setIsSending(true); + try { + console.log('sending message'); + await props.send(textRef.current.value, { + topic: 'user-message', + attachedFiles: Array.from(filesToSend.values()), + }); + } finally { + fileRef.current.files = null; + textRef.current.value = ''; + setIsSending(false); + filesToSend.clear(); + setFilesToSend(filesToSend); + textRef.current.focus(); + } + }, + [room], + ); + return ( + <> +
    +
    + {Array.from(filesToSend.values()).map((file) => ( +
    + {file.name} + +
    + ))} +
    +
    + + + ev.stopPropagation()} + onKeyDown={(ev) => ev.stopPropagation()} + onKeyUp={(ev) => ev.stopPropagation()} + /> + +
    +
    + + ); +} diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index ca61b230a..49fe71abb 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -31,3 +31,5 @@ export { ChatEntry, formatChatMessageLinks, } from '../components/ChatEntry'; + +export * from './controls/RichUserInput'; diff --git a/packages/react/src/hooks/useChatInput.tsx b/packages/react/src/hooks/useChatInput.tsx new file mode 100644 index 000000000..f42acdd6d --- /dev/null +++ b/packages/react/src/hooks/useChatInput.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { useRoomContext } from '../context'; +import { ChatMessage, SendTextOptions } from 'livekit-client'; + +interface ChatInputProps { + onSend: (text: string, options: SendTextOptions) => Promise; +} + +export function useChatInput({ onSend }: ChatInputProps) { + const fileRef = React.useRef(null); + const textRef = React.useRef(null); + const room = useRoomContext(); + + const [isSending, setIsSending] = React.useState(false); + const [filesToSend, setFilesToSend] = React.useState>(new Map()); + + const onInput = () => { + if (fileRef.current === null) { + return; + } + const files = fileRef.current.files; + if (files) { + for (let i = 0; i < files?.length; i++) { + const item = files.item(i); + if (item) { + filesToSend.set(item.name, item); + } + } + } + setFilesToSend(new Map(filesToSend)); + }; + + const handleFileDelete = (file: File) => { + filesToSend.delete(file.name); + setFilesToSend(new Map(filesToSend)); + }; + + const handleSubmit = React.useCallback( + async (ev: React.FormEvent) => { + ev.preventDefault(); + if (!textRef.current || !fileRef.current) { + return; + } + setIsSending(true); + try { + console.log('sending message'); + await onSend(textRef.current.value, { + topic: 'user-message', + attachedFiles: Array.from(filesToSend.values()), + }); + } finally { + fileRef.current.files = null; + textRef.current.value = ''; + setIsSending(false); + filesToSend.clear(); + setFilesToSend(filesToSend); + textRef.current.focus(); + } + }, + [room], + ); +} diff --git a/packages/react/src/hooks/useFileSend.ts b/packages/react/src/hooks/useFileSend.ts new file mode 100644 index 000000000..63a331ef5 --- /dev/null +++ b/packages/react/src/hooks/useFileSend.ts @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { useRoomContext } from '../context'; + +/** + * @alpha + */ +export function useFileSend() { + const room = useRoomContext(); + + const [isSending, setIsSending] = React.useState(false); + const [progress, setProgress] = React.useState(0); + + const sendFile = async (file: File) => { + setIsSending(true); + try { + await room.localParticipant.sendFile(file, { mimeType: file.type, topic: 'user-file' }); + } finally { + setIsSending(false); + } + }; + + return { isSending, progress, send: sendFile }; +} diff --git a/packages/react/src/prefabs/Chat.tsx b/packages/react/src/prefabs/Chat.tsx index 7fe8fd80d..f47996757 100644 --- a/packages/react/src/prefabs/Chat.tsx +++ b/packages/react/src/prefabs/Chat.tsx @@ -5,8 +5,8 @@ import { cloneSingleChild } from '../utils'; import type { MessageFormatter } from '../components/ChatEntry'; import { ChatEntry } from '../components/ChatEntry'; import { useChat } from '../hooks/useChat'; -import { ChatToggle } from '../components'; -import { ChatCloseIcon } from '../assets/icons'; +import { ChatToggle, RichUserInput } from '../components'; +import ChatCloseIcon from '../assets/icons/ChatCloseIcon'; /** @public */ export interface ChatProps extends React.HTMLAttributes, ChatOptions { @@ -32,29 +32,17 @@ export function Chat({ channelTopic, ...props }: ChatProps) { - const inputRef = React.useRef(null); const ulRef = React.useRef(null); const chatOptions: ChatOptions = React.useMemo(() => { return { messageDecoder, messageEncoder, channelTopic }; }, [messageDecoder, messageEncoder, channelTopic]); - const { send, chatMessages, isSending } = useChat(chatOptions); + const { chatMessages, send, isSending } = useChat(chatOptions); const layoutContext = useMaybeLayoutContext(); const lastReadMsgAt = React.useRef(0); - async function handleSubmit(event: React.FormEvent) { - event.preventDefault(); - if (inputRef.current && inputRef.current.value.trim() !== '') { - if (send) { - await send(inputRef.current.value); - inputRef.current.value = ''; - inputRef.current.focus(); - } - } - } - React.useEffect(() => { if (ulRef) { ulRef.current?.scrollTo({ top: ulRef.current.scrollHeight }); @@ -96,7 +84,11 @@ export function Chat({ )} -
      +
        {props.children ? chatMessages.map((msg, idx) => cloneSingleChild(props.children, { @@ -121,21 +113,7 @@ export function Chat({ ); })}
      -
      - ev.stopPropagation()} - onKeyDown={(ev) => ev.stopPropagation()} - onKeyUp={(ev) => ev.stopPropagation()} - /> - -
      + ); } diff --git a/packages/react/src/prefabs/MediaDeviceMenu.tsx b/packages/react/src/prefabs/MediaDeviceMenu.tsx index 95b374ad1..a6c6398a0 100644 --- a/packages/react/src/prefabs/MediaDeviceMenu.tsx +++ b/packages/react/src/prefabs/MediaDeviceMenu.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { MediaDeviceSelect } from '../components/controls/MediaDeviceSelect'; import { log } from '@livekit/components-core'; import type { LocalAudioTrack, LocalVideoTrack } from 'livekit-client'; +import { useMediaDevices } from '../hooks'; /** @public */ export interface MediaDeviceMenuProps extends React.ButtonHTMLAttributes { @@ -112,7 +113,7 @@ export function MediaDeviceMenu({ {props.children} {/** only render when enabled in order to make sure that the permissions are requested only if the menu is enabled */} - {!props.disabled && ( + {
      )}
      - )} + } ); } diff --git a/packages/react/src/prefabs/VoiceAssistantControlBar.tsx b/packages/react/src/prefabs/VoiceAssistantControlBar.tsx index ece660ec6..40f9ec8d6 100644 --- a/packages/react/src/prefabs/VoiceAssistantControlBar.tsx +++ b/packages/react/src/prefabs/VoiceAssistantControlBar.tsx @@ -1,7 +1,6 @@ import { Track } from 'livekit-client'; import * as React from 'react'; import { MediaDeviceMenu } from './MediaDeviceMenu'; -import { DisconnectButton } from '../components/controls/DisconnectButton'; import { TrackToggle } from '../components/controls/TrackToggle'; import { useLocalParticipant, @@ -103,7 +102,7 @@ export function VoiceAssistantControlBar({ )} - {visibleControls.leave && {'Disconnect'}} + {/* {visibleControls.leave && {'Disconnect'}} */} ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 717f766f6..f17e6cec2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -158,6 +158,64 @@ importers: specifier: 5.4.2 version: 5.4.2 + examples/openGPT: + dependencies: + '@livekit/components-react': + specifier: workspace:* + version: link:../../packages/react + '@livekit/components-styles': + specifier: workspace:* + version: link:../../packages/styles + '@livekit/krisp-noise-filter': + specifier: 0.2.12 + version: 0.2.12(livekit-client@2.9.1) + '@livekit/track-processors': + specifier: ^0.3.2 + version: 0.3.2(livekit-client@2.9.1) + '@phosphor-icons/react': + specifier: ^2.1.7 + version: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + livekit-client: + specifier: ^2.5.7 + version: 2.9.1 + livekit-server-sdk: + specifier: ^2.6.1 + version: 2.6.1 + next: + specifier: ^14.2.13 + version: 14.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.84.0) + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + react-markdown: + specifier: ^9.0.1 + version: 9.0.3(@types/react@18.3.18)(react@18.3.1) + sass: + specifier: ^1.54.6 + version: 1.84.0 + devDependencies: + '@types/node': + specifier: ^18.19.14 + version: 18.19.75 + '@types/react': + specifier: ^18.2.55 + version: 18.3.18 + '@types/react-dom': + specifier: ^18.2.18 + version: 18.3.5(@types/react@18.3.18) + eslint-config-next: + specifier: ^12.3.4 + version: 12.3.4(eslint@8.57.1)(typescript@5.4.2) + source-map-loader: + specifier: ^4.0.2 + version: 4.0.2(webpack@5.97.1) + typescript: + specifier: 5.4.2 + version: 5.4.2 + packages/core: dependencies: '@floating-ui/dom': @@ -2282,6 +2340,13 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@phosphor-icons/react@2.1.7': + resolution: {integrity: sha512-g2e2eVAn1XG2a+LI09QU3IORLhnFNAFkNbo2iwbX6NOKSLOwvEMmTa7CgOzEbgNWR47z8i8kwjdvYZ5fkGx1mQ==} + engines: {node: '>=10'} + peerDependencies: + react: '>= 16.8' + react-dom: '>= 16.8' + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -3323,6 +3388,9 @@ packages: '@types/cross-spawn@6.0.6': resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/detect-port@1.3.5': resolution: {integrity: sha512-Rf3/lB9WkDfIL9eEKaSYKc+1L/rNVYBjThk22JTqQw0YozXarX8YljFAz+HCoC6h4B4KwCMsBPZHaFezwT4BNA==} @@ -3347,6 +3415,9 @@ packages: '@types/eslint@9.6.1': resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@0.0.51': resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} @@ -3371,6 +3442,9 @@ packages: '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/heft-jest@1.0.1': resolution: {integrity: sha512-cF2iEUpvGh2WgLowHVAdjI05xuDo+GwCA8hGV3Q5PBl8apjd6BTcpPFQ2uPlfUM7BLpgur2xpYo8VeBXopMI4A==} @@ -3407,6 +3481,9 @@ packages: '@types/md5@2.3.5': resolution: {integrity: sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} @@ -3419,6 +3496,9 @@ packages: '@types/minimatch@5.1.2': resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node-fetch@2.6.12': resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} @@ -3484,6 +3564,9 @@ packages: '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} @@ -4089,6 +4172,9 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -4227,6 +4313,9 @@ packages: capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.1.2: resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} engines: {node: '>=12'} @@ -4242,6 +4331,18 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} @@ -4338,6 +4439,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -4526,6 +4630,9 @@ packages: decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + dedent@1.5.3: resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} peerDependencies: @@ -4619,6 +4726,9 @@ packages: engines: {node: '>= 4.0.0'} hasBin: true + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5063,6 +5173,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -5457,6 +5570,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-jsx-runtime@2.3.2: + resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -5478,6 +5597,9 @@ packages: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -5560,6 +5682,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -5572,6 +5697,12 @@ packages: resolution: {integrity: sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==} engines: {node: '>=8'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arguments@1.2.0: resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} engines: {node: '>= 0.4'} @@ -5621,6 +5752,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-deflate@1.0.0: resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} @@ -5657,6 +5791,9 @@ packages: resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -5685,6 +5822,10 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-plain-object@2.0.4: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} @@ -6135,6 +6276,9 @@ packages: resolution: {integrity: sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==} engines: {node: '>= 0.6.0'} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -6208,9 +6352,33 @@ packages: mdast-util-definitions@4.0.0: resolution: {integrity: sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==} + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + mdast-util-to-string@1.1.0: resolution: {integrity: sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==} + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.0.28: resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} @@ -6238,6 +6406,69 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + micromark-core-commonmark@2.0.2: + resolution: {integrity: sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.0.4: + resolution: {integrity: sha512-N6hXjrin2GTJDe3MVjf5FuXpm12PGm80BrUAeub9XFXca8JZbP+oIwY4LJSVwFUCL1IPm/WwSVUN7goFHmSGGQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.1: + resolution: {integrity: sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==} + + micromark@4.0.1: + resolution: {integrity: sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -6592,6 +6823,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -6867,6 +7101,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -6964,6 +7201,12 @@ packages: react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + react-markdown@9.0.3: + resolution: {integrity: sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -7071,6 +7314,12 @@ packages: remark-external-links@8.0.0: resolution: {integrity: sha512-5vPSX0kHoSsqtdftSHhIYofVINC8qmp0nctkeU9YoJwV3YfiBRiI6cbFRJ0oI/1F9xS+bopXG0m2KS8VFscuKA==} + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.1: + resolution: {integrity: sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==} + remark-slug@6.1.0: resolution: {integrity: sha512-oGCxDF9deA8phWvxFuyr3oSJsdyUAxMFbA0mZ7Y1Sas+emILtO+e5WutF9564gDsEN4IXaQXm5pFo6MLH+YmwQ==} @@ -7339,6 +7588,9 @@ packages: space-separated-tokens@1.1.5: resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} @@ -7440,6 +7692,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -7468,6 +7723,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-to-object@1.0.8: + resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + styled-jsx@5.1.1: resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -7671,6 +7929,12 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -7894,6 +8158,9 @@ packages: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unique-string@2.0.0: resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} engines: {node: '>=8'} @@ -7901,12 +8168,27 @@ packages: unist-util-is@4.1.0: resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + unist-util-visit-parents@3.1.1: resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + unist-util-visit@2.0.3: resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==} + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -7999,6 +8281,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@2.1.9: resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -8258,6 +8546,9 @@ packages: resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} engines: {node: '>=12.20'} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@ampproject/remapping@2.3.0': @@ -10130,6 +10421,11 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true + '@phosphor-icons/react@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@pkgjs/parseargs@0.11.0': optional: true @@ -11755,6 +12051,10 @@ snapshots: dependencies: '@types/node': 18.19.75 + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + '@types/detect-port@1.3.5': {} '@types/doctrine@0.0.3': {} @@ -11777,6 +12077,10 @@ snapshots: '@types/estree': 1.0.6 '@types/json-schema': 7.0.15 + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.6 + '@types/estree@0.0.51': {} '@types/estree@1.0.6': {} @@ -11808,6 +12112,10 @@ snapshots: dependencies: '@types/node': 18.19.75 + '@types/hast@3.0.4': + dependencies: + '@types/unist': 2.0.11 + '@types/heft-jest@1.0.1': dependencies: '@types/jest': 29.5.14 @@ -11843,6 +12151,10 @@ snapshots: '@types/md5@2.3.5': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 2.0.11 + '@types/mdx@2.0.13': {} '@types/mime-types@2.1.4': {} @@ -11851,6 +12163,8 @@ snapshots: '@types/minimatch@5.1.2': {} + '@types/ms@2.1.0': {} + '@types/node-fetch@2.6.12': dependencies: '@types/node': 18.19.75 @@ -11915,6 +12229,8 @@ snapshots: '@types/unist@2.0.11': {} + '@types/unist@3.0.3': {} + '@types/uuid@9.0.8': {} '@types/webpack@5.28.5': @@ -12703,6 +13019,8 @@ snapshots: babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.8) + bail@2.0.2: {} + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -12852,6 +13170,8 @@ snapshots: tslib: 2.8.1 upper-case-first: 2.0.2 + ccount@2.0.1: {} + chai@5.1.2: dependencies: assertion-error: 2.0.1 @@ -12882,6 +13202,14 @@ snapshots: char-regex@1.0.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chardet@0.7.0: {} charenc@0.0.2: {} @@ -12966,6 +13294,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -13139,6 +13469,10 @@ snapshots: decimal.js@10.4.3: {} + decode-named-character-reference@1.0.2: + dependencies: + character-entities: 2.0.2 + dedent@1.5.3: {} deep-eql@5.0.2: {} @@ -13234,6 +13568,10 @@ snapshots: transitivePeerDependencies: - supports-color + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + diff-sequences@29.6.3: {} dir-glob@3.0.1: @@ -13989,6 +14327,8 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -14473,6 +14813,30 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-jsx-runtime@2.3.2: + dependencies: + '@types/estree': 1.0.6 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + style-to-object: 1.0.8 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + he@1.2.0: {} header-case@2.0.4: @@ -14490,6 +14854,8 @@ snapshots: html-tags@3.3.1: {} + html-url-attributes@3.0.1: {} + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -14565,6 +14931,8 @@ snapshots: inherits@2.0.4: {} + inline-style-parser@0.2.4: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -14575,6 +14943,13 @@ snapshots: is-absolute-url@3.0.3: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-arguments@1.2.0: dependencies: call-bound: 1.0.3 @@ -14632,6 +15007,8 @@ snapshots: call-bound: 1.0.3 has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} + is-deflate@1.0.0: {} is-docker@2.2.1: {} @@ -14659,6 +15036,8 @@ snapshots: is-gzip@1.0.0: {} + is-hexadecimal@2.0.1: {} + is-interactive@1.0.0: {} is-map@2.0.3: {} @@ -14679,6 +15058,8 @@ snapshots: is-path-inside@3.0.3: {} + is-plain-obj@4.1.0: {} + is-plain-object@2.0.4: dependencies: isobject: 3.0.1 @@ -15397,6 +15778,8 @@ snapshots: loglevel@1.9.1: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -15466,8 +15849,97 @@ snapshots: dependencies: unist-util-visit: 2.0.3 + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + mdast-util-to-string@1.1.0: {} + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdn-data@2.0.28: {} mdn-data@2.0.30: {} @@ -15486,6 +15958,139 @@ snapshots: methods@1.1.2: {} + micromark-core-commonmark@2.0.2: + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.0.4 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.1 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.1 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.0.4: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.1: {} + + micromark@4.0.1: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.0(supports-color@5.5.0) + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.0.4 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -15844,6 +16449,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.0.2 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.26.2 @@ -16094,6 +16709,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@6.5.0: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -16213,6 +16830,23 @@ snapshots: react-is@18.2.0: {} + react-markdown@9.0.3(@types/react@18.3.18)(react@18.3.1): + dependencies: + '@types/hast': 3.0.4 + '@types/react': 18.3.18 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.2 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 18.3.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.1 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-refresh@0.14.2: {} react-remove-scroll-bar@2.3.8(@types/react@18.3.18)(react@18.3.1): @@ -16355,6 +16989,23 @@ snapshots: space-separated-tokens: 1.1.5 unist-util-visit: 2.0.3 + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.1 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + remark-slug@6.1.0: dependencies: github-slugger: 1.5.0 @@ -16674,6 +17325,8 @@ snapshots: space-separated-tokens@1.1.5: {} + space-separated-tokens@2.0.2: {} + spawndamnit@3.0.1: dependencies: cross-spawn: 7.0.6 @@ -16805,6 +17458,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -16825,6 +17483,10 @@ snapshots: strip-json-comments@3.1.1: {} + style-to-object@1.0.8: + dependencies: + inline-style-parser: 0.2.4 + styled-jsx@5.1.1(react@18.3.1): dependencies: client-only: 0.0.1 @@ -17038,6 +17700,10 @@ snapshots: tree-kill@1.2.2: {} + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@1.4.3(typescript@5.4.2): dependencies: typescript: 5.4.2 @@ -17251,23 +17917,56 @@ snapshots: unicorn-magic@0.1.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + unique-string@2.0.0: dependencies: crypto-random-string: 2.0.0 unist-util-is@4.1.0: {} + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit-parents@3.1.1: dependencies: '@types/unist': 2.0.11 unist-util-is: 4.1.0 + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit@2.0.3: dependencies: '@types/unist': 2.0.11 unist-util-is: 4.1.0 unist-util-visit-parents: 3.1.1 + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + universalify@0.1.2: {} universalify@2.0.1: {} @@ -17352,6 +18051,16 @@ snapshots: vary@1.1.2: {} + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + vite-node@2.1.9(@types/node@22.13.1)(sass@1.84.0)(terser@5.38.1): dependencies: cac: 6.7.14 @@ -17704,3 +18413,5 @@ snapshots: yocto-queue@0.1.0: {} yocto-queue@1.1.1: {} + + zwitch@2.0.4: {} From 00ab71974aee6c8d56baca69fade14ec0dba5ead Mon Sep 17 00:00:00 2001 From: lukasIO Date: Tue, 11 Feb 2025 14:26:24 +0100 Subject: [PATCH 07/21] backwards compatibility chat api --- examples/openGPT/pages/api/livekit/token.ts | 7 +- examples/openGPT/pages/index.tsx | 12 +- packages/core/src/components/chat.ts | 234 ++++-------------- packages/core/src/observables/dataChannel.ts | 15 +- packages/core/src/observables/room.ts | 39 +-- .../core/src/track-reference/test-utils.ts | 4 +- packages/core/src/utilis.test.ts | 2 + .../src/components/controls/RichUserInput.tsx | 4 +- packages/react/src/hooks/index.ts | 2 +- packages/react/src/hooks/useToken.ts | 36 +++ 10 files changed, 119 insertions(+), 236 deletions(-) diff --git a/examples/openGPT/pages/api/livekit/token.ts b/examples/openGPT/pages/api/livekit/token.ts index b434a895d..47bf4631a 100644 --- a/examples/openGPT/pages/api/livekit/token.ts +++ b/examples/openGPT/pages/api/livekit/token.ts @@ -2,8 +2,9 @@ import { NextApiRequest, NextApiResponse } from 'next'; import { AccessToken } from 'livekit-server-sdk'; import type { AccessTokenOptions, VideoGrant } from 'livekit-server-sdk'; -const apiKey = process.env.LK_API_KEY; -const apiSecret = process.env.LK_API_SECRET; +const apiKey = process.env.LIVEKIT_API_KEY; +const apiSecret = process.env.LIVEKIT_API_SECRET; +const url = process.env.LIVEKIT_URL; const createToken = async (userInfo: AccessTokenOptions, grant: VideoGrant) => { const at = new AccessToken(apiKey, apiSecret, userInfo); @@ -43,7 +44,7 @@ export default async function handleToken(req: NextApiRequest, res: NextApiRespo }; const token = await createToken({ identity, name, metadata }, grant); - res.status(200).json({ identity, accessToken: token }); + res.status(200).json({ identity, accessToken: token, url }); } catch (e) { res.statusMessage = (e as Error).message; res.status(500).end(); diff --git a/examples/openGPT/pages/index.tsx b/examples/openGPT/pages/index.tsx index b0d06e62b..95b44c5d7 100644 --- a/examples/openGPT/pages/index.tsx +++ b/examples/openGPT/pages/index.tsx @@ -2,7 +2,6 @@ import { LiveKitRoom, - useToken, setLogLevel, useConnectionState, Chat, @@ -11,6 +10,7 @@ import { VoiceAssistantControlBar, useRoomContext, RoomAudioRenderer, + useConnectCredentials, } from '@livekit/components-react'; import { ChatText, Headphones } from '@phosphor-icons/react'; import { useKrispNoiseFilter } from '@livekit/components-react/krisp'; @@ -18,7 +18,7 @@ import { useKrispNoiseFilter } from '@livekit/components-react/krisp'; import type { NextPage } from 'next'; import { generateRandomUserId } from '../lib/helper'; import { FormEvent, useEffect, useMemo, useState } from 'react'; -import { ConnectionState, Track, AgentState } from 'livekit-client'; +import { ConnectionState, Track } from 'livekit-client'; function GptUi() { const connectionState = useConnectionState(); @@ -123,7 +123,11 @@ const MinimalExample: NextPage = () => { }; }, []); - const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, tokenOptions); + const { token, url } = useConnectCredentials( + process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, + roomName, + tokenOptions, + ); return (
      @@ -131,7 +135,7 @@ const MinimalExample: NextPage = () => { video={false} audio={false} token={token} - serverUrl={process.env.NEXT_PUBLIC_LK_SERVER_URL} + serverUrl={url} onMediaDeviceFailure={(e) => { console.error(e); alert( diff --git a/packages/core/src/components/chat.ts b/packages/core/src/components/chat.ts index 1b3bb60a7..cd19ce31b 100644 --- a/packages/core/src/components/chat.ts +++ b/packages/core/src/components/chat.ts @@ -1,8 +1,13 @@ /* eslint-disable camelcase */ -import type { Participant, Room, ChatMessage } from 'livekit-client'; -import { RoomEvent } from 'livekit-client'; -import { BehaviorSubject, Subject, scan, map, takeUntil, from } from 'rxjs'; -import { DataTopic } from '../observables/dataChannel'; +import type { Participant, Room, ChatMessage, SendTextOptions } from 'livekit-client'; +import { compareVersions, RoomEvent } from 'livekit-client'; +import { BehaviorSubject, Subject, scan, map, takeUntil, from, filter } from 'rxjs'; +import { + DataTopic, + LegacyDataTopic, + sendMessage, + setupDataMessageHandler, +} from '../observables/dataChannel'; /** @public */ export type { ChatMessage }; @@ -13,11 +18,11 @@ export interface ReceivedChatMessage extends ChatMessage { } export interface LegacyChatMessage extends ChatMessage { - ignore?: boolean; + ignoreLegacy?: boolean; } export interface LegacyReceivedChatMessage extends ReceivedChatMessage { - ignore?: boolean; + ignoreLegacy?: boolean; } /** @@ -41,165 +46,27 @@ export type ChatOptions = { updateChannelTopic?: string; }; -// type RawMessage = { -// payload: Uint8Array; -// topic: string | undefined; -// from: Participant | undefined; -// }; - -// const encoder = new TextEncoder(); -// const decoder = new TextDecoder(); - const topicSubjectMap: Map>> = new Map(); -// const encode = (message: LegacyReceivedChatMessage) => encoder.encode(JSON.stringify(message)); - -// const decode = (message: Uint8Array) => -// JSON.parse(decoder.decode(message)) as LegacyReceivedChatMessage | ReceivedChatMessage; - -// export function setupChat(room: Room, options?: ChatOptions) { -// const onDestroyObservable = new Subject(); - -// const serverSupportsChatApi = () => -// room.serverInfo?.edition === 1 || -// (!!room.serverInfo?.version && compareVersions(room.serverInfo?.version, '1.17.2') > 0); - -// const { messageDecoder, messageEncoder, channelTopic, updateChannelTopic } = options ?? {}; - -// const topic = channelTopic ?? DataTopic.CHAT; - -// const updateTopic = updateChannelTopic ?? DataTopic.CHAT_UPDATE; - -// let needsSetup = false; -// if (!topicSubjectMap.has(room)) { -// needsSetup = true; -// } -// const topicMap = topicSubjectMap.get(room) ?? new Map>(); -// const messageSubject = topicMap.get(topic) ?? new Subject(); -// topicMap.set(topic, messageSubject); -// topicSubjectMap.set(room, topicMap); - -// if (needsSetup) { -// /** Subscribe to all appropriate messages sent over the wire. */ -// const { messageObservable } = setupDataMessageHandler(room, [topic, updateTopic]); -// messageObservable.pipe(takeUntil(onDestroyObservable)).subscribe(messageSubject); -// } -// const { chatObservable, send: sendChatMessage } = setupChatMessageHandler(room); - -// const finalMessageDecoder = messageDecoder ?? decode; - -// /** Build up the message array over time. */ -// const messagesObservable = merge( -// messageSubject.pipe( -// map((msg) => { -// const parsedMessage = finalMessageDecoder(msg.payload); -// const newMessage = { ...parsedMessage, from: msg.from }; -// if (isIgnorableChatMessage(newMessage)) { -// return undefined; -// } -// return newMessage; -// }), -// ), -// chatObservable.pipe( -// map(([msg, participant]) => { -// return { ...msg, from: participant }; -// }), -// ), -// ).pipe( -// scan((acc, value) => { -// // ignore legacy message updates -// if (!value) { -// return acc; -// } -// // handle message updates -// if ( -// 'id' in value && -// acc.find((msg) => msg.from?.identity === value.from?.identity && msg.id === value.id) -// ) { -// const replaceIndex = acc.findIndex((msg) => msg.id === value.id); -// if (replaceIndex > -1) { -// const originalMsg = acc[replaceIndex]; -// acc[replaceIndex] = { -// ...value, -// timestamp: originalMsg.timestamp, -// editTimestamp: value.editTimestamp ?? value.timestamp, -// }; -// } - -// return [...acc]; -// } -// return [...acc, value]; -// }, []), -// takeUntil(onDestroyObservable), -// ); - -// const isSending$ = new BehaviorSubject(false); - -// const finalMessageEncoder = messageEncoder ?? encode; - -// const send = async (message: string) => { -// isSending$.next(true); -// try { -// const chatMessage = await sendChatMessage(message); -// const encodedLegacyMsg = finalMessageEncoder({ -// ...chatMessage, -// ignore: serverSupportsChatApi(), -// }); -// await sendMessage(room.localParticipant, encodedLegacyMsg, { -// reliable: true, -// topic, -// }); -// return chatMessage; -// } finally { -// isSending$.next(false); -// } -// }; - -// const update = async (message: string, originalMessageOrId: string | ChatMessage) => { -// const timestamp = Date.now(); -// const originalMessage: ChatMessage = -// typeof originalMessageOrId === 'string' -// ? { id: originalMessageOrId, message: '', timestamp } -// : originalMessageOrId; -// isSending$.next(true); -// try { -// const editedMessage = await room.localParticipant.editChatMessage(message, originalMessage); -// const encodedLegacyMessage = finalMessageEncoder(editedMessage); -// await sendMessage(room.localParticipant, encodedLegacyMessage, { -// topic: updateTopic, -// reliable: true, -// }); -// return editedMessage; -// } finally { -// isSending$.next(false); -// } -// }; - -// function destroy() { -// onDestroyObservable.next(); -// onDestroyObservable.complete(); -// topicSubjectMap.delete(room); -// } -// room.once(RoomEvent.Disconnected, destroy); +function isIgnorableChatMessage(msg: ReceivedChatMessage | LegacyReceivedChatMessage) { + return (msg as LegacyChatMessage).ignoreLegacy == true; +} -// return { -// messageObservable: messagesObservable, -// isSendingObservable: isSending$, -// send, -// update, -// }; -// } +const decodeLegacyMsg = (message: Uint8Array) => + JSON.parse(new TextDecoder().decode(message)) as LegacyReceivedChatMessage | ReceivedChatMessage; -// function isIgnorableChatMessage( -// msg: ReceivedChatMessage | LegacyReceivedChatMessage, -// ): msg is ReceivedChatMessage { -// return (msg as LegacyChatMessage).ignore == true; -// } +const encodeLegacyMsg = (message: LegacyReceivedChatMessage) => + new TextEncoder().encode(JSON.stringify(message)); export function setupChat(room: Room, options?: ChatOptions) { + const serverSupportsDataStreams = () => + room.serverInfo?.edition === 1 || + (!!room.serverInfo?.version && compareVersions(room.serverInfo?.version, '1.8.2') > 0); + const onDestroyObservable = new Subject(); const topic = options?.channelTopic ?? DataTopic.CHAT; + const legacyTopic = options?.channelTopic ?? LegacyDataTopic.CHAT; let needsSetup = false; if (!topicSubjectMap.has(room)) { @@ -210,6 +77,8 @@ export function setupChat(room: Room, options?: ChatOptions) { topicMap.set(topic, messageSubject); topicSubjectMap.set(room, topicMap); + const finalMessageDecoder = options?.messageDecoder ?? decodeLegacyMsg; + if (needsSetup) { room.registerTextStreamHandler(topic, async (reader, participantInfo) => { const { id, timestamp } = reader.info; @@ -228,12 +97,26 @@ export function setupChat(room: Room, options?: ChatOptions) { next: (value) => messageSubject.next(value), }); }); + const { messageObservable } = setupDataMessageHandler(room, [legacyTopic]); + messageObservable + .pipe( + map((msg) => { + const parsedMessage = finalMessageDecoder(msg.payload); + if (isIgnorableChatMessage(parsedMessage)) { + return undefined; + } + const newMessage: ReceivedChatMessage = { ...parsedMessage, from: msg.from }; + return newMessage; + }), + filter((msg) => !!msg), + takeUntil(onDestroyObservable), + ) + .subscribe(messageSubject); } /** Build up the message array over time. */ const messagesObservable = messageSubject.pipe( scan((acc, value) => { - // handle message updates if ( 'id' in value && acc.find((msg) => msg.from?.identity === value.from?.identity && msg.id === value.id) @@ -246,7 +129,6 @@ export function setupChat(room: Room, options?: ChatOptions) { timestamp: originalMsg.timestamp, }; } - return [...acc]; } return [...acc, value]; @@ -255,11 +137,14 @@ export function setupChat(room: Room, options?: ChatOptions) { ); const isSending$ = new BehaviorSubject(false); + const finalMessageEncoder = options?.messageEncoder ?? encodeLegacyMsg; - const send = async (message: string) => { + const send = async (message: string, options: SendTextOptions) => { + options.topic ??= topic; isSending$.next(true); + try { - const info = await room.localParticipant.sendText(message, { topic }); + const info = await room.localParticipant.sendText(message, options); const chatMsg: ReceivedChatMessage = { id: info.id, timestamp: Date.now(), @@ -267,32 +152,20 @@ export function setupChat(room: Room, options?: ChatOptions) { from: room.localParticipant, }; messageSubject.next(chatMsg); + const encodedLegacyMsg = finalMessageEncoder({ + ...chatMsg, + ignoreLegacy: serverSupportsDataStreams(), + }); + await sendMessage(room.localParticipant, encodedLegacyMsg, { + reliable: true, + topic: legacyTopic, + }); return chatMsg; } finally { isSending$.next(false); } }; - const update = async (messageId: string, message: string) => { - isSending$.next(true); - try { - throw Error('not implemented'); - // const info = await room.localParticipant.updateText(messageId, message); - - // const chatMsg: ReceivedChatMessage = { - // id: info.id, - // timestamp: info.timestamp, - // editTimestamp: info.timestamp, - // message, - // from: room.localParticipant, - // }; - // messageSubject.next(chatMsg); - // return chatMsg; - } finally { - isSending$.next(false); - } - }; - function destroy() { onDestroyObservable.next(); onDestroyObservable.complete(); @@ -306,6 +179,5 @@ export function setupChat(room: Room, options?: ChatOptions) { messageObservable: messagesObservable, isSendingObservable: isSending$, send, - update, }; } diff --git a/packages/core/src/observables/dataChannel.ts b/packages/core/src/observables/dataChannel.ts index 885e7bd3e..361c321b2 100644 --- a/packages/core/src/observables/dataChannel.ts +++ b/packages/core/src/observables/dataChannel.ts @@ -8,12 +8,16 @@ import { } from 'livekit-client'; import type { Subscriber } from 'rxjs'; import { Observable, filter, map } from 'rxjs'; -import { createChatObserver, createDataObserver, createTextStreamObserver } from './room'; +import { createChatObserver, createDataObserver } from './room'; import { ReceivedChatMessage } from '../components/chat'; export const DataTopic = { + CHAT: 'lk.chat', +} as const; + +/** @deprecated */ +export const LegacyDataTopic = { CHAT: 'lk-chat-topic', - CHAT_UPDATE: 'lk-chat-update-topic', } as const; /** Publish data from the LocalParticipant. */ @@ -83,13 +87,12 @@ export function setupDataMessageHandler( export function setupChatMessageHandler(room: Room) { const chatObservable = createChatObserver(room); - const textStreamObservable = createTextStreamObserver(room); const send = async (text: string, options: SendTextOptions): Promise => { const msg = await room.localParticipant.sendChatMessage(text, options); await room.localParticipant.sendText(text, options); - console.log('attached files', options.attachedFiles); - return { ...msg, from: room.localParticipant, attachedFiles: options.attachedFiles }; + console.log('attached files', options.attachments); + return { ...msg, from: room.localParticipant, attachedFiles: options.attachments }; }; const edit = async (text: string, originalMsg: ChatMessage) => { @@ -97,5 +100,5 @@ export function setupChatMessageHandler(room: Room) { return msg; }; - return { chatObservable, send, edit, textStreamObservable }; + return { chatObservable, send, edit }; } diff --git a/packages/core/src/observables/room.ts b/packages/core/src/observables/room.ts index ae403479b..4359b624f 100644 --- a/packages/core/src/observables/room.ts +++ b/packages/core/src/observables/room.ts @@ -1,11 +1,10 @@ import type { Subscriber, Subscription } from 'rxjs'; import { Subject, map, Observable, startWith, finalize, filter, concat } from 'rxjs'; -import type { ChatMessage, Participant, TrackPublication } from 'livekit-client'; +import type { Participant, TrackPublication } from 'livekit-client'; import { LocalParticipant, Room, RoomEvent, Track } from 'livekit-client'; // @ts-ignore some module resolutions (other than 'node') choke on this import type { RoomEventCallbacks } from 'livekit-client/dist/src/room/Room'; import { log } from '../logger'; -import { ReceivedChatMessage } from '../components/chat'; export function observeRoomEvents(room: Room, ...events: RoomEvent[]): Observable { const observable = new Observable((subscribe) => { const onRoomUpdate = () => { @@ -221,42 +220,6 @@ export function createDataObserver(room: Room) { return roomEventSelector(room, RoomEvent.DataReceived); } -export function createTextStreamObserver(room: Room) { - const chatMessages: Map = new Map(); - - const chatMessageSubject = new Subject(); - - room.on(RoomEvent.TextStreamReceived, async (info, stream, participant) => { - if (info.isFinite && info.topic === 'chat') { - handleChatMessage( - { - id: info.messageId, - timestamp: info.timestamp, - message: (await stream.readAll()).join(''), - }, - participant, - ); - } else { - for await (const msg of stream) { - handleChatMessage( - { - id: info.messageId, - timestamp: info.timestamp, - message: [chatMessages.get(info.messageId)?.message ?? '', msg].join(''), - }, - participant, - ); - } - } - }); - - function handleChatMessage(msg: ChatMessage, participant?: Participant) { - chatMessages.set(msg.id, msg); - chatMessageSubject.next({ ...msg, from: participant }); - } - return chatMessageSubject; -} - export function createChatObserver(room: Room) { return roomEventSelector(room, RoomEvent.ChatMessage); } diff --git a/packages/core/src/track-reference/test-utils.ts b/packages/core/src/track-reference/test-utils.ts index 1cd681ef0..86435aa1a 100644 --- a/packages/core/src/track-reference/test-utils.ts +++ b/packages/core/src/track-reference/test-utils.ts @@ -23,6 +23,7 @@ export const mockTrackReferencePublished = (id: string, source: Track.Source): T : Track.Kind.Audio; return { participant: new Participant(`${id}`, `${id}`), + // @ts-ignore publication: new TrackPublication(kind, `${id}`, `${id}`), source: source, }; @@ -43,8 +44,9 @@ export const mockTrackReferenceSubscribed = ( ? Track.Kind.Video : Track.Kind.Audio; + // @ts-ignore const publication = new TrackPublication(kind, `${id}`, `${id}`); - // @ts-expect-error + // @ts-ignore publication.track = {}; return { participant: options.mockParticipant diff --git a/packages/core/src/utilis.test.ts b/packages/core/src/utilis.test.ts index 63febc41e..4fecfb24d 100644 --- a/packages/core/src/utilis.test.ts +++ b/packages/core/src/utilis.test.ts @@ -5,9 +5,11 @@ import type { PinState } from './types'; describe('Test isTrackReferencePinned', () => { const participantA = new Participant('dummy-participant', 'A_id', 'track_A_name'); + // @ts-ignore const trackA = new TrackPublication(Track.Kind.Video, 'track_A_id', 'track_A_name'); trackA.trackSid = 'track_a_sid'; const participantB = new Participant('participant_B', 'B_id', 'B_name'); + // @ts-ignore const trackB = new TrackPublication(Track.Kind.Video, 'track_B_id', 'track_B_name'); trackB.trackSid = 'track_b_sid'; const trackReferenceA = { diff --git a/packages/react/src/components/controls/RichUserInput.tsx b/packages/react/src/components/controls/RichUserInput.tsx index f92284a20..df8d8dcb1 100644 --- a/packages/react/src/components/controls/RichUserInput.tsx +++ b/packages/react/src/components/controls/RichUserInput.tsx @@ -45,8 +45,8 @@ export function RichUserInput(props: RichUserInputProps) { try { console.log('sending message'); await props.send(textRef.current.value, { - topic: 'user-message', - attachedFiles: Array.from(filesToSend.values()), + topic: 'lk.chat', + attachments: Array.from(filesToSend.values()), }); } finally { fileRef.current.files = null; diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts index 3cb767b32..8e4d953e5 100644 --- a/packages/react/src/hooks/index.ts +++ b/packages/react/src/hooks/index.ts @@ -35,7 +35,7 @@ export { type UseStartAudioProps, useStartAudio } from './useStartAudio'; export { type UseStartVideoProps, useStartVideo } from './useStartVideo'; export { type UseSwipeOptions, useSwipe } from './useSwipe'; export { type UseChatToggleProps, useChatToggle } from './useChatToggle'; -export { type UseTokenOptions, type UserInfo, useToken } from './useToken'; +export { type UseTokenOptions, type UserInfo, useToken, useConnectCredentials } from './useToken'; export { useTrackMutedIndicator } from './useTrackMutedIndicator'; export { type UseTrackToggleProps, useTrackToggle } from './useTrackToggle'; export { type UseTracksHookReturnType, type UseTracksOptions, useTracks } from './useTracks'; diff --git a/packages/react/src/hooks/useToken.ts b/packages/react/src/hooks/useToken.ts index dc1f7396b..23b96f95f 100644 --- a/packages/react/src/hooks/useToken.ts +++ b/packages/react/src/hooks/useToken.ts @@ -52,3 +52,39 @@ export function useToken( }, [tokenEndpoint, roomName, JSON.stringify(options)]); return token; } + +export function useConnectCredentials( + tokenEndpoint: string | undefined, + roomName: string, + options: UseTokenOptions = {}, +) { + const [token, setToken] = React.useState(undefined); + const [url, setUrl] = React.useState(undefined); + const [identity, setIdentity] = React.useState(undefined); + + React.useEffect(() => { + if (tokenEndpoint === undefined) { + throw Error('token endpoint needs to be defined'); + } + if (options.userInfo?.identity === undefined) { + return; + } + const tokenFetcher = async () => { + log.debug('fetching token'); + const params = new URLSearchParams({ ...options.userInfo, roomName }); + const res = await fetch(`${tokenEndpoint}?${params.toString()}`); + if (!res.ok) { + log.error( + `Could not fetch token. Server responded with status ${res.status}: ${res.statusText}`, + ); + return; + } + const { accessToken, url, identity } = await res.json(); + setToken(accessToken); + setIdentity(identity); + setUrl(url); + }; + tokenFetcher(); + }, [tokenEndpoint, roomName, JSON.stringify(options)]); + return { url, token, identity }; +} From 935005745edda7f4e49ea821df03f4051901a130 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Tue, 11 Feb 2025 14:36:08 +0100 Subject: [PATCH 08/21] remove unneeded stuff --- packages/react/src/prefabs/Chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/prefabs/Chat.tsx b/packages/react/src/prefabs/Chat.tsx index f47996757..ecd1b3aca 100644 --- a/packages/react/src/prefabs/Chat.tsx +++ b/packages/react/src/prefabs/Chat.tsx @@ -38,7 +38,7 @@ export function Chat({ return { messageDecoder, messageEncoder, channelTopic }; }, [messageDecoder, messageEncoder, channelTopic]); - const { chatMessages, send, isSending } = useChat(chatOptions); + const { chatMessages, send } = useChat(chatOptions); const layoutContext = useMaybeLayoutContext(); const lastReadMsgAt = React.useRef(0); From 66a8049ecb1626a7f4108b1c029e7f323d3e1b5c Mon Sep 17 00:00:00 2001 From: lukasIO Date: Tue, 11 Feb 2025 17:36:19 +0100 Subject: [PATCH 09/21] user friendliness updataes --- examples/nextjs/pages/minimal.tsx | 2 +- examples/openGPT/lib/AgentChat.tsx | 81 +++++++++++++++++++ examples/openGPT/package.json | 2 +- examples/openGPT/pages/index.tsx | 58 ++++++------- packages/core/src/components/chat.ts | 7 +- packages/core/src/observables/dataChannel.ts | 1 - packages/react/src/components/ChatEntry.tsx | 20 +---- .../src/components/controls/RichUserInput.tsx | 50 +++++------- packages/react/src/prefabs/Chat.tsx | 38 ++++++--- packages/styles/scss/prefabs/chat.scss | 5 ++ pnpm-lock.yaml | 6 +- 11 files changed, 178 insertions(+), 92 deletions(-) create mode 100644 examples/openGPT/lib/AgentChat.tsx diff --git a/examples/nextjs/pages/minimal.tsx b/examples/nextjs/pages/minimal.tsx index 27153fec2..42d4f7877 100644 --- a/examples/nextjs/pages/minimal.tsx +++ b/examples/nextjs/pages/minimal.tsx @@ -3,7 +3,7 @@ import { LiveKitRoom, useToken, VideoConference, setLogLevel } from '@livekit/components-react'; import type { NextPage } from 'next'; import { generateRandomUserId } from '../lib/helper'; -import { useMemo } from 'react'; +import { useMemo, useCallback } from 'react'; const MinimalExample: NextPage = () => { const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null; diff --git a/examples/openGPT/lib/AgentChat.tsx b/examples/openGPT/lib/AgentChat.tsx new file mode 100644 index 000000000..41e11946f --- /dev/null +++ b/examples/openGPT/lib/AgentChat.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import { ChatEntry, ChatProps, useChat } from '@livekit/components-react'; + +export function AgentChat({ messageFormatter, ...props }: ChatProps) { + const ulRef = React.useRef(null); + const textRef = React.useRef(null); + + const { chatMessages, send, isSending } = useChat(); + + async function handleSubmit(event?: React.FormEvent) { + event?.preventDefault(); + if (textRef.current && textRef.current.value.trim() !== '') { + await send(textRef.current.value); + textRef.current.value = ''; + textRef.current.focus(); + handleInput(); + } + } + + React.useEffect(() => { + if (ulRef) { + ulRef.current?.scrollTo({ top: ulRef.current.scrollHeight }); + } + }, [ulRef, chatMessages]); + + const handleInput = () => { + if (!textRef.current) { + return; + } + textRef.current.style.height = 'auto'; // Reset height + textRef.current.style.height = textRef.current.scrollHeight + 'px'; // Set new height + }; + + const handleKeyUp: React.KeyboardEventHandler = (event) => { + event.stopPropagation(); + if (event.shiftKey) { + return; + } + if (event.key === 'Enter') { + handleSubmit(); + } + }; + + return ( +
      +
        + {chatMessages.map((msg, idx, allMsg) => { + return ( + + ); + })} +
      +
      +