diff --git a/app/api/completion/route.ts b/app/api/completion/route.ts index 93cd9af..91615d2 100644 --- a/app/api/completion/route.ts +++ b/app/api/completion/route.ts @@ -1,30 +1,48 @@ -import { createGoogleGenerativeAI } from '@ai-sdk/google'; -import { generateText } from 'ai'; -import { headers } from 'next/headers'; -import { NextResponse } from 'next/server'; +import { createGoogleGenerativeAI } from "@ai-sdk/google"; +import { createOpenAI } from "@ai-sdk/openai"; +import { generateText } from "ai"; +import { headers } from "next/headers"; +import { NextResponse } from "next/server"; export async function POST(req: Request) { const headersList = await headers(); - const googleApiKey = headersList.get('X-Google-API-Key'); + const googleApiKey = headersList.get("X-Google-API-Key"); + const openaiApiKey = headersList.get("X-OpenAI-API-Key"); + const openrouterApiKey = headersList.get("X-OpenRouter-API-Key"); - if (!googleApiKey) { + const { prompt, isTitle, messageId, threadId } = await req.json(); + + let model; + + if (googleApiKey) { + const google = createGoogleGenerativeAI({ + apiKey: googleApiKey, + }); + model = google("gemini-2.5-flash-preview-04-17"); + } else if (openaiApiKey) { + const openai = createOpenAI({ + apiKey: openaiApiKey, + }); + model = openai("gpt-4.1-mini"); + } else if (openrouterApiKey) { + const openrouter = createOpenAI({ + baseURL: "https://openrouter.ai/api/v1", + apiKey: openrouterApiKey, + }); + model = openrouter("deepseek/deepseek-chat-v3-0324:free"); + } else { return NextResponse.json( { - error: 'Google API key is required to enable chat title generation.', + error: + "At least one API key is required to enable chat title generation.", }, { status: 400 } ); } - const google = createGoogleGenerativeAI({ - apiKey: googleApiKey, - }); - - const { prompt, isTitle, messageId, threadId } = await req.json(); - try { const { text: title } = await generateText({ - model: google('gemini-2.5-flash-preview-04-17'), + model, system: `\n - you will generate a short title based on the first message a user begins a conversation with - ensure it is not more than 80 characters long @@ -36,9 +54,9 @@ export async function POST(req: Request) { return NextResponse.json({ title, isTitle, messageId, threadId }); } catch (error) { - console.error('Failed to generate title:', error); + console.error("Failed to generate title:", error); return NextResponse.json( - { error: 'Failed to generate title' }, + { error: "Failed to generate title" }, { status: 500 } ); } diff --git a/frontend/components/APIKeyForm.tsx b/frontend/components/APIKeyForm.tsx index 86336e0..19b15f4 100644 --- a/frontend/components/APIKeyForm.tsx +++ b/frontend/components/APIKeyForm.tsx @@ -18,12 +18,16 @@ import { useAPIKeyStore } from '@/frontend/stores/APIKeyStore'; import { Badge } from './ui/badge'; const formSchema = z.object({ - google: z.string().trim().min(1, { - message: 'Google API key is required for Title Generation', - }), + google: z.string().trim().optional(), openrouter: z.string().trim().optional(), openai: z.string().trim().optional(), -}); +}).refine( + (data) => data.google || data.openrouter || data.openai, + { + message: 'At least one API key is required', + path: ['google'], + } +); type FormValues = z.infer; @@ -81,7 +85,6 @@ const Form = () => { placeholder="AIza..." register={register} error={errors.google} - required /> state.hasRequiredKeys()); + useAutoSelectModel(); const { textareaRef, adjustHeight } = useAutoResizeTextarea({ minHeight: 72, diff --git a/frontend/components/ChatSidebar.tsx b/frontend/components/ChatSidebar.tsx index 8181d5f..ca65cff 100644 --- a/frontend/components/ChatSidebar.tsx +++ b/frontend/components/ChatSidebar.tsx @@ -13,14 +13,16 @@ import { Button, buttonVariants } from './ui/button'; import { deleteThread, getThreads } from '@/frontend/dexie/queries'; import { useLiveQuery } from 'dexie-react-hooks'; import { Link, useNavigate, useParams } from 'react-router'; -import { X } from 'lucide-react'; +import { X, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { memo } from 'react'; +import { useTitleLoadingStore } from '@/frontend/stores/TitleLoadingStore'; export default function ChatSidebar() { const { id } = useParams(); const navigate = useNavigate(); const threads = useLiveQuery(() => getThreads(), []); + const isLoading = useTitleLoadingStore((state) => state.isLoading); return ( @@ -45,7 +47,12 @@ export default function ChatSidebar() { navigate(`/chat/${thread.id}`); }} > - {thread.title} + + {thread.title} + {isLoading(thread.id) && ( + + )} +