From bdfbfb7ae9fb3417238bf7c9483e5287e817475e Mon Sep 17 00:00:00 2001 From: Stef Lewandowski Date: Tue, 29 Oct 2024 17:40:38 +0000 Subject: [PATCH 1/5] feat: show the section that is streaming in during testing --- .../Chat/Chat/hooks/useAilaStreamingStatus.ts | 90 ++++++++++++++----- .../AppComponents/Chat/chat-lhs-header.tsx | 8 +- 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/apps/nextjs/src/components/AppComponents/Chat/Chat/hooks/useAilaStreamingStatus.ts b/apps/nextjs/src/components/AppComponents/Chat/Chat/hooks/useAilaStreamingStatus.ts index 50168c43e..c549243ce 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/Chat/hooks/useAilaStreamingStatus.ts +++ b/apps/nextjs/src/components/AppComponents/Chat/Chat/hooks/useAilaStreamingStatus.ts @@ -1,10 +1,45 @@ import { useMemo, useEffect } from "react"; +import type { LessonPlanKeys } from "@oakai/aila/src/protocol/schema"; +import { LessonPlanKeysSchema } from "@oakai/aila/src/protocol/schema"; import { aiLogger } from "@oakai/logger"; import type { Message } from "ai"; const log = aiLogger("chat"); +function findStreamingSections(message: Message | undefined): { + streamingSections: LessonPlanKeys[]; + streamingSection: LessonPlanKeys | undefined; + content: string | undefined; +} { + if (!message?.content) { + return { + streamingSections: [], + streamingSection: undefined, + content: undefined, + }; + } + const { content } = message; + const pathMatches: RegExpExecArray[] = []; + let match: RegExpExecArray | null; + while ((match = /"path":"\/([^/"]*)(?:\/|")/g.exec(content)) !== null) { + pathMatches.push(match); + } + + const streamingSections: LessonPlanKeys[] = pathMatches + .map((match) => match[1]) + .filter((i): i is string => typeof i === "string") + .map((section) => { + const result = LessonPlanKeysSchema.safeParse(section); + return result.success ? result.data : undefined; + }) + .filter((section): section is LessonPlanKeys => section !== undefined); + const streamingSection: LessonPlanKeys | undefined = + streamingSections[streamingSections.length - 1]; + + return { streamingSections, streamingSection, content }; +} + export type AilaStreamingStatus = | "Loading" | "RequestMade" @@ -18,36 +53,49 @@ export const useAilaStreamingStatus = ({ }: { isLoading: boolean; messages: Message[]; -}): AilaStreamingStatus => { - const ailaStreamingStatus = useMemo(() => { +}): { + status: AilaStreamingStatus; + streamingSection: LessonPlanKeys | undefined; + streamingSections: LessonPlanKeys[] | undefined; +} => { + const { status, streamingSection, streamingSections } = useMemo(() => { const moderationStart = `MODERATION_START`; const chatStart = `CHAT_START`; - if (messages.length === 0) return "Idle"; + if (messages.length === 0) + return { + status: "Idle" as AilaStreamingStatus, + streamingSection: undefined, + }; const lastMessage = messages[messages.length - 1]; + let status: AilaStreamingStatus = "Idle"; + const { streamingSections, streamingSection, content } = + findStreamingSections(lastMessage); + if (isLoading) { - if (!lastMessage) return "Loading"; - const { content } = lastMessage; - if (lastMessage.role === "user") { - return "RequestMade"; - } else if (content.includes(moderationStart)) { - return "Moderating"; - } else if ( - content.includes(`"type":"prompt"`) || - content.includes(`\\"type\\":\\"prompt\\"`) - ) { - return "StreamingChatResponse"; - } else if (content.includes(chatStart)) { - return "StreamingLessonPlan"; + if (!lastMessage || !content) { + status = "Loading"; + } else { + if (lastMessage.role === "user") { + status = "RequestMade"; + } else if (content.includes(moderationStart)) { + status = "Moderating"; + } else if (content.includes(`"type":"text"`)) { + status = "StreamingChatResponse"; + } else if (content.includes(chatStart)) { + status = "StreamingLessonPlan"; + } else { + status = "Loading"; + } } - return "Loading"; } - return "Idle"; + + return { status, streamingSections, streamingSection }; }, [isLoading, messages]); useEffect(() => { - log.info("ailaStreamingStatus set:", ailaStreamingStatus); - }, [ailaStreamingStatus]); + log.info("ailaStreamingStatus set:", status); + }, [status]); - return ailaStreamingStatus; + return { status, streamingSection, streamingSections }; }; diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.tsx index d0acd9c16..e07e4898a 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.tsx @@ -25,9 +25,13 @@ const ChatLhsHeader = ({
{process.env.NEXT_PUBLIC_ENVIRONMENT !== "production" && (
+
{chat.iteration ?? 0}
{chat.ailaStreamingStatus}
+
+ {chat.streamingSection} +
)}
-
+
From 5e0e6cbbb44ad66e4ad55a5b0379a76336a706df Mon Sep 17 00:00:00 2001 From: Stef Lewandowski Date: Tue, 29 Oct 2024 17:58:49 +0000 Subject: [PATCH 2/5] Add the streaming section to the provider --- .../components/ContextProviders/ChatProvider.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx b/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx index de9fac36b..d6d0bc38d 100644 --- a/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx +++ b/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx @@ -13,6 +13,7 @@ import { generateMessageId } from "@oakai/aila/src/helpers/chat/generateMessageI import { parseMessageParts } from "@oakai/aila/src/protocol/jsonPatchProtocol"; import type { AilaPersistedChat, + LessonPlanKeys, LooseLessonPlan, } from "@oakai/aila/src/protocol/schema"; import { isToxic } from "@oakai/core/src/utils/ailaModeration/helpers"; @@ -64,6 +65,8 @@ export type ChatContextProps = { queuedUserAction: string | null; queueUserAction: (action: string) => void; executeQueuedAction: () => Promise; + streamingSection: LessonPlanKeys | undefined; + streamingSections: LessonPlanKeys[] | undefined; }; const ChatContext = createContext(null); @@ -382,7 +385,11 @@ export function ChatProvider({ id, children }: Readonly) { ? lastModeration : toxicInitialModeration; - const ailaStreamingStatus = useAilaStreamingStatus({ isLoading, messages }); + const { + status: ailaStreamingStatus, + streamingSection, + streamingSections, + } = useAilaStreamingStatus({ isLoading, messages }); useEffect(() => { if (toxicModeration) { @@ -416,6 +423,8 @@ export function ChatProvider({ id, children }: Readonly) { queuedUserAction, queueUserAction, executeQueuedAction, + streamingSection, + streamingSections, }), [ id, @@ -441,6 +450,8 @@ export function ChatProvider({ id, children }: Readonly) { queuedUserAction, queueUserAction, executeQueuedAction, + streamingSection, + streamingSections, ], ); From 62431e2bc6e699fe15e7d1af8567229ed1b6c25e Mon Sep 17 00:00:00 2001 From: Stef Lewandowski Date: Tue, 29 Oct 2024 18:03:16 +0000 Subject: [PATCH 3/5] Display the text of the section being edited --- .../components/AppComponents/Chat/chat-list.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-list.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-list.tsx index fa0c5c7f6..d6581c198 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-list.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-list.tsx @@ -4,6 +4,7 @@ import type { Dispatch, SetStateAction } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import type { PersistedModerationBase } from "@oakai/core/src/utils/ailaModeration/moderationSchema"; +import { camelCaseToTitleCase } from "@oakai/core/src/utils/camelCaseConversion"; import { OakBox, OakFlex, OakIcon, OakSpan } from "@oaknational/oak-components"; import type { Message } from "ai"; import Link from "next/link"; @@ -32,7 +33,8 @@ function DemoLimitMessage({ id }: Readonly<{ id: string }>) { message={{ id: "demo-limit", role: "assistant", - content: `{"type": "error", "message": "**Your lesson is complete**\\nYou can no longer edit this lesson. [Create new lesson.](/aila)"}`, + content: + '{"type": "error", "message": "**Your lesson is complete**\\nYou can no longer edit this lesson. [Create new lesson.](/aila)"}', }} persistedModerations={[]} separator={} @@ -125,13 +127,17 @@ export const ChatMessagesDisplay = ({ ailaStreamingStatus: AilaStreamingStatus; demo: DemoContextProps; }) => { - const { lessonPlan, isStreaming } = useLessonChat(); + const { lessonPlan, isStreaming, streamingSection } = useLessonChat(); const { setDialogWindow } = useDialog(); const { totalSections, totalSectionsComplete } = useProgressForDownloads({ lessonPlan, isStreaming, }); + const workingOnItMessage = streamingSection + ? `Editing ${camelCaseToTitleCase(streamingSection)}…` + : "Working on it…"; + return ( <> {messages.map((message) => { @@ -158,7 +164,7 @@ export const ChatMessagesDisplay = ({ message={{ id: "working-on-it-initial", role: "assistant", - content: "Working on it…", + content: workingOnItMessage, }} lastModeration={lastModeration} persistedModerations={[]} @@ -185,7 +191,7 @@ export const ChatMessagesDisplay = ({ ? { id: "working-on-it-initial", role: "assistant", - content: "Working on it…", + content: workingOnItMessage, } : message } @@ -208,7 +214,7 @@ export const ChatMessagesDisplay = ({ message={{ id: "working-on-it-initial", role: "assistant", - content: "Working on it…", + content: workingOnItMessage, }} lastModeration={lastModeration} persistedModerations={[]} From a026a3f91593680a3e18f4786bbfcf06940801ec Mon Sep 17 00:00:00 2001 From: Stef Lewandowski Date: Tue, 29 Oct 2024 18:10:15 +0000 Subject: [PATCH 4/5] Remove chat iteration for now --- .../nextjs/src/components/AppComponents/Chat/chat-lhs-header.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.tsx index e07e4898a..53a6e073c 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.tsx @@ -25,7 +25,6 @@ const ChatLhsHeader = ({
{process.env.NEXT_PUBLIC_ENVIRONMENT !== "production" && (
-
{chat.iteration ?? 0}
{chat.ailaStreamingStatus}
From 3c0f590355ca99372aac2449dae967def8a65e73 Mon Sep 17 00:00:00 2001 From: Stef Lewandowski Date: Tue, 29 Oct 2024 18:23:08 +0000 Subject: [PATCH 5/5] Satisfy sonar about infinite loop risk --- .../Chat/Chat/hooks/useAilaStreamingStatus.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/nextjs/src/components/AppComponents/Chat/Chat/hooks/useAilaStreamingStatus.ts b/apps/nextjs/src/components/AppComponents/Chat/Chat/hooks/useAilaStreamingStatus.ts index c549243ce..0c6f514e5 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/Chat/hooks/useAilaStreamingStatus.ts +++ b/apps/nextjs/src/components/AppComponents/Chat/Chat/hooks/useAilaStreamingStatus.ts @@ -22,8 +22,15 @@ function findStreamingSections(message: Message | undefined): { const { content } = message; const pathMatches: RegExpExecArray[] = []; let match: RegExpExecArray | null; - while ((match = /"path":"\/([^/"]*)(?:\/|")/g.exec(content)) !== null) { + const regex = /"path":"\/([^/"]*)(?:\/|")"/g; + let startIndex = 0; + while ((match = regex.exec(content.slice(startIndex))) !== null) { pathMatches.push(match); + startIndex += match.index + match[0].length; + if (pathMatches.length > 100) { + log.warn("Too many path matches found, stopping search"); + break; + } } const streamingSections: LessonPlanKeys[] = pathMatches