Skip to content

Commit 06428a8

Browse files
authored
Merge branch 'main' into mme/preserve-whitespace
2 parents 56f1a61 + 1578965 commit 06428a8

29 files changed

Lines changed: 757 additions & 91 deletions

CopilotKit/examples/next-openai/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@langchain/core": "^0.1.29",
2121
"@langchain/langgraph": "^0.0.7",
2222
"@langchain/openai": "^0.0.14",
23+
"clsx": "^1.2.1",
2324
"langchain": "^0.1.19",
2425
"next": "^13.4.9",
2526
"next-themes": "^0.2.1",
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useCopilotAction } from "@copilotkit/react-core";
2+
import { SlideModel } from "../types";
3+
import { SlidePreview } from "../components/misc/SlidePreview";
4+
5+
interface AppendSlideParams {
6+
setSlides: (fn: (slides: SlideModel[]) => SlideModel[]) => void;
7+
setCurrentSlideIndex: (fn: (i: number) => number) => void;
8+
slides: SlideModel[];
9+
}
10+
11+
export default function useAppendSlide({
12+
setSlides,
13+
setCurrentSlideIndex,
14+
slides,
15+
}: AppendSlideParams) {
16+
useCopilotAction({
17+
name: "appendSlide",
18+
description:
19+
"Add a slide after all the existing slides. Call this function multiple times to add multiple slides.",
20+
parameters: [
21+
{
22+
name: "content",
23+
description:
24+
"The content of the slide. MUST consist of a title, then an empty newline, then a few bullet points. Always between 1-3 bullet points - no more, no less.",
25+
},
26+
{
27+
name: "backgroundImageDescription",
28+
description:
29+
"What to display in the background of the slide. For example, 'dog', 'house', etc.",
30+
},
31+
{
32+
name: "spokenNarration",
33+
description:
34+
"The text to read while presenting the slide. Should be distinct from the slide's content, " +
35+
"and can include additional context, references, etc. Will be read aloud as-is. " +
36+
"Should be a few sentences long, clear, and smooth to read." +
37+
"DO NOT include meta-commentary, such as 'in this slide', 'we explore', etc.",
38+
},
39+
],
40+
41+
handler: async ({ content, backgroundImageDescription, spokenNarration }) => {
42+
const newSlide: SlideModel = {
43+
content,
44+
backgroundImageDescription,
45+
spokenNarration,
46+
};
47+
48+
setSlides((slides) => [...slides, newSlide]);
49+
setCurrentSlideIndex((i) => slides.length);
50+
},
51+
render: (props) => {
52+
return <SlidePreview {...props.args} done={props.status === "complete"} />;
53+
},
54+
});
55+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useCopilotAction } from "@copilotkit/react-core";
2+
import { SlideModel } from "../types";
3+
import { SlidePreview } from "../components/misc/SlidePreview";
4+
5+
interface UpdateSlideParams {
6+
partialUpdateSlide: (partialSlide: Partial<SlideModel>) => void;
7+
}
8+
9+
export default function useUpdateSlide({ partialUpdateSlide }: UpdateSlideParams) {
10+
useCopilotAction({
11+
name: "updateSlide",
12+
description: "Update the current slide.",
13+
parameters: [
14+
{
15+
name: "content",
16+
description: "The content of the slide. Should generally consist of a few bullet points.",
17+
},
18+
{
19+
name: "backgroundImageDescription",
20+
description:
21+
"What to display in the background of the slide. For example, 'dog', 'house', etc.",
22+
},
23+
{
24+
name: "spokenNarration",
25+
description:
26+
"The spoken narration for the slide. This is what the user will hear when the slide is shown.",
27+
},
28+
],
29+
handler: async ({ content, backgroundImageDescription, spokenNarration }) => {
30+
partialUpdateSlide({
31+
content,
32+
backgroundImageDescription,
33+
spokenNarration,
34+
});
35+
},
36+
render: (props) => {
37+
return <SlidePreview {...props.args} done={props.status === "complete"} />;
38+
},
39+
});
40+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import clsx from "clsx";
2+
3+
interface ActionButtonProps {
4+
children: React.ReactNode;
5+
onClick?: () => void;
6+
disabled?: boolean;
7+
inProgress?: boolean;
8+
}
9+
10+
export function ActionButton({ children, onClick, disabled, inProgress }: ActionButtonProps) {
11+
return (
12+
<button
13+
onClick={onClick}
14+
disabled={disabled || inProgress}
15+
className={clsx(
16+
"text-white font-bold w-7 h-7 flex items-center justify-center rounded-md",
17+
disabled ? "opacity-50 cursor-not-allowed" : "hover:border hover:border-white",
18+
inProgress && "animate-bounce text-blue-400 cursor-not-allowed hover:border-none",
19+
)}
20+
>
21+
{children}
22+
</button>
23+
);
24+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { SlideModel } from "../../types";
2+
import { ActionButton } from "./ActionButton";
3+
import { PlusCircleIcon } from "@heroicons/react/24/outline";
4+
5+
interface AddSlideButtonProps {
6+
currentSlideIndex: number;
7+
setCurrentSlideIndex: (fn: (i: number) => number) => void;
8+
setSlides: (fn: (slides: SlideModel[]) => SlideModel[]) => void;
9+
}
10+
11+
export function AddSlideButton({
12+
currentSlideIndex,
13+
setCurrentSlideIndex,
14+
setSlides,
15+
}: AddSlideButtonProps) {
16+
return (
17+
<ActionButton
18+
onClick={() => {
19+
const newSlide: SlideModel = {
20+
content: "",
21+
backgroundImageDescription: "random",
22+
spokenNarration: "",
23+
};
24+
setSlides((slides) => [
25+
...slides.slice(0, currentSlideIndex + 1),
26+
newSlide,
27+
...slides.slice(currentSlideIndex + 1),
28+
]);
29+
setCurrentSlideIndex((i) => i + 1);
30+
}}
31+
>
32+
<PlusCircleIcon className="h-5 w-5" />
33+
</ActionButton>
34+
);
35+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { SlideModel } from "../../types";
2+
import { ActionButton } from "./ActionButton";
3+
import { TrashIcon } from "@heroicons/react/24/outline";
4+
5+
interface DeleteSlideButtonProps {
6+
currentSlideIndex: number;
7+
setCurrentSlideIndex: (fn: (i: number) => number) => void;
8+
slides: SlideModel[];
9+
setSlides: (fn: (slides: SlideModel[]) => SlideModel[]) => void;
10+
}
11+
12+
export function DeleteSlideButton({
13+
currentSlideIndex,
14+
setCurrentSlideIndex,
15+
slides,
16+
setSlides,
17+
}: DeleteSlideButtonProps) {
18+
return (
19+
<ActionButton
20+
disabled={slides.length == 1}
21+
onClick={() => {
22+
// delete the current slide
23+
setSlides((slides) => [
24+
...slides.slice(0, currentSlideIndex),
25+
...slides.slice(currentSlideIndex + 1),
26+
]);
27+
setCurrentSlideIndex((i) => 0);
28+
}}
29+
>
30+
<TrashIcon className="h-5 w-5" />
31+
</ActionButton>
32+
);
33+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { CopilotContextParams, CopilotTask } from "@copilotkit/react-core";
2+
import { useState } from "react";
3+
import { ActionButton } from "./ActionButton";
4+
import { SparklesIcon } from "@heroicons/react/24/outline";
5+
6+
interface GenerateSlideButtonProps {
7+
context: CopilotContextParams;
8+
}
9+
10+
export function GenerateSlideButton({ context }: GenerateSlideButtonProps) {
11+
const [isGeneratingSlide, setIsGeneratingSlide] = useState(false);
12+
return (
13+
<ActionButton
14+
inProgress={isGeneratingSlide}
15+
onClick={async () => {
16+
try {
17+
let slideContent = prompt("What should the new slide be about?");
18+
if (slideContent === null) {
19+
return;
20+
}
21+
setIsGeneratingSlide(true);
22+
const generateSlideTask = new CopilotTask({
23+
instructions:
24+
"Make a new slide given this user input: " +
25+
slideContent +
26+
"\n DO NOT carry out research",
27+
});
28+
await generateSlideTask.run(context);
29+
} finally {
30+
setIsGeneratingSlide(false);
31+
}
32+
}}
33+
>
34+
<SparklesIcon className={"h-5 w-5"} />
35+
</ActionButton>
36+
);
37+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import clsx from "clsx";
2+
3+
interface NavButtonProps {
4+
children: React.ReactNode;
5+
onClick?: () => void;
6+
disabled?: boolean;
7+
}
8+
9+
export function NavButton({ children, onClick, disabled }: NavButtonProps) {
10+
return (
11+
<button
12+
onClick={onClick}
13+
disabled={disabled}
14+
className={clsx(
15+
"w-7 h-7 border border-white rounded-md flex justify-center items-center",
16+
"focus:outline-none",
17+
disabled ? "opacity-40 cursor-not-allowed" : "hover:bg-white hover:text-black",
18+
)}
19+
>
20+
{children}
21+
</button>
22+
);
23+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
interface PerformResearchSwitchProps {
2+
isEnabled: boolean;
3+
setIsEnabled: (fn: (b: boolean) => boolean) => void;
4+
}
5+
6+
export const PerformResearchSwitch = ({ isEnabled, setIsEnabled }: PerformResearchSwitchProps) => {
7+
return (
8+
<label className="flex items-center cursor-pointer pl-4">
9+
<div className="relative">
10+
<input
11+
type="checkbox"
12+
className="sr-only"
13+
checked={isEnabled}
14+
onChange={() => setIsEnabled((b) => !b)}
15+
/>
16+
<div
17+
className={`w-10 h-4 ${
18+
isEnabled ? "bg-blue-500" : "bg-gray-400"
19+
} rounded-full shadow-inner transition-colors`}
20+
></div>
21+
22+
<div
23+
className={`absolute w-6 h-6 bg-white rounded-full shadow -left-1 -top-1 transition-transform ${
24+
isEnabled ? "transform translate-x-full" : ""
25+
}`}
26+
></div>
27+
</div>
28+
<span className="text-sm font-normal ml-2">Perform Research?</span>
29+
</label>
30+
);
31+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useState } from "react";
2+
import { ActionButton } from "./ActionButton";
3+
import { SpeakerWaveIcon } from "@heroicons/react/24/outline";
4+
import { resetGlobalAudio, speak } from "../../utils/globalAudio";
5+
6+
interface SpeakCurrentSlideButtonProps {
7+
spokenNarration: string;
8+
}
9+
10+
export function SpeakCurrentSlideButton({ spokenNarration }: SpeakCurrentSlideButtonProps) {
11+
const [isSpeaking, setIsSpeaking] = useState(false);
12+
return (
13+
<ActionButton inProgress={isSpeaking}>
14+
<SpeakerWaveIcon
15+
className="h-5 w-5"
16+
onClick={async () => {
17+
resetGlobalAudio();
18+
try {
19+
setIsSpeaking(true);
20+
await speak(spokenNarration);
21+
} finally {
22+
setIsSpeaking(false);
23+
}
24+
}}
25+
/>
26+
</ActionButton>
27+
);
28+
}

0 commit comments

Comments
 (0)