Skip to content

Commit

Permalink
model selector for web
Browse files Browse the repository at this point in the history
  • Loading branch information
ctate committed May 21, 2024
1 parent b56f06a commit 57d9f44
Show file tree
Hide file tree
Showing 10 changed files with 247 additions and 5 deletions.
12 changes: 11 additions & 1 deletion platforms/web/app/action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,19 @@ import changeVoice from "./tools/changeVoice";
import saveVoiceSetting from "./tools/saveVoiceSetting";
import pokedex from "./tools/pokedex";
import makeRecipe from "./tools/recipe";
import getSettings from "@/utils/getSettings";
import saveSettings from "@/utils/saveSettings";

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY || "",
});

export async function saveSetting(key: string, value: string): Promise<void> {
"use server";

await saveSettings(key, value);
}

async function submitUserImage(
input: string,
imageUrl: string
Expand Down Expand Up @@ -134,8 +142,10 @@ async function submitUserMessage(input: string): Promise<UIState> {
},
]);

const { model } = await getSettings();

const ui = render({
model: "gpt-3.5-turbo-0125",
model,
provider: openai,
messages: [
{
Expand Down
12 changes: 12 additions & 0 deletions platforms/web/app/api/saveSetting/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import saveSettings from "@/utils/saveSettings";

export async function POST(req: Request) {
const { key, value } = (await req.json()) as {
key: string;
value: string;
};

await saveSettings(key, value);

return Response.json({});
}
Binary file modified platforms/web/app/favicon.ico
Binary file not shown.
17 changes: 14 additions & 3 deletions platforms/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import AudioRecorder from "@/components/AudioRecorder";
import classNames from "@/utils/classNames";
import WebcamSwitcher from "@/components/Webcam";
import { useIdleCursor } from "@/hooks/useIdleCursor";
import Header from "@/components/Header";
import { AppContextProvider } from "@/context/AppContext";

export default function Page() {
function Content() {
const cursorVisible = useIdleCursor();

const audioRef = useRef<HTMLAudioElement>(null);
Expand Down Expand Up @@ -65,7 +67,8 @@ export default function Page() {
}, [cursorVisible]);

return (
<>
<AppContextProvider>
<Header />
<div
className={classNames(
"flex flex-col items-center justify-center min-h-dvh",
Expand Down Expand Up @@ -190,6 +193,14 @@ export default function Page() {
<WebcamSwitcher onSnap={(imageUrl) => setImageUrl(imageUrl)} />
)}
<audio ref={audioRef} />
</>
</AppContextProvider>
);
}

export default function Page() {
return (
<AppContextProvider>
<Content />
</AppContextProvider>
);
}
71 changes: 71 additions & 0 deletions platforms/web/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useAppContext } from "@/context/AppContext";
import { Popover } from "./Popover";
import { useRef, useState } from "react";
import classNames from "@/utils/classNames";
import PROVIDERS from "@/constants/PROVIDERS";
import { saveSetting } from "@/app/action";
import RightArrowIcon from "./icons/RightArrowIcon";
import ExpandArrowIcon from "./icons/ExpandArrowIcon";

export default function Header() {
const { selectedModel, setSelectedModel } = useAppContext();

const targetRef = useRef<HTMLButtonElement>(null);
const [showPopover, setShowPopover] = useState(false);

return (
<header className="flex justify-center p-4">
<button
className="flex gap-2 items-center"
onClick={() => setShowPopover(!showPopover)}
ref={targetRef}
>
<span>{selectedModel}</span>
<span
className={classNames("transition-transform", {
"rotate-90": showPopover,
})}
>
<ExpandArrowIcon color="gray" size={16} />
</span>
</button>
<Popover
isOpen={showPopover}
onClose={() => setShowPopover(false)}
target={targetRef.current}
>
<ul>
{PROVIDERS.map((provider) => (
<li key={provider.name}>
<div className="font-bold">{provider.name}</div>
<ul>
{provider.models.map((model) => (
<li key={model}>
<button
onClick={async () => {
void fetch("/api/saveSetting", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
key: "model",
value: model,
}),
});
setSelectedModel(model);
setShowPopover(false);
}}
>
{model}
</button>
</li>
))}
</ul>
</li>
))}
</ul>
</Popover>
</header>
);
}
80 changes: 80 additions & 0 deletions platforms/web/components/Popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useRef, useEffect, Ref, useState } from "react";
import { createPortal } from "react-dom";

interface PopoverProps {
children: React.ReactNode;
isOpen?: boolean;
onClose?: () => void;
position?: "top" | "bottom" | "left" | "right";
target?: HTMLElement | null;
}

export const Popover: React.FC<PopoverProps> = ({
children,
onClose = () => {},
isOpen = false,
position = "bottom",
target,
}) => {
const contentRef = useRef<HTMLDivElement>(null);

const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
contentRef.current &&
!contentRef.current.contains(event.target as Node) &&
target &&
!target.contains(event.target as Node)
) {
onClose();
}
};

const handleResize = () => {
const left =
(target?.getBoundingClientRect().left || 0) -
((contentRef.current?.getBoundingClientRect().width || 0) -
(target?.getBoundingClientRect().width || 0)) /
2;

const top =
(target?.getBoundingClientRect().top || 0) +
(target?.getBoundingClientRect().height || 0) +
5;

setLeft(left);
setTop(top);
};

handleResize();

document.addEventListener("mousedown", handleClickOutside);
window.addEventListener("resize", handleResize);

return () => {
document.removeEventListener("mousedown", handleClickOutside);
window.removeEventListener("resize", handleResize);
};
}, [onClose, target]);

if (!isOpen) {
return null;
}

return createPortal(
<div
className="bg-gray-900 fixed p-4 rounded-2xl"
ref={contentRef}
style={{
left,
top,
}}
>
{children}
</div>,
document.body
);
};
18 changes: 18 additions & 0 deletions platforms/web/components/icons/ExpandArrowIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default function ExpandArrowIcon({ color = "white", size = 32 }) {
return (
<svg
fill="none"
viewBox="0 0 24 24"
height={size}
width={size}
xmlns="http://www.w3.org/2000/svg"
>
<path
clipRule="evenodd"
d="m7.2072 20.7072c-.39052-.3905-.39052-1.0237 0-1.4142l7.2929-7.2929-7.2929-7.29292c-.39052-.39052-.39052-1.02369 0-1.41421l.70711-.70711c.39052-.39052 1.02368-.39052 1.41421 0l8.35358 8.35354c.5858.5858.5858 1.5356 0 2.1213l-8.35358 8.3536c-.39052.3905-1.02369.3905-1.41421 0z"
fill={color}
fillRule="evenodd"
/>
</svg>
);
}
8 changes: 8 additions & 0 deletions platforms/web/constants/PROVIDERS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const PROVIDERS = [
{
name: "OpenAI",
models: ["gpt-3.5-turbo", "gpt-4o", "gpt-4-turbo", "gpt-4"],
},
];

export default PROVIDERS;
28 changes: 28 additions & 0 deletions platforms/web/context/AppContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { createContext, useContext, useState, ReactNode } from "react";

interface AppContextType {
selectedModel: string;
setSelectedModel: (model: string) => void;
}

const AppContext = createContext<AppContextType | undefined>(undefined);

export const AppContextProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [selectedModel, setSelectedModel] = useState("gpt-4");

return (
<AppContext.Provider value={{ selectedModel, setSelectedModel }}>
{children}
</AppContext.Provider>
);
};

export const useAppContext = (): AppContextType => {
const context = useContext(AppContext);
if (context === undefined) {
throw new Error("useAppContext must be used within a AppContextProvider");
}
return context;
};
6 changes: 5 additions & 1 deletion platforms/web/utils/getSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ export default async function getSettings() {
const file = join(process.cwd(), ".crystal/settings.json");
const content = existsSync(file) ? await readFile(file, "utf8") : "{}";
const data = JSON.parse(content) as {
model?: string;
voice?: "alloy" | "echo" | "fable" | "onyx" | "nova" | "shimmer";
};
return data;
return {
model: data.model || "gpt-3.5-turbo",
voice: data.voice || "alloy",
};
}

0 comments on commit 57d9f44

Please sign in to comment.