wrap inputs in real forms, so enter submits instead of doing nothing
24 panel and screen files had their fields sitting in a <div> instead of a <form> — typing a value and pressing Enter did nothing, and the button was the only way in. Fixed the sites that are a real form, a single field with an obvious action, or a multi-field editor; left search/filter boxes and a few genuinely ambiguous sites alone (see TODO.md for the full breakdown). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+82
-68
@@ -226,6 +226,23 @@ export const AIHarnessesSection = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const submitLocalUrl = (ev?: React.FormEvent) => {
|
||||
ev?.preventDefault();
|
||||
if (probeState.step === 'url' && localUrl.trim()) handleProbe();
|
||||
};
|
||||
|
||||
const submitAuth = (ev?: React.FormEvent) => {
|
||||
ev?.preventDefault();
|
||||
if (probeState.step !== 'auth') return;
|
||||
const ready = probeState.probe.authType === 'basic' ? authUsername && authPassword : authApiKey;
|
||||
if (ready) handleAuthSubmit(probeState.probe);
|
||||
};
|
||||
|
||||
const submitApiKey = (ev: React.FormEvent, providerId: string) => {
|
||||
ev.preventDefault();
|
||||
if (keyInputs[providerId]) saveApiKey(providerId);
|
||||
};
|
||||
|
||||
const removeLocalProvider = async (id: string) => {
|
||||
await client.delete(`/server-settings/chat-providers/local-providers/${id}`);
|
||||
queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_PROVIDERS'] });
|
||||
@@ -291,57 +308,60 @@ export const AIHarnessesSection = () => {
|
||||
{addingLocal ? (
|
||||
<div className="flex flex-col gap-2 max-w-sm">
|
||||
{/* Step 1: Name + URL inputs stacked */}
|
||||
<Input
|
||||
type="text"
|
||||
className="h-8 text-xs"
|
||||
placeholder="Name (e.g. My Ollama)"
|
||||
value={localName}
|
||||
onChange={(ev) => setLocalName(ev.target.value)}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Escape') resetLocalForm();
|
||||
}}
|
||||
disabled={probeState.step !== 'url'}
|
||||
autoFocus
|
||||
/>
|
||||
<Input
|
||||
type="url"
|
||||
className="h-8 text-xs"
|
||||
placeholder="http://localhost:11434"
|
||||
value={localUrl}
|
||||
onChange={(ev) => setLocalUrl(ev.target.value)}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Escape') resetLocalForm();
|
||||
if (ev.key === 'Enter' && localUrl.trim() && probeState.step === 'url') handleProbe();
|
||||
}}
|
||||
disabled={probeState.step !== 'url'}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
{probeState.step === 'url' && (
|
||||
<Button
|
||||
<form onSubmit={submitLocalUrl} className="contents">
|
||||
<Input
|
||||
type="text"
|
||||
className="h-8 text-xs"
|
||||
placeholder="Name (e.g. My Ollama)"
|
||||
value={localName}
|
||||
onChange={(ev) => setLocalName(ev.target.value)}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Escape') resetLocalForm();
|
||||
}}
|
||||
disabled={probeState.step !== 'url'}
|
||||
autoFocus
|
||||
/>
|
||||
<Input
|
||||
type="url"
|
||||
className="h-8 text-xs"
|
||||
placeholder="http://localhost:11434"
|
||||
value={localUrl}
|
||||
onChange={(ev) => setLocalUrl(ev.target.value)}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Escape') resetLocalForm();
|
||||
}}
|
||||
disabled={probeState.step !== 'url'}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
{probeState.step === 'url' && (
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="h-7 text-xs bg-duck-dark text-white hover:bg-duck-dark/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 cursor-pointer disabled:opacity-40"
|
||||
disabled={!localUrl.trim()}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
{(probeState.step === 'probing' || probeState.step === 'saving') && (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-duck-teal shrink-0" />
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="h-7 text-xs bg-duck-dark text-white hover:bg-duck-dark/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 cursor-pointer disabled:opacity-40"
|
||||
disabled={!localUrl.trim()}
|
||||
onClick={() => handleProbe()}
|
||||
onClick={resetLocalForm}
|
||||
className="text-duck-dark/40 hover:text-duck-dark/70 dark:text-foreground/40 dark:hover:text-foreground/70 cursor-pointer transition-colors text-xs"
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
{(probeState.step === 'probing' || probeState.step === 'saving') && (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-duck-teal shrink-0" />
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetLocalForm}
|
||||
className="text-duck-dark/40 hover:text-duck-dark/70 dark:text-foreground/40 dark:hover:text-foreground/70 cursor-pointer transition-colors text-xs"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Step 2: Auth form (if needed) */}
|
||||
{probeState.step === 'auth' && (
|
||||
<div className="flex flex-col gap-2 rounded-md border border-duck-dark/10 dark:border-foreground/10 p-3">
|
||||
<form
|
||||
onSubmit={submitAuth}
|
||||
className="flex flex-col gap-2 rounded-md border border-duck-dark/10 dark:border-foreground/10 p-3"
|
||||
>
|
||||
<span className="text-duck-dark/70 dark:text-foreground/70 font-medium">
|
||||
{probeState.probe.name} requires authentication
|
||||
</span>
|
||||
@@ -361,17 +381,12 @@ export const AIHarnessesSection = () => {
|
||||
placeholder="Password"
|
||||
value={authPassword}
|
||||
onChange={(ev) => setAuthPassword(ev.target.value)}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter' && authUsername && authPassword)
|
||||
handleAuthSubmit(probeState.probe);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="h-7 text-xs w-fit bg-duck-dark text-white hover:bg-duck-dark/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 cursor-pointer disabled:opacity-40"
|
||||
disabled={!authUsername || !authPassword}
|
||||
onClick={() => handleAuthSubmit(probeState.probe)}
|
||||
>
|
||||
Authenticate
|
||||
</Button>
|
||||
@@ -384,23 +399,19 @@ export const AIHarnessesSection = () => {
|
||||
placeholder="API Key"
|
||||
value={authApiKey}
|
||||
onChange={(ev) => setAuthApiKey(ev.target.value)}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter' && authApiKey) handleAuthSubmit(probeState.probe);
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="h-7 text-xs w-fit bg-duck-dark text-white hover:bg-duck-dark/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 cursor-pointer disabled:opacity-40"
|
||||
disabled={!authApiKey}
|
||||
onClick={() => handleAuthSubmit(probeState.probe)}
|
||||
>
|
||||
Authenticate
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
@@ -460,7 +471,10 @@ export const AIHarnessesSection = () => {
|
||||
</button>
|
||||
</div>
|
||||
{editingProvider === provider.key && (
|
||||
<div className="flex items-center gap-2 pl-4">
|
||||
<form
|
||||
onSubmit={(ev) => submitApiKey(ev, provider.providerId)}
|
||||
className="flex items-center gap-2 pl-4"
|
||||
>
|
||||
<Input
|
||||
type="password"
|
||||
className="h-7 text-xs flex-1"
|
||||
@@ -470,15 +484,13 @@ export const AIHarnessesSection = () => {
|
||||
setKeyInputs((prev) => ({ ...prev, [provider.providerId]: ev.target.value }))
|
||||
}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter' && keyInputs[provider.providerId]) saveApiKey(provider.providerId);
|
||||
if (ev.key === 'Escape') setEditingProvider(null);
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
type="submit"
|
||||
disabled={keyInputs[provider.providerId] === undefined || savingKey === provider.providerId}
|
||||
onClick={() => saveApiKey(provider.providerId)}
|
||||
className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
|
||||
title="Save key"
|
||||
>
|
||||
@@ -491,7 +503,7 @@ export const AIHarnessesSection = () => {
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@@ -501,7 +513,11 @@ export const AIHarnessesSection = () => {
|
||||
if (!provider || connectedProviders.some((cp) => cp.providerId === provider.providerId))
|
||||
return null;
|
||||
return (
|
||||
<div key={provider.providerId} className="flex items-center gap-2">
|
||||
<form
|
||||
key={provider.providerId}
|
||||
onSubmit={(ev) => submitApiKey(ev, provider.providerId)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<label className="w-36 text-duck-dark/70 dark:text-foreground/70 shrink-0 truncate font-medium text-[11px]">
|
||||
{provider.key}
|
||||
</label>
|
||||
@@ -514,15 +530,13 @@ export const AIHarnessesSection = () => {
|
||||
setKeyInputs((prev) => ({ ...prev, [provider.providerId]: ev.target.value }))
|
||||
}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter' && keyInputs[provider.providerId]) saveApiKey(provider.providerId);
|
||||
if (ev.key === 'Escape') setEditingProvider(null);
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
type="submit"
|
||||
disabled={keyInputs[provider.providerId] === undefined || savingKey === provider.providerId}
|
||||
onClick={() => saveApiKey(provider.providerId)}
|
||||
className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
|
||||
title="Save key"
|
||||
>
|
||||
@@ -535,7 +549,7 @@ export const AIHarnessesSection = () => {
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
})()}
|
||||
{unconnectedProviders.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user