minor(gui): Disable simulation button if no provider found

This commit is contained in:
2026-07-10 22:36:03 +05:30
parent 7df685365e
commit 382507e71a
2 changed files with 110 additions and 69 deletions

View File

@@ -25,11 +25,15 @@ export default function ConfigPage() {
const [config, setConfig] = useState<ConfigStatus | null>(null);
const [instances, setInstances] = useState<LLMProviderInstance[]>([]);
const [mappings, setMappings] = useState<Record<string, string>>({});
const [availableProviders, setAvailableProviders] = useState<LLMProviderMeta[]>([]);
const [availableProviders, setAvailableProviders] = useState<
LLMProviderMeta[]
>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [selectedInstanceId, setSelectedInstanceId] = useState<string | null>(null);
const [selectedInstanceId, setSelectedInstanceId] = useState<string | null>(
null,
);
const [editName, setEditName] = useState("");
const [editProvider, setEditProvider] = useState("google-genai");
const [editKey, setEditKey] = useState("");
@@ -57,8 +61,12 @@ export default function ConfigPage() {
setEditName(inst.name);
setEditProvider(inst.providerName);
setEditKey("");
const pMeta = availableProviders.find((p) => p.id === inst.providerName);
setEditModel(inst.modelName || pMeta?.defaultModel || "gemini-2.5-flash");
const pMeta = availableProviders.find(
(p) => p.id === inst.providerName,
);
setEditModel(
inst.modelName || pMeta?.defaultModel || "gemini-2.5-flash",
);
setEditIsActive(inst.isActive);
}
}
@@ -128,13 +136,24 @@ export default function ConfigPage() {
setLoading(false);
return;
}
const created = await createProviderInstance(editName, editProvider, editKey, editModel || undefined);
const created = await createProviderInstance(
editName,
editProvider,
editKey,
editModel || undefined,
);
if (editIsActive) {
await setActiveProviderInstance(created.id);
}
setSelectedInstanceId(created.id);
} else {
await updateProviderInstance(selectedInstanceId, editName, editProvider, editKey || undefined, editModel || undefined);
await updateProviderInstance(
selectedInstanceId,
editName,
editProvider,
editKey || undefined,
editModel || undefined,
);
if (editIsActive) {
await setActiveProviderInstance(selectedInstanceId);
}
@@ -151,7 +170,8 @@ export default function ConfigPage() {
const handleDelete = async () => {
if (selectedInstanceId === "new" || selectedInstanceId === null) return;
if (!confirm("Are you sure you want to delete this provider instance?")) return;
if (!confirm("Are you sure you want to delete this provider instance?"))
return;
try {
setLoading(true);
@@ -167,7 +187,10 @@ export default function ConfigPage() {
}
};
const handleUpdateMapping = async (task: string, providerInstanceId: string) => {
const handleUpdateMapping = async (
task: string,
providerInstanceId: string,
) => {
try {
setLoading(true);
await setProviderMapping(task, providerInstanceId);
@@ -192,13 +215,15 @@ export default function ConfigPage() {
{config && !loading && (
<>
<section className="mb-8 border-b border-gray-200 pb-6">
<section className="mb-8 pb-6">
<h2 className="mb-3 text-lg">LLM Provider Instances</h2>
<div className="mt-4 grid min-h-[400px] grid-cols-1 overflow-hidden rounded-xl border border-gray-200 bg-white md:grid-cols-[30%_70%]">
{/* 30% area */}
<div className="flex flex-col border-r border-gray-200 bg-gray-50">
<div className="flex items-center justify-between border-b border-gray-200 bg-gray-100 px-4 py-4">
<h3 className="m-0 text-[0.95rem] font-semibold text-[#111]">Instances</h3>
<h3 className="m-0 text-[0.95rem] font-semibold text-[#111]">
Instances
</h3>
<button
onClick={() => setSelectedInstanceId("new")}
className="cursor-pointer rounded-md bg-emerald-500 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-emerald-600"
@@ -223,7 +248,9 @@ export default function ConfigPage() {
: "border-l-transparent"
}`}
>
<div className="text-sm font-medium text-[#111]">{inst.name}</div>
<div className="text-sm font-medium text-[#111]">
{inst.name}
</div>
<div className="mt-1 flex items-center justify-between text-xs text-gray-500">
<span>{inst.providerName}</span>
{inst.isActive && (
@@ -245,7 +272,10 @@ export default function ConfigPage() {
Press + to add or select an existing Instance to edit
</div>
) : (
<form onSubmit={handleSave} className="flex h-full flex-col justify-between">
<form
onSubmit={handleSave}
className="flex h-full flex-col justify-between"
>
<div className="flex flex-1 flex-col gap-5 p-6">
<h3 className="m-0 mb-2 text-lg font-semibold text-[#111]">
{selectedInstanceId === "new"
@@ -254,7 +284,10 @@ export default function ConfigPage() {
</h3>
<div className="flex flex-col gap-1.5">
<label htmlFor="formName" className="text-xs font-medium text-gray-700">
<label
htmlFor="formName"
className="text-xs font-medium text-gray-700"
>
Friendly Name
</label>
<input
@@ -269,7 +302,10 @@ export default function ConfigPage() {
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="formProvider" className="text-xs font-medium text-gray-700">
<label
htmlFor="formProvider"
className="text-xs font-medium text-gray-700"
>
Provider Type
</label>
<select
@@ -286,13 +322,20 @@ export default function ConfigPage() {
</select>
{editProvider && availableProviders.length > 0 && (
<span className="mt-1 block rounded border border-gray-200 bg-gray-100 px-3 py-2 text-xs text-gray-600">
{availableProviders.find((p) => p.id === editProvider)?.description}
{
availableProviders.find(
(p) => p.id === editProvider,
)?.description
}
</span>
)}
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="formKey" className="text-xs font-medium text-gray-700">
<label
htmlFor="formKey"
className="text-xs font-medium text-gray-700"
>
API Key
</label>
<input
@@ -311,7 +354,10 @@ export default function ConfigPage() {
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="formModel" className="text-xs font-medium text-gray-700">
<label
htmlFor="formModel"
className="text-xs font-medium text-gray-700"
>
Model Name
</label>
<input
@@ -332,7 +378,10 @@ export default function ConfigPage() {
onChange={(e) => setEditIsActive(e.target.checked)}
className="h-4 w-4 cursor-pointer"
/>
<label htmlFor="formActive" className="cursor-pointer text-xs font-medium text-gray-700">
<label
htmlFor="formActive"
className="cursor-pointer text-xs font-medium text-gray-700"
>
Set as Active Instance
</label>
</div>
@@ -367,37 +416,58 @@ export default function ConfigPage() {
</div>
</section>
<section className="mb-8 border-b border-gray-200 pb-6">
<section className="mb-8 pb-6">
<h2 className="mb-3 text-lg">Task Provider Routing</h2>
<p className="my-4 rounded border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-800">
Configure which LLM Provider Key Instance should handle each specific simulation
task. Mappings default to the currently <strong>Active</strong> instance if not
specified.
Configure which LLM Provider Key Instance should handle each
specific simulation task. Mappings default to the currently{" "}
<strong>Active</strong> instance if not specified.
</p>
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
{[
{ key: "actor-prose", label: "Actor Prose Generation", desc: "Generates roleplay/narrative prose for Non-Player Characters." },
{ key: "llm-validator", label: "LLM Validator", desc: "Arbitrates and validates proposed actions against the world state rules." },
{ key: "intent-decoder", label: "Intent Decoder", desc: "Splits raw prose actions into structured intents (Player and NPC)." },
{ key: "timedelta", label: "TimeDelta Generator", desc: "Calculates the duration of character actions to advance the game clock." },
{
key: "actor-prose",
label: "Actor Prose Generation",
desc: "Generates roleplay/narrative prose for Non-Player Characters.",
},
{
key: "llm-validator",
label: "LLM Validator",
desc: "Arbitrates and validates proposed actions against the world state rules.",
},
{
key: "intent-decoder",
label: "Intent Decoder",
desc: "Splits raw prose actions into structured intents (Player and NPC).",
},
{
key: "timedelta",
label: "TimeDelta Generator",
desc: "Calculates the duration of character actions to advance the game clock.",
},
].map((task) => (
<div
key={task.key}
className="flex flex-col justify-between gap-3 rounded-lg border border-gray-200 bg-gray-50 p-4"
>
<div className="flex flex-col gap-1 text-xs">
<strong className="text-sm text-[#111]">{task.label}</strong>
<strong className="text-sm text-[#111]">
{task.label}
</strong>
<span className="mt-0.5 text-gray-500">{task.desc}</span>
</div>
<select
value={mappings[task.key] || ""}
onChange={(e) => handleUpdateMapping(task.key, e.target.value)}
onChange={(e) =>
handleUpdateMapping(task.key, e.target.value)
}
className="w-full rounded border border-gray-300 bg-white px-2 py-1.5 text-xs"
>
<option value="">Use Default Provider</option>
{instances.map((inst) => (
<option key={inst.id} value={inst.id}>
{inst.name} ({inst.providerName}){inst.isActive ? " [Active]" : ""}
{inst.name} ({inst.providerName})
{inst.isActive ? " [Active]" : ""}
</option>
))}
</select>
@@ -406,35 +476,15 @@ export default function ConfigPage() {
</div>
</section>
<section className="mb-8 border-b border-gray-200 pb-6">
<h2 className="mb-3 text-lg">Environment Variables Default</h2>
<div className="flex justify-between border-b border-gray-100 py-1.5">
<span className="text-sm text-gray-500">Default Model</span>
<span className="text-sm">
<code className="font-mono text-sm">{config.model}</code>
</span>
</div>
<div className="flex justify-between border-b border-gray-100 py-1.5">
<span className="text-sm text-gray-500">Default API Key (.env)</span>
<span
className={
config.apiKeySet
? "text-sm text-green-600"
: "text-sm font-medium text-red-600"
}
>
{config.apiKeySet
? `✓ Set (${config.apiKeyPreview})`
: "✗ NOT SET"}
</span>
</div>
</section>
<section className="mb-8 border-b border-gray-200 pb-6">
<section className="mb-8 pb-6">
<h2 className="mb-3 text-lg">Available Scenarios</h2>
{config.availableScenarios.length === 0 ? (
<p className="mt-3 rounded border border-amber-200 bg-amber-100 px-3 py-2 text-xs text-amber-800">
No scenarios found in <code className="font-mono text-xs">content/demo/scenarios/</code>.
No scenarios found in{" "}
<code className="font-mono text-xs">
content/demo/scenarios/
</code>
.
</p>
) : (
<table className="w-full border-collapse text-sm">
@@ -453,7 +503,9 @@ export default function ConfigPage() {
<tr key={s.path}>
<td className="border-b border-gray-100 p-2">{s.name}</td>
<td className="border-b border-gray-100 p-2">
<code className="font-mono text-xs text-blue-600">{s.path}</code>
<code className="font-mono text-xs text-blue-600">
{s.path}
</code>
</td>
</tr>
))}
@@ -461,17 +513,6 @@ export default function ConfigPage() {
</table>
)}
</section>
<section className="mb-8 border-b border-gray-200 pb-6">
<h2 className="mb-3 text-lg">Engine Packages</h2>
<p className="mt-3 rounded border border-amber-200 bg-amber-100 px-3 py-2 text-xs text-amber-800">
All <code className="font-mono text-xs">@omnia/*</code> workspace packages are
consumed via <code className="font-mono text-xs">transpilePackages</code> in{" "}
<code className="font-mono text-xs">next.config.ts</code>. The native{" "}
<code className="font-mono text-xs">better-sqlite3</code> module is externalized
via <code className="font-mono text-xs">serverExternalPackages</code>.
</p>
</section>
</>
)}
</div>

View File

@@ -510,7 +510,7 @@ export function PlayView() {
)}
</select>
</div>
<button type="submit" disabled={loading}>
<button type="submit" disabled={loading || providerInstances.length === 0}>
{loading ? "Starting..." : "Start Simulation"}
</button>
</form>
@@ -534,7 +534,7 @@ export function PlayView() {
</span>
</div>
<div className="card-actions">
<button onClick={() => handleResume(s.id)} disabled={loading}>
<button onClick={() => handleResume(s.id)} disabled={loading || providerInstances.length === 0}>
Resume
</button>
<button