Skip to content

Commit 5f93201

Browse files
committed
feat(deep-research): strengthen Firecrawl API key validation
- Block form submission when Firecrawl API key is empty, invalid, or being validated - Add validation on blur and Enter key events - Remove "Get API Key" button from tab order - Add comprehensive validation state to Start button (disabled when empty/invalid/validating) - Re-validate key on form submission as final safety check - Display validation errors in both input area and form error summary - Auto-expand API configuration section on validation errors This improves form security and UX by: 1. Preventing submissions with missing or invalid API keys 2. Providing immediate feedback on key presence and validity 3. Streamlining keyboard navigation 4. Making validation state clearly visible
1 parent 70ad2c5 commit 5f93201

File tree

2 files changed

+132
-7
lines changed

2 files changed

+132
-7
lines changed

webview-ui/src/features/deep-research/GetStarted.tsx

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,22 @@ import {
1515
CollapsibleTrigger,
1616
} from "@/components/ui"
1717

18-
import { deepResearchModels, ProviderId, ResearchSession, researchSessionSchema } from "./types"
18+
import {
19+
deepResearchModels,
20+
ProviderId,
21+
ResearchSession,
22+
researchSessionSchema,
23+
validateFirecrawlApiKey,
24+
} from "./types"
1925
import { useResearchSession } from "./useResearchSession"
2026
import { useProvider } from "./useProvider"
2127
import { Providers } from "./Providers"
2228
import { Models } from "./Models"
2329

2430
export const GetStarted = () => {
2531
const [isProvidersOpen, setIsProvidersOpen] = useState(false)
32+
const [isValidatingFirecrawl, setIsValidatingFirecrawl] = useState(false)
33+
const [firecrawlError, setFirecrawlError] = useState<string | null>(null)
2634
const { setSession } = useResearchSession()
2735
const { provider, providers, setProviderValue } = useProvider()
2836

@@ -44,10 +52,39 @@ export const GetStarted = () => {
4452
handleSubmit,
4553
control,
4654
setValue,
55+
getValues,
4756
formState: { errors },
4857
} = form
4958

50-
const onSubmit = useCallback((data: ResearchSession) => setSession(data), [setSession])
59+
const onSubmit = useCallback(
60+
async (data: ResearchSession) => {
61+
// Check for empty key first
62+
if (!data.firecrawlApiKey) {
63+
setFirecrawlError("Firecrawl API key is required.")
64+
setIsProvidersOpen(true)
65+
return
66+
}
67+
68+
// Validate Firecrawl API key before submission
69+
setIsValidatingFirecrawl(true)
70+
try {
71+
const isValid = await validateFirecrawlApiKey(data.firecrawlApiKey)
72+
if (!isValid) {
73+
setFirecrawlError("Invalid Firecrawl API key. Please check your credentials.")
74+
setIsProvidersOpen(true)
75+
return
76+
}
77+
setFirecrawlError(null)
78+
setSession(data)
79+
} catch (error) {
80+
setFirecrawlError("Failed to validate Firecrawl API key. Please try again.")
81+
setIsProvidersOpen(true)
82+
} finally {
83+
setIsValidatingFirecrawl(false)
84+
}
85+
},
86+
[setSession, setIsProvidersOpen],
87+
)
5188

5289
useEffect(() => {
5390
setValue("providerId", provider?.providerId ?? ProviderId.OpenRouter)
@@ -117,14 +154,80 @@ export const GetStarted = () => {
117154
{...field}
118155
type="password"
119156
placeholder="fc-..."
120-
className="flex-1"
121-
onBlur={() => setProviderValue("firecrawlApiKey", field.value)}
157+
className={cn(
158+
"flex-1",
159+
(errors.firecrawlApiKey || firecrawlError) &&
160+
"border-destructive",
161+
)}
162+
onKeyDown={async (e) => {
163+
if (e.key === "Enter") {
164+
e.preventDefault() // Prevent form submission
165+
if (field.value) {
166+
setIsValidatingFirecrawl(true)
167+
setFirecrawlError(null)
168+
try {
169+
const isValid = await validateFirecrawlApiKey(
170+
field.value,
171+
)
172+
if (!isValid) {
173+
setFirecrawlError(
174+
"Invalid Firecrawl API key. Please check your credentials.",
175+
)
176+
}
177+
} catch (error) {
178+
setFirecrawlError(
179+
"Failed to validate Firecrawl API key. Please try again.",
180+
)
181+
} finally {
182+
setIsValidatingFirecrawl(false)
183+
}
184+
}
185+
}
186+
}}
187+
onBlur={async () => {
188+
setProviderValue("firecrawlApiKey", field.value)
189+
if (field.value) {
190+
setIsValidatingFirecrawl(true)
191+
setFirecrawlError(null)
192+
try {
193+
const isValid = await validateFirecrawlApiKey(
194+
field.value,
195+
)
196+
if (!isValid) {
197+
setFirecrawlError(
198+
"Invalid Firecrawl API key. Please check your credentials.",
199+
)
200+
}
201+
} catch (error) {
202+
setFirecrawlError(
203+
"Failed to validate Firecrawl API key. Please try again.",
204+
)
205+
} finally {
206+
setIsValidatingFirecrawl(false)
207+
}
208+
}
209+
}}
122210
/>
211+
{(errors.firecrawlApiKey || firecrawlError) && (
212+
<div className="text-destructive text-sm">
213+
{firecrawlError || errors.firecrawlApiKey?.message}
214+
</div>
215+
)}
216+
{isValidatingFirecrawl && (
217+
<div className="text-muted-foreground text-sm">
218+
Validating API key...
219+
</div>
220+
)}
123221
<div className="flex flex-row items-center justify-between gap-2">
124222
<div className="text-muted-foreground">
125223
Firecrawl turns websites into LLM-ready data.
126224
</div>
127-
<Button variant="outline" size="sm" asChild>
225+
<Button
226+
variant="outline"
227+
size="sm"
228+
asChild
229+
tabIndex={-1} // Remove from tab order
230+
>
128231
<a href="https://www.firecrawl.com/">Get API Key</a>
129232
</Button>
130233
</div>
@@ -213,7 +316,11 @@ export const GetStarted = () => {
213316
)}
214317
/>
215318
</Card>
216-
<Button variant="default" size="lg" type="submit">
319+
<Button
320+
variant="default"
321+
size="lg"
322+
type="submit"
323+
disabled={isValidatingFirecrawl || !!firecrawlError || !getValues("firecrawlApiKey")}>
217324
<span className="codicon codicon-rocket" />
218325
<span className="font-bold text-lg">Start</span>
219326
</Button>
@@ -223,6 +330,7 @@ export const GetStarted = () => {
223330
{error?.message}
224331
</div>
225332
))}
333+
{firecrawlError && <div className="text-red-500">{firecrawlError}</div>}
226334
</div>
227335
</form>
228336
</FormProvider>

webview-ui/src/features/deep-research/types.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,26 @@ export type Provider = ProviderMetadata & {
141141
}
142142

143143
/**
144-
* Research Session
144+
* Validates a Firecrawl API key by making a test request
145145
*/
146+
export async function validateFirecrawlApiKey(key: string): Promise<boolean> {
147+
if (!key) return false
148+
try {
149+
const response = await fetch("https://api.firecrawl.dev/v1/team/credit-usage", {
150+
method: "GET",
151+
headers: {
152+
Authorization: `Bearer ${key}`,
153+
},
154+
})
155+
return response.ok
156+
} catch (error) {
157+
return false
158+
}
159+
}
146160

161+
/**
162+
* Research Session
163+
*/
147164
export const researchSessionSchema = z.object({
148165
providerId: z.nativeEnum(ProviderId),
149166
providerApiKey: z.string().min(1, { message: "Provider API key is required." }),

0 commit comments

Comments
 (0)