← back to Open Seo
src/client/features/ai-search/components/PromptExplorerForm.tsx
191 lines
import type { FormEvent } from "react";
import {
formatCountryLabel,
formatModelLabel,
} from "@/client/features/ai-search/platformLabels";
import {
PROMPT_EXPLORER_MAX_PROMPT_LENGTH,
PROMPT_EXPLORER_MODELS,
WEB_SEARCH_COUNTRY_CODES,
type PromptExplorerModel,
type WebSearchCountryCode,
} from "@/types/schemas/ai-search";
type FormValues = {
prompt: string;
highlightBrand: string;
models: PromptExplorerModel[];
webSearch: boolean;
webSearchCountryCode: WebSearchCountryCode;
};
type Props = {
form: FormValues;
onPromptChange: (value: string) => void;
onHighlightBrandChange: (value: string) => void;
onModelsChange: (value: PromptExplorerModel[]) => void;
onWebSearchChange: (value: boolean) => void;
onCountryChange: (value: WebSearchCountryCode) => void;
onSubmit: (event: FormEvent) => void;
isLoading: boolean;
validationError: string | null;
};
function isCountryCode(value: string): value is WebSearchCountryCode {
return (WEB_SEARCH_COUNTRY_CODES as readonly string[]).includes(value);
}
function parseCountryCode(value: string): WebSearchCountryCode {
return isCountryCode(value) ? value : "US";
}
export function PromptExplorerForm({
form,
onPromptChange,
onHighlightBrandChange,
onModelsChange,
onWebSearchChange,
onCountryChange,
onSubmit,
isLoading,
validationError,
}: Props) {
const toggleModel = (model: PromptExplorerModel) => {
if (form.models.includes(model)) {
onModelsChange(form.models.filter((m) => m !== model));
} else {
onModelsChange([...form.models, model]);
}
};
const promptCharCount = form.prompt.length;
const promptOverLimit = promptCharCount > PROMPT_EXPLORER_MAX_PROMPT_LENGTH;
return (
<form
onSubmit={onSubmit}
className="card border border-base-300 bg-base-100"
>
<div className="card-body gap-5">
<div className="space-y-1.5">
<label
className="block text-sm font-medium"
htmlFor="prompt-explorer-prompt"
>
Prompt
</label>
<textarea
id="prompt-explorer-prompt"
className={`textarea textarea-bordered w-full resize-none ${
promptOverLimit ? "textarea-error" : ""
}`}
rows={3}
value={form.prompt}
maxLength={PROMPT_EXPLORER_MAX_PROMPT_LENGTH + 50}
onChange={(event) => onPromptChange(event.target.value)}
aria-invalid={promptOverLimit ? true : undefined}
autoFocus
/>
<div className="flex items-center justify-between text-xs text-base-content/60">
<span>What your customers might ask AI.</span>
<span
className={`tabular-nums ${promptOverLimit ? "font-medium text-error" : ""}`}
>
{promptCharCount}/{PROMPT_EXPLORER_MAX_PROMPT_LENGTH}
</span>
</div>
</div>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<div className="space-y-1.5">
<label
className="block text-sm font-medium"
htmlFor="prompt-explorer-brand"
>
Highlight brand (optional)
</label>
<input
id="prompt-explorer-brand"
type="text"
className="input input-bordered w-full"
value={form.highlightBrand}
onChange={(event) => onHighlightBrandChange(event.target.value)}
autoComplete="off"
spellCheck={false}
/>
<p className="text-xs text-base-content/60">
We'll flag whether each model mentions this brand.
</p>
</div>
<div className="space-y-1.5">
<span className="block text-sm font-medium">Models</span>
<div className="flex flex-wrap items-center gap-x-5 gap-y-2 pt-1.5">
{PROMPT_EXPLORER_MODELS.map((model) => {
const isActive = form.models.includes(model);
return (
<label
key={model}
className="flex cursor-pointer items-center gap-2"
>
<input
type="checkbox"
className="checkbox checkbox-sm"
checked={isActive}
onChange={() => toggleModel(model)}
/>
<span className="text-sm">{formatModelLabel(model)}</span>
</label>
);
})}
</div>
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-base-300 pt-4">
<div className="flex flex-wrap items-center gap-3">
<label className="flex cursor-pointer items-center gap-2">
<input
type="checkbox"
className="checkbox checkbox-sm"
checked={form.webSearch}
onChange={(event) => onWebSearchChange(event.target.checked)}
/>
<span className="text-sm">
Allow web search (more current answers)
</span>
</label>
<select
id="prompt-explorer-country"
aria-label="Web search location"
className="select select-bordered select-sm min-w-0 sm:max-w-xs"
value={form.webSearchCountryCode}
onChange={(event) =>
onCountryChange(parseCountryCode(event.target.value))
}
disabled={!form.webSearch}
>
{WEB_SEARCH_COUNTRY_CODES.map((code) => (
<option key={code} value={code}>
{formatCountryLabel(code)}
</option>
))}
</select>
</div>
<button
type="submit"
className="btn btn-primary shrink-0 px-6"
disabled={isLoading || form.models.length === 0}
>
{isLoading ? "Running…" : "Run"}
</button>
</div>
{validationError ? (
<p className="text-sm text-error">{validationError}</p>
) : null}
</div>
</form>
);
}