All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m41s
149 lines
6.6 KiB
TypeScript
149 lines
6.6 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
export function GenerateTab({ importType }: { importType: "flashcards" | "quizzes" | "connections" | "crossword" }) {
|
|
const [instructions, setInstructions] = useState("");
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [saveResult, setSaveResult] = useState<"success" | "error" | null>(null);
|
|
const [copied, setCopied] = useState(false);
|
|
const [packCount, setPackCount] = useState(1);
|
|
|
|
const batchOverride = importType === "connections" && packCount > 1
|
|
? `TEMPORARY BATCH OVERRIDE:\nGenerate exactly ${packCount} distinct Connections packs. Return one raw JSON array containing exactly ${packCount} objects that each follow the schema below. Make the packs meaningfully different from one another. This override takes precedence over any later instruction to return one object.\n\n`
|
|
: "";
|
|
const displayedInstructions = `${batchOverride}${instructions}`;
|
|
|
|
useEffect(() => {
|
|
fetch(`/api/settings/llm-instructions?type=${importType}`)
|
|
.then((res) => res.json())
|
|
.then((data) => setInstructions(data.value))
|
|
.finally(() => setLoading(false));
|
|
}, [importType]);
|
|
|
|
async function handleSave() {
|
|
setSaving(true);
|
|
setSaveResult(null);
|
|
try {
|
|
const res = await fetch(`/api/settings/llm-instructions?type=${importType}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ value: instructions }),
|
|
});
|
|
if (!res.ok) throw new Error("Failed to save");
|
|
setSaveResult("success");
|
|
} catch {
|
|
setSaveResult("error");
|
|
} finally {
|
|
setSaving(false);
|
|
setTimeout(() => setSaveResult(null), 2500);
|
|
}
|
|
}
|
|
|
|
async function handleReset() {
|
|
if (!confirm("Reset to default instructions?")) return;
|
|
setSaving(true);
|
|
try {
|
|
const res = await fetch(`/api/settings/llm-instructions?type=${importType}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ value: "__RESET__" }),
|
|
});
|
|
const data = await res.json();
|
|
setInstructions(data.value);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
async function handleCopy() {
|
|
await navigator.clipboard.writeText(displayedInstructions);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
}
|
|
|
|
if (loading) {
|
|
return <div className="animate-subtle-pulse text-text-muted">Loading instructions...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<p className="text-sm text-text-secondary">
|
|
Copy these instructions and paste them into an LLM chat along with your study material. The LLM will generate JSON you can paste into the Import tab.
|
|
</p>
|
|
|
|
{importType === "connections" && (
|
|
<div className="rounded-xl border border-border bg-bg-surface-alt/60 p-4">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div>
|
|
<label htmlFor="connections-pack-count" className="text-sm font-bold text-text-heading">Number of game packs</label>
|
|
<p className="text-xs text-text-muted">This temporarily updates the copied prompt. Your saved instructions stay unchanged.</p>
|
|
</div>
|
|
<output htmlFor="connections-pack-count" className="grid h-11 min-w-12 place-items-center rounded-lg border border-border bg-bg-surface px-3 text-lg font-extrabold text-text-heading">{packCount}</output>
|
|
</div>
|
|
<input id="connections-pack-count" type="range" min={1} max={10} step={1} value={packCount} onChange={(event) => setPackCount(Number(event.target.value))} className="mt-4 w-full accent-primary" />
|
|
<div className="mt-1 flex justify-between text-[10px] font-bold text-text-muted" aria-hidden><span>1</span><span>5</span><span>10</span></div>
|
|
</div>
|
|
)}
|
|
|
|
<textarea
|
|
value={displayedInstructions}
|
|
onChange={(event) => setInstructions(batchOverride && event.target.value.startsWith(batchOverride) ? event.target.value.slice(batchOverride.length) : event.target.value)}
|
|
rows={14}
|
|
className="w-full px-4 py-3 rounded-lg border border-border bg-bg-surface-alt/50 text-sm text-text-body font-mono leading-relaxed focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all duration-200 resize-y"
|
|
/>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={handleCopy}
|
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-white text-sm font-medium hover:bg-primary-hover transition-all duration-200 cursor-pointer"
|
|
>
|
|
{copied ? (
|
|
<>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
Copied!
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
|
</svg>
|
|
Copy to Clipboard
|
|
</>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg border text-sm transition-all duration-200 cursor-pointer disabled:opacity-50 ${
|
|
saveResult === "success" ? "border-success text-success bg-success-bg" :
|
|
saveResult === "error" ? "border-error text-error bg-error-bg" :
|
|
"border-border text-text-secondary hover:bg-bg-surface-alt"
|
|
}`}
|
|
>
|
|
{saving ? "Saving..." :
|
|
saveResult === "success" ? (
|
|
<>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
Saved!
|
|
</>
|
|
) :
|
|
saveResult === "error" ? "Failed to Save" :
|
|
"Save Changes"}
|
|
</button>
|
|
<button
|
|
onClick={handleReset}
|
|
disabled={saving}
|
|
className="px-4 py-2 rounded-lg text-sm text-text-muted hover:text-error hover:bg-error-bg transition-all duration-200 cursor-pointer disabled:opacity-50"
|
|
>
|
|
Reset to Default
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|