Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions examples/nextjs-example/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion examples/nextjs-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"next": "15.5.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwind-merge": "^3.3.1"
"tailwind-merge": "^3.3.1",
"usehooks-ts": "^3.1.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,31 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { getOAuthKeyUrl } from "@/lib/config";
import { OAUTH_CALLBACK_URL, OPENROUTER_CODE_VERIFIER_KEY } from "@/lib/config";
import { useOpenRouter } from "@/lib/hooks/use-openrouter-client";
import { ExternalLink } from "lucide-react";

interface NotConnectedDialogProps {
open: boolean;
}

export function NotConnectedDialog({ open }: NotConnectedDialogProps) {
const { client } = useOpenRouter();

const handleGotoOAuth = async () => {
const { codeChallenge, codeVerifier } =
await client.oAuth.createSHA256CodeChallenge();

const url = await client.oAuth.createAuthorizationUrl({
codeChallenge,
callbackUrl: OAUTH_CALLBACK_URL,
codeChallengeMethod: "S256",
});

localStorage.setItem(OPENROUTER_CODE_VERIFIER_KEY, codeVerifier);
window.location.href = url;
};

return (
<Dialog open={open} onOpenChange={() => {}}>
<DialogContent
Expand All @@ -39,19 +56,12 @@ export function NotConnectedDialog({ open }: NotConnectedDialogProps) {
Claude, and more. Authentication is required to make API calls.
</p>

<Button asChild className="w-full">
<a
href={getOAuthKeyUrl()}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="mr-2 h-4 w-4" />
Connect to OpenRouter
</a>
<Button onClick={handleGotoOAuth} className="w-full">
<ExternalLink className="mr-2 h-4 w-4" />
Connect to OpenRouter
</Button>
</div>
</DialogContent>
</Dialog>
);
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import type React from "react";

import { useState, useRef, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
Expand All @@ -13,12 +12,11 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { MessageSquare, Send, Settings, User, Bot } from "lucide-react";
import { OpenRouterCore } from "@openrouter/sdk/core";
import { chatSend, SendAcceptEnum } from "@openrouter/sdk/funcs/chatSend";
import { OPENROUTER_KEY_LOCALSTORAGE_KEY } from "@/lib/config";
import { useApiKey } from "@/lib/hooks/use-api-key";
import { useOpenRouter } from "@/lib/hooks/use-openrouter-client";
import { Message as OpenRouterMessageRequest } from "@openrouter/sdk/models";
import useLocalStorage from "@/lib/hooks/use-local-storage";
import { Bot, MessageSquare, Send, Settings, User } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { NotConnectedDialog } from "./_dialogs/not-connected";

type Message = OpenRouterMessageRequest & {
Expand All @@ -27,9 +25,8 @@ type Message = OpenRouterMessageRequest & {
};

export default function Page() {
const { value: apiKey, isPending: isApiKeyPending } = useLocalStorage<
string | null
>(OPENROUTER_KEY_LOCALSTORAGE_KEY, null);
const { client: openRouter } = useOpenRouter();
const [apiKey] = useApiKey();

const [messages, setMessages] = useState<Message[]>([
{
Expand All @@ -39,11 +36,12 @@ export default function Page() {
"Hello! I'm your AI assistant powered by OpenRouter. I can help you with a wide variety of tasks. What would you like to know or discuss today?",
},
]);
const [input, setInput] = useState("");
const [selectedModel, setSelectedModel] = useState("gpt-4");
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);

const inputRef = useRef<HTMLInputElement>(null);

const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
Expand All @@ -53,6 +51,9 @@ export default function Page() {
}, [messages]);

const handleSend = async () => {
const input = inputRef.current?.value;
if (!input) return;

setIsLoading(true);

if (!input.trim() || isLoading) return;
Expand All @@ -72,7 +73,7 @@ export default function Page() {
];

setMessages(updatedMessages);
setInput("");
inputRef.current!.value = "";

// Add an empty assistant message to stream into
const assistantMessage: Message = {
Expand All @@ -88,27 +89,16 @@ export default function Page() {
throw new Error("API key is required but not present.");
}

const openRouter = new OpenRouterCore({ apiKey });

const result = await chatSend(
openRouter,
{
model: "openai/gpt-4o",
maxTokens: 1000,
messages: updatedMessages,
stream: true,
},
{ acceptHeaderOverride: SendAcceptEnum.textEventStream },
);

if (!result.ok) {
alert("Error: " + result.error.message);
return setIsLoading(false);
}
const result = await openRouter.chat.send({
model: "openai/gpt-4o",
maxTokens: 1000,
messages: updatedMessages,
stream: true,
});

// Stream chunks into the latest message
const chunks: string[] = [];
for await (const chunk of result.value) {
for await (const chunk of result) {
chunks.push(chunk.data.choices[0].delta.content || "");
setMessages((prev) => {
const newMessages = [...prev];
Expand All @@ -134,7 +124,7 @@ export default function Page() {

return (
<>
<NotConnectedDialog open={!isApiKeyPending && apiKey === null} />
<NotConnectedDialog open={!apiKey} />
<div className="flex flex-col h-screen bg-background">
{/* Header */}
<div className="border-b border-border bg-card">
Expand Down Expand Up @@ -221,18 +211,13 @@ export default function Page() {
<div className="border-border bg-card p-4 max-w-4xl mx-auto w-full">
<div className="flex gap-2">
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
ref={inputRef}
onKeyPress={handleKeyPress}
placeholder="Type your message..."
className="flex-1"
disabled={isLoading}
/>
<Button
onClick={handleSend}
disabled={!input.trim() || isLoading}
size="icon"
>
<Button onClick={handleSend} disabled={isLoading} size="icon">
<Send className="h-4 w-4" />
</Button>
</div>
Expand Down
11 changes: 11 additions & 0 deletions examples/nextjs-example/src/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";

import { OpenRouterClientProvider } from "@/lib/hooks/use-openrouter-client";

export default function Layout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <OpenRouterClientProvider>{children}</OpenRouterClientProvider>;
}
Loading