Skip to content

Commit

Permalink
Add functioncalling as one of the status and fix bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
bodhish committed Oct 30, 2024
1 parent 36dcf21 commit 7e85878
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 150 deletions.
253 changes: 105 additions & 148 deletions src/components/Copilot/CopilotPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,23 +60,9 @@ export default function CopilotPopup(props: {

const currentCopilot = copilotStorage.find((c) => c.patientId === patientId);

const [thinkingStates, setThinkingStates] = useState<ThinkingStates>({
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<ThinkingStates>(
INITIAL_THINKING_STATES,
);

const configureCopilot = async () => {
const openai = new OpenAI({
Expand Down Expand Up @@ -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;
Expand All @@ -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.");
}
}
};
Expand All @@ -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,
});
Expand All @@ -222,6 +212,7 @@ export default function CopilotPopup(props: {
await callFunction(run);
}

// Show final stage complete
setThinkingStates((prev) => ({
...prev,
generating: { ...prev.generating, completed: true },
Expand All @@ -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<HTMLAudioElement | null>(null);

const stopAllAudio = () => {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -499,35 +439,52 @@ export default function CopilotPopup(props: {
{orderedChats?.map((message) => (
<CopilotChatBlock message={message} key={message.id} />
))}
{copilotThinking && chat.trim() && (
<CopilotTempMessage message={chat} />
)}
{copilotThinking && (
<div className="mt-4 rounded-lg bg-white p-4 shadow-sm">
<div className="flex flex-col gap-3">
{Object.values(thinkingStates).map((state) => (
<div key={state.stage} className="flex items-center gap-3">
{state.completed ? (
<div className="h-5 w-5 text-green-500">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z"
clipRule="evenodd"
/>
</svg>
</div>
) : (
<div className="h-5 w-5">
<Spinner className="h-5 w-5" />
</div>
)}
<span
className={`text-sm ${state.completed ? "text-green-600" : "text-secondary-600"}`}
>
{state.message}
</span>
<div key={state.stage} className="flex flex-col gap-2">
<div className="flex items-center gap-3">
{state.completed ? (
<div className="h-5 w-5 text-green-500">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z"
clipRule="evenodd"
/>
</svg>
</div>
) : (
<div className="h-5 w-5">
<Spinner className="h-5 w-5" />
</div>
)}
<span
className={`text-sm ${
state.completed
? "text-green-600"
: "text-secondary-600"
}`}
>
{state.message}
</span>
</div>
{state.stage === "function_calling" &&
state.functionName && (
<div className="ml-8 flex items-center gap-2 text-xs text-secondary-500">
<span>
{state.completed ? "✓" : "•"} {state.functionName}
</span>
</div>
)}
</div>
))}
</div>
Expand Down
29 changes: 29 additions & 0 deletions src/components/Copilot/CopilotTempMessage.tsx
Original file line number Diff line number Diff line change
@@ -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();

Check failure on line 11 in src/components/Copilot/CopilotTempMessage.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook "useAuthUser" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?

return (
<div className="flex flex-row-reverse justify-end gap-2">
<div className="shrink-0">
<Avatar
name={authUser.first_name}
imageUrl={authUser.read_profile_picture_url}
className="h-8 shrink-0 rounded-full"
/>
</div>
<div className="flex flex-1 justify-end">
<div className="inline-block rounded-xl border border-transparent bg-gradient-to-tr from-blue-500 to-primary-400 px-4 py-2 text-white">
{message}
</div>
</div>
</div>
);
}
8 changes: 6 additions & 2 deletions src/components/Copilot/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

0 comments on commit 7e85878

Please sign in to comment.