Skip to content
Closed
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
Binary file modified bun.lockb
Binary file not shown.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
"lint": "eslint"
},
"dependencies": {
"@ai-sdk/anthropic": "^2.0.40",
"@ai-sdk/google": "^2.0.26",
"@ai-sdk/openai": "^2.0.53",
"@ai-sdk/react": "^2.0.78",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@vercel/analytics": "^1.5.0",
Expand Down
146 changes: 113 additions & 33 deletions src/app/api/agent/route.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,132 @@
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { convertToModelMessages, stepCountIs, streamText, UIMessage } from 'ai';
import { NextRequest } from 'next/server';

import { getModelConfig, getProviderFromModelId, DEFAULT_MODELS, getBestDefaultModel } from '@/lib/models/config';
import type { ProviderId } from '@/lib/models/config';

export const maxDuration = 300;

export async function POST(req: NextRequest) {
/**
* Get the appropriate AI model instance based on model ID and API keys
*/
function getModel(modelId: string, apiKeys: Record<ProviderId, string | null>) {
const provider = getProviderFromModelId(modelId);

if (!provider) {
// Fallback to OpenAI default if model not found
const openaiKey = apiKeys.openai;
if (!openaiKey) {
throw new Error('OpenAI API key is required');
}
const openai = createOpenAI({ apiKey: openaiKey });
return openai(DEFAULT_MODELS.openai);
}

const { messages }: { messages: UIMessage[] } = await req.json();
const apiKey = apiKeys[provider];
if (!apiKey) {
throw new Error(`${provider} API key is required for model ${modelId}`);
}

// Get API key from query parameter
const apiKey = req.nextUrl.searchParams.get('apiKey');
switch (provider) {
case 'openai': {
const openai = createOpenAI({ apiKey });
return openai(modelId);
}
case 'anthropic': {
const anthropic = createAnthropic({ apiKey });
return anthropic(modelId);
}
case 'google': {
const google = createGoogleGenerativeAI({ apiKey });
return google(modelId);
}
default:
throw new Error(`Unsupported provider: ${provider}`);
}
}

// Use API key from query param if provided, otherwise fall back to environment variable
const effectiveApiKey = apiKey;
// const effectiveApiKey = apiKey || process.env.OPENAI_API_KEY;
export async function POST(req: NextRequest) {
try {
const { messages, modelId }: { messages: UIMessage[]; modelId?: string } = await req.json();

if (!effectiveApiKey) {
return new Response(
JSON.stringify({ error: 'API key is required. Please add your OpenAI API key in settings.' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
// Get API keys from query parameters
const openaiKey = req.nextUrl.searchParams.get('openaiKey');
const anthropicKey = req.nextUrl.searchParams.get('anthropicKey');
const googleKey = req.nextUrl.searchParams.get('googleKey');

// Build API keys object
const apiKeys: Record<ProviderId, string | null> = {
openai: openaiKey,
anthropic: anthropicKey,
google: googleKey,
};

// const anthropic = createAnthropic({ apiKey: process.env.ANTROPIC_API_KEY! })
const openai = createOpenAI({ apiKey: effectiveApiKey })
// Determine which model to use
// Only use default if no modelId was explicitly provided
let targetModelId = modelId;

const result = streamText({
// model: anthropic('claude-sonnet-4-20250514'
model: openai('gpt-4'),
messages: convertToModelMessages(messages),
// If no modelId provided, use best available based on API keys
if (!targetModelId || targetModelId.trim() === '') {
targetModelId = getBestDefaultModel(apiKeys);
}

onError: (e => {
console.log("❌❌❌❌ Error in agent: ", e)
}),
// Validate model exists
let modelConfig = getModelConfig(targetModelId);
if (!modelConfig) {
// If model was explicitly provided but not found, return error
if (modelId && modelId.trim() !== '') {
return new Response(
JSON.stringify({ error: `Model "${targetModelId}" not found. Please select a valid model.` }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// If it was a default that's not found, try to find any valid model for the provider
const fallbackModelId = getBestDefaultModel(apiKeys);
const fallbackConfig = getModelConfig(fallbackModelId);
if (fallbackConfig) {
targetModelId = fallbackModelId;
modelConfig = fallbackConfig;
} else {
return new Response(
JSON.stringify({ error: `No valid model configuration available` }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
}

onFinish: (e => {
console.log("✅✅✅✅ Agent finished: ", e)
// finish reason
console.log("✅✅✅✅ Agent finished reason: ", e.finishReason)
}),
// Get the model instance
let model;
try {
model = getModel(targetModelId, apiKeys);
} catch (error: any) {
console.error('Error creating model instance:', error);
const errorMessage = error.message || 'API key is required. Please add your API key in settings.';
return new Response(
JSON.stringify({ error: errorMessage }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}

// system: `Answer in short and concise sentences.`,
const result = streamText({
model,
messages: convertToModelMessages(messages),

stopWhen: stepCountIs(15),
tools: {}
onError: (e) => {
console.error("Error in agent: ", e);
},

});
stopWhen: stepCountIs(15),
tools: {}
});

return result.toUIMessageStreamResponse();
return result.toUIMessageStreamResponse();
} catch (error: any) {
console.error('Error in API route:', error);
return new Response(
JSON.stringify({ error: error.message || 'Internal server error' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
2 changes: 1 addition & 1 deletion src/app/chats/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export default function ChatPage() {
onClick={() => router.push('/chats')}
className="bg-black/10 gap-2 shadow-none w-8"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><title>arrow-left</title><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><line x1="2.75" y1="9" x2="15.25" y2="9"></line><polyline points="7 13.25 2.75 9 7 4.75"></polyline></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><title>arrow-left</title><g fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" stroke="currentColor"><line x1="2.75" y1="9" x2="15.25" y2="9"></line><polyline points="7 13.25 2.75 9 7 4.75"></polyline></g></svg>
{/* All Chats */}
</Button>
<ChatTitleEditor
Expand Down
6 changes: 3 additions & 3 deletions src/app/chats/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ function ChatsPage() {
Create New Chat
</Button>
<Link href={'https://x.com/kartik_builds/'} target='_blank'>
<svg viewBox="0 0 24 24" aria-hidden="true" className='size-5' fill="currentColor" stroke="none" stroke-width="1px" opacity="1" filter="none"><g><path d="M21.742 21.75l-7.563-11.179 7.056-8.321h-2.456l-5.691 6.714-4.54-6.714H2.359l7.29 10.776L2.25 21.75h2.456l6.035-7.118 4.818 7.118h6.191-.008zM7.739 3.818L18.81 20.182h-2.447L5.29 3.818h2.447z"></path></g></svg>
<svg viewBox="0 0 24 24" aria-hidden="true" className='size-5' fill="currentColor" stroke="none" strokeWidth="1px" opacity="1" filter="none"><g><path d="M21.742 21.75l-7.563-11.179 7.056-8.321h-2.456l-5.691 6.714-4.54-6.714H2.359l7.29 10.776L2.25 21.75h2.456l6.035-7.118 4.818 7.118h6.191-.008zM7.739 3.818L18.81 20.182h-2.447L5.29 3.818h2.447z"></path></g></svg>
</Link>
</div>
</div>
Expand All @@ -107,7 +107,7 @@ function ChatsPage() {
) : chats.length === 0 ? (
<div className="text-center py-12 bg-white rounded-3xl border border-black/10 flex flex-col items-center justify-center">
<div className='text-black/50'>
<svg xmlns="http://www.w3.org/2000/svg" className='size-8' width="18" height="18" viewBox="0 0 18 18"><title>msg-content</title><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M9,1.75C4.996,1.75,1.75,4.996,1.75,9c0,1.319,.358,2.552,.973,3.617,.43,.806-.053,2.712-.973,3.633,1.25,.068,2.897-.497,3.633-.973,.489,.282,1.264,.656,2.279,.848,.433,.082,.881,.125,1.338,.125,4.004,0,7.25-3.246,7.25-7.25S13.004,1.75,9,1.75Z"></path><line x1="5.75" y1="7.25" x2="12.25" y2="7.25"></line><line x1="5.75" y1="10.75" x2="10.25" y2="10.75"></line></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" className='size-8' width="18" height="18" viewBox="0 0 18 18"><title>msg-content</title><g fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" stroke="currentColor"><path d="M9,1.75C4.996,1.75,1.75,4.996,1.75,9c0,1.319,.358,2.552,.973,3.617,.43,.806-.053,2.712-.973,3.633,1.25,.068,2.897-.497,3.633-.973,.489,.282,1.264,.656,2.279,.848,.433,.082,.881,.125,1.338,.125,4.004,0,7.25-3.246,7.25-7.25S13.004,1.75,9,1.75Z"></path><line x1="5.75" y1="7.25" x2="12.25" y2="7.25"></line><line x1="5.75" y1="10.75" x2="10.25" y2="10.75"></line></g></svg>
</div>
<h3 className="font-medium text-black mt-4">No chats yet</h3>
<p className="text-black/50 text-sm">Create your first chat to get started</p>
Expand Down Expand Up @@ -142,7 +142,7 @@ function ChatsPage() {
{deleting === chat.id ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
) : (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><title>trash</title><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M13.6977 7.75L13.35 14.35C13.294 15.4201 12.416 16.25 11.353 16.25H6.64804C5.58404 16.25 4.70703 15.42 4.65103 14.35L4.30334 7.75"></path> <path d="M2.75 4.75H15.25"></path> <path d="M6.75 4.75V2.75C6.75 2.2 7.198 1.75 7.75 1.75H10.25C10.802 1.75 11.25 2.2 11.25 2.75V4.75"></path></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><title>trash</title><g fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" stroke="currentColor"><path d="M13.6977 7.75L13.35 14.35C13.294 15.4201 12.416 16.25 11.353 16.25H6.64804C5.58404 16.25 4.70703 15.42 4.65103 14.35L4.30334 7.75"></path> <path d="M2.75 4.75H15.25"></path> <path d="M6.75 4.75V2.75C6.75 2.2 7.198 1.75 7.75 1.75H10.25C10.802 1.75 11.25 2.2 11.25 2.75V4.75"></path></g></svg>
)}
</Button>
</div>
Expand Down
32 changes: 27 additions & 5 deletions src/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@import 'tailwindcss';
@import "tw-animate-css";
@import 'tw-animate-css';

@custom-variant dark (&:is(.dark *));

Expand Down Expand Up @@ -129,10 +129,32 @@ button {
}

@layer base {
* {
@apply border-border outline-ring/50;
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
body {
@apply bg-background text-foreground;
}
}

/* width */
.floatiog-menu::-webkit-scrollbar {
width: 8px;
border-radius: 99px;
}

/* Track */
.floatiog-menu::-webkit-scrollbar-track {
background: transparent;
}

/* Handle */
.floatiog-menu::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 99px;
}

/* Handle on hover */
.floatiog-menu::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
}
35 changes: 30 additions & 5 deletions src/app/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,43 @@

import { usePlaygroundStore } from '@/store/Playground'
import { usePathname } from 'next/navigation'
import { getAllApiKeys } from '@/lib/storage'
import type { ProviderId } from '@/lib/models/config'

import React, { useEffect } from 'react'
import React, { useEffect, useRef } from 'react'

function Provider({ children }: { children: React.ReactNode }) {

const pathname = usePathname()
const store = usePlaygroundStore()

const setApiKey = usePlaygroundStore((state) => state.setApiKey)
const reset = usePlaygroundStore((state) => state.reset)
const prevPathnameRef = useRef<string | null>(null)

// Load API keys from storage on mount (only once)
useEffect(() => {
const allKeys = getAllApiKeys();
Object.entries(allKeys).forEach(([provider, key]) => {
if (key) {
setApiKey(provider as ProviderId, key);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Only run once on mount

// Reset store when pathname changes (but not on initial mount)
useEffect(() => {
store.reset()
}, [pathname])
// Skip reset on initial mount
if (prevPathnameRef.current === null) {
prevPathnameRef.current = pathname;
return;
}

// Only reset if pathname actually changed
if (prevPathnameRef.current !== pathname) {
prevPathnameRef.current = pathname;
reset();
}
}, [pathname, reset])

return (
<div>
Expand Down
Loading