Files
cruciverba_1/webapp/components/crossword-config-form.tsx

184 lines
5.6 KiB
TypeScript

"use client";
import { startTransition, useState } from "react";
import { useRouter } from "next/navigation";
import type { Locale } from "@/lib/i18n";
import type { CrosswordRequest } from "@/lib/types";
const DEFAULTS = {
topic: "transport",
difficulty: "medium",
initialWordCount: 19,
themedFillCount: 10,
targetEmptyRatio: 0.1667,
seed: 2,
lexiconFile: "lexicon_it_curated_llm_aggressive.json",
};
type CrosswordConfigFormProps = {
locale: Locale;
dict: {
topic: string;
difficulty: string;
initialWords: string;
themedFillCount: string;
targetEmptyRatio: string;
seed: string;
lexiconFile: string;
submit: string;
submitting: string;
note: string;
};
};
export function CrosswordConfigForm({ locale, dict }: CrosswordConfigFormProps) {
const router = useRouter();
const [topic, setTopic] = useState(DEFAULTS.topic);
const [difficulty, setDifficulty] = useState(DEFAULTS.difficulty);
const [initialWordCount, setInitialWordCount] = useState(DEFAULTS.initialWordCount);
const [themedFillCount, setThemedFillCount] = useState(DEFAULTS.themedFillCount);
const [targetEmptyRatio, setTargetEmptyRatio] = useState(DEFAULTS.targetEmptyRatio);
const [seed, setSeed] = useState(DEFAULTS.seed);
const [lexiconFile, setLexiconFile] = useState(DEFAULTS.lexiconFile);
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setIsSubmitting(true);
setErrorMessage("");
const requestPayload: CrosswordRequest = {
schema_version: "1.0",
request_id: `req-${Date.now()}`,
requested_at: new Date().toISOString(),
generator: {
topic: topic
.split(",")
.map((item) => item.trim())
.filter(Boolean),
difficulty,
seed,
initial_word_count: initialWordCount,
themed_fill_count: themedFillCount,
target_empty_ratio: targetEmptyRatio,
diffxy: 7,
time_limit_seconds: 8,
max_candidates_per_word: 12,
lexicon_file: lexiconFile,
definitions_enabled: true,
definition_style: "classic",
preferred_output_language: locale,
},
};
try {
const response = await fetch("/api/crosswords/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestPayload),
});
if (!response.ok) {
const errorPayload = await response.json().catch(() => ({}));
throw new Error(errorPayload.message || "Unable to generate crossword");
}
const generated = await response.json();
startTransition(() => {
router.push(`/${locale}/crosswords/${generated.crossword_id}`);
});
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : "Unexpected error");
setIsSubmitting(false);
}
}
return (
<form className="config-form" onSubmit={handleSubmit}>
<div className="config-form__grid">
<div className="field">
<label htmlFor="topic">{dict.topic}</label>
<input id="topic" value={topic} onChange={(event) => setTopic(event.target.value)} />
</div>
<div className="field">
<label htmlFor="difficulty">{dict.difficulty}</label>
<select
id="difficulty"
value={difficulty}
onChange={(event) => setDifficulty(event.target.value)}
>
<option value="easy">easy</option>
<option value="medium">medium</option>
<option value="hard">hard</option>
<option value="expert">expert</option>
</select>
</div>
<div className="field">
<label htmlFor="initialWordCount">{dict.initialWords}</label>
<input
id="initialWordCount"
type="number"
value={initialWordCount}
onChange={(event) => setInitialWordCount(Number(event.target.value))}
/>
</div>
<div className="field">
<label htmlFor="themedFillCount">{dict.themedFillCount}</label>
<input
id="themedFillCount"
type="number"
value={themedFillCount}
onChange={(event) => setThemedFillCount(Number(event.target.value))}
/>
</div>
<div className="field">
<label htmlFor="targetEmptyRatio">{dict.targetEmptyRatio}</label>
<input
id="targetEmptyRatio"
type="number"
step="0.0001"
value={targetEmptyRatio}
onChange={(event) => setTargetEmptyRatio(Number(event.target.value))}
/>
</div>
<div className="field">
<label htmlFor="seed">{dict.seed}</label>
<input
id="seed"
type="number"
value={seed}
onChange={(event) => setSeed(Number(event.target.value))}
/>
</div>
</div>
<div className="field">
<label htmlFor="lexiconFile">{dict.lexiconFile}</label>
<input
id="lexiconFile"
value={lexiconFile}
onChange={(event) => setLexiconFile(event.target.value)}
/>
</div>
<div className="actions">
<button className="button" type="submit" disabled={isSubmitting}>
{isSubmitting ? dict.submitting : dict.submit}
</button>
<span className="muted">{dict.note}</span>
</div>
{errorMessage ? <p className="form-error">{errorMessage}</p> : null}
</form>
);
}