chore: Merge branch 'master' into feat/scenario-builder

This commit is contained in:
2026-07-15 20:16:52 +05:30
88 changed files with 10151 additions and 5532 deletions

View File

@@ -4,7 +4,12 @@ import path from "path";
import fs from "fs";
import { simulationManager } from "@/lib/simulation";
import type { SimSnapshot } from "@/lib/simulation";
import { ProviderManager, ModelProviderInstance, AVAILABLE_PROVIDERS, ModelProviderMeta } from "@omnia/llm";
import {
ProviderManager,
ModelProviderInstance,
AVAILABLE_PROVIDERS,
ModelProviderMeta,
} from "@omnia/llm";
import { ScenarioSchema } from "@omnia/scenario";
function resolveScenarioPath(relative: string): string {
@@ -26,8 +31,7 @@ function resolveScenarioPath(relative: string): string {
}
type ActionResult =
| { ok: true; snapshot: SimSnapshot }
| { ok: false; error: string };
{ ok: true; snapshot: SimSnapshot } | { ok: false; error: string };
export async function startSimulation(input: {
scenario?: string;
@@ -169,8 +173,7 @@ export async function getConfigStatus(): Promise<{
}
export async function listSavedSimulations(): Promise<
| { ok: true; sessions: SimSnapshot[] }
| { ok: false; error: string }
{ ok: true; sessions: SimSnapshot[] } | { ok: false; error: string }
> {
try {
const sessions = simulationManager.listSavedSessions();
@@ -198,7 +201,9 @@ export async function resumeSimulation(simId: string): Promise<ActionResult> {
}
}
export async function getScenarioEntities(scenarioPath: string): Promise<
export async function getScenarioEntities(
scenarioPath: string,
): Promise<
| { ok: true; entities: { id: string; name: string }[] }
| { ok: false; error: string }
> {
@@ -208,10 +213,12 @@ export async function getScenarioEntities(scenarioPath: string): Promise<
return { ok: false, error: `Scenario file not found: ${scenarioPath}` };
}
const content = JSON.parse(fs.readFileSync(resolved, "utf-8"));
const entities = (content.entities || []).map((e: { id: string; name?: string }) => ({
id: e.id,
name: e.name || e.id,
}));
const entities = (content.entities || []).map(
(e: { id: string; name?: string }) => ({
id: e.id,
name: e.name || e.id,
}),
);
return { ok: true, entities };
} catch (err) {
return {
@@ -221,9 +228,9 @@ export async function getScenarioEntities(scenarioPath: string): Promise<
}
}
export async function deleteSimulation(simId: string): Promise<
{ ok: true } | { ok: false; error: string }
> {
export async function deleteSimulation(
simId: string,
): Promise<{ ok: true } | { ok: false; error: string }> {
try {
simulationManager.deleteSession(simId);
return { ok: true };
@@ -235,7 +242,9 @@ export async function deleteSimulation(simId: string): Promise<
}
}
export async function listProviderInstances(): Promise<ModelProviderInstance[]> {
export async function listProviderInstances(): Promise<
ModelProviderInstance[]
> {
return ProviderManager.list();
}
@@ -247,7 +256,14 @@ export async function createProviderInstance(
type: "generative" | "embedding" = "generative",
maxContext?: number,
): Promise<ModelProviderInstance> {
return ProviderManager.create(name, providerName, apiKey, modelName, type, maxContext);
return ProviderManager.create(
name,
providerName,
apiKey,
modelName,
type,
maxContext,
);
}
export async function deleteProviderInstance(id: string): Promise<void> {
@@ -267,7 +283,15 @@ export async function updateProviderInstance(
type: "generative" | "embedding" = "generative",
maxContext?: number,
): Promise<void> {
ProviderManager.update(id, name, providerName, apiKey, modelName, type, maxContext);
ProviderManager.update(
id,
name,
providerName,
apiKey,
modelName,
type,
maxContext,
);
}
export async function getProviderMappings(): Promise<Record<string, string>> {
@@ -285,11 +309,15 @@ export async function getAvailableProviders(): Promise<ModelProviderMeta[]> {
return AVAILABLE_PROVIDERS;
}
export async function regenerateEmbeddings(newProviderInstanceId?: string): Promise<void> {
export async function regenerateEmbeddings(
newProviderInstanceId?: string,
): Promise<void> {
await simulationManager.regenerateAllEmbeddings(newProviderInstanceId);
}
export async function saveScenario(scenario: unknown): Promise<{ ok: true } | { ok: false; error: string }> {
export async function saveScenario(
scenario: unknown,
): Promise<{ ok: true } | { ok: false; error: string }> {
try {
const parsed = ScenarioSchema.parse(scenario);
const cwd = process.cwd();
@@ -301,14 +329,16 @@ export async function saveScenario(scenario: unknown): Promise<{ ok: true } | {
fs.writeFileSync(filePath, JSON.stringify(parsed, null, 2), "utf-8");
return { ok: true };
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : String(err) };
return {
ok: false,
error: err instanceof Error ? err.message : String(err),
};
}
}
export async function loadScenarioJson(scenarioPath: string): Promise<
| { ok: true; scenario: unknown }
| { ok: false; error: string }
> {
export async function loadScenarioJson(
scenarioPath: string,
): Promise<{ ok: true; scenario: unknown } | { ok: false; error: string }> {
try {
const resolved = resolveScenarioPath(scenarioPath);
if (!fs.existsSync(resolved)) {
@@ -317,6 +347,9 @@ export async function loadScenarioJson(scenarioPath: string): Promise<
const content = JSON.parse(fs.readFileSync(resolved, "utf-8"));
return { ok: true, scenario: content };
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : String(err) };
return {
ok: false,
error: err instanceof Error ? err.message : String(err),
};
}
}

View File

@@ -3,26 +3,22 @@
import { useEffect, useState, useMemo } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import {
import {
SidebarProvider,
Sidebar,
SidebarContent,
} from "@/components/ui/sidebar";
import {
getConfigStatus,
loadScenarioJson,
saveScenario
} from "@/app/actions";
import { getConfigStatus, loadScenarioJson, saveScenario } from "@/app/actions";
import type { Scenario } from "@omnia/scenario";
import {
Save,
FileJson,
Globe,
MapPin,
Users,
Sparkles,
import {
Save,
FileJson,
Globe,
MapPin,
Users,
Sparkles,
Info,
Eye
Eye,
} from "lucide-react";
// Import refactored builder components
@@ -30,14 +26,18 @@ import { MetadataTab } from "@/components/builder/MetadataTab";
import { LocationsTab } from "@/components/builder/LocationsTab";
import { EntitiesTab } from "@/components/builder/EntitiesTab";
import { JsonTab } from "@/components/builder/JsonTab";
import type {
LocationData,
EntityData,
AttributeData
import type {
LocationData,
EntityData,
AttributeData,
} from "@/components/builder/types";
const generateUUID = () => {
if (typeof window !== "undefined" && window.crypto && window.crypto.randomUUID) {
if (
typeof window !== "undefined" &&
window.crypto &&
window.crypto.randomUUID
) {
return window.crypto.randomUUID();
}
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
@@ -57,12 +57,16 @@ export default function BuilderPage() {
const [selectedTemplatePath, setSelectedTemplatePath] = useState("");
// Tabs: "metadata", "locations", "entities", "json"
const [activeTab, setActiveTab] = useState<"metadata" | "locations" | "entities" | "json">("metadata");
const [activeTab, setActiveTab] = useState<
"metadata" | "locations" | "entities" | "json"
>("metadata");
// Form State
const [scenarioId, setScenarioId] = useState("");
const [name, setName] = useState("My Custom Scenario");
const [description, setDescription] = useState("A custom scenario template created via builder.");
const [description, setDescription] = useState(
"A custom scenario template created via builder.",
);
const [startTime, setStartTime] = useState("2026-07-06T12:00:00.000Z");
const [worldAttributes, setWorldAttributes] = useState<AttributeData[]>([]);
const [locations, setLocations] = useState<LocationData[]>([]);
@@ -73,7 +77,10 @@ export default function BuilderPage() {
const [selectedEntIndex, setSelectedEntIndex] = useState(0);
// Status & Notification Banners
const [statusMessage, setStatusMessage] = useState<{ text: string; type: "success" | "error" | "info" } | null>(null);
const [statusMessage, setStatusMessage] = useState<{
text: string;
type: "success" | "error" | "info";
} | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Initialize dynamic UUIDs client-side to prevent NextJS SSR hydration mismatch
@@ -82,16 +89,25 @@ export default function BuilderPage() {
const uId = generateUUID();
const locId = generateUUID();
const entId = generateUUID();
setScenarioId(uId);
setLocations([{ id: locId, attributes: [], connections: [] }]);
setEntities([{
id: entId,
locationId: locId,
attributes: [{ name: "role", value: "adventurer", visibility: "PUBLIC", allowedEntities: [] }],
aliases: {},
initialMemories: []
}]);
setEntities([
{
id: entId,
locationId: locId,
attributes: [
{
name: "role",
value: "adventurer",
visibility: "PUBLIC",
allowedEntities: [],
},
],
aliases: {},
initialMemories: [],
},
]);
}
}, [scenarioId]);
@@ -119,8 +135,14 @@ export default function BuilderPage() {
}, [statusMessage]);
// Populate helper lists
const locationIds = useMemo(() => locations.map(l => l.id).filter(Boolean), [locations]);
const entityIds = useMemo(() => entities.map(e => e.id).filter(Boolean), [entities]);
const locationIds = useMemo(
() => locations.map((l) => l.id).filter(Boolean),
[locations],
);
const entityIds = useMemo(
() => entities.map((e) => e.id).filter(Boolean),
[entities],
);
// Load selected template
const handleLoadTemplate = async (path: string) => {
@@ -129,13 +151,19 @@ export default function BuilderPage() {
try {
const res = await loadScenarioJson(path);
if (!res.ok) {
setStatusMessage({ text: res.error || "Failed to load template.", type: "error" });
setStatusMessage({
text: res.error || "Failed to load template.",
type: "error",
});
return;
}
const s = res.scenario as Scenario;
if (!s) {
setStatusMessage({ text: "Scenario template was empty.", type: "error" });
setStatusMessage({
text: "Scenario template was empty.",
type: "error",
});
return;
}
@@ -143,9 +171,9 @@ export default function BuilderPage() {
setName(s.name || "Loaded Scenario");
setDescription(s.description || "");
setStartTime(s.startTime || "2026-07-06T12:00:00.000Z");
// World attributes
const wAttrs = (s.world?.attributes || []).map(a => ({
const wAttrs = (s.world?.attributes || []).map((a) => ({
name: a.name,
value: a.value,
visibility: a.visibility,
@@ -154,16 +182,16 @@ export default function BuilderPage() {
setWorldAttributes(wAttrs);
// Locations
const locs = (s.locations || []).map(l => ({
const locs = (s.locations || []).map((l) => ({
id: l.id,
parentId: l.parentId || undefined,
attributes: (l.attributes || []).map(a => ({
attributes: (l.attributes || []).map((a) => ({
name: a.name,
value: a.value,
visibility: a.visibility,
allowedEntities: a.allowedEntities || [],
})),
connections: (l.connections || []).map(c => ({
connections: (l.connections || []).map((c) => ({
targetId: c.targetId,
portalName: c.portalName,
portalStateDescriptor: c.portalStateDescriptor,
@@ -172,21 +200,25 @@ export default function BuilderPage() {
bidirectional: c.bidirectional ?? true,
})),
}));
setLocations(locs.length > 0 ? locs : [{ id: generateUUID(), attributes: [], connections: [] }]);
setLocations(
locs.length > 0
? locs
: [{ id: generateUUID(), attributes: [], connections: [] }],
);
setSelectedLocIndex(0);
// Entities
const ents = (s.entities || []).map(e => ({
const ents = (s.entities || []).map((e) => ({
id: e.id,
locationId: e.locationId || undefined,
attributes: (e.attributes || []).map(a => ({
attributes: (e.attributes || []).map((a) => ({
name: a.name,
value: a.value,
visibility: a.visibility,
allowedEntities: a.allowedEntities || [],
})),
aliases: e.aliases || {},
initialMemories: (e.initialMemories || []).map(m => ({
initialMemories: (e.initialMemories || []).map((m) => ({
id: m.id || generateUUID(),
timestamp: m.timestamp || s.startTime,
locationId: m.locationId || null,
@@ -199,18 +231,38 @@ export default function BuilderPage() {
targetIds: m.intent.targetIds || [],
modifiers: m.intent.modifiers || [],
},
outcome: m.outcome ? {
isValid: m.outcome.isValid,
reason: m.outcome.reason
} : undefined,
outcome: m.outcome
? {
isValid: m.outcome.isValid,
reason: m.outcome.reason,
}
: undefined,
})),
}));
setEntities(ents.length > 0 ? ents : [{ id: generateUUID(), locationId: locs[0]?.id || generateUUID(), attributes: [], aliases: {}, initialMemories: [] }]);
setEntities(
ents.length > 0
? ents
: [
{
id: generateUUID(),
locationId: locs[0]?.id || generateUUID(),
attributes: [],
aliases: {},
initialMemories: [],
},
],
);
setSelectedEntIndex(0);
setStatusMessage({ text: "Template loaded successfully!", type: "success" });
setStatusMessage({
text: "Template loaded successfully!",
type: "success",
});
} catch (err) {
setStatusMessage({ text: err instanceof Error ? err.message : String(err), type: "error" });
setStatusMessage({
text: err instanceof Error ? err.message : String(err),
type: "error",
});
}
};
@@ -221,73 +273,114 @@ export default function BuilderPage() {
name: name.trim(),
description: description.trim(),
startTime: startTime.trim(),
world: worldAttributes.length > 0 ? {
attributes: worldAttributes.map(a => ({
name: a.name.trim(),
value: a.value.trim(),
visibility: a.visibility,
...(a.visibility === "PRIVATE" && a.allowedEntities.length > 0 ? { allowedEntities: a.allowedEntities } : {})
}))
} : undefined,
locations: locations.map(l => ({
world:
worldAttributes.length > 0
? {
attributes: worldAttributes.map((a) => ({
name: a.name.trim(),
value: a.value.trim(),
visibility: a.visibility,
...(a.visibility === "PRIVATE" && a.allowedEntities.length > 0
? { allowedEntities: a.allowedEntities }
: {}),
})),
}
: undefined,
locations: locations.map((l) => ({
id: l.id.trim(),
...(l.parentId ? { parentId: l.parentId } : {}),
...(l.attributes.length > 0 ? {
attributes: l.attributes.map(a => ({
name: a.name.trim(),
value: a.value.trim(),
visibility: a.visibility,
...(a.visibility === "PRIVATE" && a.allowedEntities.length > 0 ? { allowedEntities: a.allowedEntities } : {})
}))
} : {}),
...(l.connections.length > 0 ? {
connections: l.connections.map(c => ({
targetId: c.targetId,
...(c.portalName ? { portalName: c.portalName.trim() } : {}),
...(c.portalStateDescriptor ? { portalStateDescriptor: c.portalStateDescriptor.trim() } : {}),
visionProp: Number(c.visionProp),
soundProp: Number(c.soundProp),
bidirectional: !!c.bidirectional
}))
} : {})
...(l.attributes.length > 0
? {
attributes: l.attributes.map((a) => ({
name: a.name.trim(),
value: a.value.trim(),
visibility: a.visibility,
...(a.visibility === "PRIVATE" && a.allowedEntities.length > 0
? { allowedEntities: a.allowedEntities }
: {}),
})),
}
: {}),
...(l.connections.length > 0
? {
connections: l.connections.map((c) => ({
targetId: c.targetId,
...(c.portalName ? { portalName: c.portalName.trim() } : {}),
...(c.portalStateDescriptor
? { portalStateDescriptor: c.portalStateDescriptor.trim() }
: {}),
visionProp: Number(c.visionProp),
soundProp: Number(c.soundProp),
bidirectional: !!c.bidirectional,
})),
}
: {}),
})),
entities: entities.map(e => ({
entities: entities.map((e) => ({
id: e.id.trim(),
...(e.locationId ? { locationId: e.locationId } : {}),
...(e.attributes.length > 0 ? {
attributes: e.attributes.map(a => ({
name: a.name.trim(),
value: a.value.trim(),
visibility: a.visibility,
...(a.visibility === "PRIVATE" && a.allowedEntities.length > 0 ? { allowedEntities: a.allowedEntities } : {})
}))
} : {}),
...(e.attributes.length > 0
? {
attributes: e.attributes.map((a) => ({
name: a.name.trim(),
value: a.value.trim(),
visibility: a.visibility,
...(a.visibility === "PRIVATE" && a.allowedEntities.length > 0
? { allowedEntities: a.allowedEntities }
: {}),
})),
}
: {}),
...(Object.keys(e.aliases).length > 0 ? { aliases: e.aliases } : {}),
...(e.initialMemories.length > 0 ? {
initialMemories: e.initialMemories.map(m => ({
id: m.id,
timestamp: m.timestamp,
locationId: m.locationId,
intent: {
type: m.intent.type,
originalText: m.intent.originalText.trim(),
description: m.intent.description.trim(),
...(m.intent.selfDescription ? { selfDescription: m.intent.selfDescription.trim() } : {}),
actorId: m.intent.actorId,
targetIds: m.intent.targetIds,
...(m.intent.modifiers && m.intent.modifiers.length > 0 ? { modifiers: m.intent.modifiers } : [])
},
...(m.outcome ? { outcome: { isValid: !!m.outcome.isValid, reason: m.outcome.reason.trim() } } : {})
}))
} : {})
}))
...(e.initialMemories.length > 0
? {
initialMemories: e.initialMemories.map((m) => ({
id: m.id,
timestamp: m.timestamp,
locationId: m.locationId,
intent: {
type: m.intent.type,
originalText: m.intent.originalText.trim(),
description: m.intent.description.trim(),
...(m.intent.selfDescription
? { selfDescription: m.intent.selfDescription.trim() }
: {}),
actorId: m.intent.actorId,
targetIds: m.intent.targetIds,
...(m.intent.modifiers && m.intent.modifiers.length > 0
? { modifiers: m.intent.modifiers }
: []),
},
...(m.outcome
? {
outcome: {
isValid: !!m.outcome.isValid,
reason: m.outcome.reason.trim(),
},
}
: {}),
})),
}
: {}),
})),
};
}, [scenarioId, name, description, startTime, worldAttributes, locations, entities]);
}, [
scenarioId,
name,
description,
startTime,
worldAttributes,
locations,
entities,
]);
// Save scenario to server
const handleSaveToServer = async () => {
if (!scenarioId.trim()) {
setStatusMessage({ text: "Scenario Template ID is required to save.", type: "error" });
setStatusMessage({
text: "Scenario Template ID is required to save.",
type: "error",
});
return;
}
setIsSubmitting(true);
@@ -295,15 +388,24 @@ export default function BuilderPage() {
try {
const res = await saveScenario(compiledScenario);
if (res.ok) {
setStatusMessage({ text: `Scenario template saved as ${scenarioId}.json successfully!`, type: "success" });
setStatusMessage({
text: `Scenario template saved as ${scenarioId}.json successfully!`,
type: "success",
});
// Refresh template list
const config = await getConfigStatus();
setAvailableScenarios(config.availableScenarios);
} else {
setStatusMessage({ text: res.error || "Failed to save scenario.", type: "error" });
setStatusMessage({
text: res.error || "Failed to save scenario.",
type: "error",
});
}
} catch (err) {
setStatusMessage({ text: err instanceof Error ? err.message : String(err), type: "error" });
setStatusMessage({
text: err instanceof Error ? err.message : String(err),
type: "error",
});
} finally {
setIsSubmitting(false);
}
@@ -331,14 +433,17 @@ export default function BuilderPage() {
return (
<SidebarProvider>
<div className="flex flex-1 min-h-0 w-full overflow-hidden bg-background">
{/* Save Status Banner */}
{statusMessage && (
<div className={`fixed top-4 right-4 z-50 max-w-sm border p-4 shadow-lg animate-fade-in ${
statusMessage.type === "success" ? "bg-emerald-950/80 border-emerald-500 text-emerald-300" :
statusMessage.type === "error" ? "bg-destructive/10 border-destructive text-destructive" :
"bg-secondary/90 border-border text-foreground"
}`}>
<div
className={`fixed top-4 right-4 z-50 max-w-sm border p-4 shadow-lg animate-fade-in ${
statusMessage.type === "success"
? "bg-emerald-950/80 border-emerald-500 text-emerald-300"
: statusMessage.type === "error"
? "bg-destructive/10 border-destructive text-destructive"
: "bg-secondary/90 border-border text-foreground"
}`}
>
<div className="flex items-start gap-3">
<Info className="size-4 shrink-0 mt-0.5" />
<div className="text-xs">{statusMessage.text}</div>
@@ -347,65 +452,74 @@ export default function BuilderPage() {
)}
{/* Viewport-level Vertical Sidebar on the Left Side */}
<Sidebar collapsible="none" className="h-full border-r border-border/30 bg-card shrink-0">
<Sidebar
collapsible="none"
className="h-full border-r border-border/30 bg-card shrink-0"
>
<SidebarContent className="flex flex-col justify-between h-full bg-card p-6">
<div className="flex flex-col gap-2 font-head">
<span className="text-[10px] font-mono uppercase tracking-wider text-muted-foreground block mb-2">Configuration</span>
<span className="text-[10px] font-mono uppercase tracking-wider text-muted-foreground block mb-2">
Configuration
</span>
<button
onClick={() => setActiveTab("metadata")}
className={`w-full text-left px-4 py-2.5 text-xs font-mono font-bold tracking-wide border transition-all flex items-center gap-2 cursor-pointer ${
activeTab === "metadata"
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
activeTab === "metadata"
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
: "border-border/30 hover:bg-secondary text-muted-foreground hover:text-foreground"
}`}
>
<Globe className="size-3.5" />
<Globe className="size-3.5" />
<span>World Metadata</span>
</button>
<button
onClick={() => setActiveTab("locations")}
className={`w-full text-left px-4 py-2.5 text-xs font-mono font-bold tracking-wide border transition-all flex items-center gap-2 cursor-pointer ${
activeTab === "locations"
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
activeTab === "locations"
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
: "border-border/30 hover:bg-secondary text-muted-foreground hover:text-foreground"
}`}
>
<MapPin className="size-3.5" />
<MapPin className="size-3.5" />
<span>Locations</span>
<span className="ml-auto text-[10px] font-mono border border-muted-foreground/20 bg-muted/10 px-1 rounded">{locations.length}</span>
<span className="ml-auto text-[10px] font-mono border border-muted-foreground/20 bg-muted/10 px-1 rounded">
{locations.length}
</span>
</button>
<button
onClick={() => setActiveTab("entities")}
className={`w-full text-left px-4 py-2.5 text-xs font-mono font-bold tracking-wide border transition-all flex items-center gap-2 cursor-pointer ${
activeTab === "entities"
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
activeTab === "entities"
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
: "border-border/30 hover:bg-secondary text-muted-foreground hover:text-foreground"
}`}
>
<Users className="size-3.5" />
<Users className="size-3.5" />
<span>Entities</span>
<span className="ml-auto text-[10px] font-mono border border-muted-foreground/20 bg-muted/10 px-1 rounded">{entities.length}</span>
<span className="ml-auto text-[10px] font-mono border border-muted-foreground/20 bg-muted/10 px-1 rounded">
{entities.length}
</span>
</button>
<button
onClick={() => setActiveTab("json")}
className={`w-full text-left px-4 py-2.5 text-xs font-mono font-bold tracking-wide border transition-all flex items-center gap-2 cursor-pointer ${
activeTab === "json"
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
activeTab === "json"
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
: "border-border/30 hover:bg-secondary text-muted-foreground hover:text-foreground"
}`}
>
<Eye className="size-3.5" />
<Eye className="size-3.5" />
<span>Live JSON Preview</span>
</button>
</div>
{/* Sidebar Footer link */}
<div className="border-t border-border/10 pt-3 flex items-center justify-between text-[10px] text-muted-foreground">
<button
<button
onClick={() => router.push("/")}
className="w-full py-1 text-center hover:text-foreground text-primary font-bold uppercase transition-colors cursor-pointer"
>
@@ -418,7 +532,6 @@ export default function BuilderPage() {
{/* Main Centered Content Pane on the Right */}
<main className="flex-1 overflow-y-auto px-10 py-8 min-h-0 flex flex-col">
<div className="mx-auto max-w-[1200px] w-full flex-1 flex flex-col min-h-0 gap-6">
{/* Header block with Page Name and Load/Save Actions */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-border/20 pb-4 shrink-0">
<div>
@@ -426,14 +539,17 @@ export default function BuilderPage() {
<Sparkles className="size-6 text-primary" /> Scenario Builder
</h1>
<p className="text-xs text-muted-foreground mt-0.5">
Define entities, spatial layouts, and initial metadata for custom Omnia scenarios.
Define entities, spatial layouts, and initial metadata for
custom Omnia scenarios.
</p>
</div>
<div className="flex items-center gap-3 self-end md:self-auto">
{/* Load Template dropdown */}
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-muted-foreground whitespace-nowrap">Load:</span>
<span className="text-xs font-mono text-muted-foreground whitespace-nowrap">
Load:
</span>
<select
className="bg-card border border-border/30 px-2 py-1 text-xs outline-none h-8 w-44 rounded"
value={selectedTemplatePath}
@@ -443,24 +559,26 @@ export default function BuilderPage() {
}}
>
<option value="">-- Choose existing --</option>
{availableScenarios.map(sc => (
<option key={sc.path} value={sc.path}>{sc.name}</option>
{availableScenarios.map((sc) => (
<option key={sc.path} value={sc.path}>
{sc.name}
</option>
))}
</select>
</div>
<Button
variant="outline"
size="sm"
<Button
variant="outline"
size="sm"
onClick={handleDownloadJson}
className="h-8 text-xs flex gap-1.5 items-center cursor-pointer"
>
<FileJson className="size-3.5" /> Export JSON
</Button>
<Button
variant="default"
size="sm"
<Button
variant="default"
size="sm"
onClick={handleSaveToServer}
disabled={isSubmitting}
className="h-8 text-xs flex gap-1.5 items-center cursor-pointer"
@@ -472,7 +590,6 @@ export default function BuilderPage() {
{/* Active configuration tab form */}
<div className="flex-1 min-h-0">
{/* TAB 1: World Metadata & Attributes */}
{activeTab === "metadata" && (
<MetadataTab
@@ -521,14 +638,17 @@ export default function BuilderPage() {
{activeTab === "json" && (
<JsonTab
compiledScenario={compiledScenario}
onCopySuccess={() => setStatusMessage({ text: "JSON copied to clipboard!", type: "success" })}
onCopySuccess={() =>
setStatusMessage({
text: "JSON copied to clipboard!",
type: "success",
})
}
/>
)}
</div>
</div>
</main>
</div>
</SidebarProvider>
);

View File

@@ -5,11 +5,13 @@ import { Suspense } from "react";
export default function PlayPage() {
return (
<Suspense fallback={
<div className="flex items-center justify-center min-h-[400px]">
<div className="animate-spin text-primary">Loading...</div>
</div>
}>
<Suspense
fallback={
<div className="flex items-center justify-center min-h-[400px]">
<div className="animate-spin text-primary">Loading...</div>
</div>
}
>
<PlayView />
</Suspense>
);

View File

@@ -224,7 +224,8 @@ export function ConfigView() {
</SelectItem>
{instances
.filter(
(inst) => (inst.type || "generative") === task.type,
(inst) =>
(inst.type || "generative") === task.type,
)
.map((inst) => (
<SelectItem key={inst.id} value={inst.id}>

View File

@@ -282,7 +282,9 @@ export function ProviderInstancesConfig({
<ItemDescription>{inst.providerName}</ItemDescription>
<div className="flex flex-row gap-1.5">
{inst.isActive && <Badge>Active</Badge>}
<Badge variant="outline">{inst.type === "generative" ? "gen" : "embed"}</Badge>
<Badge variant="outline">
{inst.type === "generative" ? "gen" : "embed"}
</Badge>
</div>
</ItemContent>
</Item>

View File

@@ -44,7 +44,9 @@ export function DashboardView() {
const [scenarios, setScenarios] = useState<
{ path: string; name: string; description: string }[]
>([]);
const [, setProviderInstances] = useState<ModelProviderInstance[]>([]);
const [providerInstances, setProviderInstances] = useState<
ModelProviderInstance[]
>([]);
// Modal State
const [scenarioForModal, setScenarioForModal] = useState<{
@@ -203,6 +205,24 @@ export function DashboardView() {
</div>
)}
{providerInstances.length === 0 && !loadingData && (
<div className="mb-6 border border-yellow-500/30 bg-yellow-500/10 px-4 py-3 text-sm text-yellow-600 flex flex-col gap-2">
<span className="font-semibold flex items-center gap-1">
No LLM providers have been configured.
</span>
<span>
Please go to the{" "}
<Link
href="/config"
className="underline font-medium hover:text-yellow-700"
>
Configuration
</Link>{" "}
page to set up at least one provider before running simulations.
</span>
</div>
)}
{/* Simulations Section */}
<section className="mb-10">
<h2 className="text-headline-lg text-primary mb-6 animate-fade-in">
@@ -235,8 +255,16 @@ export function DashboardView() {
{savedSessions.map((s) => (
<div
key={s.id}
onClick={() => handleResume(s.id)}
className="flex-shrink-0 w-72 border border-border/30 bg-card p-5 cursor-pointer shadow-sm hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:shadow-sm transition-all relative group"
onClick={
providerInstances.length === 0
? undefined
: () => handleResume(s.id)
}
className={`flex-shrink-0 w-72 border border-border/30 bg-card p-5 shadow-sm transition-all relative group ${
providerInstances.length === 0
? "opacity-50 cursor-not-allowed filter grayscale"
: "cursor-pointer hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:shadow-sm"
}`}
>
<div className="flex flex-col gap-1 text-sm mb-4">
<strong className="text-body-md text-foreground block">
@@ -337,6 +365,7 @@ export function DashboardView() {
name={s.name}
description={s.description}
onClick={() => handleScenarioClick(s)}
disabled={providerInstances.length === 0}
/>
))}
</div>

View File

@@ -422,242 +422,247 @@ export function PlayView() {
</p>
</div>
</div>
{/* Simulation Global Controls */}
<div className="flex gap-2 shrink-0">
{snapshot.status !== "done" && snapshot.status !== "error" && (
<>
{snapshot.status === "running" &&
(loading ? (
<Button
variant="secondary"
size="sm"
onClick={() => {
pauseRequestedRef.current = true;
}}
>
Pause
</Button>
) : (
<Button
variant="secondary"
size="sm"
onClick={() => runSteps(snapshot.id)}
>
Resume
</Button>
))}
<Button
variant="destructive"
size="sm"
onClick={() => {
pauseRequestedRef.current = true;
router.push("/");
}}
>
Stop
</Button>
</>
)}
</div>
</div>
<div className="flex items-center justify-between text-xs font-mono mt-1 pt-1.5 border-t border-border/10">
<span className="text-muted-foreground">
Status:{" "}
<span className="text-primary font-bold">
{getUnifiedStatus()}
</span>
</span>
<span className="text-muted-foreground">
Turn:{" "}
<span className="text-foreground font-bold">{snapshot.turn}</span>
</span>
</div>
{statusMessage() && (
<p className="text-xs font-medium text-primary mt-1 font-mono">
{loading && "⏳ "}
{statusMessage()}
</p>
)}
</header>
{/* Scrollable Center Viewport */}
<main className="flex-1 overflow-y-auto px-8 py-6">
{activeTab === "interact" ? (
<div className="flex flex-col gap-4 max-w-[800px] mx-auto pb-12">
{(() => {
const playerEntity = snapshot.entities.find((e) => e.isPlayer);
return snapshot.log.map((entry, i) => (
<LogEntryCard
key={i}
entry={entry}
onShowPrompt={setSelectedEntryForModal}
isPlayerCard={entry.entityId === playerEntity?.id}
/>
));
})()}
{loading && (
<div className="flex items-center gap-2 text-sm italic text-muted-foreground p-2 font-mono">
<Spinner />
{statusText || "Processing..."}
</div>
)}
<div ref={logEndRef} />
</div>
) : (
<div className="max-w-[800px] mx-auto space-y-6 pb-12">
{/* Simulation Info */}
<div className="border border-border/30 bg-card p-6 shadow-[2px_2px_0_0_var(--border)]">
<h3 className="text-headline-sm text-primary mb-4 border-b border-dotted border-border/20 pb-2">
Simulation Info
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm font-mono">
<div className="flex flex-col gap-1 border-b border-border/10 pb-2">
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Session ID
</span>
<span className="text-foreground font-bold break-all">
{snapshot.id}
</span>
</div>
<div className="flex flex-col gap-1 border-b border-border/10 pb-2">
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Max Turns
</span>
<span className="text-foreground font-bold">
{snapshot.maxTurns}
</span>
</div>
<div className="flex flex-col gap-1 border-b border-border/10 pb-2">
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Turn Count
</span>
<span className="text-foreground font-bold">
{snapshot.turn}
</span>
</div>
<div className="flex flex-col gap-1 border-b border-border/10 pb-2">
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Entities Registered
</span>
<span className="text-foreground font-bold">
{snapshot.entities.length}
</span>
</div>
</div>
</div>
{/* Entities Involved */}
<div className="border border-border/30 bg-card p-6 shadow-[2px_2px_0_0_var(--border)]">
<h3 className="text-headline-sm text-primary mb-4 border-b border-dotted border-border/20 pb-2">
Entities Involved
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{snapshot.entities.map((ent) => (
<div
key={ent.id}
className="border border-border/20 bg-secondary/20 p-4 shadow-[1px_1px_0_0_var(--border)] flex justify-between items-center"
>
<div>
<strong className="text-sm text-foreground block font-head tracking-wide">
{ent.name}
</strong>
<span className="text-xs text-muted-foreground font-mono block mt-1">
ID: {ent.id}
</span>
</div>
<div className="flex items-center gap-2">
{ent.isPlayer ? (
<span className="bg-primary/20 text-primary border border-primary/30 px-2 py-0.5 text-xs font-mono">
PLAYER
</span>
) : (
<span className="bg-secondary/60 text-muted-foreground border border-border/20 px-2 py-0.5 text-xs font-mono">
NPC
</span>
)}
</div>
</div>
))}
</div>
</div>
</div>
)}
</main>
{/* Sticky Chat / Interaction Input Footer */}
{activeTab === "interact" && (
<footer className="sticky bottom-0 bg-background/95 backdrop-blur-xs border-t border-dotted border-border/20 px-8 py-4 z-10 shrink-0">
<div className="max-w-[800px] mx-auto">
{snapshot.status === "waiting_player" &&
snapshot.waitingEntity ? (
<div className="border border-border/30 bg-card p-4 shadow-[2px_2px_0_0_var(--border)]">
<details className="mb-3">
<summary className="cursor-pointer text-sm font-medium font-head text-primary select-none outline-none">
<strong>
Your context as {snapshot.waitingEntity.name}
</strong>
</summary>
<pre className="text-xs whitespace-pre-wrap bg-input border border-border/20 p-2 max-h-[150px] overflow-y-auto mt-2 font-mono">
{snapshot.waitingEntity.userContext}
</pre>
</details>
<form
onSubmit={handleSubmitAction}
className="flex flex-col gap-2"
>
<Textarea
value={playerInput}
onChange={(e) => setPlayerInput(e.target.value)}
placeholder="Describe what your character does, says, or thinks..."
rows={3}
disabled={loading}
/>
{/* Simulation Global Controls */}
<div className="flex gap-2 shrink-0">
{snapshot.status !== "done" && snapshot.status !== "error" && (
<>
{snapshot.status === "running" &&
(loading ? (
<Button
variant="secondary"
size="sm"
onClick={() => {
pauseRequestedRef.current = true;
}}
>
Pause
</Button>
) : (
<Button
variant="secondary"
size="sm"
onClick={() => runSteps(snapshot.id)}
>
Resume
</Button>
))}
<Button
type="submit"
disabled={loading || !playerInput.trim()}
variant="destructive"
size="sm"
onClick={() => {
pauseRequestedRef.current = true;
router.push("/");
}}
>
{loading ? "Processing..." : "Submit Action"}
Stop
</Button>
</form>
</div>
) : snapshot.status === "done" || snapshot.status === "error" ? (
<div className="flex justify-between items-center bg-card border border-border/30 p-4 shadow-[2px_2px_0_0_var(--border)]">
<span className="text-sm font-mono text-muted-foreground">
{snapshot.status === "error"
? "Simulation finished with an error."
: "Simulation complete."}
</span>
<Button
onClick={() => {
router.push("/");
}}
size="sm"
>
{snapshot.status === "error"
? "Back to Dashboard"
: "New Simulation"}
</Button>
</div>
) : null}
</>
)}
</div>
</div>
</footer>
<div className="flex items-center justify-between text-xs font-mono mt-1 pt-1.5 border-t border-border/10">
<span className="text-muted-foreground">
Status:{" "}
<span className="text-primary font-bold">
{getUnifiedStatus()}
</span>
</span>
<span className="text-muted-foreground">
Turn:{" "}
<span className="text-foreground font-bold">
{snapshot.turn}
</span>
</span>
</div>
{statusMessage() && (
<p className="text-xs font-medium text-primary mt-1 font-mono">
{loading && "⏳ "}
{statusMessage()}
</p>
)}
</header>
{/* Scrollable Center Viewport */}
<main className="flex-1 overflow-y-auto px-8 py-6">
{activeTab === "interact" ? (
<div className="flex flex-col gap-4 max-w-[800px] mx-auto pb-12">
{(() => {
const playerEntity = snapshot.entities.find(
(e) => e.isPlayer,
);
return snapshot.log.map((entry, i) => (
<LogEntryCard
key={i}
entry={entry}
onShowPrompt={setSelectedEntryForModal}
isPlayerCard={entry.entityId === playerEntity?.id}
/>
));
})()}
{loading && (
<div className="flex items-center gap-2 text-sm italic text-muted-foreground p-2 font-mono">
<Spinner />
{statusText || "Processing..."}
</div>
)}
<div ref={logEndRef} />
</div>
) : (
<div className="max-w-[800px] mx-auto space-y-6 pb-12">
{/* Simulation Info */}
<div className="border border-border/30 bg-card p-6 shadow-[2px_2px_0_0_var(--border)]">
<h3 className="text-headline-sm text-primary mb-4 border-b border-dotted border-border/20 pb-2">
Simulation Info
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm font-mono">
<div className="flex flex-col gap-1 border-b border-border/10 pb-2">
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Session ID
</span>
<span className="text-foreground font-bold break-all">
{snapshot.id}
</span>
</div>
<div className="flex flex-col gap-1 border-b border-border/10 pb-2">
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Max Turns
</span>
<span className="text-foreground font-bold">
{snapshot.maxTurns}
</span>
</div>
<div className="flex flex-col gap-1 border-b border-border/10 pb-2">
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Turn Count
</span>
<span className="text-foreground font-bold">
{snapshot.turn}
</span>
</div>
<div className="flex flex-col gap-1 border-b border-border/10 pb-2">
<span className="text-muted-foreground text-xs uppercase tracking-wider">
Entities Registered
</span>
<span className="text-foreground font-bold">
{snapshot.entities.length}
</span>
</div>
</div>
</div>
{/* Entities Involved */}
<div className="border border-border/30 bg-card p-6 shadow-[2px_2px_0_0_var(--border)]">
<h3 className="text-headline-sm text-primary mb-4 border-b border-dotted border-border/20 pb-2">
Entities Involved
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{snapshot.entities.map((ent) => (
<div
key={ent.id}
className="border border-border/20 bg-secondary/20 p-4 shadow-[1px_1px_0_0_var(--border)] flex justify-between items-center"
>
<div>
<strong className="text-sm text-foreground block font-head tracking-wide">
{ent.name}
</strong>
<span className="text-xs text-muted-foreground font-mono block mt-1">
ID: {ent.id}
</span>
</div>
<div className="flex items-center gap-2">
{ent.isPlayer ? (
<span className="bg-primary/20 text-primary border border-primary/30 px-2 py-0.5 text-xs font-mono">
PLAYER
</span>
) : (
<span className="bg-secondary/60 text-muted-foreground border border-border/20 px-2 py-0.5 text-xs font-mono">
NPC
</span>
)}
</div>
</div>
))}
</div>
</div>
</div>
)}
</main>
{/* Sticky Chat / Interaction Input Footer */}
{activeTab === "interact" && (
<footer className="sticky bottom-0 bg-background/95 backdrop-blur-xs border-t border-dotted border-border/20 px-8 py-4 z-10 shrink-0">
<div className="max-w-[800px] mx-auto">
{snapshot.status === "waiting_player" &&
snapshot.waitingEntity ? (
<div className="border border-border/30 bg-card p-4 shadow-[2px_2px_0_0_var(--border)]">
<details className="mb-3">
<summary className="cursor-pointer text-sm font-medium font-head text-primary select-none outline-none">
<strong>
Your context as {snapshot.waitingEntity.name}
</strong>
</summary>
<pre className="text-xs whitespace-pre-wrap bg-input border border-border/20 p-2 max-h-[150px] overflow-y-auto mt-2 font-mono">
{snapshot.waitingEntity.userContext}
</pre>
</details>
<form
onSubmit={handleSubmitAction}
className="flex flex-col gap-2"
>
<Textarea
value={playerInput}
onChange={(e) => setPlayerInput(e.target.value)}
placeholder="Describe what your character does, says, or thinks..."
rows={3}
disabled={loading}
/>
<Button
type="submit"
disabled={loading || !playerInput.trim()}
>
{loading ? "Processing..." : "Submit Action"}
</Button>
</form>
</div>
) : snapshot.status === "done" ||
snapshot.status === "error" ? (
<div className="flex justify-between items-center bg-card border border-border/30 p-4 shadow-[2px_2px_0_0_var(--border)]">
<span className="text-sm font-mono text-muted-foreground">
{snapshot.status === "error"
? "Simulation finished with an error."
: "Simulation complete."}
</span>
<Button
onClick={() => {
router.push("/");
}}
size="sm"
>
{snapshot.status === "error"
? "Back to Dashboard"
: "New Simulation"}
</Button>
</div>
) : null}
</div>
</footer>
)}
</div>
{error && !loading && (
<div className="fixed bottom-4 right-4 z-50 border border-destructive bg-destructive/90 text-destructive-foreground px-4 py-3 shadow-[3px_3px_0_0_var(--border)] text-sm">
{error}
</div>
)}
{selectedEntryForModal && (
<PromptModal
entry={selectedEntryForModal}
onClose={() => setSelectedEntryForModal(null)}
/>
)}
</div>
{error && !loading && (
<div className="fixed bottom-4 right-4 z-50 border border-destructive bg-destructive/90 text-destructive-foreground px-4 py-3 shadow-[3px_3px_0_0_var(--border)] text-sm">
{error}
</div>
)}
{selectedEntryForModal && (
<PromptModal
entry={selectedEntryForModal}
onClose={() => setSelectedEntryForModal(null)}
/>
)}
</div>
</SidebarProvider>
);
}

View File

@@ -53,8 +53,16 @@ export function PromptModal({ entry, onClose }: PromptModalProps) {
const sections: { label: string; type: string; content: string }[] = [
{ label: "System Prompt", type: "system", content: systemPrompt },
{ label: "World Info", type: "world", content: worldStr },
{ label: "Recent Events", type: "events", content: recentStr || "(No recent events.)" },
{ label: "Long-Term Memories", type: "memories", content: ledgerStr || "(No long-term memories.)" },
{
label: "Recent Events",
type: "events",
content: recentStr || "(No recent events.)",
},
{
label: "Long-Term Memories",
type: "memories",
content: ledgerStr || "(No long-term memories.)",
},
];
const totalLen = sections.reduce((sum, s) => sum + s.content.length, 0);

View File

@@ -9,17 +9,39 @@ interface ScenarioCardProps {
name: string;
description: string;
onClick: () => void;
disabled?: boolean;
}
export function ScenarioCard({ name, description, onClick }: ScenarioCardProps) {
export function ScenarioCard({
name,
description,
onClick,
disabled,
}: ScenarioCardProps) {
return (
<div
onClick={onClick}
className="flex-shrink-0 w-64 border border-border/30 bg-card p-5 cursor-pointer shadow-sm hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:shadow-sm transition-all"
onClick={disabled ? undefined : onClick}
className={`flex-shrink-0 w-64 border border-border/30 bg-card p-5 shadow-sm transition-all ${
disabled
? "opacity-50 cursor-not-allowed filter grayscale"
: "cursor-pointer hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:shadow-sm"
}`}
>
<strong className="text-body-md text-foreground block mb-2">{name}</strong>
<p className="text-xs text-muted-foreground leading-relaxed mb-1">{truncate(description, 80)}</p>
<span className="mt-4 flex items-center justify-center size-7 border border-primary bg-primary/10 text-primary font-mono text-sm font-bold">{'>'}</span>
<strong className="text-body-md text-foreground block mb-2">
{name}
</strong>
<p className="text-xs text-muted-foreground leading-relaxed mb-1">
{truncate(description, 80)}
</p>
<span
className={`mt-4 flex items-center justify-center size-7 border font-mono text-sm font-bold ${
disabled
? "border-muted text-muted bg-muted/10"
: "border-primary bg-primary/10 text-primary"
}`}
>
{">"}
</span>
</div>
);
}

View File

@@ -1,14 +1,14 @@
"use client"
"use client";
import * as React from "react"
import { ChevronDownIcon } from "lucide-react"
import { Accordion as AccordionPrimitive } from "radix-ui"
import * as React from "react";
import { ChevronDownIcon } from "lucide-react";
import { Accordion as AccordionPrimitive } from "radix-ui";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
// Smooth, premium easing for the open/close — fast out of the gate, gentle
// settle. Shared by the panel height and the chevron so they move in lockstep.
const EASE = "ease-[cubic-bezier(0.32,0.72,0,1)]"
const EASE = "ease-[cubic-bezier(0.32,0.72,0,1)]";
function Accordion({
className,
@@ -20,7 +20,7 @@ function Accordion({
className={cn("flex w-full flex-col gap-3", className)}
{...props}
/>
)
);
}
function AccordionItem({
@@ -32,11 +32,11 @@ function AccordionItem({
data-slot="accordion-item"
className={cn(
"overflow-hidden rounded border-2 bg-background text-foreground shadow-md transition-shadow duration-200 hover:shadow-sm data-[state=open]:shadow-sm",
className
className,
)}
{...props}
/>
)
);
}
function AccordionTrigger({
@@ -50,7 +50,7 @@ function AccordionTrigger({
data-slot="accordion-trigger"
className={cn(
"flex flex-1 cursor-pointer items-center justify-between gap-4 px-4 py-3 text-left font-head transition-colors hover:bg-muted/50 data-[state=open]:bg-muted/40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary [&[data-state=open]>svg]:rotate-180",
className
className,
)}
{...props}
>
@@ -60,12 +60,12 @@ function AccordionTrigger({
data-slot="accordion-trigger-icon"
className={cn(
"h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-300",
EASE
EASE,
)}
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
);
}
function AccordionContent({
@@ -89,13 +89,13 @@ function AccordionContent({
// Fade + nudge the content as the panel opens/closes, synced to the slide.
"group-data-[state=closed]/panel:-translate-y-1 group-data-[state=closed]/panel:opacity-0",
"[&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
className
className,
)}
>
{children}
</div>
</AccordionPrimitive.Content>
)
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@@ -1,8 +1,8 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Slot } from "radix-ui";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded border px-2 py-0.5 text-xs font-head font-medium whitespace-nowrap transition-all focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive [&>svg]:pointer-events-none [&>svg]:size-3!",
@@ -24,8 +24,8 @@ const badgeVariants = cva(
defaultVariants: {
variant: "default",
},
}
)
},
);
function Badge({
className,
@@ -34,7 +34,7 @@ function Badge({
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
const Comp = asChild ? Slot.Root : "span";
return (
<Comp
@@ -43,7 +43,7 @@ function Badge({
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
);
}
export { Badge, badgeVariants }
export { Badge, badgeVariants };

View File

@@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium border border-border/30 outline-none transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 shadow-sm active:translate-x-[1px] active:translate-y-[1px] active:shadow-xs focus-visible:ring-2 focus-visible:ring-ring",
@@ -12,11 +12,11 @@ const buttonVariants = cva(
default: "bg-primary text-primary-foreground hover:bg-primary-hover",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"bg-background hover:bg-secondary",
outline: "bg-background hover:bg-secondary",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary-foreground/10",
ghost: "border-transparent shadow-none active:translate-x-0 active:translate-y-0 active:shadow-none hover:bg-secondary",
ghost:
"border-transparent shadow-none active:translate-x-0 active:translate-y-0 active:shadow-none hover:bg-secondary",
link: "border-transparent shadow-none active:translate-x-0 active:translate-y-0 active:shadow-none text-primary hover:underline",
},
size: {
@@ -30,27 +30,28 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
extends
React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Card({
className,
@@ -13,11 +13,11 @@ function Card({
data-size={size}
className={cn(
"group/card flex flex-col gap-(--card-spacing) overflow-hidden border border-border/30 bg-card py-(--card-spacing) text-sm text-card-foreground shadow-[2px_2px_0_0_var(--border)] [--card-spacing:--spacing(4)] data-[size=sm]:[--card-spacing:--spacing(3)]",
className
className,
)}
{...props}
/>
)
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -26,24 +26,21 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] border-b border-dotted border-border/20 pb-4 mb-2",
className
className,
)}
{...props}
/>
)
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"font-head text-headline-sm text-foreground",
className
)}
className={cn("font-head text-headline-sm text-foreground", className)}
{...props}
/>
)
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
@@ -53,7 +50,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
@@ -62,11 +59,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
className,
)}
{...props}
/>
)
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -76,7 +73,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
className={cn("px-(--card-spacing)", className)}
{...props}
/>
)
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -85,11 +82,11 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-footer"
className={cn(
"flex items-center border-t border-dotted border-border/20 bg-muted/30 px-(--card-spacing) pt-(--card-spacing)",
className
className,
)}
{...props}
/>
)
);
}
export {
@@ -100,4 +97,4 @@ export {
CardAction,
CardDescription,
CardContent,
}
};

View File

@@ -1,10 +1,10 @@
"use client"
"use client";
import * as React from "react"
import { CheckIcon } from "lucide-react"
import { Checkbox as CheckboxPrimitive } from "radix-ui"
import * as React from "react";
import { CheckIcon } from "lucide-react";
import { Checkbox as CheckboxPrimitive } from "radix-ui";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Checkbox({
className,
@@ -15,7 +15,7 @@ function Checkbox({
data-slot="checkbox"
className={cn(
"peer relative flex size-5 shrink-0 items-center justify-center rounded border-2 bg-input shadow-sm transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive data-checked:border-border data-checked:bg-primary data-checked:text-primary-foreground",
className
className,
)}
{...props}
>
@@ -26,7 +26,7 @@ function Checkbox({
<CheckIcon />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
);
}
export { Checkbox }
export { Checkbox };

View File

@@ -1,34 +1,34 @@
"use client"
"use client";
import * as React from "react"
import { XIcon } from "lucide-react"
import { Dialog as DialogPrimitive } from "radix-ui"
import * as React from "react";
import { XIcon } from "lucide-react";
import { Dialog as DialogPrimitive } from "radix-ui";
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
@@ -40,11 +40,11 @@ function DialogOverlay({
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-foreground/20 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
className,
)}
{...props}
/>
)
);
}
function DialogContent({
@@ -53,7 +53,7 @@ function DialogContent({
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
showCloseButton?: boolean;
}) {
return (
<DialogPortal>
@@ -62,7 +62,7 @@ function DialogContent({
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded border-2 bg-popover p-4 text-sm text-popover-foreground shadow-md duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
className,
)}
{...props}
>
@@ -81,7 +81,7 @@ function DialogContent({
)}
</DialogPrimitive.Content>
</DialogPortal>
)
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -91,7 +91,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
);
}
function DialogFooter({
@@ -100,14 +100,14 @@ function DialogFooter({
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
showCloseButton?: boolean;
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t-2 bg-muted/50 p-4 sm:flex-row sm:justify-end",
className
className,
)}
{...props}
>
@@ -118,7 +118,7 @@ function DialogFooter({
</DialogPrimitive.Close>
)}
</div>
)
);
}
function DialogTitle({
@@ -128,13 +128,10 @@ function DialogTitle({
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"font-head text-base leading-none font-medium",
className
)}
className={cn("font-head text-base leading-none font-medium", className)}
{...props}
/>
)
);
}
function DialogDescription({
@@ -146,11 +143,11 @@ function DialogDescription({
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
className,
)}
{...props}
/>
)
);
}
export {
@@ -164,4 +161,4 @@ export {
DialogPortal,
DialogTitle,
DialogTrigger,
}
};

View File

@@ -1,6 +1,6 @@
import { cva, type VariantProps } from "class-variance-authority"
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
@@ -8,11 +8,11 @@ function Empty({ className, ...props }: React.ComponentProps<"div">) {
data-slot="empty"
className={cn(
"flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded border-2 bg-card p-6 text-center text-balance",
className
className,
)}
{...props}
/>
)
);
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -22,7 +22,7 @@ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex max-w-sm flex-col items-center gap-2", className)}
{...props}
/>
)
);
}
const emptyMediaVariants = cva(
@@ -37,8 +37,8 @@ const emptyMediaVariants = cva(
defaultVariants: {
variant: "default",
},
}
)
},
);
function EmptyMedia({
className,
@@ -52,20 +52,17 @@ function EmptyMedia({
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
)
);
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn(
"text-sm font-medium tracking-tight",
className
)}
className={cn("text-sm font-medium tracking-tight", className)}
{...props}
/>
)
);
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
@@ -74,11 +71,11 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
data-slot="empty-description"
className={cn(
"text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
className,
)}
{...props}
/>
)
);
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -87,11 +84,11 @@ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
data-slot="empty-content"
className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance",
className
className,
)}
{...props}
/>
)
);
}
export {
@@ -101,4 +98,4 @@ export {
EmptyDescription,
EmptyContent,
EmptyMedia,
}
};

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
@@ -9,11 +9,11 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded border-2 bg-input px-3 py-2 text-sm shadow-sm transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive",
className
className,
)}
{...props}
/>
)
);
}
export { Input }
export { Input };

View File

@@ -1,10 +1,10 @@
import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
@@ -13,11 +13,11 @@ function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
data-slot="item-group"
className={cn(
"group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2",
className
className,
)}
{...props}
/>
)
);
}
function ItemSeparator({
@@ -31,7 +31,7 @@ function ItemSeparator({
className={cn("my-2", className)}
{...props}
/>
)
);
}
const itemVariants = cva(
@@ -53,8 +53,8 @@ const itemVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
function Item({
className,
@@ -69,7 +69,7 @@ function Item({
{
className: cn(itemVariants({ variant, size, className })),
},
props
props,
),
render,
state: {
@@ -77,7 +77,7 @@ function Item({
variant,
size,
},
})
});
}
const itemMediaVariants = cva(
@@ -94,8 +94,8 @@ const itemMediaVariants = cva(
defaultVariants: {
variant: "default",
},
}
)
},
);
function ItemMedia({
className,
@@ -109,7 +109,7 @@ function ItemMedia({
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
)
);
}
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -118,11 +118,11 @@ function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
data-slot="item-content"
className={cn(
"flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none",
className
className,
)}
{...props}
/>
)
);
}
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -131,11 +131,11 @@ function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
data-slot="item-title"
className={cn(
"line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-medium underline-offset-4",
className
className,
)}
{...props}
/>
)
);
}
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
@@ -144,11 +144,11 @@ function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
data-slot="item-description"
className={cn(
"line-clamp-2 text-left text-sm leading-normal font-normal text-muted-foreground group-data-[size=xs]/item:text-xs [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
className,
)}
{...props}
/>
)
);
}
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
@@ -158,7 +158,7 @@ function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex items-center gap-2", className)}
{...props}
/>
)
);
}
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -167,11 +167,11 @@ function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="item-header"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
className,
)}
{...props}
/>
)
);
}
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -180,11 +180,11 @@ function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
data-slot="item-footer"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
className,
)}
{...props}
/>
)
);
}
export {
@@ -198,4 +198,4 @@ export {
ItemDescription,
ItemHeader,
ItemFooter,
}
};

View File

@@ -1,9 +1,9 @@
"use client"
"use client";
import * as React from "react"
import { Label as LabelPrimitive } from "radix-ui"
import * as React from "react";
import { Label as LabelPrimitive } from "radix-ui";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Label({
className,
@@ -14,11 +14,11 @@ function Label({
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-head font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
className,
)}
{...props}
/>
)
);
}
export { Label }
export { Label };

View File

@@ -1,9 +1,9 @@
import * as React from "react"
import { cva } from "class-variance-authority"
import { ChevronDownIcon } from "lucide-react"
import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui"
import * as React from "react";
import { cva } from "class-variance-authority";
import { ChevronDownIcon } from "lucide-react";
import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function NavigationMenu({
className,
@@ -11,7 +11,7 @@ function NavigationMenu({
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean
viewport?: boolean;
}) {
return (
<NavigationMenuPrimitive.Root
@@ -19,14 +19,14 @@ function NavigationMenu({
data-viewport={viewport}
className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className
className,
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
)
);
}
function NavigationMenuList({
@@ -38,11 +38,11 @@ function NavigationMenuList({
data-slot="navigation-menu-list"
className={cn(
"group flex flex-1 list-none items-center justify-center gap-0",
className
className,
)}
{...props}
/>
)
);
}
function NavigationMenuItem({
@@ -55,12 +55,12 @@ function NavigationMenuItem({
className={cn("relative", className)}
{...props}
/>
)
);
}
const navigationMenuTriggerStyle = cva(
"group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center px-2.5 py-1.5 text-sm font-medium transition-all outline-none hover:bg-secondary hover:text-foreground focus:bg-secondary focus:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:pointer-events-none disabled:opacity-50 data-popup-open:bg-secondary data-popup-open:text-foreground data-open:bg-secondary data-open:text-foreground"
)
"group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center px-2.5 py-1.5 text-sm font-medium transition-all outline-none hover:bg-secondary hover:text-foreground focus:bg-secondary focus:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:pointer-events-none disabled:opacity-50 data-popup-open:bg-secondary data-popup-open:text-foreground data-open:bg-secondary data-open:text-foreground",
);
function NavigationMenuTrigger({
className,
@@ -79,7 +79,7 @@ function NavigationMenuTrigger({
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
)
);
}
function NavigationMenuContent({
@@ -91,11 +91,11 @@ function NavigationMenuContent({
data-slot="navigation-menu-content"
className={cn(
"top-0 left-0 w-full p-1 ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:border-border/30 group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow-md group-data-[viewport=false]/navigation-menu:duration-300 data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none md:absolute md:w-auto group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95",
className
className,
)}
{...props}
/>
)
);
}
function NavigationMenuViewport({
@@ -105,19 +105,19 @@ function NavigationMenuViewport({
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center"
"absolute top-full left-0 isolate z-50 flex justify-center",
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center relative mt-1.5 h-(--radix-navigation-menu-viewport-height) w-full overflow-hidden border border-border/30 bg-popover text-popover-foreground shadow-md duration-100 md:w-(--radix-navigation-menu-viewport-width) data-open:animate-in data-open:zoom-in-90 data-closed:animate-out data-closed:zoom-out-90",
className
className,
)}
{...props}
/>
</div>
)
);
}
function NavigationMenuLink({
@@ -129,11 +129,11 @@ function NavigationMenuLink({
data-slot="navigation-menu-link"
className={cn(
"flex items-center gap-2 p-2 text-sm transition-all outline-none hover:bg-secondary hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary in-data-[slot=navigation-menu-content]:rounded-sm data-active:bg-primary/15 data-active:text-primary [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
/>
)
);
}
function NavigationMenuIndicator({
@@ -145,13 +145,13 @@ function NavigationMenuIndicator({
data-slot="navigation-menu-indicator"
className={cn(
"top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:animate-in data-[state=visible]:fade-in",
className
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
)
);
}
export {
@@ -164,4 +164,4 @@ export {
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
}
};

View File

@@ -1,12 +1,12 @@
"use client"
"use client";
import * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import * as React from "react";
import { Select as SelectPrimitive } from "@base-ui/react/select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root
const Select = SelectPrimitive.Root;
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
return (
@@ -15,7 +15,7 @@ function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
className={cn("scroll-my-1 p-1", className)}
{...props}
/>
)
);
}
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
@@ -25,7 +25,7 @@ function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
className={cn("flex flex-1 text-left", className)}
{...props}
/>
)
);
}
function SelectTrigger({
@@ -34,7 +34,7 @@ function SelectTrigger({
children,
...props
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default"
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
@@ -42,7 +42,7 @@ function SelectTrigger({
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded border border-border/30 bg-input py-2 pr-2 pl-2.5 text-sm whitespace-nowrap shadow-sm transition-colors outline-none select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
>
@@ -53,7 +53,7 @@ function SelectTrigger({
}
/>
</SelectPrimitive.Trigger>
)
);
}
function SelectContent({
@@ -83,7 +83,10 @@ function SelectContent({
<SelectPrimitive.Popup
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded border border-border/30 bg-popover text-popover-foreground shadow-md duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
className={cn(
"relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded border border-border/30 bg-popover text-popover-foreground shadow-md duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
>
<SelectScrollUpButton />
@@ -92,7 +95,7 @@ function SelectContent({
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
)
);
}
function SelectLabel({
@@ -105,7 +108,7 @@ function SelectLabel({
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
{...props}
/>
)
);
}
function SelectItem({
@@ -118,7 +121,7 @@ function SelectItem({
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-sm py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
className,
)}
{...props}
>
@@ -133,7 +136,7 @@ function SelectItem({
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
)
);
}
function SelectSeparator({
@@ -146,7 +149,7 @@ function SelectSeparator({
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
);
}
function SelectScrollUpButton({
@@ -158,13 +161,13 @@ function SelectScrollUpButton({
data-slot="select-scroll-up-button"
className={cn(
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpArrow>
)
);
}
function SelectScrollDownButton({
@@ -176,13 +179,13 @@ function SelectScrollDownButton({
data-slot="select-scroll-down-button"
className={cn(
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
className,
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownArrow>
)
);
}
export {
@@ -196,4 +199,4 @@ export {
SelectSeparator,
SelectTrigger,
SelectValue,
}
};

View File

@@ -1,9 +1,9 @@
"use client"
"use client";
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
@@ -11,7 +11,7 @@ const Separator = React.forwardRef<
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
@@ -20,12 +20,12 @@ const Separator = React.forwardRef<
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
className,
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
),
);
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator }
export { Separator };

View File

@@ -1,19 +1,19 @@
"use client"
"use client";
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
@@ -22,13 +22,13 @@ const SheetOverlay = React.forwardRef<
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
className,
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
@@ -46,11 +46,12 @@ const sheetVariants = cva(
defaultVariants: {
side: "right",
},
}
)
},
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
extends
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
@@ -71,8 +72,8 @@ const SheetContent = React.forwardRef<
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({
className,
@@ -81,12 +82,12 @@ const SheetHeader = ({
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
className,
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({
className,
@@ -95,12 +96,12 @@ const SheetFooter = ({
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
className,
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
@@ -111,8 +112,8 @@ const SheetTitle = React.forwardRef<
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
@@ -123,8 +124,8 @@ const SheetDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
@@ -137,4 +138,4 @@ export {
SheetFooter,
SheetTitle,
SheetDescription,
}
};

View File

@@ -1,55 +1,55 @@
"use client"
"use client";
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { PanelLeftIcon } from "lucide-react"
import { Slot } from "radix-ui"
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { PanelLeftIcon } from "lucide-react";
import { Slot } from "radix-ui";
import { cn } from "@/lib/utils"
import { useIsMobile } from "@/hooks/use-mobile"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { cn } from "@/lib/utils";
import { useIsMobile } from "@/hooks/use-mobile";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
} from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
} from "@/components/ui/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext)
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context
return context;
}
function SidebarProvider({
@@ -61,36 +61,36 @@ function SidebarProvider({
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState)
setOpenProp(openState);
} else {
_setOpen(openState)
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open]
)
[setOpenProp, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
@@ -99,18 +99,18 @@ function SidebarProvider({
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
event.preventDefault();
toggleSidebar();
}
}
};
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
@@ -122,8 +122,8 @@ function SidebarProvider({
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
@@ -138,14 +138,14 @@ function SidebarProvider({
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
className
className,
)}
{...props}
>
{children}
</div>
</SidebarContext.Provider>
)
);
}
function Sidebar({
@@ -157,11 +157,11 @@ function Sidebar({
dir,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
@@ -169,13 +169,13 @@ function Sidebar({
data-slot="sidebar"
className={cn(
"flex h-full w-(--sidebar-width) flex-col bg-card text-sidebar-foreground",
className
className,
)}
{...props}
>
{children}
</div>
)
);
}
if (isMobile) {
@@ -201,7 +201,7 @@ function Sidebar({
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
);
}
return (
@@ -222,7 +222,7 @@ function Sidebar({
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
)}
/>
<div
@@ -234,7 +234,7 @@ function Sidebar({
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r-2 group-data-[side=right]:border-l-2",
className
className,
)}
{...props}
>
@@ -247,7 +247,7 @@ function Sidebar({
</div>
</div>
</div>
)
);
}
function SidebarTrigger({
@@ -255,7 +255,7 @@ function SidebarTrigger({
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar()
const { toggleSidebar } = useSidebar();
return (
<Button
@@ -265,19 +265,19 @@ function SidebarTrigger({
size="icon"
className={cn(className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
);
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar()
const { toggleSidebar } = useSidebar();
return (
<button
@@ -294,11 +294,11 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
className,
)}
{...props}
/>
)
);
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
@@ -307,11 +307,11 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
data-slot="sidebar-inset"
className={cn(
"relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className
className,
)}
{...props}
/>
)
);
}
function SidebarInput({
@@ -325,7 +325,7 @@ function SidebarInput({
className={cn("h-8 w-full bg-background shadow-none", className)}
{...props}
/>
)
);
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -336,7 +336,7 @@ function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
);
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -347,7 +347,7 @@ function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
);
}
function SidebarSeparator({
@@ -361,7 +361,7 @@ function SidebarSeparator({
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
)
);
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -371,11 +371,11 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
data-sidebar="content"
className={cn(
"no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
className,
)}
{...props}
/>
)
);
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
@@ -386,7 +386,7 @@ function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
);
}
function SidebarGroupLabel({
@@ -394,7 +394,7 @@ function SidebarGroupLabel({
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "div"
const Comp = asChild ? Slot.Root : "div";
return (
<Comp
@@ -402,11 +402,11 @@ function SidebarGroupLabel({
data-sidebar="group-label"
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
className
className,
)}
{...props}
/>
)
);
}
function SidebarGroupAction({
@@ -414,7 +414,7 @@ function SidebarGroupAction({
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "button"
const Comp = asChild ? Slot.Root : "button";
return (
<Comp
@@ -422,11 +422,11 @@ function SidebarGroupAction({
data-sidebar="group-action"
className={cn(
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
className
className,
)}
{...props}
/>
)
);
}
function SidebarGroupContent({
@@ -440,7 +440,7 @@ function SidebarGroupContent({
className={cn("w-full text-sm", className)}
{...props}
/>
)
);
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
@@ -451,7 +451,7 @@ function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
className={cn("flex w-full min-w-0 flex-col gap-0", className)}
{...props}
/>
)
);
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
@@ -462,7 +462,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
className={cn("group/menu-item relative", className)}
{...props}
/>
)
);
}
const sidebarMenuButtonVariants = cva(
@@ -484,8 +484,8 @@ const sidebarMenuButtonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
function SidebarMenuButton({
asChild = false,
@@ -496,12 +496,12 @@ function SidebarMenuButton({
className,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot.Root : "button"
const { isMobile, state } = useSidebar()
const Comp = asChild ? Slot.Root : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
@@ -512,16 +512,16 @@ function SidebarMenuButton({
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
);
if (!tooltip) {
return button
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
};
}
return (
@@ -534,7 +534,7 @@ function SidebarMenuButton({
{...tooltip}
/>
</Tooltip>
)
);
}
function SidebarMenuAction({
@@ -543,10 +543,10 @@ function SidebarMenuAction({
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
asChild?: boolean;
showOnHover?: boolean;
}) {
const Comp = asChild ? Slot.Root : "button"
const Comp = asChild ? Slot.Root : "button";
return (
<Comp
@@ -556,11 +556,11 @@ function SidebarMenuAction({
"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0",
className
className,
)}
{...props}
/>
)
);
}
function SidebarMenuBadge({
@@ -573,11 +573,11 @@ function SidebarMenuBadge({
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground",
className
className,
)}
{...props}
/>
)
);
}
function SidebarMenuSkeleton({
@@ -585,12 +585,12 @@ function SidebarMenuSkeleton({
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean
showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const [width] = React.useState(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
})
return `${Math.floor(Math.random() * 40) + 50}%`;
});
return (
<div
@@ -615,7 +615,7 @@ function SidebarMenuSkeleton({
}
/>
</div>
)
);
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
@@ -625,11 +625,11 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden",
className
className,
)}
{...props}
/>
)
);
}
function SidebarMenuSubItem({
@@ -643,7 +643,7 @@ function SidebarMenuSubItem({
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
)
);
}
function SidebarMenuSubButton({
@@ -653,11 +653,11 @@ function SidebarMenuSubButton({
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}) {
const Comp = asChild ? Slot.Root : "a"
const Comp = asChild ? Slot.Root : "a";
return (
<Comp
@@ -667,11 +667,11 @@ function SidebarMenuSubButton({
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 active:bg-accent active:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-primary data-active:text-primary-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
className
className,
)}
{...props}
/>
)
);
}
export {
@@ -699,4 +699,4 @@ export {
SidebarSeparator,
SidebarTrigger,
useSidebar,
}
};

View File

@@ -1,4 +1,4 @@
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Skeleton({
className,
@@ -10,7 +10,7 @@ function Skeleton({
className={cn("animate-pulse rounded-md bg-muted/60", className)}
{...props}
/>
)
);
}
export { Skeleton }
export { Skeleton };

View File

@@ -1,6 +1,6 @@
import { Loader2Icon } from "lucide-react"
import { Loader2Icon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return (
@@ -11,7 +11,7 @@ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
className={cn("size-4 animate-spin", className)}
{...props}
/>
)
);
}
export { Spinner }
export { Spinner };

View File

@@ -1,8 +1,8 @@
"use client"
"use client";
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
@@ -16,7 +16,7 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
{...props}
/>
</div>
)
);
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
@@ -26,7 +26,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
className={cn("[&_tr]:border-b-2", className)}
{...props}
/>
)
);
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
@@ -36,7 +36,7 @@ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
);
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
@@ -45,11 +45,11 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
data-slot="table-footer"
className={cn(
"border-t-2 bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
className,
)}
{...props}
/>
)
);
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
@@ -58,11 +58,11 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
data-slot="table-row"
className={cn(
"border-b-2 transition-colors hover:bg-accent has-aria-expanded:bg-accent data-[state=selected]:bg-accent",
className
className,
)}
{...props}
/>
)
);
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
@@ -71,11 +71,11 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
data-slot="table-head"
className={cn(
"h-10 bg-muted px-2 text-left align-middle font-head font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
className
className,
)}
{...props}
/>
)
);
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
@@ -84,11 +84,11 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
className
className,
)}
{...props}
/>
)
);
}
function TableCaption({
@@ -101,7 +101,7 @@ function TableCaption({
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
)
);
}
export {
@@ -113,4 +113,4 @@ export {
TableRow,
TableCell,
TableCaption,
}
};

View File

@@ -1,10 +1,10 @@
"use client"
"use client";
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Tabs as TabsPrimitive } from "radix-ui"
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Tabs as TabsPrimitive } from "radix-ui";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Tabs({
className,
@@ -17,11 +17,11 @@ function Tabs({
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-horizontal:flex-col",
className
className,
)}
{...props}
/>
)
);
}
const tabsListVariants = cva(
@@ -36,8 +36,8 @@ const tabsListVariants = cva(
defaultVariants: {
variant: "default",
},
}
)
},
);
function TabsList({
className,
@@ -52,7 +52,7 @@ function TabsList({
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
)
);
}
function TabsTrigger({
@@ -67,11 +67,11 @@ function TabsTrigger({
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-primary data-active:text-primary-foreground group-data-[variant=line]/tabs-list:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-1 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-1 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className
className,
)}
{...props}
/>
)
);
}
function TabsContent({
@@ -84,7 +84,7 @@ function TabsContent({
className={cn("flex-1 text-sm outline-none", className)}
{...props}
/>
)
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
@@ -8,11 +8,11 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded border-2 bg-input px-3 py-2 text-sm shadow-sm transition-colors outline-none placeholder:text-muted-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive",
className
className,
)}
{...props}
/>
)
);
}
export { Textarea }
export { Textarea };

View File

@@ -1,15 +1,15 @@
"use client"
"use client";
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
@@ -20,11 +20,11 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
className
className,
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@@ -1,19 +1,21 @@
import * as React from "react"
import * as React from "react";
const MOBILE_BREAKPOINT = 768
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(mql.matches)
}
mql.addEventListener("change", onChange)
setIsMobile(mql.matches)
return () => mql.removeEventListener("change", onChange)
}, [])
setIsMobile(mql.matches);
};
mql.addEventListener("change", onChange);
setIsMobile(mql.matches);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile
return !!isMobile;
}

View File

@@ -46,6 +46,7 @@ export interface EntityInfo {
id: string;
name: string;
isPlayer: boolean;
isAgent: boolean;
}
export interface WaitingContext {

View File

@@ -17,7 +17,12 @@ for (const c of envCandidates) {
}
}
import { BufferRepository, LedgerRepository, HandoffEngine, checkHandoffTrigger } from "@omnia/memory";
import {
BufferRepository,
LedgerRepository,
HandoffEngine,
checkHandoffTrigger,
} from "@omnia/memory";
import { Architect, AliasDeltaGenerator } from "@omnia/architect";
import {
ActorAgent,
@@ -25,7 +30,17 @@ import {
IActorProseGenerator,
buildBufferEntryForIntent,
} from "@omnia/actor";
import { GeminiProvider, ILLMProvider, MockLLMProvider, ProviderManager, OpenRouterProvider, IEmbeddingProvider, GeminiEmbeddingProvider, MockEmbeddingProvider, ModelProviderInstance } from "@omnia/llm";
import {
GeminiProvider,
ILLMProvider,
MockLLMProvider,
ProviderManager,
OpenRouterProvider,
IEmbeddingProvider,
GeminiEmbeddingProvider,
MockEmbeddingProvider,
ModelProviderInstance,
} from "@omnia/llm";
import { ScenarioLoader } from "@omnia/scenario";
import type {
@@ -69,15 +84,22 @@ interface SavedState {
providerMappings: Record<string, string>;
}
function loadSessionState(db: Database.Database, id: string): SavedState | null {
function loadSessionState(
db: Database.Database,
id: string,
): SavedState | null {
try {
db.prepare(`
db.prepare(
`
CREATE TABLE IF NOT EXISTS gui_meta (
id TEXT PRIMARY KEY,
state_json TEXT
)
`).run();
const row = db.prepare(`SELECT state_json FROM gui_meta WHERE id = ?`).get(id) as { state_json: string } | undefined;
`,
).run();
const row = db
.prepare(`SELECT state_json FROM gui_meta WHERE id = ?`)
.get(id) as { state_json: string } | undefined;
return row ? (JSON.parse(row.state_json) as SavedState) : null;
} catch {
return null;
@@ -129,7 +151,13 @@ class SimulationManager {
if (!activeInstance) {
const envKey = process.env.GOOGLE_API_KEY;
if (envKey) {
activeInstance = ProviderManager.create("Default (Env)", "google-genai", envKey, undefined, "generative");
activeInstance = ProviderManager.create(
"Default (Env)",
"google-genai",
envKey,
undefined,
"generative",
);
}
}
if (!activeInstance) {
@@ -143,7 +171,8 @@ class SimulationManager {
entities: [],
log: [],
entityIndex: 0,
error: "No active LLM Provider Instance found. Please configure a key in Settings first.",
error:
"No active LLM Provider Instance found. Please configure a key in Settings first.",
};
}
@@ -184,6 +213,7 @@ class SimulationManager {
id: e.id,
name: (e.attributes.get("name")?.getValue() as string) || e.id,
isPlayer: false,
isAgent: e.isAgent,
}));
let playerEntityId: string | undefined;
@@ -192,8 +222,7 @@ class SimulationManager {
if (!matched) {
for (const ent of rawEntities) {
const nameAttr = ent.attributes.get("name")?.getValue() as
| string
| undefined;
string | undefined;
if (nameAttr?.toLowerCase() === playEntityName.toLowerCase()) {
matched = ent;
break;
@@ -203,8 +232,7 @@ class SimulationManager {
if (!matched) {
for (const ent of rawEntities) {
const nameAttr = ent.attributes.get("name")?.getValue() as
| string
| undefined;
string | undefined;
if (
nameAttr?.toLowerCase().includes(playEntityName.toLowerCase()) ||
ent.id.toLowerCase().includes(playEntityName.toLowerCase())
@@ -232,7 +260,7 @@ class SimulationManager {
inst = active;
}
const key = inst ? inst.apiKey : (process.env.GOOGLE_API_KEY || "");
const key = inst ? inst.apiKey : process.env.GOOGLE_API_KEY || "";
const providerName = inst ? inst.providerName : "google-genai";
const modelName = inst ? inst.modelName : undefined;
const instanceName = inst ? inst.name : undefined;
@@ -254,7 +282,7 @@ class SimulationManager {
inst = ProviderManager.getActive("embedding");
}
const key = inst ? inst.apiKey : (process.env.GOOGLE_API_KEY || "");
const key = inst ? inst.apiKey : process.env.GOOGLE_API_KEY || "";
const providerName = inst ? inst.providerName : "google-genai";
const modelName = inst ? inst.modelName : undefined;
@@ -340,6 +368,12 @@ class SimulationManager {
const info = session.entities[session.entityIndex];
if (!info.isAgent) {
session.entityIndex++;
this.save(session);
return this.snapshot(session);
}
if (info.isPlayer) {
await this.preparePlayerTurn(session, info);
this.save(session);
@@ -402,8 +436,14 @@ class SimulationManager {
},
};
if (session.decoderProvider.lastCalls && session.decoderProvider.lastCalls.length > 0) {
const call = session.decoderProvider.lastCalls[session.decoderProvider.lastCalls.length - 1];
if (
session.decoderProvider.lastCalls &&
session.decoderProvider.lastCalls.length > 0
) {
const call =
session.decoderProvider.lastCalls[
session.decoderProvider.lastCalls.length - 1
];
entry.decoderPrompt = {
systemPrompt: call.systemPrompt,
userContext: call.userContext,
@@ -487,15 +527,17 @@ class SimulationManager {
session: SimSession,
info: EntityInfo,
): Promise<void> {
const worldState = session.coreRepo.loadWorldState(
session.worldInstanceId,
);
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
if (!worldState) throw new Error("World state lost");
const entity = worldState.getEntity(info.id);
if (!entity) throw new Error(`Entity "${info.id}" not found`);
const promptBuilder = new ActorPromptBuilder(session.bufferRepo, session.ledgerRepo, 20);
const promptBuilder = new ActorPromptBuilder(
session.bufferRepo,
session.ledgerRepo,
20,
);
const { systemPrompt, userContext } = promptBuilder.build(
worldState,
entity,
@@ -514,9 +556,7 @@ class SimulationManager {
session: SimSession,
info: EntityInfo,
): Promise<void> {
const worldState = session.coreRepo.loadWorldState(
session.worldInstanceId,
);
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
if (!worldState) throw new Error("World state lost");
const entity = worldState.getEntity(info.id);
@@ -539,8 +579,14 @@ class SimulationManager {
timestamp: worldState.clock.get().toISOString(),
};
if (session.actorProvider.lastCalls && session.actorProvider.lastCalls.length > 0) {
const actorCall = session.actorProvider.lastCalls[session.actorProvider.lastCalls.length - 1];
if (
session.actorProvider.lastCalls &&
session.actorProvider.lastCalls.length > 0
) {
const actorCall =
session.actorProvider.lastCalls[
session.actorProvider.lastCalls.length - 1
];
entry.rawPrompt = {
systemPrompt: actorCall.systemPrompt,
userContext: actorCall.userContext,
@@ -548,8 +594,14 @@ class SimulationManager {
entry.usage = actorCall.usage;
}
if (session.decoderProvider.lastCalls && session.decoderProvider.lastCalls.length > 0) {
const decoderCall = session.decoderProvider.lastCalls[session.decoderProvider.lastCalls.length - 1];
if (
session.decoderProvider.lastCalls &&
session.decoderProvider.lastCalls.length > 0
) {
const decoderCall =
session.decoderProvider.lastCalls[
session.decoderProvider.lastCalls.length - 1
];
entry.decoderPrompt = {
systemPrompt: decoderCall.systemPrompt,
userContext: decoderCall.userContext,
@@ -558,10 +610,7 @@ class SimulationManager {
}
for (const intent of result.intents.intents) {
const outcome = await session.architect.processIntent(
worldState,
intent,
);
const outcome = await session.architect.processIntent(worldState, intent);
const ts = worldState.clock.get().toISOString();
entry.intents.push({
@@ -593,10 +642,7 @@ class SimulationManager {
(intent.type === "dialogue" || intent.type === "action")
) {
for (const [, other] of worldState.entities) {
if (
other.id !== info.id &&
other.locationId === entity.locationId
) {
if (other.id !== info.id && other.locationId === entity.locationId) {
const observerEntry = buildBufferEntryForIntent(
intent,
ts,
@@ -619,9 +665,7 @@ class SimulationManager {
}
private async runHandoffResolution(session: SimSession): Promise<void> {
const worldState = session.coreRepo.loadWorldState(
session.worldInstanceId,
);
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
if (!worldState) throw new Error("World state lost");
const handoffEngine = new HandoffEngine(
@@ -633,24 +677,36 @@ class SimulationManager {
const entities = Array.from(worldState.entities.values());
for (const entity of entities) {
if (!entity.isAgent) continue;
const bufferEntries = session.bufferRepo.listForOwner(entity.id);
const maxContext = session.handoffProvider.maxContext !== undefined ? session.handoffProvider.maxContext : 32768;
const maxContext =
session.handoffProvider.maxContext !== undefined
? session.handoffProvider.maxContext
: 32768;
const trigger = checkHandoffTrigger(entity, bufferEntries, worldState.clock.get(), maxContext);
const trigger = checkHandoffTrigger(
entity,
bufferEntries,
worldState.clock.get(),
maxContext,
);
if (trigger !== "none") {
await handoffEngine.runHandoff(entity, bufferEntries, worldState.clock.get());
await handoffEngine.runHandoff(
entity,
bufferEntries,
worldState.clock.get(),
);
}
}
}
private async runAliasResolution(session: SimSession): Promise<void> {
const worldState = session.coreRepo.loadWorldState(
session.worldInstanceId,
);
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
if (!worldState) throw new Error("World state lost");
const entities = Array.from(worldState.entities.values());
for (const viewer of entities) {
if (!viewer.isAgent) continue;
if (!viewer.locationId) continue;
for (const target of entities) {
if (viewer.id === target.id) continue;
@@ -723,18 +779,36 @@ class SimulationManager {
if (!inst) {
const envKey = process.env.GOOGLE_API_KEY;
if (envKey) {
inst = ProviderManager.create("Default (Env)", "google-genai", envKey, undefined, "generative");
inst = ProviderManager.create(
"Default (Env)",
"google-genai",
envKey,
undefined,
"generative",
);
}
}
if (!inst) {
throw new Error(`No active LLM Provider Instance found for task "${task}". Please configure a key in Settings first.`);
throw new Error(
`No active LLM Provider Instance found for task "${task}". Please configure a key in Settings first.`,
);
}
if (inst.providerName === "google-genai") {
return new GeminiProvider(inst.apiKey, inst.modelName, inst.name, inst.maxContext);
return new GeminiProvider(
inst.apiKey,
inst.modelName,
inst.name,
inst.maxContext,
);
} else if (inst.providerName === "openrouter") {
return new OpenRouterProvider(inst.apiKey, inst.modelName, inst.name, inst.maxContext);
return new OpenRouterProvider(
inst.apiKey,
inst.modelName,
inst.name,
inst.maxContext,
);
} else {
return new MockLLMProvider([]);
}
@@ -749,12 +823,20 @@ class SimulationManager {
if (!inst) {
const envKey = process.env.GOOGLE_API_KEY;
if (envKey) {
inst = ProviderManager.create("Default Embed (Env)", "google-genai", envKey, "gemini-embedding-001", "embedding");
inst = ProviderManager.create(
"Default Embed (Env)",
"google-genai",
envKey,
"gemini-embedding-001",
"embedding",
);
}
}
if (!inst) {
throw new Error(`No active Embedding Provider Instance found for task "embeddings". Please configure an embedding key in Settings first.`);
throw new Error(
`No active Embedding Provider Instance found for task "embeddings". Please configure an embedding key in Settings first.`,
);
}
if (inst.providerName === "google-genai") {
@@ -824,7 +906,9 @@ class SimulationManager {
if (!fs.existsSync(dbDir)) return [];
const snapshots: SimSnapshot[] = [];
const files = fs.readdirSync(dbDir).filter(f => f.startsWith("sim-") && f.endsWith(".db"));
const files = fs
.readdirSync(dbDir)
.filter((f) => f.startsWith("sim-") && f.endsWith(".db"));
for (const file of files) {
const id = file.replace(".db", "");
@@ -872,15 +956,19 @@ class SimulationManager {
const dbDir = path.resolve(process.cwd(), "data");
if (!fs.existsSync(dbDir)) return;
const files = fs.readdirSync(dbDir).filter(f => f.startsWith("sim-") && f.endsWith(".db"));
const files = fs
.readdirSync(dbDir)
.filter((f) => f.startsWith("sim-") && f.endsWith(".db"));
const list = ProviderManager.list();
let inst = newProviderInstanceId ? list.find((p) => p.id === newProviderInstanceId) : null;
let inst = newProviderInstanceId
? list.find((p) => p.id === newProviderInstanceId)
: null;
if (!inst || inst.type !== "embedding") {
inst = ProviderManager.getActive("embedding");
}
const key = inst ? inst.apiKey : (process.env.GOOGLE_API_KEY || "");
const key = inst ? inst.apiKey : process.env.GOOGLE_API_KEY || "";
const providerName = inst ? inst.providerName : "google-genai";
const modelName = inst ? inst.modelName : undefined;
@@ -898,12 +986,16 @@ class SimulationManager {
const db = activeSession ? activeSession.db : new Database(dbPath);
try {
const rows = db.prepare(`SELECT id, content FROM ledger_entries`).all() as { id: string; content: string }[];
const rows = db
.prepare(`SELECT id, content FROM ledger_entries`)
.all() as { id: string; content: string }[];
for (const row of rows) {
const vector = await embeddingProvider.embed(row.content);
const buffer = Buffer.from(new Float32Array(vector).buffer);
db.prepare(`UPDATE ledger_entries SET embedding = ? WHERE id = ?`).run(buffer, row.id);
db.prepare(
`UPDATE ledger_entries SET embedding = ? WHERE id = ?`,
).run(buffer, row.id);
}
} catch (err) {
console.error(`Failed to regenerate embeddings for ${file}:`, err);
@@ -932,18 +1024,26 @@ class SimulationManager {
providerMappings: session.providerMappings,
};
session.db.prepare(`
session.db
.prepare(
`
CREATE TABLE IF NOT EXISTS gui_meta (
id TEXT PRIMARY KEY,
state_json TEXT
)
`).run();
`,
)
.run();
session.db.prepare(`
session.db
.prepare(
`
INSERT INTO gui_meta (id, state_json)
VALUES (?, ?)
ON CONFLICT(id) DO UPDATE SET state_json = excluded.state_json
`).run(session.worldInstanceId, JSON.stringify(state));
`,
)
.run(session.worldInstanceId, JSON.stringify(state));
}
getSnapshot(id: string): SimSnapshot | null {

View File

@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -23,9 +19,7 @@
}
],
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
}
},
"include": [
@@ -35,7 +29,5 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}