184 lines
5.6 KiB
TypeScript
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>
|
|
);
|
|
}
|