From 9999063a05a810c09643ee024e9fe71d543dce98 Mon Sep 17 00:00:00 2001 From: Safwan Erooth Date: Sun, 26 Jul 2026 06:12:28 +0400 Subject: [PATCH 1/2] feat: add provider starter onboarding --- frontend/src/pages/AiProvidersPage.tsx | 162 ++++++++++++++++++++++++- 1 file changed, 159 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/AiProvidersPage.tsx b/frontend/src/pages/AiProvidersPage.tsx index ee734d51..2eb6137c 100644 --- a/frontend/src/pages/AiProvidersPage.tsx +++ b/frontend/src/pages/AiProvidersPage.tsx @@ -1,4 +1,4 @@ -import { Settings, Loader2 } from 'lucide-react'; +import { ArrowRight, Check, ExternalLink, KeyRound, Loader2, Settings, Sparkles } from 'lucide-react'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../components/ui/card'; import { Button } from '../components/ui/button'; import { Badge } from '../components/ui/badge'; @@ -14,8 +14,7 @@ import { Input } from '../components/ui/input'; import { Label } from '../components/ui/label'; import { PageLayout, FilterBar, GridView, LoadMoreButton } from '../components/dashboard'; import { useInfiniteScroll } from '../hooks/useInfiniteScroll'; -import { getProviders, getProvider, updateProvider, createProvider } from '../services/providerApi'; -import { getModels } from '../services/providerApi'; +import { createModel, getModels, getProvider, getProviders, updateProvider, createProvider } from '../services/providerApi'; import { useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; @@ -29,6 +28,43 @@ interface AiProvidersPageProps { addProviderKey?: number; } +type StarterPath = 'openrouter' | 'google'; + +const STARTER_PATHS: Record = { + openrouter: { + providerName: 'OpenRouter', + providerBrand: 'openrouter', + modelName: 'openrouter/free', + title: 'Try OpenRouter Free', + description: 'Start with a zero-cost router that selects an available free model for each request.', + caution: 'Best for learning and demos. Free models can change, have lower limits, and are not a production reliability tier.', + signupUrl: 'https://openrouter.ai/keys', + signupLabel: 'Get an OpenRouter key', + freePricing: true, + }, + google: { + providerName: 'Google', + providerBrand: 'google', + modelName: 'gemini-2.5-flash', + title: 'Try Gemini with Google AI Studio', + description: 'Use Gemini 2.5 Flash with the Google Gemini API free tier to explore Huf.', + caution: 'Free-tier limits apply. Google states that free-tier content may be used to improve its products; avoid sensitive production data.', + signupUrl: 'https://aistudio.google.com/app/apikey', + signupLabel: 'Get a Google AI Studio key', + freePricing: false, + }, +}; + export function AiProvidersPage({ addProviderKey }: AiProvidersPageProps) { const [searchParams, setSearchParams] = useSearchParams(); const configureHandledRef = useRef(false); @@ -38,6 +74,9 @@ export function AiProvidersPage({ addProviderKey }: AiProvidersPageProps) { const [isEditing, setIsEditing] = useState(false); const [loadingProvider, setLoadingProvider] = useState(false); const [saving, setSaving] = useState(false); + const [starterPath, setStarterPath] = useState(null); + const [starterApiKey, setStarterApiKey] = useState(''); + const [starterSaving, setStarterSaving] = useState(false); const [formData, setFormData] = useState({ provider_name: '', api_key: '', @@ -239,6 +278,64 @@ export function AiProvidersPage({ addProviderKey }: AiProvidersPageProps) { } }; + const handleStarterSetup = async () => { + if (!starterPath || !starterApiKey.trim()) { + toast.error('Enter an API key to continue'); + return; + } + + const starter = STARTER_PATHS[starterPath]; + setStarterSaving(true); + try { + const providersResponse = await getProviders({ limit: 100 }); + const allProviders = Array.isArray(providersResponse) ? providersResponse : providersResponse.items; + const existingProvider = allProviders.find( + (provider) => provider.provider_name.toLowerCase() === starter.providerName.toLowerCase(), + ); + const provider = existingProvider + ? await updateProvider(existingProvider.name, { + api_key: starterApiKey.trim(), + provider_brand: starter.providerBrand, + }) + : await createProvider({ + provider_name: starter.providerName, + api_key: starterApiKey.trim(), + provider_brand: starter.providerBrand, + }); + + const currentModels = await getModels(); + const modelExists = currentModels.some( + (model) => model.model_name === starter.modelName && model.provider === provider.name, + ); + if (!modelExists) { + await createModel({ + model_name: starter.modelName, + provider: provider.name, + modalities: 'Text', + use_custom_pricing: starter.freePricing ? 1 : 0, + input_cost_per_1m_tokens: starter.freePricing ? 0 : null, + output_cost_per_1m_tokens: starter.freePricing ? 0 : null, + }); + } + + toast.success(`${starter.providerName} is ready`, { + description: `You can now create an agent with ${starter.modelName}. The key has not been tested yet.`, + }); + setStarterPath(null); + setStarterApiKey(''); + reset(); + const refreshedModels = await getModels(); + setModels(refreshedModels); + } catch (setupError) { + toast.error('Could not finish starter setup', { + description: 'Huf could not save this starter setup. Check the details and try again.', + }); + console.error(setupError); + } finally { + setStarterSaving(false); + } + }; + useSaveShortcut({ onSave: handleSave, enabled: configureModalOpen && !loadingProvider, @@ -257,6 +354,28 @@ export function AiProvidersPage({ addProviderKey }: AiProvidersPageProps) { /> } > + + +
+ + Get started with AI +
+ + Pick a free-friendly starter path, add its key, and Huf will prepare a model for your first agent. + +
+ + {(Object.entries(STARTER_PATHS) as [StarterPath, typeof STARTER_PATHS[StarterPath]][]).map(([path, starter]) => ( + + ))} + +
{error && !initialLoading && (

Failed to load providers

@@ -413,6 +532,43 @@ export function AiProvidersPage({ addProviderKey }: AiProvidersPageProps) { + + !open && setStarterPath(null)}> + + {starterPath && (() => { + const starter = STARTER_PATHS[starterPath]; + return <> + + {starter.title} + {starter.description} + +
+
+ Good to know: {starter.caution} +
+
+ + setStarterApiKey(event.target.value)} /> + + {starter.signupLabel} + +
+
+

Huf will configure

+

Provider: {starter.providerName} · Model: {starter.modelName}

+

Your key is stored in Huf's encrypted password field.

+
+
+ + + + + ; + })()} +
+
); } From 1334cc31f9c72aebddf85a6fb532f3e897af140b Mon Sep 17 00:00:00 2001 From: Safwan Erooth Date: Sun, 26 Jul 2026 06:34:47 +0400 Subject: [PATCH 2/2] feat: guide starter setup to agent creation --- frontend/src/pages/AiProvidersPage.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/AiProvidersPage.tsx b/frontend/src/pages/AiProvidersPage.tsx index 2eb6137c..c199b53f 100644 --- a/frontend/src/pages/AiProvidersPage.tsx +++ b/frontend/src/pages/AiProvidersPage.tsx @@ -16,7 +16,7 @@ import { PageLayout, FilterBar, GridView, LoadMoreButton } from '../components/d import { useInfiniteScroll } from '../hooks/useInfiniteScroll'; import { createModel, getModels, getProvider, getProviders, updateProvider, createProvider } from '../services/providerApi'; import { useEffect, useRef, useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; import type { AIProvider, AIModel } from '../types/agent.types'; import { ProviderBrandSelect } from '@/components/providers/ProviderBrandSelect'; @@ -66,6 +66,7 @@ const STARTER_PATHS: Record([]); @@ -75,6 +76,7 @@ export function AiProvidersPage({ addProviderKey }: AiProvidersPageProps) { const [loadingProvider, setLoadingProvider] = useState(false); const [saving, setSaving] = useState(false); const [starterPath, setStarterPath] = useState(null); + const [completedStarter, setCompletedStarter] = useState(null); const [starterApiKey, setStarterApiKey] = useState(''); const [starterSaving, setStarterSaving] = useState(false); const [formData, setFormData] = useState({ @@ -321,6 +323,7 @@ export function AiProvidersPage({ addProviderKey }: AiProvidersPageProps) { toast.success(`${starter.providerName} is ready`, { description: `You can now create an agent with ${starter.modelName}. The key has not been tested yet.`, }); + setCompletedStarter(starterPath); setStarterPath(null); setStarterApiKey(''); reset(); @@ -365,7 +368,20 @@ export function AiProvidersPage({ addProviderKey }: AiProvidersPageProps) { - {(Object.entries(STARTER_PATHS) as [StarterPath, typeof STARTER_PATHS[StarterPath]][]).map(([path, starter]) => ( + {completedStarter ? ( +
+
+ +
+

Your {STARTER_PATHS[completedStarter].providerName} starter is ready

+

Next, create an agent and choose {STARTER_PATHS[completedStarter].modelName}.

+
+
+ +
+ ) : (Object.entries(STARTER_PATHS) as [StarterPath, typeof STARTER_PATHS[StarterPath]][]).map(([path, starter]) => (