Skip to content

Commit

Permalink
Add api key support
Browse files Browse the repository at this point in the history
  • Loading branch information
SuveenE committed Jan 21, 2025
1 parent af9fe77 commit ed4fe2c
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 14 deletions.
13 changes: 11 additions & 2 deletions app/api/gpt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ const createOpenAIClient = async (
role: "CLUE_GIVER" | "GUESSER",
sessionId: string,
gameState: GameState,
apiKey: string,
) => {
return observeOpenAI(
new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
apiKey: apiKey,
}),
{
generationName: role === "CLUE_GIVER" ? "clue giver" : "guesser",
Expand All @@ -30,6 +31,14 @@ const createOpenAIClient = async (

export async function POST(request: Request) {
try {
const apiKey = request.headers.get("X-API-Key");
if (!apiKey) {
return NextResponse.json(
{ error: "API key is required" },
{ status: 401 },
);
}

const body = await request.json();
const {
role,
Expand All @@ -41,7 +50,7 @@ export async function POST(request: Request) {
gameState: GameState;
} = body;

const openai = await createOpenAIClient(role, sessionId, gameState);
const openai = await createOpenAIClient(role, sessionId, gameState, apiKey);

const prompt = generatePrompt(role, gameState);

Expand Down
105 changes: 93 additions & 12 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import GitHubLink from "@/components/GitHubLink";
import CustomGameDialog from "@/components/CustomGameDialog";
import { Switch } from "@/components/ui/switch";
import { WORD_LIST } from "@/data/wordsList";
import testGame from "@/data/testGame2.json";
import testGame from "@/data/testGame7.json";
import { ClueResponse, GuessResponse } from "@/types/requests";
import { getStoredApiKey } from "@/utils/encryption";
import ApiKeyDialog from "@/components/ApiKeyDialog";
import { SettingsIcon, HomeIcon } from "lucide-react";

export default function Home() {
const [gameState, setGameState] = useState<GameState | null>(null);
Expand All @@ -33,6 +36,12 @@ export default function Home() {
const [isReplayEnd, setIsReplayEnd] = useState(false);
const [sessionId, setSessionId] = useState<string>("");
const [isO1, setIsO1] = useState(false);
const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false);
const [pendingAction, setPendingAction] = useState<"start" | "custom" | null>(
null,
);
const [settingsOpen, setSettingsOpen] = useState(false);

useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const uuid = crypto.randomUUID().slice(0, 6);
Expand Down Expand Up @@ -104,7 +113,41 @@ export default function Home() {
isGameStarted,
]);

const handleApiKeySubmit = () => {
setApiKeyDialogOpen(false);
if (pendingAction === "start") {
handleStartGame();
} else if (pendingAction === "custom") {
setDialogOpen(true);
}
setPendingAction(null);
};

const handleStartClick = () => {
if (!getStoredApiKey()) {
setPendingAction("start");
setApiKeyDialogOpen(true);
} else {
handleStartGame();
}
};

const handleCustomClick = () => {
if (!getStoredApiKey()) {
setPendingAction("custom");
setApiKeyDialogOpen(true);
} else {
setDialogOpen(true);
}
};

async function handleAITurn() {
const apiKey = getStoredApiKey();
if (!apiKey) {
console.error("No API key found");
return;
}

if (!gameState || gameState.gameOver) return;

// If no lastClue, get a new clue
Expand All @@ -113,7 +156,10 @@ export default function Home() {
const path = isO1 ? "/api/o1" : "/api/gpt";
const clueResponse = await fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: {
"Content-Type": "application/json",
"X-API-Key": apiKey,
},
body: JSON.stringify({ role: "CLUE_GIVER", sessionId, gameState }),
});

Expand Down Expand Up @@ -177,7 +223,10 @@ export default function Home() {
const path = isO1 ? "/api/o1" : "/api/gpt";
const guessResponse = await fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: {
"Content-Type": "application/json",
"X-API-Key": apiKey,
},
body: JSON.stringify({ role: "GUESSER", sessionId, gameState }),
});

Expand Down Expand Up @@ -456,8 +505,27 @@ export default function Home() {
return (
<main className="min-h-screen px-1 py-8 md:p-8 overflow-y-scroll">
<div className="max-w-7xl mx-auto">
<div className="relative">
{process.env.NEXT_PUBLIC_ENVIRONMENT !== "production" && (
<div className="relative flex justify-between items-start">
<button
onClick={() => setSettingsOpen(true)}
className="hidden md:block fixed left-4 top-4 p-2 rounded-full bg-white shadow-md
hover:bg-gray-50 transition-colors duration-200
text-gray-600 hover:text-gray-800"
aria-label="Settings"
>
<SettingsIcon className="w-5 h-5" />
</button>
<button
onClick={() => window.location.reload()}
className="hidden md:block fixed left-16 top-4 p-2 rounded-full bg-white shadow-md
hover:bg-gray-50 transition-colors duration-200
text-gray-600 hover:text-gray-800"
aria-label="Settings"
>
<HomeIcon className="w-5 h-5" />
</button>

{process.env.NEXT_PUBLIC_ENVIRONMENT === "production" && (
<button
onClick={replayTestGame}
className="hidden md:block fixed right-4 top-4 inline-flex items-center justify-center rounded-xl
Expand All @@ -484,11 +552,11 @@ export default function Home() {
<div className="flex gap-4">
{!isGameStarted &&
!isReplaying &&
process.env.NEXT_PUBLIC_ENVIRONMENT !== "production" && (
process.env.NEXT_PUBLIC_ENVIRONMENT === "production" && (
<>
<button
onClick={() => setDialogOpen(true)}
className="inline-flex items-center justify-center rounded-xl
onClick={handleCustomClick}
className="hidden md:inline-flex items-center justify-center rounded-xl
bg-gradient-to-r from-indigo-600 to-blue-600
px-4 py-2 text-sm font-semibold text-white shadow-sm
hover:from-indigo-500 hover:to-blue-500
Expand All @@ -497,16 +565,16 @@ export default function Home() {
Custom
</button>
<button
onClick={handleStartGame}
className="inline-flex items-center justify-center rounded-xl
onClick={handleStartClick}
className="hidden md:inline-flex items-center justify-center rounded-xl
bg-gradient-to-r from-indigo-600 to-blue-600
px-4 py-2 text-sm font-semibold text-white shadow-sm
hover:from-indigo-500 hover:to-blue-500
transition-all duration-200"
>
Start Game
</button>
<div className="flex items-center gap-2 rounded-xl p-2">
<div className="hidden md:flex items-center gap-2 rounded-xl p-2">
<p className="text-sm font-bold">gpt-4o</p>
<Switch
checked={isO1}
Expand All @@ -520,7 +588,7 @@ export default function Home() {
{process.env.NEXT_PUBLIC_ENVIRONMENT === "production" && (
<button
onClick={replayTestGame}
className="inline-flex items-center justify-center rounded-xl
className="inline-flex md:hidden items-center justify-center rounded-xl
bg-gradient-to-r from-indigo-600 to-blue-600
px-3 md:px-4 py-2 text-[10px] md:text-sm font-semibold text-white shadow-sm
hover:from-indigo-500 hover:to-blue-500
Expand Down Expand Up @@ -595,6 +663,9 @@ export default function Home() {
</div>
<SpymasterView cards={gameState?.cards ?? []} />
</div>
<p className="md:hidden text-center text-xs text-neutral-500">
Visit the website in a PC for more features.
</p>

<div className="flex flex-col md:flex-row gap-8 justify-center">
<div
Expand Down Expand Up @@ -628,6 +699,16 @@ export default function Home() {
</div>
</div>
<GitHubLink />
<ApiKeyDialog
open={apiKeyDialogOpen}
onOpenChange={setApiKeyDialogOpen}
onSubmit={handleApiKeySubmit}
/>
<ApiKeyDialog
open={settingsOpen}
onOpenChange={setSettingsOpen}
onSubmit={() => setSettingsOpen(false)}
/>
</main>
);
}
125 changes: 125 additions & 0 deletions components/ApiKeyDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { useState, useEffect } from "react";
import { storeApiKey, getStoredApiKey, deleteApiKey } from "@/utils/encryption";

interface ApiKeyDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: () => void;
}

export default function ApiKeyDialog({
open,
onOpenChange,
onSubmit,
}: ApiKeyDialogProps) {
const [apiKey, setApiKey] = useState("");
const [hasStoredKey, setHasStoredKey] = useState(false);

useEffect(() => {
if (open) {
const storedKey = getStoredApiKey();
setHasStoredKey(!!storedKey);
setApiKey(""); // Clear input when dialog opens
}
}, [open]);

const handleSubmit = () => {
if (!apiKey.trim()) return;
storeApiKey(apiKey.trim());
onSubmit();
};

const handleDelete = () => {
deleteApiKey();
setHasStoredKey(false);
setApiKey("");
};

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[425px] bg-white rounded-full">
<DialogHeader>
<DialogTitle>OpenAI API Key Settings</DialogTitle>
<DialogDescription>
{hasStoredKey
? "Update your stored API key or enter a new one."
: "Enter your OpenAI API key to start playing."}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
{hasStoredKey && (
<div className="flex items-center justify-between mb-4">
<div className="text-sm text-green-600 flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500" />
API key is stored
</div>
<button
onClick={handleDelete}
className="text-sm text-red-600 hover:text-red-700 flex items-center gap-1"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
Delete key
</button>
</div>
)}
<input
type="password"
placeholder={hasStoredKey ? "Enter new API key..." : "sk-..."}
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="w-full p-2 border rounded-md"
/>
<p className="text-xs text-gray-500">
Get your API key from{" "}
<a
href="https://platform.openai.com/api-keys"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
OpenAI Dashboard
</a>
</p>
</div>
</div>
<div className="flex justify-end gap-2">
<button
onClick={() => onOpenChange(false)}
className="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-800"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={!apiKey.trim()}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-xl hover:bg-blue-500 disabled:opacity-50"
>
{hasStoredKey ? "Update" : "Save & Continue"}
</button>
</div>
</DialogContent>
</Dialog>
);
}
26 changes: 26 additions & 0 deletions utils/encryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const ENCRYPTION_KEY = "codenames-ai-key"; // This is just for basic obfuscation

export function encryptApiKey(apiKey: string): string {
const encodedKey = btoa(apiKey);
return encodedKey;
}

export function decryptApiKey(encryptedKey: string): string {
const decodedKey = atob(encryptedKey);
return decodedKey;
}

export function getStoredApiKey(): string | null {
const encryptedKey = localStorage.getItem(ENCRYPTION_KEY);
if (!encryptedKey) return null;
return decryptApiKey(encryptedKey);
}

export function storeApiKey(apiKey: string): void {
const encryptedKey = encryptApiKey(apiKey);
localStorage.setItem(ENCRYPTION_KEY, encryptedKey);
}

export function deleteApiKey(): void {
localStorage.removeItem(ENCRYPTION_KEY);
}

0 comments on commit ed4fe2c

Please sign in to comment.