diff --git a/src/components/Copilot/CopilotPopup.tsx b/src/components/Copilot/CopilotPopup.tsx index 3b92b55b71a..ace04e40409 100644 --- a/src/components/Copilot/CopilotPopup.tsx +++ b/src/components/Copilot/CopilotPopup.tsx @@ -9,15 +9,39 @@ import { Assistant } from "openai/resources/beta/assistants"; import { Thread } from "openai/resources/beta/threads/threads"; import CopilotChatInput from "./CopilotChatInput"; import { CopilotChatBlock } from "./CopilotChatBlock"; -import { CopilotStorage, ThinkingState, ThinkingStates } from "./types"; +import { ThinkingStates } from "./types"; import { Run } from "openai/resources/beta/threads/runs/runs"; import Spinner from "@/components/Common/Spinner"; +import { CopilotTempMessage } from "./CopilotTempMessage"; const openai = new OpenAI({ apiKey: import.meta.env.REACT_COPILOT_API_KEY, dangerouslyAllowBrowser: true, }); +const INITIAL_THINKING_STATES: ThinkingStates = { + processing: { + stage: "processing", + message: "Processing context and history...", + completed: false, + }, + analyzing: { + stage: "analyzing", + message: "Analyzing your request...", + completed: false, + }, + function_calling: { + stage: "function_calling", + message: "Calling required functions...", + completed: false, + }, + generating: { + stage: "generating", + message: "Generating response...", + completed: false, + }, +}; + export default function CopilotPopup(props: { patientId: string; consultationId: string; @@ -36,23 +60,9 @@ export default function CopilotPopup(props: { const currentCopilot = copilotStorage.find((c) => c.patientId === patientId); - const [thinkingStates, setThinkingStates] = useState({ - analyzing: { - stage: "analyzing", - message: "Analyzing your request...", - completed: false, - }, - processing: { - stage: "processing", - message: "Processing context and history...", - completed: false, - }, - generating: { - stage: "generating", - message: "Generating response...", - completed: false, - }, - }); + const [thinkingStates, setThinkingStates] = useState( + INITIAL_THINKING_STATES, + ); const configureCopilot = async () => { const openai = new OpenAI({ @@ -124,12 +134,21 @@ export default function CopilotPopup(props: { }; const callFunction = async (run: Run) => { - if ( - run.required_action && - run.required_action.submit_tool_outputs && - run.required_action.submit_tool_outputs.tool_calls && - copilotThread - ) { + if (run.required_action?.submit_tool_outputs?.tool_calls && copilotThread) { + const functionNames = run.required_action.submit_tool_outputs.tool_calls + .map((t) => t.function.name) + .join(", "); + + setThinkingStates((prev) => ({ + ...prev, + function_calling: { + ...prev.function_calling, + message: `Calling ${functionNames}...`, + functionName: functionNames, + completed: false, + }, + })); + const toolOutputs = []; for (const tool of run.required_action.submit_tool_outputs.tool_calls) { let output; @@ -141,18 +160,25 @@ export default function CopilotPopup(props: { output, }); } + if (toolOutputs.length > 0) { const toolRun = await openai.beta.threads.runs.submitToolOutputsAndPoll( copilotThread.id, run.id, { tool_outputs: toolOutputs }, ); + + setThinkingStates((prev) => ({ + ...prev, + function_calling: { + ...prev.function_calling, + completed: true, + }, + })); + if (run.status === "requires_action") { await callFunction(toolRun); } - console.log("Tool outputs submitted successfully."); - } else { - console.log("No tool outputs to submit."); } } }; @@ -164,56 +190,20 @@ export default function CopilotPopup(props: { stopAllAudio(); setCopilotThinking(true); - setThinkingStates({ - analyzing: { - stage: "analyzing", - message: "Analyzing your request...", - completed: false, - }, - processing: { - stage: "processing", - message: "Processing context and history...", - completed: false, - }, - generating: { - stage: "generating", - message: "Generating response...", - completed: false, - }, - }); - - setTimeout(() => { - setThinkingStates((prev) => ({ - ...prev, - analyzing: { ...prev.analyzing, completed: true }, - })); - }, 1000); - - setTimeout(() => { - setThinkingStates((prev) => ({ - ...prev, - processing: { ...prev.processing, completed: true }, - })); - }, 2000); - - if (chatView.current) { - const { scrollHeight, scrollTop, clientHeight } = chatView.current; - const wasAtBottom = scrollHeight - scrollTop - clientHeight < 10; - if (wasAtBottom) { - setTimeout(() => { - chatView.current?.scrollTo({ - top: chatView.current.scrollHeight, - behavior: "smooth", - }); - }, 100); - } - } + // Reset thinking states + setThinkingStates(INITIAL_THINKING_STATES); await openai.beta.threads.messages.create(copilotThread.id, { role: "user", content: message, }); + // Show second stage complete + setThinkingStates((prev) => ({ + ...prev, + processing: { ...prev.processing, completed: true }, + })); + const run = await openai.beta.threads.runs.createAndPoll(copilotThread.id, { assistant_id: copilotAssistant.id, }); @@ -222,6 +212,7 @@ export default function CopilotPopup(props: { await callFunction(run); } + // Show final stage complete setThinkingStates((prev) => ({ ...prev, generating: { ...prev.generating, completed: true }, @@ -232,41 +223,6 @@ export default function CopilotPopup(props: { setCopilotThinking(false); }; - const generateAudio = async (text: string) => { - const mediaSource = new MediaSource(); - - const audio = new Audio(); - audio.src = URL.createObjectURL(mediaSource); - audioRef.current = audio; - audio.play(); - - mediaSource.addEventListener("sourceopen", async () => { - const sourceBuffer = mediaSource.addSourceBuffer("audio/mpeg"); // Adjust MIME type if needed - - const response = await openai.audio.speech.create({ - model: "tts-1", - voice: "alloy", - input: text, - }); - const reader = response.body?.getReader(); - if (!reader) return; - - // eslint-disable-next-line no-constant-condition - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - sourceBuffer.appendBuffer(value); - - await new Promise((resolve) => { - sourceBuffer.addEventListener("updateend", resolve, { once: true }); - }); - } - - mediaSource.endOfStream(); - }); - }; - const audioRef = useRef(null); const stopAllAudio = () => { @@ -341,23 +297,7 @@ export default function CopilotPopup(props: { ); // Reset thinking states - setThinkingStates({ - analyzing: { - stage: "analyzing", - message: "Analyzing your request...", - completed: false, - }, - processing: { - stage: "processing", - message: "Processing context and history...", - completed: false, - }, - generating: { - stage: "generating", - message: "Generating response...", - completed: false, - }, - }); + setThinkingStates(INITIAL_THINKING_STATES); // Scroll chat to top if (chatView.current) { @@ -499,35 +439,52 @@ export default function CopilotPopup(props: { {orderedChats?.map((message) => ( ))} + {copilotThinking && chat.trim() && ( + + )} {copilotThinking && (
{Object.values(thinkingStates).map((state) => ( -
- {state.completed ? ( -
- - - -
- ) : ( -
- -
- )} - - {state.message} - +
+
+ {state.completed ? ( +
+ + + +
+ ) : ( +
+ +
+ )} + + {state.message} + +
+ {state.stage === "function_calling" && + state.functionName && ( +
+ + {state.completed ? "✓" : "•"} {state.functionName} + +
+ )}
))}
diff --git a/src/components/Copilot/CopilotTempMessage.tsx b/src/components/Copilot/CopilotTempMessage.tsx new file mode 100644 index 00000000000..ddf594e35b9 --- /dev/null +++ b/src/components/Copilot/CopilotTempMessage.tsx @@ -0,0 +1,29 @@ +import { Avatar } from "../Common/Avatar"; +import useAuthUser from "@/common/hooks/useAuthUser"; + +interface CopilotTempMessageProps { + message: string; +} + +export function CopilotTempMessage({ message }: CopilotTempMessageProps) { + if (!message.trim()) return null; + + const authUser = useAuthUser(); + + return ( +
+
+ +
+
+
+ {message} +
+
+
+ ); +} diff --git a/src/components/Copilot/types.ts b/src/components/Copilot/types.ts index 9ef3dfe990b..72bdf04ccfa 100644 --- a/src/components/Copilot/types.ts +++ b/src/components/Copilot/types.ts @@ -5,11 +5,15 @@ export type CopilotStorage = { }; export type ThinkingState = { - stage: "analyzing" | "processing" | "generating" | "complete"; + stage: "analyzing" | "processing" | "generating" | "function_calling"; message: string; completed: boolean; + functionName?: string; }; export type ThinkingStates = { - [key: string]: ThinkingState; + analyzing: ThinkingState; + processing: ThinkingState; + generating: ThinkingState; + function_calling: ThinkingState; };