From a1136aa3258c47715bf2023992621f2898fc1eae Mon Sep 17 00:00:00 2001 From: Alenvelocity Date: Sat, 7 Jun 2025 22:29:40 +0530 Subject: [PATCH] feat: onborading flow, add support for Anthropic Models, update settings ui --- app/api/chat/route.ts | 6 + frontend/components/APIKeyForm.tsx | 195 ++++++++++++++-------- frontend/components/Onboarding.tsx | 258 +++++++++++++++++++++++++++++ frontend/routes/Home.tsx | 11 +- frontend/routes/Settings.tsx | 163 ++++++++++++++++-- frontend/stores/APIKeyStore.ts | 3 +- lib/models.ts | 19 +++ package.json | 1 + pnpm-lock.yaml | 15 ++ 9 files changed, 573 insertions(+), 98 deletions(-) create mode 100644 frontend/components/Onboarding.tsx diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 49116d0..e0984e6 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,6 +1,7 @@ import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { createOpenAI } from '@ai-sdk/openai'; import { createOpenRouter } from '@openrouter/ai-sdk-provider'; +import { createAnthropic } from '@ai-sdk/anthropic'; import { streamText, smoothStream } from 'ai'; import { headers } from 'next/headers'; import { getModelConfig, AIModel } from '@/lib/models'; @@ -34,6 +35,11 @@ export async function POST(req: NextRequest) { aiModel = openrouter(modelConfig.modelId); break; + case 'anthropic': + const anthropic = createAnthropic({ apiKey }); + aiModel = anthropic(modelConfig.modelId); + break; + default: return new Response( JSON.stringify({ error: 'Unsupported model provider' }), diff --git a/frontend/components/APIKeyForm.tsx b/frontend/components/APIKeyForm.tsx index 86336e0..b08a9ae 100644 --- a/frontend/components/APIKeyForm.tsx +++ b/frontend/components/APIKeyForm.tsx @@ -5,14 +5,7 @@ import { FieldError, useForm, UseFormRegister } from 'react-hook-form'; import { Button } from '@/frontend/components/ui/button'; import { Input } from '@/frontend/components/ui/input'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/frontend/components/ui/card'; -import { Key } from 'lucide-react'; +import { Key, ExternalLink, Shield } from 'lucide-react'; import { toast } from 'sonner'; import { useAPIKeyStore } from '@/frontend/stores/APIKeyStore'; import { Badge } from './ui/badge'; @@ -23,30 +16,50 @@ const formSchema = z.object({ }), openrouter: z.string().trim().optional(), openai: z.string().trim().optional(), + anthropic: z.string().trim().optional(), }); type FormValues = z.infer; -export default function APIKeyForm() { +interface APIKeyFormProps { + selectedProviders?: string[]; +} + +export default function APIKeyForm({ selectedProviders }: APIKeyFormProps = {}) { return ( - - -
- - Add Your API Keys To Start Chatting +
+
+
+ +

Configure Your AI Providers

+
+

+ Add API keys for the AI models you want to use. Keys are stored securely in your browser and never sent to our servers. +

+ +
+
+
+ +
+
+

+ Privacy First +

+

+ Your API keys are stored locally and encrypted. We never have access to them. +

+
+
- - Keys are stored locally in your browser. - - - -
- - +
+ + +
); } -const Form = () => { +const Form = ({ selectedProviders }: { selectedProviders?: string[] }) => { const { keys, setKeys } = useAPIKeyStore(); const { @@ -71,38 +84,62 @@ const Form = () => { [setKeys] ); + const providerFields = [ + { + id: 'google', + label: 'Google API Key', + models: ['Gemini 2.5 Flash', 'Gemini 2.5 Pro'], + linkUrl: 'https://aistudio.google.com/apikey', + placeholder: 'AIza...', + required: true, + }, + { + id: 'openai', + label: 'OpenAI API Key', + models: ['GPT-4o', 'GPT-4.1-mini'], + linkUrl: 'https://platform.openai.com/settings/organization/api-keys', + placeholder: 'sk-...', + required: false, + }, + { + id: 'anthropic', + label: 'Anthropic API Key', + models: ['Claude 3.5 Sonnet', 'Claude 3.5 Haiku'], + linkUrl: 'https://console.anthropic.com/settings/keys', + placeholder: 'sk-ant-...', + required: false, + }, + { + id: 'openrouter', + label: 'OpenRouter API Key', + models: ['Deepseek R1 0528', 'Deepseek V3'], + linkUrl: 'https://openrouter.ai/settings/keys', + placeholder: 'sk-or-...', + required: false, + }, + ]; + + const fieldsToShow = selectedProviders + ? providerFields.filter(field => selectedProviders.includes(field.id)) + : providerFields; + return ( - - - - - +
+ {fieldsToShow.map((field) => ( + + ))} +
+
+ {steps.map((_, index) => ( +
+ ))} +
+
+ +
+ + ); + } + + return ( +
+ + +
+ {currentStep > 0 && ( + + )} +
+ {steps.map((_, index) => ( +
+ ))} +
+
+
+
+ +
+
+ {currentStepData.title} + {currentStepData.description} + + + + {currentStep === 0 && ( +
+
+
+ OpenAI GPT + Google Gemini + Anthropic + OpenRouter +
+
+
+ )} + + {currentStep === 1 && ( +
+
+

+ Choose which AI providers you'd like to use. You can always add more later in settings. +

+
+
+ {providers.map((provider) => ( +
toggleProvider(provider.id)} + > +
+
+
+

{provider.name}

+ {provider.required && ( + Required + )} + {selectedProviders.includes(provider.id) && ( + + )} +
+

+ {provider.description} +

+
+ {provider.models.map((model) => ( + + {model} + + ))} +
+
+
+
+ ))} +
+
+

+ Note: Google Gemini is required for generating conversation titles. + You can use other providers for your main conversations. +

+
+
+ )} + + {currentStep === 3 && ( +
+
+ +

+ Setup Complete! +

+

+ You're all set to start chatting with AI +

+
+
+ )} + +
+ {currentStep < 3 ? ( + + ) : ( + + )} +
+
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/routes/Home.tsx b/frontend/routes/Home.tsx index fb575b5..645c14f 100644 --- a/frontend/routes/Home.tsx +++ b/frontend/routes/Home.tsx @@ -1,4 +1,4 @@ -import APIKeyManager from '@/frontend/components/APIKeyForm'; +import Onboarding from '@/frontend/components/Onboarding'; import Chat from '@/frontend/components/Chat'; import { v4 as uuidv4 } from 'uuid'; import { useAPIKeyStore } from '../stores/APIKeyStore'; @@ -12,12 +12,9 @@ export default function Home() { if (!isAPIKeysHydrated || !isModelStoreHydrated) return null; - if (!hasRequiredKeys) - return ( -
- -
- ); + if (!hasRequiredKeys) { + return ; + } return ; } diff --git a/frontend/routes/Settings.tsx b/frontend/routes/Settings.tsx index 2f33d72..be9d486 100644 --- a/frontend/routes/Settings.tsx +++ b/frontend/routes/Settings.tsx @@ -1,24 +1,153 @@ -import APIKeyForm from '@/frontend/components/APIKeyForm'; +import React, { useState } from 'react'; import { Link } from 'react-router'; -import { buttonVariants } from '../components/ui/button'; -import { ArrowLeftIcon } from 'lucide-react'; +import { + ArrowLeftIcon, + Key, + Shield, + Trash2, +} from 'lucide-react'; +import { Button, buttonVariants } from '../components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card'; +import { useAPIKeyStore } from '../stores/APIKeyStore'; +import APIKeyForm from '@/frontend/components/APIKeyForm'; +import ThemeToggler from '@/frontend/components/ui/ThemeToggler'; +import { getModelConfig } from '@/lib/models'; + +type SettingsTab = 'api-keys' | 'privacy' export default function Settings() { + const [activeTab, setActiveTab] = useState('api-keys'); + + const tabs = [ + { id: 'api-keys' as const, label: 'API Keys', icon: Key }, + { id: 'privacy' as const, label: 'Privacy & Data', icon: Shield }, + ]; + + const handleClearAllData = () => { + if (confirm('Are you sure you want to clear all data? This will remove all API keys and chat history.')) { + localStorage.clear(); + window.location.href = '/'; + } + }; + return ( -
- - - Back to Chat - -
- +
+
+
+
+
+ + + Back to Chat + +
+

Settings

+

Manage your AI assistant preferences

+
+
+ +
+
+
+ +
+
+ + +
+
+ {activeTab === 'api-keys' && ( +
+ +
+ )} + + {activeTab === 'privacy' && ( +
+ +
+
+
+ +

Privacy & Data

+
+ +
+
+
+ +
+
+

+ Local Storage Only +

+

+ Your data never leaves your device. Everything is stored locally in your browser. +

+
+
+
+
+ +
+
+ +

+ Clear All Data +

+
+ +

+ This will permanently delete all your API keys, chat history, and settings. This action cannot be undone. +

+ + +
+
+
+ )} +
+
+
-
+
); } diff --git a/frontend/stores/APIKeyStore.ts b/frontend/stores/APIKeyStore.ts index 2f92be9..fbdd684 100644 --- a/frontend/stores/APIKeyStore.ts +++ b/frontend/stores/APIKeyStore.ts @@ -1,7 +1,7 @@ import { create, Mutate, StoreApi } from 'zustand'; import { persist } from 'zustand/middleware'; -export const PROVIDERS = ['google', 'openrouter', 'openai'] as const; +export const PROVIDERS = ['google', 'openrouter', 'openai', 'anthropic'] as const; export type Provider = (typeof PROVIDERS)[number]; type APIKeys = Record; @@ -39,6 +39,7 @@ export const useAPIKeyStore = create()( google: '', openrouter: '', openai: '', + anthropic: '', }, setKeys: (newKeys) => { diff --git a/lib/models.ts b/lib/models.ts index a868083..299d180 100644 --- a/lib/models.ts +++ b/lib/models.ts @@ -7,6 +7,9 @@ export const AI_MODELS = [ 'Gemini 2.5 Flash', 'GPT-4o', 'GPT-4.1-mini', + 'Claude 3.5 Sonnet', + 'Claude 3.5 Haiku', + 'Claude 3.7 Sonnet', ] as const; export type AIModel = (typeof AI_MODELS)[number]; @@ -48,6 +51,22 @@ export const MODEL_CONFIGS = { provider: 'openai', headerKey: 'X-OpenAI-API-Key', }, + 'Claude 3.5 Sonnet': { + modelId: 'claude-3-5-sonnet-20241022', + provider: 'anthropic', + headerKey: 'X-Anthropic-API-Key', + }, + 'Claude 3.7 Sonnet': { + modelId: 'claude-3-7-sonnet-20250219', + provider: 'anthropic', + headerKey: 'X-Anthropic-API-Key', + }, + + 'Claude 3.5 Haiku': { + modelId: 'claude-3-5-haiku-20241022', + provider: 'anthropic', + headerKey: 'X-Anthropic-API-Key', + }, } as const satisfies Record; export const getModelConfig = (modelName: AIModel): ModelConfig => { diff --git a/package.json b/package.json index 4ce4a43..659b953 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@ai-sdk/anthropic": "^1.2.12", "@ai-sdk/google": "^1.2.19", "@ai-sdk/openai": "^1.3.22", "@ai-sdk/react": "^1.2.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9a440c..d3954d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@ai-sdk/anthropic': + specifier: ^1.2.12 + version: 1.2.12(zod@3.25.56) '@ai-sdk/google': specifier: ^1.2.19 version: 1.2.19(zod@3.25.56) @@ -168,6 +171,12 @@ importers: packages: + '@ai-sdk/anthropic@1.2.12': + resolution: {integrity: sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + '@ai-sdk/google@1.2.19': resolution: {integrity: sha512-Xgl6eftIRQ4srUdCzxM112JuewVMij5q4JLcNmHcB68Bxn9dpr3MVUSPlJwmameuiQuISIA8lMB+iRiRbFsaqA==} engines: {node: '>=18'} @@ -3117,6 +3126,12 @@ packages: snapshots: + '@ai-sdk/anthropic@1.2.12(zod@3.25.56)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.56) + zod: 3.25.56 + '@ai-sdk/google@1.2.19(zod@3.25.56)': dependencies: '@ai-sdk/provider': 1.1.3