mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-21 19:42:48 +05:30
chore: Format files
This commit is contained in:
10
.github/workflows/deploy-docs.yml
vendored
10
.github/workflows/deploy-docs.yml
vendored
@@ -5,9 +5,9 @@ on:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'web/docs/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- '.github/workflows/deploy-docs.yml'
|
||||
- "web/docs/**"
|
||||
- "pnpm-lock.yaml"
|
||||
- ".github/workflows/deploy-docs.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -41,4 +41,4 @@ jobs:
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
workingDirectory: 'web/docs'
|
||||
workingDirectory: "web/docs"
|
||||
|
||||
@@ -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";
|
||||
|
||||
function resolveScenarioPath(relative: string): string {
|
||||
const cwd = process.cwd();
|
||||
@@ -25,8 +30,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;
|
||||
@@ -168,8 +172,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();
|
||||
@@ -197,7 +200,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 }
|
||||
> {
|
||||
@@ -207,10 +212,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 {
|
||||
@@ -220,9 +227,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 };
|
||||
@@ -234,7 +241,9 @@ export async function deleteSimulation(simId: string): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
export async function listProviderInstances(): Promise<ModelProviderInstance[]> {
|
||||
export async function listProviderInstances(): Promise<
|
||||
ModelProviderInstance[]
|
||||
> {
|
||||
return ProviderManager.list();
|
||||
}
|
||||
|
||||
@@ -246,7 +255,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> {
|
||||
@@ -266,7 +282,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>> {
|
||||
@@ -284,6 +308,8 @@ 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);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ export default function BuilderPage() {
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto w-full">
|
||||
<div className="mx-auto max-w-[800px] px-10 py-12">
|
||||
<h1 className="mb-6 text-headline-lg text-primary animate-fade-in">Scenario Builder</h1>
|
||||
<h1 className="mb-6 text-headline-lg text-primary animate-fade-in">
|
||||
Scenario Builder
|
||||
</h1>
|
||||
<div className="border border-border/30 bg-card p-6 shadow-[2px_2px_0_0_var(--border)] min-h-[300px] flex flex-col items-center justify-center">
|
||||
<p className="text-body-md text-muted-foreground font-mono text-center">
|
||||
Scenario builder interface coming soon...
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -11,15 +11,25 @@ interface ScenarioCardProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function ScenarioCard({ name, description, onClick }: ScenarioCardProps) {
|
||||
export function ScenarioCard({
|
||||
name,
|
||||
description,
|
||||
onClick,
|
||||
}: 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"
|
||||
>
|
||||
<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 border-primary bg-primary/10 text-primary font-mono text-sm font-bold">
|
||||
{">"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,12 @@ import tseslint from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ["**/dist/**", "**/node_modules/**", "**/.astro/**", "**/.next/**"],
|
||||
ignores: [
|
||||
"**/dist/**",
|
||||
"**/node_modules/**",
|
||||
"**/.astro/**",
|
||||
"**/.next/**",
|
||||
],
|
||||
},
|
||||
|
||||
js.configs.recommended,
|
||||
|
||||
@@ -11,7 +11,7 @@ describe("ActorPromptBuilder with Long-Term Memory Integration", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
db = new Database(":memory:");
|
||||
|
||||
|
||||
// Core database schemas for testing
|
||||
db.exec(`
|
||||
CREATE TABLE objects (
|
||||
@@ -32,8 +32,11 @@ describe("ActorPromptBuilder with Long-Term Memory Integration", () => {
|
||||
});
|
||||
|
||||
it("should inject both recent memory and recalled long-term memory with subjective aliases resolved", () => {
|
||||
const world = new WorldState("world-123", new Date("2024-01-10T12:00:00.000Z"));
|
||||
|
||||
const world = new WorldState(
|
||||
"world-123",
|
||||
new Date("2024-01-10T12:00:00.000Z"),
|
||||
);
|
||||
|
||||
const alice = new Entity("alice", "tavern");
|
||||
// Add subjective alias for bob
|
||||
alice.aliases.set("bob", "Strider");
|
||||
@@ -85,7 +88,10 @@ describe("ActorPromptBuilder with Long-Term Memory Integration", () => {
|
||||
});
|
||||
|
||||
it("should not explode if ledger contains no memories or is empty", () => {
|
||||
const world = new WorldState("world-123", new Date("2024-01-10T12:00:00.000Z"));
|
||||
const world = new WorldState(
|
||||
"world-123",
|
||||
new Date("2024-01-10T12:00:00.000Z"),
|
||||
);
|
||||
const alice = new Entity("alice", "tavern");
|
||||
world.addEntity(alice);
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ export class Architect {
|
||||
private timeDeltaGenerator: TimeDeltaGenerator;
|
||||
|
||||
constructor(
|
||||
llmProvider: ILLMProvider | { validator: ILLMProvider; timedelta: ILLMProvider },
|
||||
llmProvider:
|
||||
ILLMProvider | { validator: ILLMProvider; timedelta: ILLMProvider },
|
||||
private repo?: SQLiteRepository,
|
||||
) {
|
||||
let valProv: ILLMProvider;
|
||||
@@ -60,8 +61,12 @@ export class Architect {
|
||||
if (intent.type === "monologue") {
|
||||
return {
|
||||
isValid: true,
|
||||
reason: "Monologue intent bypasses validation (internal thought, not perceivable).",
|
||||
timeDelta: { minutesToAdvance: 0, explanation: "Internal thought — no time elapsed." },
|
||||
reason:
|
||||
"Monologue intent bypasses validation (internal thought, not perceivable).",
|
||||
timeDelta: {
|
||||
minutesToAdvance: 0,
|
||||
explanation: "Internal thought — no time elapsed.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./llm-validator.js";
|
||||
export * from "./architect.js";
|
||||
export * from "./delta.js";
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ export class LLMValidator {
|
||||
if (intent.type === "monologue") {
|
||||
return {
|
||||
isValid: true,
|
||||
reason: "Monologue intents are internal thoughts and bypass validation.",
|
||||
reason:
|
||||
"Monologue intents are internal thoughts and bypass validation.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import Database from "better-sqlite3";
|
||||
import { WorldState, Entity, SQLiteRepository, AttributeVisibility } from "@omnia/core";
|
||||
import {
|
||||
WorldState,
|
||||
Entity,
|
||||
SQLiteRepository,
|
||||
AttributeVisibility,
|
||||
} from "@omnia/core";
|
||||
import { MockLLMProvider } from "@omnia/llm";
|
||||
import { Architect, AliasDeltaGenerator } from "@omnia/architect";
|
||||
import { Intent } from "@omnia/intent";
|
||||
@@ -96,7 +101,10 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", ()
|
||||
const db = new Database(":memory:");
|
||||
const repo = new SQLiteRepository(db);
|
||||
|
||||
const world = new WorldState("world-xyz", new Date("2026-07-06T12:00:00.000Z"));
|
||||
const world = new WorldState(
|
||||
"world-xyz",
|
||||
new Date("2026-07-06T12:00:00.000Z"),
|
||||
);
|
||||
const alice = new Entity("alice");
|
||||
world.addEntity(alice);
|
||||
|
||||
@@ -106,8 +114,14 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", ()
|
||||
// Setup mock LLM responses:
|
||||
// First call: validateIntent (ValidationResult)
|
||||
// Second call: TimeDeltaGenerator (TimeDelta)
|
||||
const mockValidation = { isValid: true, reason: "Alice has the lockpick kit and skill." };
|
||||
const mockTimeDelta = { minutesToAdvance: 20, explanation: "Picking a lock takes time." };
|
||||
const mockValidation = {
|
||||
isValid: true,
|
||||
reason: "Alice has the lockpick kit and skill.",
|
||||
};
|
||||
const mockTimeDelta = {
|
||||
minutesToAdvance: 20,
|
||||
explanation: "Picking a lock takes time.",
|
||||
};
|
||||
const llmProvider = new MockLLMProvider([mockValidation, mockTimeDelta]);
|
||||
|
||||
const architect = new Architect(llmProvider, repo);
|
||||
@@ -131,7 +145,9 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", ()
|
||||
expect(result.timeDelta!.minutesToAdvance).toBe(20);
|
||||
|
||||
// Verify clock was advanced locally
|
||||
const expectedTime = new Date(new Date("2026-07-06T12:00:00.000Z").getTime() + 20 * 60_000);
|
||||
const expectedTime = new Date(
|
||||
new Date("2026-07-06T12:00:00.000Z").getTime() + 20 * 60_000,
|
||||
);
|
||||
expect(world.clock.get().toISOString()).toBe(expectedTime.toISOString());
|
||||
|
||||
// Verify it was persisted to the database
|
||||
@@ -151,7 +167,10 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", ()
|
||||
world.addEntity(bob);
|
||||
repo.saveWorldState(world);
|
||||
|
||||
const mockValidation = { isValid: false, reason: "Bob is bound by chains." };
|
||||
const mockValidation = {
|
||||
isValid: false,
|
||||
reason: "Bob is bound by chains.",
|
||||
};
|
||||
const llmProvider = new MockLLMProvider([mockValidation]); // TimeDeltaGenerator shouldn't be called
|
||||
|
||||
const architect = new Architect(llmProvider, repo);
|
||||
@@ -188,8 +207,16 @@ describe("AliasDeltaGenerator Unit Tests (Tier 1)", () => {
|
||||
const world = new WorldState("world-1");
|
||||
const viewer = new Entity("viewer-1");
|
||||
const target = new Entity("target-1");
|
||||
target.addAttribute("appearance", "A tall elf with silver hair", AttributeVisibility.PUBLIC);
|
||||
target.addAttribute("clothing", "A green tunic", AttributeVisibility.PUBLIC);
|
||||
target.addAttribute(
|
||||
"appearance",
|
||||
"A tall elf with silver hair",
|
||||
AttributeVisibility.PUBLIC,
|
||||
);
|
||||
target.addAttribute(
|
||||
"clothing",
|
||||
"A green tunic",
|
||||
AttributeVisibility.PUBLIC,
|
||||
);
|
||||
world.addEntity(viewer);
|
||||
world.addEntity(target);
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ export function naturalizeTime(now: Date, past: Date): string {
|
||||
const nowHour = now.getHours();
|
||||
const pastIsWaking = pastHour >= 5 && pastHour < 22;
|
||||
const nowIsWaking = nowHour >= 5 && nowHour < 22;
|
||||
|
||||
|
||||
const isSameSubjectiveDay = pastIsWaking && nowIsWaking && deltaHours < 18;
|
||||
|
||||
if (isSameSubjectiveDay) {
|
||||
|
||||
@@ -10,4 +10,3 @@ export * from "./world.js";
|
||||
export * from "./clock.js";
|
||||
export * from "./repository.js";
|
||||
export * from "./alias.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { AttributableObject, Attribute, serializeAttributes } from "./attribute.js";
|
||||
import {
|
||||
AttributableObject,
|
||||
Attribute,
|
||||
serializeAttributes,
|
||||
} from "./attribute.js";
|
||||
import { Entity } from "./entity.js";
|
||||
import { WorldClock } from "./clock.js";
|
||||
import { resolveAlias } from "./alias.js";
|
||||
@@ -54,8 +58,15 @@ export function serializeObjectiveWorldState(worldState: WorldState): string {
|
||||
// Serialize world attributes
|
||||
if (worldState.attributes.size > 0) {
|
||||
lines.push("World Attributes:");
|
||||
const worldAttrsStr = serializeAttributes(Array.from(worldState.attributes.values()));
|
||||
lines.push(worldAttrsStr.split("\n").map(l => " " + l).join("\n"));
|
||||
const worldAttrsStr = serializeAttributes(
|
||||
Array.from(worldState.attributes.values()),
|
||||
);
|
||||
lines.push(
|
||||
worldAttrsStr
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
// Serialize locations and their attributes/portals
|
||||
@@ -63,33 +74,46 @@ export function serializeObjectiveWorldState(worldState: WorldState): string {
|
||||
if (worldState.locations.size > 0) {
|
||||
for (const loc of worldState.locations.values()) {
|
||||
lines.push(` - Location [ID: ${loc.id}]:`);
|
||||
|
||||
|
||||
const parentId = (loc as { parentId?: string | null }).parentId;
|
||||
if (parentId) {
|
||||
lines.push(` * Parent Location ID: ${parentId}`);
|
||||
}
|
||||
|
||||
|
||||
if (loc.attributes.size > 0) {
|
||||
const locAttrsStr = serializeAttributes(Array.from(loc.attributes.values()));
|
||||
lines.push(locAttrsStr.split("\n").map(l => " " + l).join("\n"));
|
||||
const locAttrsStr = serializeAttributes(
|
||||
Array.from(loc.attributes.values()),
|
||||
);
|
||||
lines.push(
|
||||
locAttrsStr
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n"),
|
||||
);
|
||||
} else {
|
||||
lines.push(" * (No attributes)");
|
||||
}
|
||||
|
||||
const connections = (loc as { connections?: unknown[] }).connections as {
|
||||
targetId: string;
|
||||
portalName?: string;
|
||||
portalStateDescriptor?: string;
|
||||
visionProp: number;
|
||||
soundProp: number;
|
||||
bidirectional: boolean;
|
||||
}[] | undefined;
|
||||
const connections = (loc as { connections?: unknown[] }).connections as
|
||||
| {
|
||||
targetId: string;
|
||||
portalName?: string;
|
||||
portalStateDescriptor?: string;
|
||||
visionProp: number;
|
||||
soundProp: number;
|
||||
bidirectional: boolean;
|
||||
}[]
|
||||
| undefined;
|
||||
|
||||
if (connections && connections.length > 0) {
|
||||
lines.push(" * Connections:");
|
||||
for (const conn of connections) {
|
||||
const portalStr = conn.portalName ? ` via ${conn.portalName} (${conn.portalStateDescriptor || "normal"})` : "";
|
||||
lines.push(` -> To: ${conn.targetId}${portalStr} (Vision: ${conn.visionProp}, Sound: ${conn.soundProp})`);
|
||||
const portalStr = conn.portalName
|
||||
? ` via ${conn.portalName} (${conn.portalStateDescriptor || "normal"})`
|
||||
: "";
|
||||
lines.push(
|
||||
` -> To: ${conn.targetId}${portalStr} (Vision: ${conn.visionProp}, Sound: ${conn.soundProp})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,8 +130,15 @@ export function serializeObjectiveWorldState(worldState: WorldState): string {
|
||||
lines.push(` * Location ID: ${entity.locationId}`);
|
||||
}
|
||||
if (entity.attributes.size > 0) {
|
||||
const entityAttrsStr = serializeAttributes(Array.from(entity.attributes.values()));
|
||||
lines.push(entityAttrsStr.split("\n").map(l => " " + l).join("\n"));
|
||||
const entityAttrsStr = serializeAttributes(
|
||||
Array.from(entity.attributes.values()),
|
||||
);
|
||||
lines.push(
|
||||
entityAttrsStr
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n"),
|
||||
);
|
||||
} else {
|
||||
lines.push(" * (No attributes)");
|
||||
}
|
||||
@@ -119,7 +150,6 @@ export function serializeObjectiveWorldState(worldState: WorldState): string {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Serializes a single attribute the way a viewer perceives it — name and
|
||||
* value only, no visibility/ACL metadata (the viewer already sees only
|
||||
@@ -158,13 +188,23 @@ export function serializeSubjectiveWorldState(
|
||||
const worldVisible = worldState.getVisibleAttributesFor(viewerId);
|
||||
if (worldVisible.length > 0) {
|
||||
lines.push("World (as you know it):");
|
||||
lines.push(serializeVisibleAttributes(worldVisible).split("\n").map((l) => " " + l).join("\n"));
|
||||
lines.push(
|
||||
serializeVisibleAttributes(worldVisible)
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Self ---
|
||||
lines.push(`Self (${viewerAlias}):`);
|
||||
const selfVisible = viewer.getVisibleAttributesFor(viewerId);
|
||||
lines.push(serializeVisibleAttributes(selfVisible).split("\n").map((l) => " " + l).join("\n"));
|
||||
lines.push(
|
||||
serializeVisibleAttributes(selfVisible)
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n"),
|
||||
);
|
||||
|
||||
// --- Location / perceived entities ---
|
||||
lines.push("What you perceive around you:");
|
||||
@@ -175,7 +215,12 @@ export function serializeSubjectiveWorldState(
|
||||
const locVisible = location.getVisibleAttributesFor(viewerId);
|
||||
if (locVisible.length > 0) {
|
||||
lines.push(" Location attributes:");
|
||||
lines.push(serializeVisibleAttributes(locVisible).split("\n").map((l) => " " + l).join("\n"));
|
||||
lines.push(
|
||||
serializeVisibleAttributes(locVisible)
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -199,7 +244,12 @@ export function serializeSubjectiveWorldState(
|
||||
const alias = resolveAlias(viewer, e.id);
|
||||
lines.push(` - ${alias}:`);
|
||||
const eVisible = e.getVisibleAttributesFor(viewerId);
|
||||
lines.push(serializeVisibleAttributes(eVisible).split("\n").map((l) => " " + l).join("\n"));
|
||||
lines.push(
|
||||
serializeVisibleAttributes(eVisible)
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
lines.push(" You are alone here.");
|
||||
|
||||
@@ -11,23 +11,39 @@ describe("TimeNaturalization Unit Tests (Tier 1)", () => {
|
||||
|
||||
test("Tier 1: relative time ranges (< 6 hours)", () => {
|
||||
const now = new Date(2026, 6, 8, 12, 0, 0);
|
||||
|
||||
|
||||
// < 1 minute
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 59, 30))).toBe("just now");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 59, 30))).toBe(
|
||||
"just now",
|
||||
);
|
||||
// < 3 minutes
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 58, 0))).toBe("moments ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 58, 0))).toBe(
|
||||
"moments ago",
|
||||
);
|
||||
// < 10 minutes
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 52, 0))).toBe("a few minutes ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 52, 0))).toBe(
|
||||
"a few minutes ago",
|
||||
);
|
||||
// < 30 minutes
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 40, 0))).toBe("several minutes ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 40, 0))).toBe(
|
||||
"several minutes ago",
|
||||
);
|
||||
// < 45 minutes
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 20, 0))).toBe("about half an hour ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 20, 0))).toBe(
|
||||
"about half an hour ago",
|
||||
);
|
||||
// < 1.5 hours (90 minutes)
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 10, 50, 0))).toBe("about an hour ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 10, 50, 0))).toBe(
|
||||
"about an hour ago",
|
||||
);
|
||||
// < 3 hours
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 9, 30, 0))).toBe("a couple hours ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 9, 30, 0))).toBe(
|
||||
"a couple hours ago",
|
||||
);
|
||||
// < 6 hours
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 7, 0, 0))).toBe("a few hours ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 7, 0, 0))).toBe(
|
||||
"a few hours ago",
|
||||
);
|
||||
});
|
||||
|
||||
test("Tier 2: Same subjective day vs yesterday periods (6h <= delta < 48h)", () => {
|
||||
@@ -35,12 +51,16 @@ describe("TimeNaturalization Unit Tests (Tier 1)", () => {
|
||||
// 9am to 3pm (delta 6h) -> morning
|
||||
const nowWaking = new Date(2026, 6, 8, 15, 0, 0);
|
||||
const pastMorning = new Date(2026, 6, 8, 9, 0, 0);
|
||||
expect(naturalizeTime(nowWaking, pastMorning)).toBe("earlier today, in the morning");
|
||||
expect(naturalizeTime(nowWaking, pastMorning)).toBe(
|
||||
"earlier today, in the morning",
|
||||
);
|
||||
|
||||
// 2pm to 9pm (delta 7h) -> afternoon
|
||||
const nowEvening = new Date(2026, 6, 8, 21, 0, 0);
|
||||
const pastAfternoon = new Date(2026, 6, 8, 14, 0, 0);
|
||||
expect(naturalizeTime(nowEvening, pastAfternoon)).toBe("earlier today, in the afternoon");
|
||||
expect(naturalizeTime(nowEvening, pastAfternoon)).toBe(
|
||||
"earlier today, in the afternoon",
|
||||
);
|
||||
|
||||
// 2. Previous night (past period: night (21-23))
|
||||
// 11pm to 5am (delta 6h) -> last night
|
||||
@@ -52,45 +72,73 @@ describe("TimeNaturalization Unit Tests (Tier 1)", () => {
|
||||
// 1am to 7am (delta 6h) -> around midnight
|
||||
const nowMidnightRun = new Date(2026, 6, 8, 7, 0, 0);
|
||||
const pastMidnight = new Date(2026, 6, 8, 1, 0, 0);
|
||||
expect(naturalizeTime(nowMidnightRun, pastMidnight)).toBe("around midnight");
|
||||
expect(naturalizeTime(nowMidnightRun, pastMidnight)).toBe(
|
||||
"around midnight",
|
||||
);
|
||||
|
||||
// 4. Late night (past period: late night (3-4))
|
||||
// 3am to 9am (delta 6h) -> late last night
|
||||
const nowLateNightRun = new Date(2026, 6, 8, 9, 0, 0);
|
||||
const pastLateNight = new Date(2026, 6, 8, 3, 0, 0);
|
||||
expect(naturalizeTime(nowLateNightRun, pastLateNight)).toBe("late last night");
|
||||
expect(naturalizeTime(nowLateNightRun, pastLateNight)).toBe(
|
||||
"late last night",
|
||||
);
|
||||
|
||||
// 5. Yesterday waking hours (delta >= 6h, diff day / waking check fails)
|
||||
// 3pm to 9am next day (delta 18h) -> yesterday afternoon
|
||||
const nowNextDay = new Date(2026, 6, 9, 9, 0, 0);
|
||||
const pastYesterdayAfternoon = new Date(2026, 6, 8, 15, 0, 0);
|
||||
expect(naturalizeTime(nowNextDay, pastYesterdayAfternoon)).toBe("yesterday afternoon");
|
||||
expect(naturalizeTime(nowNextDay, pastYesterdayAfternoon)).toBe(
|
||||
"yesterday afternoon",
|
||||
);
|
||||
});
|
||||
|
||||
test("Tier 3: Coarse relative time ranges (delta >= 48 hours)", () => {
|
||||
const now = new Date(2026, 6, 10, 12, 0, 0);
|
||||
|
||||
// 2 days
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 0, 0))).toBe("a couple days ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 0, 0))).toBe(
|
||||
"a couple days ago",
|
||||
);
|
||||
// 3-6 days
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 6, 12, 0, 0))).toBe("a few days ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 6, 12, 0, 0))).toBe(
|
||||
"a few days ago",
|
||||
);
|
||||
// 7-13 days
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 1, 12, 0, 0))).toBe("about a week ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 1, 12, 0, 0))).toBe(
|
||||
"about a week ago",
|
||||
);
|
||||
// 14-20 days
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 25, 12, 0, 0))).toBe("a couple weeks ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 25, 12, 0, 0))).toBe(
|
||||
"a couple weeks ago",
|
||||
);
|
||||
// 21-29 days
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 15, 12, 0, 0))).toBe("a few weeks ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 15, 12, 0, 0))).toBe(
|
||||
"a few weeks ago",
|
||||
);
|
||||
// 30-59 days
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 1, 12, 0, 0))).toBe("about a month ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 1, 12, 0, 0))).toBe(
|
||||
"about a month ago",
|
||||
);
|
||||
// 60-89 days
|
||||
expect(naturalizeTime(now, new Date(2026, 4, 1, 12, 0, 0))).toBe("a couple months ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 4, 1, 12, 0, 0))).toBe(
|
||||
"a couple months ago",
|
||||
);
|
||||
// 90-179 days
|
||||
expect(naturalizeTime(now, new Date(2026, 3, 1, 12, 0, 0))).toBe("a few months ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 3, 1, 12, 0, 0))).toBe(
|
||||
"a few months ago",
|
||||
);
|
||||
// 180-364 days
|
||||
expect(naturalizeTime(now, new Date(2026, 0, 1, 12, 0, 0))).toBe("many months ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 0, 1, 12, 0, 0))).toBe(
|
||||
"many months ago",
|
||||
);
|
||||
// 365-729 days
|
||||
expect(naturalizeTime(now, new Date(2025, 6, 1, 12, 0, 0))).toBe("about a year ago");
|
||||
expect(naturalizeTime(now, new Date(2025, 6, 1, 12, 0, 0))).toBe(
|
||||
"about a year ago",
|
||||
);
|
||||
// >= 730 days
|
||||
expect(naturalizeTime(now, new Date(2023, 6, 1, 12, 0, 0))).toBe("years ago");
|
||||
expect(naturalizeTime(now, new Date(2023, 6, 1, 12, 0, 0))).toBe(
|
||||
"years ago",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,11 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => {
|
||||
const llm = new MockLLMProvider([mockResponse]);
|
||||
const decoder = new IntentDecoder(llm);
|
||||
|
||||
const result = await decoder.decode(world, "alice", "Alice opened the chest.");
|
||||
const result = await decoder.decode(
|
||||
world,
|
||||
"alice",
|
||||
"Alice opened the chest.",
|
||||
);
|
||||
|
||||
expect(result.intents).toHaveLength(1);
|
||||
expect(result.intents[0].type).toBe("action");
|
||||
|
||||
@@ -5,8 +5,5 @@
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../core" },
|
||||
{ "path": "../llm" }
|
||||
]
|
||||
"references": [{ "path": "../core" }, { "path": "../llm" }]
|
||||
}
|
||||
|
||||
@@ -78,7 +78,8 @@ export const AVAILABLE_PROVIDERS: ModelProviderMeta[] = [
|
||||
{
|
||||
id: "openrouter",
|
||||
displayName: "OpenRouter",
|
||||
description: "Multi-model router supporting Anthropic, OpenAI, DeepSeek, and local models",
|
||||
description:
|
||||
"Multi-model router supporting Anthropic, OpenAI, DeepSeek, and local models",
|
||||
defaultModel: "google/gemini-2.5-flash",
|
||||
defaultEmbeddingModel: "openai/text-embedding-3-small",
|
||||
},
|
||||
|
||||
@@ -43,8 +43,9 @@ function getSettingsDb() {
|
||||
dbPath = path.join(dbDir, "settings.db");
|
||||
}
|
||||
const db = new Database(dbPath);
|
||||
|
||||
db.prepare(`
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS provider_instances (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
@@ -54,22 +55,29 @@ function getSettingsDb() {
|
||||
modelName TEXT,
|
||||
type TEXT NOT NULL DEFAULT 'generative'
|
||||
)
|
||||
`).run();
|
||||
`,
|
||||
).run();
|
||||
|
||||
try {
|
||||
db.prepare(`ALTER TABLE provider_instances ADD COLUMN modelName TEXT`).run();
|
||||
db.prepare(
|
||||
`ALTER TABLE provider_instances ADD COLUMN modelName TEXT`,
|
||||
).run();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
db.prepare(`ALTER TABLE provider_instances ADD COLUMN type TEXT NOT NULL DEFAULT 'generative'`).run();
|
||||
db.prepare(
|
||||
`ALTER TABLE provider_instances ADD COLUMN type TEXT NOT NULL DEFAULT 'generative'`,
|
||||
).run();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
db.prepare(`ALTER TABLE provider_instances ADD COLUMN maxContext INTEGER`).run();
|
||||
db.prepare(
|
||||
`ALTER TABLE provider_instances ADD COLUMN maxContext INTEGER`,
|
||||
).run();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -77,7 +85,9 @@ function getSettingsDb() {
|
||||
// Auto-bootstrap environment variables if DB contains 0 instances
|
||||
try {
|
||||
if (!hasBootstrapped) {
|
||||
const totalCount = db.prepare(`SELECT COUNT(*) as count FROM provider_instances`).get() as { count: number };
|
||||
const totalCount = db
|
||||
.prepare(`SELECT COUNT(*) as count FROM provider_instances`)
|
||||
.get() as { count: number };
|
||||
if (totalCount.count === 0) {
|
||||
const googleKey = process.env.GOOGLE_API_KEY;
|
||||
const openRouterKey = process.env.OPENROUTER_API_KEY;
|
||||
@@ -85,26 +95,59 @@ function getSettingsDb() {
|
||||
|
||||
if (googleKey && googleKey.trim()) {
|
||||
const id = "provider-default-google";
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, "Gemini (Env)", "google-genai", googleKey.trim(), 1, "gemini-2.5-flash", "generative", 32768);
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
"Gemini (Env)",
|
||||
"google-genai",
|
||||
googleKey.trim(),
|
||||
1,
|
||||
"gemini-2.5-flash",
|
||||
"generative",
|
||||
32768,
|
||||
);
|
||||
hasInsertedGenerative = true;
|
||||
|
||||
const embedId = "provider-default-google-embed";
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(embedId, "Gemini Embed (Env)", "google-genai", googleKey.trim(), 1, "gemini-embedding-001", "embedding", 0);
|
||||
`,
|
||||
).run(
|
||||
embedId,
|
||||
"Gemini Embed (Env)",
|
||||
"google-genai",
|
||||
googleKey.trim(),
|
||||
1,
|
||||
"gemini-embedding-001",
|
||||
"embedding",
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
if (openRouterKey && openRouterKey.trim()) {
|
||||
const id = "provider-default-openrouter";
|
||||
const isActive = hasInsertedGenerative ? 0 : 1;
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, "OpenRouter (Env)", "openrouter", openRouterKey.trim(), isActive, "google/gemini-2.5-flash", "generative", 32768);
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
"OpenRouter (Env)",
|
||||
"openrouter",
|
||||
openRouterKey.trim(),
|
||||
isActive,
|
||||
"google/gemini-2.5-flash",
|
||||
"generative",
|
||||
32768,
|
||||
);
|
||||
}
|
||||
}
|
||||
hasBootstrapped = true;
|
||||
@@ -112,7 +155,7 @@ function getSettingsDb() {
|
||||
} catch {
|
||||
// ignore write lock issues or other DB errors during bootstrap
|
||||
}
|
||||
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
@@ -138,7 +181,12 @@ export class ProviderManager {
|
||||
isActive: r.isActive === 1,
|
||||
modelName: r.modelName || undefined,
|
||||
type: (r.type as "generative" | "embedding") || "generative",
|
||||
maxContext: r.maxContext !== undefined && r.maxContext !== null ? r.maxContext : (r.type === "embedding" ? 0 : 32768),
|
||||
maxContext:
|
||||
r.maxContext !== undefined && r.maxContext !== null
|
||||
? r.maxContext
|
||||
: r.type === "embedding"
|
||||
? 0
|
||||
: 32768,
|
||||
}));
|
||||
} finally {
|
||||
db.close();
|
||||
@@ -151,24 +199,51 @@ export class ProviderManager {
|
||||
apiKey: string,
|
||||
modelName?: string,
|
||||
type: "generative" | "embedding" = "generative",
|
||||
maxContext?: number
|
||||
maxContext?: number,
|
||||
): ModelProviderInstance {
|
||||
const db = getSettingsDb();
|
||||
try {
|
||||
const id = "provider-" + Date.now();
|
||||
const activeCount = db
|
||||
.prepare(`SELECT COUNT(*) as count FROM provider_instances WHERE isActive = 1 AND type = ?`)
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as count FROM provider_instances WHERE isActive = 1 AND type = ?`,
|
||||
)
|
||||
.get(type) as { count: number };
|
||||
const isActive = activeCount.count === 0 ? 1 : 0;
|
||||
|
||||
const actualMaxContext = maxContext !== undefined ? maxContext : (type === "generative" ? 32768 : 0);
|
||||
|
||||
db.prepare(`
|
||||
const actualMaxContext =
|
||||
maxContext !== undefined
|
||||
? maxContext
|
||||
: type === "generative"
|
||||
? 32768
|
||||
: 0;
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, name, providerName, apiKey, isActive, modelName || null, type, actualMaxContext);
|
||||
|
||||
return { id, name, providerName, apiKey, isActive: isActive === 1, modelName, type, maxContext: actualMaxContext };
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
name,
|
||||
providerName,
|
||||
apiKey,
|
||||
isActive,
|
||||
modelName || null,
|
||||
type,
|
||||
actualMaxContext,
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
providerName,
|
||||
apiKey,
|
||||
isActive: isActive === 1,
|
||||
modelName,
|
||||
type,
|
||||
maxContext: actualMaxContext,
|
||||
};
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
@@ -177,15 +252,19 @@ export class ProviderManager {
|
||||
static delete(id: string): void {
|
||||
const db = getSettingsDb();
|
||||
try {
|
||||
const provider = db.prepare(`SELECT isActive, type FROM provider_instances WHERE id = ?`).get(id) as { isActive: number; type: string } | undefined;
|
||||
const provider = db
|
||||
.prepare(`SELECT isActive, type FROM provider_instances WHERE id = ?`)
|
||||
.get(id) as { isActive: number; type: string } | undefined;
|
||||
db.prepare(`DELETE FROM provider_instances WHERE id = ?`).run(id);
|
||||
|
||||
|
||||
if (provider && provider.isActive === 1) {
|
||||
const next = db
|
||||
.prepare(`SELECT id FROM provider_instances WHERE type = ? LIMIT 1`)
|
||||
.get(provider.type) as { id: string } | undefined;
|
||||
if (next) {
|
||||
db.prepare(`UPDATE provider_instances SET isActive = 1 WHERE id = ?`).run(next.id);
|
||||
db.prepare(
|
||||
`UPDATE provider_instances SET isActive = 1 WHERE id = ?`,
|
||||
).run(next.id);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -196,10 +275,16 @@ export class ProviderManager {
|
||||
static setActive(id: string): void {
|
||||
const db = getSettingsDb();
|
||||
try {
|
||||
const target = db.prepare(`SELECT type FROM provider_instances WHERE id = ?`).get(id) as { type: string } | undefined;
|
||||
const target = db
|
||||
.prepare(`SELECT type FROM provider_instances WHERE id = ?`)
|
||||
.get(id) as { type: string } | undefined;
|
||||
if (target) {
|
||||
db.prepare(`UPDATE provider_instances SET isActive = 0 WHERE type = ?`).run(target.type);
|
||||
db.prepare(`UPDATE provider_instances SET isActive = 1 WHERE id = ?`).run(id);
|
||||
db.prepare(
|
||||
`UPDATE provider_instances SET isActive = 0 WHERE type = ?`,
|
||||
).run(target.type);
|
||||
db.prepare(
|
||||
`UPDATE provider_instances SET isActive = 1 WHERE id = ?`,
|
||||
).run(id);
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
@@ -213,75 +298,64 @@ export class ProviderManager {
|
||||
apiKey?: string,
|
||||
modelName?: string,
|
||||
type: "generative" | "embedding" = "generative",
|
||||
maxContext?: number
|
||||
maxContext?: number,
|
||||
): void {
|
||||
const db = getSettingsDb();
|
||||
try {
|
||||
const actualMaxContext = maxContext !== undefined ? maxContext : (type === "generative" ? 32768 : 0);
|
||||
const actualMaxContext =
|
||||
maxContext !== undefined
|
||||
? maxContext
|
||||
: type === "generative"
|
||||
? 32768
|
||||
: 0;
|
||||
if (apiKey && apiKey.trim()) {
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE provider_instances
|
||||
SET name = ?, providerName = ?, apiKey = ?, modelName = ?, type = ?, maxContext = ?
|
||||
WHERE id = ?
|
||||
`).run(name, providerName, apiKey, modelName || null, type, actualMaxContext, id);
|
||||
`,
|
||||
).run(
|
||||
name,
|
||||
providerName,
|
||||
apiKey,
|
||||
modelName || null,
|
||||
type,
|
||||
actualMaxContext,
|
||||
id,
|
||||
);
|
||||
} else {
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE provider_instances
|
||||
SET name = ?, providerName = ?, modelName = ?, type = ?, maxContext = ?
|
||||
WHERE id = ?
|
||||
`).run(name, providerName, modelName || null, type, actualMaxContext, id);
|
||||
`,
|
||||
).run(
|
||||
name,
|
||||
providerName,
|
||||
modelName || null,
|
||||
type,
|
||||
actualMaxContext,
|
||||
id,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
static getActive(type: "generative" | "embedding" = "generative"): ModelProviderInstance | null {
|
||||
static getActive(
|
||||
type: "generative" | "embedding" = "generative",
|
||||
): ModelProviderInstance | null {
|
||||
const db = getSettingsDb();
|
||||
try {
|
||||
const row = db.prepare(`SELECT * FROM provider_instances WHERE isActive = 1 AND type = ?`).get(type) as {
|
||||
id: string;
|
||||
name: string;
|
||||
providerName: string;
|
||||
apiKey: string;
|
||||
isActive: number;
|
||||
modelName?: string;
|
||||
type: string;
|
||||
maxContext?: number;
|
||||
} | undefined;
|
||||
|
||||
if (!row) {
|
||||
const totalCount = db.prepare(`SELECT COUNT(*) as count FROM provider_instances`).get() as { count: number };
|
||||
if (totalCount.count === 0) {
|
||||
const googleKey = process.env.GOOGLE_API_KEY;
|
||||
const openRouterKey = process.env.OPENROUTER_API_KEY;
|
||||
let hasInsertedGenerative = false;
|
||||
|
||||
if (googleKey && googleKey.trim()) {
|
||||
const id = "provider-default-google";
|
||||
db.prepare(`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, "Gemini (Env)", "google-genai", googleKey.trim(), 1, "gemini-2.5-flash", "generative", 32768);
|
||||
hasInsertedGenerative = true;
|
||||
|
||||
const embedId = "provider-default-google-embed";
|
||||
db.prepare(`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(embedId, "Gemini Embed (Env)", "google-genai", googleKey.trim(), 1, "gemini-embedding-001", "embedding", 0);
|
||||
}
|
||||
|
||||
if (openRouterKey && openRouterKey.trim()) {
|
||||
const id = "provider-default-openrouter";
|
||||
const isActive = hasInsertedGenerative ? 0 : 1;
|
||||
db.prepare(`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, "OpenRouter (Env)", "openrouter", openRouterKey.trim(), isActive, "google/gemini-2.5-flash", "generative", 32768);
|
||||
}
|
||||
|
||||
const retryRow = db.prepare(`SELECT * FROM provider_instances WHERE isActive = 1 AND type = ?`).get(type) as {
|
||||
const row = db
|
||||
.prepare(
|
||||
`SELECT * FROM provider_instances WHERE isActive = 1 AND type = ?`,
|
||||
)
|
||||
.get(type) as
|
||||
| {
|
||||
id: string;
|
||||
name: string;
|
||||
providerName: string;
|
||||
@@ -290,7 +364,91 @@ export class ProviderManager {
|
||||
modelName?: string;
|
||||
type: string;
|
||||
maxContext?: number;
|
||||
} | undefined;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (!row) {
|
||||
const totalCount = db
|
||||
.prepare(`SELECT COUNT(*) as count FROM provider_instances`)
|
||||
.get() as { count: number };
|
||||
if (totalCount.count === 0) {
|
||||
const googleKey = process.env.GOOGLE_API_KEY;
|
||||
const openRouterKey = process.env.OPENROUTER_API_KEY;
|
||||
let hasInsertedGenerative = false;
|
||||
|
||||
if (googleKey && googleKey.trim()) {
|
||||
const id = "provider-default-google";
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
"Gemini (Env)",
|
||||
"google-genai",
|
||||
googleKey.trim(),
|
||||
1,
|
||||
"gemini-2.5-flash",
|
||||
"generative",
|
||||
32768,
|
||||
);
|
||||
hasInsertedGenerative = true;
|
||||
|
||||
const embedId = "provider-default-google-embed";
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
embedId,
|
||||
"Gemini Embed (Env)",
|
||||
"google-genai",
|
||||
googleKey.trim(),
|
||||
1,
|
||||
"gemini-embedding-001",
|
||||
"embedding",
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
if (openRouterKey && openRouterKey.trim()) {
|
||||
const id = "provider-default-openrouter";
|
||||
const isActive = hasInsertedGenerative ? 0 : 1;
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
"OpenRouter (Env)",
|
||||
"openrouter",
|
||||
openRouterKey.trim(),
|
||||
isActive,
|
||||
"google/gemini-2.5-flash",
|
||||
"generative",
|
||||
32768,
|
||||
);
|
||||
}
|
||||
|
||||
const retryRow = db
|
||||
.prepare(
|
||||
`SELECT * FROM provider_instances WHERE isActive = 1 AND type = ?`,
|
||||
)
|
||||
.get(type) as
|
||||
| {
|
||||
id: string;
|
||||
name: string;
|
||||
providerName: string;
|
||||
apiKey: string;
|
||||
isActive: number;
|
||||
modelName?: string;
|
||||
type: string;
|
||||
maxContext?: number;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (retryRow) {
|
||||
return {
|
||||
@@ -301,24 +459,36 @@ export class ProviderManager {
|
||||
isActive: true,
|
||||
modelName: retryRow.modelName || undefined,
|
||||
type: retryRow.type as "generative" | "embedding",
|
||||
maxContext: retryRow.maxContext !== undefined && retryRow.maxContext !== null ? retryRow.maxContext : (retryRow.type === "embedding" ? 0 : 32768),
|
||||
maxContext:
|
||||
retryRow.maxContext !== undefined &&
|
||||
retryRow.maxContext !== null
|
||||
? retryRow.maxContext
|
||||
: retryRow.type === "embedding"
|
||||
? 0
|
||||
: 32768,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no active row but some rows exist, return the first one as active, or update it
|
||||
const firstRow = db.prepare(`SELECT * FROM provider_instances WHERE type = ? LIMIT 1`).get(type) as {
|
||||
id: string;
|
||||
name: string;
|
||||
providerName: string;
|
||||
apiKey: string;
|
||||
isActive: number;
|
||||
modelName?: string;
|
||||
type: string;
|
||||
maxContext?: number;
|
||||
} | undefined;
|
||||
const firstRow = db
|
||||
.prepare(`SELECT * FROM provider_instances WHERE type = ? LIMIT 1`)
|
||||
.get(type) as
|
||||
| {
|
||||
id: string;
|
||||
name: string;
|
||||
providerName: string;
|
||||
apiKey: string;
|
||||
isActive: number;
|
||||
modelName?: string;
|
||||
type: string;
|
||||
maxContext?: number;
|
||||
}
|
||||
| undefined;
|
||||
if (firstRow) {
|
||||
db.prepare(`UPDATE provider_instances SET isActive = 1 WHERE id = ?`).run(firstRow.id);
|
||||
db.prepare(
|
||||
`UPDATE provider_instances SET isActive = 1 WHERE id = ?`,
|
||||
).run(firstRow.id);
|
||||
return {
|
||||
id: firstRow.id,
|
||||
name: firstRow.name,
|
||||
@@ -327,7 +497,12 @@ export class ProviderManager {
|
||||
isActive: true,
|
||||
modelName: firstRow.modelName || undefined,
|
||||
type: firstRow.type as "generative" | "embedding",
|
||||
maxContext: firstRow.maxContext !== undefined && firstRow.maxContext !== null ? firstRow.maxContext : (firstRow.type === "embedding" ? 0 : 32768),
|
||||
maxContext:
|
||||
firstRow.maxContext !== undefined && firstRow.maxContext !== null
|
||||
? firstRow.maxContext
|
||||
: firstRow.type === "embedding"
|
||||
? 0
|
||||
: 32768,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -341,7 +516,12 @@ export class ProviderManager {
|
||||
isActive: true,
|
||||
modelName: row.modelName || undefined,
|
||||
type: (row.type as "generative" | "embedding") || "generative",
|
||||
maxContext: row.maxContext !== undefined && row.maxContext !== null ? row.maxContext : (row.type === "embedding" ? 0 : 32768),
|
||||
maxContext:
|
||||
row.maxContext !== undefined && row.maxContext !== null
|
||||
? row.maxContext
|
||||
: row.type === "embedding"
|
||||
? 0
|
||||
: 32768,
|
||||
};
|
||||
} catch {
|
||||
const googleKey = process.env.GOOGLE_API_KEY;
|
||||
@@ -396,12 +576,14 @@ export class ProviderManager {
|
||||
static getMappings(): Record<string, string> {
|
||||
const db = getSettingsDb();
|
||||
try {
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS provider_mappings (
|
||||
task TEXT PRIMARY KEY,
|
||||
providerInstanceId TEXT NOT NULL
|
||||
)
|
||||
`).run();
|
||||
`,
|
||||
).run();
|
||||
const rows = db.prepare(`SELECT * FROM provider_mappings`).all() as {
|
||||
task: string;
|
||||
providerInstanceId: string;
|
||||
@@ -419,20 +601,24 @@ export class ProviderManager {
|
||||
static setMapping(task: string, providerInstanceId: string): void {
|
||||
const db = getSettingsDb();
|
||||
try {
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS provider_mappings (
|
||||
task TEXT PRIMARY KEY,
|
||||
providerInstanceId TEXT NOT NULL
|
||||
)
|
||||
`).run();
|
||||
`,
|
||||
).run();
|
||||
if (!providerInstanceId) {
|
||||
db.prepare(`DELETE FROM provider_mappings WHERE task = ?`).run(task);
|
||||
} else {
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_mappings (task, providerInstanceId)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(task) DO UPDATE SET providerInstanceId = excluded.providerInstanceId
|
||||
`).run(task, providerInstanceId);
|
||||
`,
|
||||
).run(task, providerInstanceId);
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { z } from "zod";
|
||||
import { ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";
|
||||
import { ILLMProvider, LLMRequest, LLMResponse, LLMCallRecord, IEmbeddingProvider } from "../llm.js";
|
||||
import {
|
||||
ChatGoogleGenerativeAI,
|
||||
GoogleGenerativeAIEmbeddings,
|
||||
} from "@langchain/google-genai";
|
||||
import {
|
||||
ILLMProvider,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
LLMCallRecord,
|
||||
IEmbeddingProvider,
|
||||
} from "../llm.js";
|
||||
import { llmConfig } from "../config.js";
|
||||
import { ProviderManager } from "../provider-manager.js";
|
||||
|
||||
export class GeminiProvider implements ILLMProvider {
|
||||
static readonly providerId = "google-genai";
|
||||
static readonly displayName = "Google Gemini";
|
||||
static readonly description = "Official Gemini integration using Google Gen AI SDK";
|
||||
static readonly description =
|
||||
"Official Gemini integration using Google Gen AI SDK";
|
||||
static readonly defaultModel = "gemini-2.5-flash";
|
||||
|
||||
providerName = "Gemini";
|
||||
@@ -17,7 +27,12 @@ export class GeminiProvider implements ILLMProvider {
|
||||
private maxContextUsed?: number;
|
||||
lastCalls: LLMCallRecord[] = [];
|
||||
|
||||
constructor(apiKey?: string, modelName?: string, providerInstanceName?: string, maxContext?: number) {
|
||||
constructor(
|
||||
apiKey?: string,
|
||||
modelName?: string,
|
||||
providerInstanceName?: string,
|
||||
maxContext?: number,
|
||||
) {
|
||||
let key = apiKey;
|
||||
let model = modelName;
|
||||
this.providerInstanceName = providerInstanceName;
|
||||
@@ -47,7 +62,9 @@ export class GeminiProvider implements ILLMProvider {
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new Error("GOOGLE_API_KEY is required to initialize GeminiProvider");
|
||||
throw new Error(
|
||||
"GOOGLE_API_KEY is required to initialize GeminiProvider",
|
||||
);
|
||||
}
|
||||
|
||||
this.modelNameUsed = model || "gemini-2.5-flash";
|
||||
@@ -60,7 +77,9 @@ export class GeminiProvider implements ILLMProvider {
|
||||
async generateStructuredResponse<T extends z.ZodTypeAny>(
|
||||
request: LLMRequest<T>,
|
||||
): Promise<LLMResponse<z.infer<T>>> {
|
||||
const structuredModel = this.model.withStructuredOutput(request.schema, { includeRaw: true });
|
||||
const structuredModel = this.model.withStructuredOutput(request.schema, {
|
||||
includeRaw: true,
|
||||
});
|
||||
const result = (await structuredModel.invoke([
|
||||
{ role: "system", content: request.systemPrompt },
|
||||
{ role: "user", content: request.userContext },
|
||||
@@ -84,7 +103,8 @@ export class GeminiProvider implements ILLMProvider {
|
||||
totalTokens: raw?.usage_metadata?.total_tokens || 0,
|
||||
modelName: this.modelNameUsed,
|
||||
providerInstanceName: this.providerInstanceName || "Default",
|
||||
maxContext: this.maxContextUsed !== undefined ? this.maxContextUsed : 32768,
|
||||
maxContext:
|
||||
this.maxContextUsed !== undefined ? this.maxContextUsed : 32768,
|
||||
};
|
||||
|
||||
this.lastCalls.push({
|
||||
@@ -123,7 +143,9 @@ export class GeminiEmbeddingProvider implements IEmbeddingProvider {
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new Error("GOOGLE_API_KEY is required to initialize GeminiEmbeddingProvider");
|
||||
throw new Error(
|
||||
"GOOGLE_API_KEY is required to initialize GeminiEmbeddingProvider",
|
||||
);
|
||||
}
|
||||
|
||||
this.model = new GoogleGenerativeAIEmbeddings({
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { z } from "zod";
|
||||
import { ILLMProvider, LLMRequest, LLMResponse, LLMCallRecord, IEmbeddingProvider } from "../llm.js";
|
||||
import {
|
||||
ILLMProvider,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
LLMCallRecord,
|
||||
IEmbeddingProvider,
|
||||
} from "../llm.js";
|
||||
|
||||
export class MockLLMProvider implements ILLMProvider {
|
||||
static readonly providerId = "mock";
|
||||
static readonly displayName = "Mock LLM Provider";
|
||||
static readonly description = "Stateless mock provider for testing and offline development";
|
||||
static readonly description =
|
||||
"Stateless mock provider for testing and offline development";
|
||||
static readonly defaultModel = "mock";
|
||||
|
||||
providerName = "mock";
|
||||
@@ -30,7 +37,10 @@ export class MockLLMProvider implements ILLMProvider {
|
||||
const parsed = request.schema.parse(next);
|
||||
return { success: true, data: parsed, usage };
|
||||
} catch (e) {
|
||||
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||||
return {
|
||||
success: false,
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { z } from "zod";
|
||||
import { ChatOpenRouter } from "@langchain/openrouter";
|
||||
import { ILLMProvider, LLMRequest, LLMResponse, LLMCallRecord } from "../llm.js";
|
||||
import {
|
||||
ILLMProvider,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
LLMCallRecord,
|
||||
} from "../llm.js";
|
||||
import { llmConfig } from "../config.js";
|
||||
import { ProviderManager } from "../provider-manager.js";
|
||||
|
||||
export class OpenRouterProvider implements ILLMProvider {
|
||||
static readonly providerId = "openrouter";
|
||||
static readonly displayName = "OpenRouter";
|
||||
static readonly description = "Multi-model router supporting Anthropic, OpenAI, DeepSeek, and local models";
|
||||
static readonly description =
|
||||
"Multi-model router supporting Anthropic, OpenAI, DeepSeek, and local models";
|
||||
static readonly defaultModel = "google/gemini-2.5-flash";
|
||||
|
||||
providerName = "OpenRouter";
|
||||
@@ -17,7 +23,12 @@ export class OpenRouterProvider implements ILLMProvider {
|
||||
private maxContextUsed?: number;
|
||||
lastCalls: LLMCallRecord[] = [];
|
||||
|
||||
constructor(apiKey?: string, modelName?: string, providerInstanceName?: string, maxContext?: number) {
|
||||
constructor(
|
||||
apiKey?: string,
|
||||
modelName?: string,
|
||||
providerInstanceName?: string,
|
||||
maxContext?: number,
|
||||
) {
|
||||
let key = apiKey;
|
||||
let model = modelName;
|
||||
this.providerInstanceName = providerInstanceName;
|
||||
@@ -47,7 +58,9 @@ export class OpenRouterProvider implements ILLMProvider {
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new Error("OPENROUTER_API_KEY is required to initialize OpenRouterProvider");
|
||||
throw new Error(
|
||||
"OPENROUTER_API_KEY is required to initialize OpenRouterProvider",
|
||||
);
|
||||
}
|
||||
|
||||
this.modelNameUsed = model || "google/gemini-2.5-flash";
|
||||
@@ -60,7 +73,9 @@ export class OpenRouterProvider implements ILLMProvider {
|
||||
async generateStructuredResponse<T extends z.ZodTypeAny>(
|
||||
request: LLMRequest<T>,
|
||||
): Promise<LLMResponse<z.infer<T>>> {
|
||||
const structuredModel = this.model.withStructuredOutput(request.schema, { includeRaw: true });
|
||||
const structuredModel = this.model.withStructuredOutput(request.schema, {
|
||||
includeRaw: true,
|
||||
});
|
||||
const result = (await structuredModel.invoke([
|
||||
{ role: "system", content: request.systemPrompt },
|
||||
{ role: "user", content: request.userContext },
|
||||
@@ -84,7 +99,8 @@ export class OpenRouterProvider implements ILLMProvider {
|
||||
totalTokens: raw?.usage_metadata?.total_tokens || 0,
|
||||
modelName: this.modelNameUsed,
|
||||
providerInstanceName: this.providerInstanceName || "Default",
|
||||
maxContext: this.maxContextUsed !== undefined ? this.maxContextUsed : 32768,
|
||||
maxContext:
|
||||
this.maxContextUsed !== undefined ? this.maxContextUsed : 32768,
|
||||
};
|
||||
|
||||
this.lastCalls.push({
|
||||
|
||||
@@ -72,7 +72,7 @@ describe("MockEmbeddingProvider Unit Tests (Tier 1)", () => {
|
||||
expect(vec1.length).toBe(768);
|
||||
expect(vec2.length).toBe(768);
|
||||
expect(vec1).toEqual(vec2); // Deterministic
|
||||
|
||||
|
||||
// Ensure values are numbers between -1.0 and 1.0 (since they are generated with Math.sin)
|
||||
expect(typeof vec1[0]).toBe("number");
|
||||
expect(vec1[0]).toBeGreaterThanOrEqual(-1.0);
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("OpenRouterProvider Unit Tests (Tier 1)", () => {
|
||||
// Save current config
|
||||
const originalKey = llmConfig.OPENROUTER_API_KEY;
|
||||
llmConfig.OPENROUTER_API_KEY = "env-dummy-key";
|
||||
|
||||
|
||||
try {
|
||||
const provider = new OpenRouterProvider();
|
||||
expect(provider.providerName).toBe("OpenRouter");
|
||||
@@ -61,7 +61,7 @@ describe("OpenRouterProvider Unit Tests (Tier 1)", () => {
|
||||
|
||||
try {
|
||||
expect(() => new OpenRouterProvider()).toThrow(
|
||||
"OPENROUTER_API_KEY is required to initialize OpenRouterProvider"
|
||||
"OPENROUTER_API_KEY is required to initialize OpenRouterProvider",
|
||||
);
|
||||
} finally {
|
||||
llmConfig.OPENROUTER_API_KEY = originalKey;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { ProviderManager, setDbPathOverride, resetHasBootstrapped } from "../src/index.js";
|
||||
import {
|
||||
ProviderManager,
|
||||
setDbPathOverride,
|
||||
resetHasBootstrapped,
|
||||
} from "../src/index.js";
|
||||
|
||||
describe("ProviderManager Bootstrapping & CRUD Unit Tests", () => {
|
||||
let tempDbPath: string;
|
||||
@@ -17,7 +21,10 @@ describe("ProviderManager Bootstrapping & CRUD Unit Tests", () => {
|
||||
resetHasBootstrapped();
|
||||
|
||||
// Generate a unique temp database path for this test run
|
||||
tempDbPath = path.resolve(process.cwd(), `test-settings-${Date.now()}-${Math.random().toString(36).substring(2)}.db`);
|
||||
tempDbPath = path.resolve(
|
||||
process.cwd(),
|
||||
`test-settings-${Date.now()}-${Math.random().toString(36).substring(2)}.db`,
|
||||
);
|
||||
setDbPathOverride(tempDbPath);
|
||||
});
|
||||
|
||||
@@ -76,8 +83,14 @@ describe("ProviderManager Bootstrapping & CRUD Unit Tests", () => {
|
||||
expect(bootstrapped.isActive).toBe(true);
|
||||
|
||||
// Edit name and key
|
||||
ProviderManager.update(bootstrapped.id, "My Gemini Key", "google-genai", "new-secret-key", "gemini-2.5-pro");
|
||||
|
||||
ProviderManager.update(
|
||||
bootstrapped.id,
|
||||
"My Gemini Key",
|
||||
"google-genai",
|
||||
"new-secret-key",
|
||||
"gemini-2.5-pro",
|
||||
);
|
||||
|
||||
const listAfterUpdate = ProviderManager.list();
|
||||
expect(listAfterUpdate.length).toBe(2);
|
||||
const updated = listAfterUpdate.find((p) => p.id === bootstrapped.id);
|
||||
|
||||
@@ -26,7 +26,11 @@ export function serializeSubjectiveBufferEntry(
|
||||
const isSelf = viewer.id === entry.intent.actorId;
|
||||
|
||||
if (isSelf) {
|
||||
let details = (entry.intent.selfDescription || entry.intent.description || entry.intent.originalText).trim();
|
||||
let details = (
|
||||
entry.intent.selfDescription ||
|
||||
entry.intent.description ||
|
||||
entry.intent.originalText
|
||||
).trim();
|
||||
if (details.length > 0) {
|
||||
details = details.charAt(0).toUpperCase() + details.slice(1);
|
||||
}
|
||||
@@ -69,7 +73,9 @@ export class BufferRepository {
|
||||
`);
|
||||
|
||||
try {
|
||||
this.db.exec(`ALTER TABLE buffer_entries ADD COLUMN pinned INTEGER DEFAULT 0;`);
|
||||
this.db.exec(
|
||||
`ALTER TABLE buffer_entries ADD COLUMN pinned INTEGER DEFAULT 0;`,
|
||||
);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { Entity, naturalizeTime } from "@omnia/core";
|
||||
import { BufferEntry, serializeSubjectiveBufferEntry, BufferRepository } from "./buffer.js";
|
||||
import {
|
||||
BufferEntry,
|
||||
serializeSubjectiveBufferEntry,
|
||||
BufferRepository,
|
||||
} from "./buffer.js";
|
||||
import { LedgerEntry, LedgerRepository } from "./ledger.js";
|
||||
import { ILLMProvider, IEmbeddingProvider } from "@omnia/llm";
|
||||
|
||||
@@ -56,12 +60,18 @@ function checkSceneExit(entity: Entity, bufferEntries: BufferEntry[]): boolean {
|
||||
|
||||
// Find the location of the most recent buffer entries
|
||||
const lastEntry = bufferEntries[bufferEntries.length - 1];
|
||||
if (lastEntry.locationId && entity.locationId && lastEntry.locationId !== entity.locationId) {
|
||||
if (
|
||||
lastEntry.locationId &&
|
||||
entity.locationId &&
|
||||
lastEntry.locationId !== entity.locationId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also check if there are entries from different locations in the buffer
|
||||
const locations = new Set(bufferEntries.map(e => e.locationId).filter(loc => loc !== null));
|
||||
const locations = new Set(
|
||||
bufferEntries.map((e) => e.locationId).filter((loc) => loc !== null),
|
||||
);
|
||||
if (locations.size > 1) {
|
||||
return true;
|
||||
}
|
||||
@@ -75,17 +85,25 @@ function checkIdleDecay(bufferEntries: BufferEntry[]): boolean {
|
||||
|
||||
// Check the last N entries
|
||||
const lastN = bufferEntries.slice(-N);
|
||||
return lastN.every(e => e.intent.type === "monologue");
|
||||
return lastN.every((e) => e.intent.type === "monologue");
|
||||
}
|
||||
|
||||
function checkAttributeTrigger(entity: Entity): boolean {
|
||||
const consciousness = entity.attributes.get("consciousness");
|
||||
if (consciousness && consciousness.getValue().toLowerCase() === "unconscious") {
|
||||
if (
|
||||
consciousness &&
|
||||
consciousness.getValue().toLowerCase() === "unconscious"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const status = entity.attributes.get("status");
|
||||
if (status && ["unconscious", "asleep", "dead", "inactive"].includes(status.getValue().toLowerCase())) {
|
||||
if (
|
||||
status &&
|
||||
["unconscious", "asleep", "dead", "inactive"].includes(
|
||||
status.getValue().toLowerCase(),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -108,7 +126,7 @@ export function checkHandoffTrigger(
|
||||
// Involuntary triggers first (hard)
|
||||
if (maxContext > 0) {
|
||||
const memoryLength = getMemorySectionLength(entity, bufferEntries, now);
|
||||
const charCeiling = maxContext * 4 * 0.60;
|
||||
const charCeiling = maxContext * 4 * 0.6;
|
||||
if (memoryLength > charCeiling) {
|
||||
return "involuntary";
|
||||
}
|
||||
@@ -201,10 +219,12 @@ export class HandoffEngine {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidatesList = candidates.map((entry) => {
|
||||
const serialized = serializeSubjectiveBufferEntry(entry, entity);
|
||||
return `ID: ${entry.id} | Timestamp: ${entry.timestamp} | Location: ${entry.locationId || "None"}\nContent: ${serialized}`;
|
||||
}).join("\n---\n");
|
||||
const candidatesList = candidates
|
||||
.map((entry) => {
|
||||
const serialized = serializeSubjectiveBufferEntry(entry, entity);
|
||||
return `ID: ${entry.id} | Timestamp: ${entry.timestamp} | Location: ${entry.locationId || "None"}\nContent: ${serialized}`;
|
||||
})
|
||||
.join("\n---\n");
|
||||
|
||||
const systemPrompt = `
|
||||
You are the memory Handoff Engine. Your task is to process a list of recent working memory buffer entries for an entity and select which memories to promote to the long-term Ledger, and which to forget or summarize.
|
||||
@@ -239,7 +259,11 @@ ${candidatesList}
|
||||
}
|
||||
|
||||
const result = response.data;
|
||||
const db = (this.bufferRepo as unknown as { db: { transaction: (fn: () => void) => void } }).db;
|
||||
const db = (
|
||||
this.bufferRepo as unknown as {
|
||||
db: { transaction: (fn: () => void) => void };
|
||||
}
|
||||
).db;
|
||||
|
||||
const ledgerEntries: LedgerEntry[] = [];
|
||||
for (const chunk of result.chunks) {
|
||||
@@ -252,7 +276,11 @@ ${candidatesList}
|
||||
}
|
||||
|
||||
ledgerEntries.push({
|
||||
id: "ledger-" + Math.random().toString(36).substr(2, 9) + "-" + Date.now(),
|
||||
id:
|
||||
"ledger-" +
|
||||
Math.random().toString(36).substr(2, 9) +
|
||||
"-" +
|
||||
Date.now(),
|
||||
ownerId: entity.id,
|
||||
timestamp: now.toISOString(),
|
||||
locationId: entity.locationId,
|
||||
|
||||
@@ -82,7 +82,7 @@ export class LedgerRepository {
|
||||
entry.importance,
|
||||
entry.embedding.length > 0
|
||||
? Buffer.from(new Float32Array(entry.embedding).buffer)
|
||||
: null
|
||||
: null,
|
||||
);
|
||||
|
||||
deleteEntities.run(entry.id);
|
||||
@@ -92,14 +92,18 @@ export class LedgerRepository {
|
||||
})();
|
||||
}
|
||||
|
||||
private mapRowToEntry(row: Record<string, unknown>, involvedEntityIds: string[]): LedgerEntry {
|
||||
private mapRowToEntry(
|
||||
row: Record<string, unknown>,
|
||||
involvedEntityIds: string[],
|
||||
): LedgerEntry {
|
||||
const embedding: number[] = row.embedding
|
||||
? Array.from(
|
||||
new Float32Array(
|
||||
(row.embedding as Buffer).buffer,
|
||||
(row.embedding as Buffer).byteOffset,
|
||||
(row.embedding as Buffer).byteLength / Float32Array.BYTES_PER_ELEMENT
|
||||
)
|
||||
(row.embedding as Buffer).byteLength /
|
||||
Float32Array.BYTES_PER_ELEMENT,
|
||||
),
|
||||
)
|
||||
: [];
|
||||
|
||||
@@ -123,7 +127,7 @@ export class LedgerRepository {
|
||||
SELECT id, owner_id, timestamp, location_id, content, quotes_json, importance, embedding
|
||||
FROM ledger_entries
|
||||
WHERE id = ?
|
||||
`
|
||||
`,
|
||||
)
|
||||
.get(id) as Record<string, unknown> | undefined;
|
||||
|
||||
@@ -133,11 +137,14 @@ export class LedgerRepository {
|
||||
.prepare(
|
||||
`
|
||||
SELECT entity_id FROM ledger_involved_entities WHERE entry_id = ?
|
||||
`
|
||||
`,
|
||||
)
|
||||
.all(id) as { entity_id: string }[];
|
||||
|
||||
return this.mapRowToEntry(row, entitiesRows.map((er) => er.entity_id));
|
||||
return this.mapRowToEntry(
|
||||
row,
|
||||
entitiesRows.map((er) => er.entity_id),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,7 +158,7 @@ export class LedgerRepository {
|
||||
ownerId: string,
|
||||
currentLocationId: string | null,
|
||||
currentInvolvedEntityIds: string[],
|
||||
limit: number = 20
|
||||
limit: number = 20,
|
||||
): LedgerEntry[] {
|
||||
let query = `
|
||||
SELECT DISTINCT le.id, le.owner_id, le.timestamp, le.location_id, le.content, le.quotes_json, le.importance, le.embedding
|
||||
@@ -182,7 +189,10 @@ export class LedgerRepository {
|
||||
`;
|
||||
params.push(limit);
|
||||
|
||||
const rows = this.db.prepare(query).all(...params) as Record<string, unknown>[];
|
||||
const rows = this.db.prepare(query).all(...params) as Record<
|
||||
string,
|
||||
unknown
|
||||
>[];
|
||||
|
||||
if (rows.length === 0) return [];
|
||||
|
||||
@@ -193,7 +203,7 @@ export class LedgerRepository {
|
||||
`
|
||||
SELECT entry_id, entity_id FROM ledger_involved_entities
|
||||
WHERE entry_id IN (${placeholders})
|
||||
`
|
||||
`,
|
||||
)
|
||||
.all(...entryIds) as { entry_id: string; entity_id: string }[];
|
||||
|
||||
@@ -205,7 +215,9 @@ export class LedgerRepository {
|
||||
entitiesMap.get(er.entry_id)!.push(er.entity_id);
|
||||
}
|
||||
|
||||
return rows.map((row) => this.mapRowToEntry(row, entitiesMap.get(row.id as string) || []));
|
||||
return rows.map((row) =>
|
||||
this.mapRowToEntry(row, entitiesMap.get(row.id as string) || []),
|
||||
);
|
||||
}
|
||||
|
||||
private fetchRawNeighbors(ownerId: string, timestamp: string): LedgerEntry[] {
|
||||
@@ -220,7 +232,7 @@ export class LedgerRepository {
|
||||
WHERE owner_id = ? AND timestamp < ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1
|
||||
`
|
||||
`,
|
||||
)
|
||||
.get(ownerId, timestamp) as Record<string, unknown> | undefined;
|
||||
|
||||
@@ -237,7 +249,7 @@ export class LedgerRepository {
|
||||
WHERE owner_id = ? AND timestamp > ?
|
||||
ORDER BY timestamp ASC
|
||||
LIMIT 1
|
||||
`
|
||||
`,
|
||||
)
|
||||
.get(ownerId, timestamp) as Record<string, unknown> | undefined;
|
||||
|
||||
@@ -269,16 +281,22 @@ export class LedgerRepository {
|
||||
importanceWeight?: number;
|
||||
relevanceWeight?: number;
|
||||
decayRate?: number;
|
||||
}
|
||||
},
|
||||
): LedgerEntry[] {
|
||||
const includeAssociativeNeighbors = options?.includeAssociativeNeighbors ?? false;
|
||||
const includeAssociativeNeighbors =
|
||||
options?.includeAssociativeNeighbors ?? false;
|
||||
const recencyWeight = options?.recencyWeight ?? 1.0;
|
||||
const importanceWeight = options?.importanceWeight ?? 1.0;
|
||||
const relevanceWeight = options?.relevanceWeight ?? 1.0;
|
||||
const decayRate = options?.decayRate ?? 0.99;
|
||||
|
||||
// Fetch candidate pool (limit 100 to provide enough options for Phase 2 ranking)
|
||||
const candidates = this.getRelevant(ownerId, currentLocationId, currentInvolvedEntityIds, 100);
|
||||
const candidates = this.getRelevant(
|
||||
ownerId,
|
||||
currentLocationId,
|
||||
currentInvolvedEntityIds,
|
||||
100,
|
||||
);
|
||||
if (candidates.length === 0) return [];
|
||||
|
||||
// Score candidates
|
||||
@@ -318,7 +336,10 @@ export class LedgerRepository {
|
||||
for (const entry of selected) {
|
||||
const rawNeighbors = this.fetchRawNeighbors(ownerId, entry.timestamp);
|
||||
for (const rn of rawNeighbors) {
|
||||
if (!finalEntries.some((fe) => fe.id === rn.id) && !neighborMap.has(rn.id)) {
|
||||
if (
|
||||
!finalEntries.some((fe) => fe.id === rn.id) &&
|
||||
!neighborMap.has(rn.id)
|
||||
) {
|
||||
neighborMap.set(rn.id, rn);
|
||||
}
|
||||
}
|
||||
@@ -333,7 +354,7 @@ export class LedgerRepository {
|
||||
`
|
||||
SELECT entry_id, entity_id FROM ledger_involved_entities
|
||||
WHERE entry_id IN (${placeholders})
|
||||
`
|
||||
`,
|
||||
)
|
||||
.all(...neighborIds) as { entry_id: string; entity_id: string }[];
|
||||
|
||||
@@ -353,7 +374,10 @@ export class LedgerRepository {
|
||||
}
|
||||
|
||||
// Sort chronologically ASC for the final prompt output
|
||||
finalEntries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
||||
finalEntries.sort(
|
||||
(a, b) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
|
||||
);
|
||||
|
||||
return finalEntries;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ describe("Memory Handoff Tests (Tier 1)", () => {
|
||||
// Add 12 older entries (older than 30 minutes)
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const minutesAgo = 60 - i;
|
||||
const timestamp = new Date(now.getTime() - minutesAgo * 60 * 1000).toISOString();
|
||||
const timestamp = new Date(
|
||||
now.getTime() - minutesAgo * 60 * 1000,
|
||||
).toISOString();
|
||||
entries.push({
|
||||
id: `entry-old-${i}`,
|
||||
ownerId: "alice",
|
||||
@@ -80,7 +82,13 @@ describe("Memory Handoff Tests (Tier 1)", () => {
|
||||
ownerId: "alice",
|
||||
timestamp: now.toISOString(),
|
||||
locationId: "room-1",
|
||||
intent: { type: "dialogue", originalText: "hello", description: "says hello", actorId: "alice", targetIds: [] },
|
||||
intent: {
|
||||
type: "dialogue",
|
||||
originalText: "hello",
|
||||
description: "says hello",
|
||||
actorId: "alice",
|
||||
targetIds: [],
|
||||
},
|
||||
};
|
||||
expect(checkHandoffTrigger(entity, [entryAtRoom1], now)).toBe("voluntary");
|
||||
|
||||
@@ -90,7 +98,13 @@ describe("Memory Handoff Tests (Tier 1)", () => {
|
||||
ownerId: "alice",
|
||||
timestamp: now.toISOString(),
|
||||
locationId: "room-2",
|
||||
intent: { type: "monologue", originalText: "think", description: "thinks", actorId: "alice", targetIds: [] },
|
||||
intent: {
|
||||
type: "monologue",
|
||||
originalText: "think",
|
||||
description: "thinks",
|
||||
actorId: "alice",
|
||||
targetIds: [],
|
||||
},
|
||||
}));
|
||||
expect(checkHandoffTrigger(entity, monologues, now)).toBe("voluntary");
|
||||
});
|
||||
@@ -114,7 +128,9 @@ describe("Memory Handoff Tests (Tier 1)", () => {
|
||||
|
||||
const entries: BufferEntry[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const timestamp = new Date(now.getTime() - (50 - i) * 60 * 1000).toISOString();
|
||||
const timestamp = new Date(
|
||||
now.getTime() - (50 - i) * 60 * 1000,
|
||||
).toISOString();
|
||||
const entry: BufferEntry = {
|
||||
id: `entry-${i}`,
|
||||
ownerId: "alice",
|
||||
@@ -147,14 +163,23 @@ describe("Memory Handoff Tests (Tier 1)", () => {
|
||||
|
||||
const llmProvider = new MockLLMProvider([mockHandoffResult]);
|
||||
const embedProvider = new MockEmbeddingProvider();
|
||||
const engine = new HandoffEngine(llmProvider, embedProvider, bufferRepo, ledgerRepo);
|
||||
const engine = new HandoffEngine(
|
||||
llmProvider,
|
||||
embedProvider,
|
||||
bufferRepo,
|
||||
ledgerRepo,
|
||||
);
|
||||
|
||||
const success = await engine.runHandoff(entity, entries, now);
|
||||
expect(success).toBe(true);
|
||||
|
||||
const ledgerRows = db.prepare("SELECT * FROM ledger_entries WHERE owner_id = ?").all("alice") as Record<string, unknown>[];
|
||||
const ledgerRows = db
|
||||
.prepare("SELECT * FROM ledger_entries WHERE owner_id = ?")
|
||||
.all("alice") as Record<string, unknown>[];
|
||||
expect(ledgerRows.length).toBe(1);
|
||||
expect(ledgerRows[0].content).toBe("Alice initiated dialogue and performed various tasks.");
|
||||
expect(ledgerRows[0].content).toBe(
|
||||
"Alice initiated dialogue and performed various tasks.",
|
||||
);
|
||||
expect(JSON.parse(ledgerRows[0].quotes_json)).toEqual(["Event 0"]);
|
||||
expect(ledgerRows[0].importance).toBe(5);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("LedgerRepository", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
db = new Database(":memory:");
|
||||
|
||||
|
||||
// We need to create a dummy objects table to satisfy foreign keys
|
||||
db.exec(`
|
||||
CREATE TABLE objects (
|
||||
@@ -51,7 +51,7 @@ describe("LedgerRepository", () => {
|
||||
expect(loaded?.content).toBe(entry.content);
|
||||
expect(loaded?.quotes).toEqual(entry.quotes);
|
||||
expect(loaded?.importance).toBe(5);
|
||||
|
||||
|
||||
// Check float precision
|
||||
expect(loaded?.embedding[0]).toBeCloseTo(0.1);
|
||||
expect(loaded?.embedding[1]).toBeCloseTo(0.2);
|
||||
@@ -113,7 +113,7 @@ describe("LedgerRepository", () => {
|
||||
});
|
||||
|
||||
const relevant = repo.getRelevant("alice", "loc1", ["bob"]);
|
||||
|
||||
|
||||
expect(relevant).toHaveLength(3);
|
||||
const ids = relevant.map((r) => r.id);
|
||||
expect(ids).toContain("mem_high_salience"); // due to importance >= 8
|
||||
@@ -210,16 +210,32 @@ describe("LedgerRepository", () => {
|
||||
});
|
||||
|
||||
// Without neighbors: only returns mem_target
|
||||
const withoutNeighbors = repo.retrieve("alice", "loc1", [], undefined, new Date("2024-01-10T14:00:00.000Z"), 1, {
|
||||
includeAssociativeNeighbors: false,
|
||||
});
|
||||
const withoutNeighbors = repo.retrieve(
|
||||
"alice",
|
||||
"loc1",
|
||||
[],
|
||||
undefined,
|
||||
new Date("2024-01-10T14:00:00.000Z"),
|
||||
1,
|
||||
{
|
||||
includeAssociativeNeighbors: false,
|
||||
},
|
||||
);
|
||||
expect(withoutNeighbors).toHaveLength(1);
|
||||
expect(withoutNeighbors[0].id).toBe("mem_target");
|
||||
|
||||
// With neighbors: returns preceding, target, and succeeding sorted chronologically
|
||||
const withNeighbors = repo.retrieve("alice", "loc1", [], undefined, new Date("2024-01-10T14:00:00.000Z"), 1, {
|
||||
includeAssociativeNeighbors: true,
|
||||
});
|
||||
const withNeighbors = repo.retrieve(
|
||||
"alice",
|
||||
"loc1",
|
||||
[],
|
||||
undefined,
|
||||
new Date("2024-01-10T14:00:00.000Z"),
|
||||
1,
|
||||
{
|
||||
includeAssociativeNeighbors: true,
|
||||
},
|
||||
);
|
||||
expect(withNeighbors).toHaveLength(3);
|
||||
expect(withNeighbors[0].id).toBe("mem_preceding");
|
||||
expect(withNeighbors[1].id).toBe("mem_target");
|
||||
|
||||
@@ -41,7 +41,9 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
|
||||
};
|
||||
|
||||
const result = serializeSubjectiveBufferEntry(entry, viewer);
|
||||
expect(result).toBe("The hooded figure says, 'Hello there' to the bartender");
|
||||
expect(result).toBe(
|
||||
"The hooded figure says, 'Hello there' to the bartender",
|
||||
);
|
||||
});
|
||||
|
||||
test("serializes action intent with outcome details", () => {
|
||||
@@ -69,7 +71,9 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
|
||||
};
|
||||
|
||||
const result = serializeSubjectiveBufferEntry(entry, viewer);
|
||||
expect(result).toBe('The hooded figure attempts to break the lock latch (Outcome: Failed - The lock is made of reinforced steel.)');
|
||||
expect(result).toBe(
|
||||
"The hooded figure attempts to break the lock latch (Outcome: Failed - The lock is made of reinforced steel.)",
|
||||
);
|
||||
});
|
||||
|
||||
test("serializes self-reference and unfamiliar actors", () => {
|
||||
@@ -110,7 +114,10 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const resultUnfamiliar = serializeSubjectiveBufferEntry(entryUnfamiliar, viewer);
|
||||
const resultUnfamiliar = serializeSubjectiveBufferEntry(
|
||||
entryUnfamiliar,
|
||||
viewer,
|
||||
);
|
||||
expect(resultUnfamiliar).toBe("An unfamiliar figure knocks on the door");
|
||||
});
|
||||
});
|
||||
@@ -118,7 +125,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
|
||||
describe("BufferRepository Persistence Tests (Tier 1)", () => {
|
||||
test("saves, loads, lists, and deletes buffer entries in SQLite database", () => {
|
||||
const db = new Database(":memory:");
|
||||
|
||||
|
||||
// We need SQLiteRepository to initialize the objects table because buffer_entries depends on objects(id) via FK
|
||||
const coreRepo = new SQLiteRepository(db);
|
||||
const repo = new BufferRepository(db);
|
||||
|
||||
@@ -12,17 +12,13 @@ describe("Scenario Validation & Schema Tests (Tier 1)", () => {
|
||||
description: "A spooky old manor.",
|
||||
startTime: "2026-07-09T08:00:00.000Z",
|
||||
world: {
|
||||
attributes: [
|
||||
{ name: "weather", value: "stormy", visibility: "PUBLIC" },
|
||||
],
|
||||
attributes: [{ name: "weather", value: "stormy", visibility: "PUBLIC" }],
|
||||
},
|
||||
locations: [
|
||||
{
|
||||
id: "lobby",
|
||||
parentId: null,
|
||||
attributes: [
|
||||
{ name: "light", value: "dim", visibility: "PUBLIC" },
|
||||
],
|
||||
attributes: [{ name: "light", value: "dim", visibility: "PUBLIC" }],
|
||||
connections: [
|
||||
{
|
||||
targetId: "kitchen",
|
||||
@@ -43,7 +39,12 @@ describe("Scenario Validation & Schema Tests (Tier 1)", () => {
|
||||
id: "investigator",
|
||||
locationId: "lobby",
|
||||
attributes: [
|
||||
{ name: "sanity", value: "100", visibility: "PRIVATE", allowedEntities: ["investigator"] },
|
||||
{
|
||||
name: "sanity",
|
||||
value: "100",
|
||||
visibility: "PRIVATE",
|
||||
allowedEntities: ["investigator"],
|
||||
},
|
||||
],
|
||||
aliases: {
|
||||
ghost: "shadowy specter",
|
||||
@@ -98,11 +99,16 @@ describe("Scenario Validation & Schema Tests (Tier 1)", () => {
|
||||
expect(world).not.toBeNull();
|
||||
expect(world!.id).toBe(targetWorldId);
|
||||
expect(world!.clock.get().toISOString()).toBe("2026-07-09T08:00:00.000Z");
|
||||
expect(world!.attributes.get("name")?.getValue()).toBe("Haunted House Mystery");
|
||||
expect(world!.attributes.get("name")?.getValue()).toBe(
|
||||
"Haunted House Mystery",
|
||||
);
|
||||
expect(world!.attributes.get("weather")?.getValue()).toBe("stormy");
|
||||
|
||||
// 2. Verify Locations loaded with connections & hierarchy
|
||||
const locations = coreRepo.listLocations(targetWorldId, (id, parentId) => new Location(id, parentId));
|
||||
const locations = coreRepo.listLocations(
|
||||
targetWorldId,
|
||||
(id, parentId) => new Location(id, parentId),
|
||||
);
|
||||
expect(locations).toHaveLength(2);
|
||||
|
||||
const lobby = locations.find((l) => l.id === "lobby");
|
||||
@@ -122,7 +128,9 @@ describe("Scenario Validation & Schema Tests (Tier 1)", () => {
|
||||
const loadedInvestigator = world!.getEntity("investigator");
|
||||
expect(loadedInvestigator).toBeDefined();
|
||||
expect(loadedInvestigator!.locationId).toBe("lobby");
|
||||
expect(loadedInvestigator!.attributes.get("sanity")?.getValue()).toBe("100");
|
||||
expect(loadedInvestigator!.attributes.get("sanity")?.getValue()).toBe(
|
||||
"100",
|
||||
);
|
||||
expect(loadedInvestigator!.aliases.get("ghost")).toBe("shadowy specter");
|
||||
|
||||
// 4. Verify pre-seeded memories loaded in BufferRepository
|
||||
|
||||
@@ -10,7 +10,10 @@ import { ScenarioLoader, ScenarioSchema } from "../src/index.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const SCENARIO_PATH = path.resolve(__dirname, "../../../content/demo/scenarios/talking-room.json");
|
||||
const SCENARIO_PATH = path.resolve(
|
||||
__dirname,
|
||||
"../../../content/demo/scenarios/talking-room.json",
|
||||
);
|
||||
|
||||
describe("Talking Room Demo Scenario Test (Tier 1)", () => {
|
||||
test("talking-room.json exists, parses, and loads correctly into database", async () => {
|
||||
@@ -37,17 +40,30 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => {
|
||||
expect(world).not.toBeNull();
|
||||
expect(world!.attributes.get("name")?.getValue()).toBe("Talking Room");
|
||||
expect(world!.attributes.get("name")?.visibility).toBe("PRIVATE");
|
||||
expect(world!.attributes.get("description")?.getValue()).toBe(scenarioJson.description);
|
||||
expect(world!.attributes.get("description")?.getValue()).toBe(
|
||||
scenarioJson.description,
|
||||
);
|
||||
expect(world!.attributes.get("description")?.visibility).toBe("PRIVATE");
|
||||
expect(world!.attributes.get("experiment_codename")?.getValue()).toBe("Project Tabula Rasa (Phase 3)");
|
||||
expect(world!.attributes.get("experiment_codename")?.visibility).toBe("PRIVATE");
|
||||
expect(world!.attributes.get("experiment_codename")?.getAllowedEntities()).toHaveLength(0); // System only!
|
||||
expect(world!.attributes.get("experiment_codename")?.getValue()).toBe(
|
||||
"Project Tabula Rasa (Phase 3)",
|
||||
);
|
||||
expect(world!.attributes.get("experiment_codename")?.visibility).toBe(
|
||||
"PRIVATE",
|
||||
);
|
||||
expect(
|
||||
world!.attributes.get("experiment_codename")?.getAllowedEntities(),
|
||||
).toHaveLength(0); // System only!
|
||||
|
||||
// 5. Assert location
|
||||
const locations = coreRepo.listLocations(worldInstanceId, (id, parentId) => new Location(id, parentId));
|
||||
const locations = coreRepo.listLocations(
|
||||
worldInstanceId,
|
||||
(id, parentId) => new Location(id, parentId),
|
||||
);
|
||||
expect(locations).toHaveLength(1);
|
||||
expect(locations[0].id).toBe("white-room");
|
||||
expect(locations[0].attributes.get("description")?.getValue()).toContain("A pristine, featureless room");
|
||||
expect(locations[0].attributes.get("description")?.getValue()).toContain(
|
||||
"A pristine, featureless room",
|
||||
);
|
||||
|
||||
// 6. Assert entities and their private attributes / allowedEntities
|
||||
const alphaId = "7c9b83b3-8cfb-4e89-8d77-626a5757d591";
|
||||
@@ -56,14 +72,14 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => {
|
||||
const alpha = world!.getEntity(alphaId);
|
||||
expect(alpha).toBeDefined();
|
||||
expect(alpha!.locationId).toBe("white-room");
|
||||
|
||||
|
||||
// Name visibility check
|
||||
const alphaName = alpha!.attributes.get("name")!;
|
||||
expect(alphaName.getValue()).toBe("Bob");
|
||||
expect(alphaName.visibility).toBe("PRIVATE");
|
||||
expect(alphaName.hasAccess(alphaId)).toBe(true);
|
||||
expect(alphaName.hasAccess(betaId)).toBe(false);
|
||||
|
||||
|
||||
// Check system-only attribute (neural_erasure_dose)
|
||||
const alphaDose = alpha!.attributes.get("neural_erasure_dose")!;
|
||||
expect(alphaDose.visibility).toBe("PRIVATE");
|
||||
@@ -81,7 +97,9 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => {
|
||||
// Verify subjective aliases can be dynamically resolved via AliasDeltaGenerator
|
||||
const { AliasDeltaGenerator } = await import("@omnia/architect");
|
||||
const { MockLLMProvider } = await import("@omnia/llm");
|
||||
const llmProvider = new MockLLMProvider([{ alias: "the person in the Beta jumpsuit" }]);
|
||||
const llmProvider = new MockLLMProvider([
|
||||
{ alias: "the person in the Beta jumpsuit" },
|
||||
]);
|
||||
const aliasGenerator = new AliasDeltaGenerator(llmProvider);
|
||||
|
||||
const generatedAlias = await aliasGenerator.generate(alpha!, beta!);
|
||||
@@ -94,7 +112,9 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => {
|
||||
const subjectiveState = serializeSubjectiveWorldState(world!, alphaId);
|
||||
expect(subjectiveState).toContain("You are at location: white-room");
|
||||
expect(subjectiveState).toContain("Location attributes:");
|
||||
expect(subjectiveState).toContain("description: A pristine, featureless room");
|
||||
expect(subjectiveState).toContain(
|
||||
"description: A pristine, featureless room",
|
||||
);
|
||||
expect(subjectiveState).toContain("lighting: Bright, uniform illumination");
|
||||
|
||||
// Verify objective world state serializes locations (physics awareness)
|
||||
@@ -102,7 +122,9 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => {
|
||||
const objectiveState = serializeObjectiveWorldState(world!);
|
||||
expect(objectiveState).toContain("Locations:");
|
||||
expect(objectiveState).toContain("- Location [ID: white-room]:");
|
||||
expect(objectiveState).toContain("description: A pristine, featureless room");
|
||||
expect(objectiveState).toContain(
|
||||
"description: A pristine, featureless room",
|
||||
);
|
||||
|
||||
// 7. Assert initial pre-seeded memories
|
||||
const alphaMemories = bufferRepo.listForOwner(alphaId);
|
||||
|
||||
11801
pnpm-lock.yaml
generated
11801
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -10,5 +10,5 @@ allowBuilds:
|
||||
unrs-resolver: true
|
||||
workerd: true
|
||||
minimumReleaseAgeExclude:
|
||||
- '@astrojs/telemetry@3.3.3'
|
||||
- "@astrojs/telemetry@3.3.3"
|
||||
- astro@7.0.7
|
||||
|
||||
@@ -14,14 +14,15 @@ describe("GeminiProvider Eval", () => {
|
||||
});
|
||||
|
||||
const response = await provider.generateStructuredResponse({
|
||||
systemPrompt: "You are a helpful assistant. Classify the tone of the user's sentence.",
|
||||
systemPrompt:
|
||||
"You are a helpful assistant. Classify the tone of the user's sentence.",
|
||||
userContext: "I absolutely love this new engine, it works perfectly!",
|
||||
schema: ToneSchema,
|
||||
});
|
||||
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.data).toBeDefined();
|
||||
|
||||
|
||||
const data = response.data!;
|
||||
expect(data.tone).toBe("positive");
|
||||
expect(data.confidence).toBeGreaterThan(0.8);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IntentDecoder } from "@omnia/intent";
|
||||
describe("IntentDecoder Live Eval (Tier 3)", () => {
|
||||
test("decodes real complex narrative prose using live Gemini API", async () => {
|
||||
expect(llmConfig.GOOGLE_API_KEY).toBeDefined();
|
||||
|
||||
|
||||
// 1. Initialize live provider and decoder
|
||||
const provider = new GeminiProvider(llmConfig.GOOGLE_API_KEY);
|
||||
const decoder = new IntentDecoder(provider);
|
||||
@@ -19,7 +19,8 @@ describe("IntentDecoder Live Eval (Tier 3)", () => {
|
||||
world.addEntity(bob);
|
||||
|
||||
// 3. Narrative prose containing both a dialogue and physical action
|
||||
const narrativeProse = '"Let\'s see if this key opens the main vault," Alice said to Bob. She slipped the silver key into the keyhole and turned it slowly.';
|
||||
const narrativeProse =
|
||||
'"Let\'s see if this key opens the main vault," Alice said to Bob. She slipped the silver key into the keyhole and turned it slowly.';
|
||||
|
||||
// 4. Decode prose using live Gemini model
|
||||
const result = await decoder.decode(world, "alice", narrativeProse);
|
||||
@@ -30,14 +31,16 @@ describe("IntentDecoder Live Eval (Tier 3)", () => {
|
||||
expect(result.intents.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Verify first intent is dialogue spoken to Bob
|
||||
const dialogueIntent = result.intents.find(i => i.type === "dialogue");
|
||||
const dialogueIntent = result.intents.find((i) => i.type === "dialogue");
|
||||
expect(dialogueIntent).toBeDefined();
|
||||
expect(dialogueIntent!.actorId).toBe("alice");
|
||||
expect(dialogueIntent!.targetIds).toContain("bob");
|
||||
expect(dialogueIntent!.originalText).toContain("Let's see if this key opens the main vault");
|
||||
expect(dialogueIntent!.originalText).toContain(
|
||||
"Let's see if this key opens the main vault",
|
||||
);
|
||||
|
||||
// Verify second intent is action
|
||||
const actionIntent = result.intents.find(i => i.type === "action");
|
||||
const actionIntent = result.intents.find((i) => i.type === "action");
|
||||
expect(actionIntent).toBeDefined();
|
||||
expect(actionIntent!.actorId).toBe("alice");
|
||||
expect(actionIntent!.originalText).toContain("slipped the silver key");
|
||||
@@ -45,28 +48,29 @@ describe("IntentDecoder Live Eval (Tier 3)", () => {
|
||||
|
||||
test("resolves targets correctly using the actor's subjective alias map", async () => {
|
||||
expect(llmConfig.GOOGLE_API_KEY).toBeDefined();
|
||||
|
||||
|
||||
const provider = new GeminiProvider(llmConfig.GOOGLE_API_KEY);
|
||||
const decoder = new IntentDecoder(provider);
|
||||
|
||||
const world = new WorldState("world-xyz");
|
||||
const alice = new Entity("alice");
|
||||
const bob = new Entity("bob");
|
||||
|
||||
|
||||
// Register alias mapping: Bob is known to Alice as "the hooded stranger"
|
||||
alice.aliases.set("bob", "the hooded stranger");
|
||||
|
||||
|
||||
world.addEntity(alice);
|
||||
world.addEntity(bob);
|
||||
|
||||
// Narrative prose referring to Bob only by the subjective alias
|
||||
const narrativeProse = "Alice handed the silver key to the hooded stranger.";
|
||||
const narrativeProse =
|
||||
"Alice handed the silver key to the hooded stranger.";
|
||||
|
||||
const result = await decoder.decode(world, "alice", narrativeProse);
|
||||
|
||||
expect(result.intents).toHaveLength(1);
|
||||
const intent = result.intents[0];
|
||||
|
||||
|
||||
expect(intent.type).toBe("action");
|
||||
expect(intent.actorId).toBe("alice");
|
||||
// Verify target resolution resolved the alias back to the correct system entity ID
|
||||
|
||||
@@ -9,10 +9,7 @@ import {
|
||||
import { MockLLMProvider } from "@omnia/llm";
|
||||
import { IntentSequence } from "@omnia/intent";
|
||||
import { Architect } from "@omnia/architect";
|
||||
import {
|
||||
BufferRepository,
|
||||
BufferEntry,
|
||||
} from "@omnia/memory";
|
||||
import { BufferRepository, BufferEntry } from "@omnia/memory";
|
||||
import {
|
||||
ActorAgent,
|
||||
ActorResponseSchema,
|
||||
@@ -53,16 +50,22 @@ describe("Actor Agent + Monologue Intent Integration (Tier 2)", () => {
|
||||
|
||||
// --- Mock LLM response queue ---
|
||||
// 1. Actor produces prose containing a thought, a spoken line, and an action.
|
||||
const mockActorProse = { narrativeProse: "I can't believe Bob hasn't noticed me yet, Alice thought. \"Hey Bob,\" she called out softly. She reached for the ledger on the table." };
|
||||
const mockActorProse = {
|
||||
narrativeProse:
|
||||
"I can't believe Bob hasn't noticed me yet, Alice thought. \"Hey Bob,\" she called out softly. She reached for the ledger on the table.",
|
||||
};
|
||||
|
||||
// 2. IntentDecoder splits that prose into 3 intents.
|
||||
const mockDecodedSequence: IntentSequence = {
|
||||
intents: [
|
||||
{
|
||||
type: "monologue",
|
||||
originalText: "I can't believe Bob hasn't noticed me yet, Alice thought.",
|
||||
description: "Alice internally reflects that Bob has not noticed her.",
|
||||
selfDescription: "You internally reflect that Bob has not noticed you.",
|
||||
originalText:
|
||||
"I can't believe Bob hasn't noticed me yet, Alice thought.",
|
||||
description:
|
||||
"Alice internally reflects that Bob has not noticed her.",
|
||||
selfDescription:
|
||||
"You internally reflect that Bob has not noticed you.",
|
||||
actorId: "alice",
|
||||
targetIds: [],
|
||||
modifiers: [],
|
||||
@@ -90,16 +93,25 @@ describe("Actor Agent + Monologue Intent Integration (Tier 2)", () => {
|
||||
|
||||
// 3. Architect: dialogue is always valid (1 min), action is valid (2 min).
|
||||
// NOTE: monologue never reaches the validator/delta generator.
|
||||
const mockDialogueValidation = { isValid: true, reason: "Alice can speak." };
|
||||
const mockActionValidation = { isValid: true, reason: "The ledger is within reach." };
|
||||
const mockActionTimeDelta = { minutesToAdvance: 2, explanation: "Reaching for the ledger takes 2 minutes." };
|
||||
const mockDialogueValidation = {
|
||||
isValid: true,
|
||||
reason: "Alice can speak.",
|
||||
};
|
||||
const mockActionValidation = {
|
||||
isValid: true,
|
||||
reason: "The ledger is within reach.",
|
||||
};
|
||||
const mockActionTimeDelta = {
|
||||
minutesToAdvance: 2,
|
||||
explanation: "Reaching for the ledger takes 2 minutes.",
|
||||
};
|
||||
|
||||
const llmProvider = new MockLLMProvider([
|
||||
mockActorProse, // 1. Actor generation
|
||||
mockDecodedSequence, // 2. IntentDecoder
|
||||
mockDialogueValidation, // 3. Architect.validateIntent (dialogue)
|
||||
mockActionValidation, // 4. Architect.validateIntent (action)
|
||||
mockActionTimeDelta, // 5. TimeDeltaGenerator (action)
|
||||
mockActorProse, // 1. Actor generation
|
||||
mockDecodedSequence, // 2. IntentDecoder
|
||||
mockDialogueValidation, // 3. Architect.validateIntent (dialogue)
|
||||
mockActionValidation, // 4. Architect.validateIntent (action)
|
||||
mockActionTimeDelta, // 5. TimeDeltaGenerator (action)
|
||||
]);
|
||||
|
||||
const actor = new ActorAgent(llmProvider, bufferRepo);
|
||||
@@ -178,9 +190,7 @@ describe("Actor Agent + Monologue Intent Integration (Tier 2)", () => {
|
||||
expect(ActorResponseSchema.parse(valid)).toEqual(valid);
|
||||
|
||||
expect(() => ActorResponseSchema.parse({})).toThrow();
|
||||
expect(() =>
|
||||
ActorResponseSchema.parse({ narrativeProse: 123 }),
|
||||
).toThrow();
|
||||
expect(() => ActorResponseSchema.parse({ narrativeProse: 123 })).toThrow();
|
||||
});
|
||||
|
||||
test("serializeSubjectiveWorldState is epistemically bounded", async () => {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import Database from "better-sqlite3";
|
||||
import { WorldState, Entity, SQLiteRepository, AttributeVisibility } from "@omnia/core";
|
||||
import {
|
||||
WorldState,
|
||||
Entity,
|
||||
SQLiteRepository,
|
||||
AttributeVisibility,
|
||||
} from "@omnia/core";
|
||||
import { MockLLMProvider } from "@omnia/llm";
|
||||
import { IntentDecoder, IntentSequence } from "@omnia/intent";
|
||||
import { Architect } from "@omnia/architect";
|
||||
@@ -51,34 +56,54 @@ describe("Omnia Integration Tests (Tier 2)", () => {
|
||||
};
|
||||
|
||||
// 2. Architect validation & delta generation responses
|
||||
const mockDialogueValidation = { isValid: true, reason: "Alice is able to speak to Bob." };
|
||||
const mockDialogueValidation = {
|
||||
isValid: true,
|
||||
reason: "Alice is able to speak to Bob.",
|
||||
};
|
||||
|
||||
const mockActionValidation = { isValid: true, reason: "The door is unlocked and reachable." };
|
||||
const mockActionTimeDelta = { minutesToAdvance: 3, explanation: "Creeping silently and opening a door takes 3 minutes." };
|
||||
const mockActionValidation = {
|
||||
isValid: true,
|
||||
reason: "The door is unlocked and reachable.",
|
||||
};
|
||||
const mockActionTimeDelta = {
|
||||
minutesToAdvance: 3,
|
||||
explanation: "Creeping silently and opening a door takes 3 minutes.",
|
||||
};
|
||||
|
||||
const llmProvider = new MockLLMProvider([
|
||||
mockIntentSequence, // Used by IntentDecoder
|
||||
mockDialogueValidation, // Used by Architect.validateIntent (Dialogue)
|
||||
mockActionValidation, // Used by Architect.validateIntent (Action)
|
||||
mockActionTimeDelta // Used by TimeDeltaGenerator (Action)
|
||||
mockIntentSequence, // Used by IntentDecoder
|
||||
mockDialogueValidation, // Used by Architect.validateIntent (Dialogue)
|
||||
mockActionValidation, // Used by Architect.validateIntent (Action)
|
||||
mockActionTimeDelta, // Used by TimeDeltaGenerator (Action)
|
||||
]);
|
||||
|
||||
const decoder = new IntentDecoder(llmProvider);
|
||||
const architect = new Architect(llmProvider, repo);
|
||||
|
||||
// 1. Decode the raw prose into structured intents
|
||||
const narrativeProse = '"Cover me," Alice whispered to Bob. She crept towards the door and pulled the handle.';
|
||||
const decodedSequence = await decoder.decode(world, "alice", narrativeProse);
|
||||
const narrativeProse =
|
||||
'"Cover me," Alice whispered to Bob. She crept towards the door and pulled the handle.';
|
||||
const decodedSequence = await decoder.decode(
|
||||
world,
|
||||
"alice",
|
||||
narrativeProse,
|
||||
);
|
||||
|
||||
expect(decodedSequence.intents).toHaveLength(2);
|
||||
|
||||
// 2. Process first intent (dialogue)
|
||||
const result1 = await architect.processIntent(world, decodedSequence.intents[0]);
|
||||
const result1 = await architect.processIntent(
|
||||
world,
|
||||
decodedSequence.intents[0],
|
||||
);
|
||||
expect(result1.isValid).toBe(true);
|
||||
expect(result1.timeDelta!.minutesToAdvance).toBe(1);
|
||||
|
||||
// 3. Process second intent (action)
|
||||
const result2 = await architect.processIntent(world, decodedSequence.intents[1]);
|
||||
const result2 = await architect.processIntent(
|
||||
world,
|
||||
decodedSequence.intents[1],
|
||||
);
|
||||
expect(result2.isValid).toBe(true);
|
||||
expect(result2.timeDelta!.minutesToAdvance).toBe(3);
|
||||
|
||||
@@ -88,7 +113,9 @@ describe("Omnia Integration Tests (Tier 2)", () => {
|
||||
|
||||
// 5. Verify database state clock was advanced and persisted
|
||||
const reloadedWorld = repo.loadWorldState("world-abc")!;
|
||||
expect(reloadedWorld.clock.get().toISOString()).toBe(expectedTime.toISOString());
|
||||
expect(reloadedWorld.clock.get().toISOString()).toBe(
|
||||
expectedTime.toISOString(),
|
||||
);
|
||||
|
||||
db.close();
|
||||
});
|
||||
@@ -128,13 +155,19 @@ describe("Omnia Integration Tests (Tier 2)", () => {
|
||||
|
||||
// LLM validation / time delta mock responses:
|
||||
// For intent1 (action):
|
||||
const mockActionValidation = { isValid: false, reason: "Hairpins cannot pick high-security locks." };
|
||||
const mockActionValidation = {
|
||||
isValid: false,
|
||||
reason: "Hairpins cannot pick high-security locks.",
|
||||
};
|
||||
// For intent2 (dialogue):
|
||||
const mockDialogueValidation = { isValid: true, reason: "Alice is free to talk." };
|
||||
const mockDialogueValidation = {
|
||||
isValid: true,
|
||||
reason: "Alice is free to talk.",
|
||||
};
|
||||
|
||||
const llmProvider = new MockLLMProvider([
|
||||
mockActionValidation, // Used by Architect.validateIntent (Action)
|
||||
mockDialogueValidation, // Used by Architect.validateIntent (Dialogue)
|
||||
mockActionValidation, // Used by Architect.validateIntent (Action)
|
||||
mockDialogueValidation, // Used by Architect.validateIntent (Dialogue)
|
||||
]);
|
||||
|
||||
const architect = new Architect(llmProvider, repo);
|
||||
|
||||
@@ -15,7 +15,13 @@ export default defineConfig({
|
||||
src: "./src/assets/img/logo.png",
|
||||
replacesTitle: true,
|
||||
},
|
||||
social: [{ icon: "github", label: "GitHub", href: "https://github.com/sortedcord/omnia-consolidated" }],
|
||||
social: [
|
||||
{
|
||||
icon: "github",
|
||||
label: "GitHub",
|
||||
href: "https://github.com/sortedcord/omnia-consolidated",
|
||||
},
|
||||
],
|
||||
sidebar: [
|
||||
{
|
||||
label: "Introduction",
|
||||
|
||||
@@ -34,11 +34,11 @@ Establishes the role, rules, and output contract:
|
||||
|
||||
Epistemically bounded, with these sections:
|
||||
|
||||
| Section | Content | Source |
|
||||
|---|---|---|
|
||||
| Current moment | The subjective present time | `worldState.clock.get().toISOString()` |
|
||||
| The world as you perceive it | Self-visible attributes, co-located entities + their visible attributes, other presences elsewhere | `serializeSubjectiveWorldState()` |
|
||||
| Your recent memory | Recent `BufferEntry`s, alias-substituted, with relative time phrasing | `serializeSubjectiveBufferEntry()` |
|
||||
| Section | Content | Source |
|
||||
| ---------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
||||
| Current moment | The subjective present time | `worldState.clock.get().toISOString()` |
|
||||
| The world as you perceive it | Self-visible attributes, co-located entities + their visible attributes, other presences elsewhere | `serializeSubjectiveWorldState()` |
|
||||
| Your recent memory | Recent `BufferEntry`s, alias-substituted, with relative time phrasing | `serializeSubjectiveBufferEntry()` |
|
||||
|
||||
No system UUIDs, no private attributes the entity lacks ACL access to, and no objective-world-state dump are present.
|
||||
|
||||
@@ -81,13 +81,13 @@ Monologue (`"monologue"`) is the third intent type. Its properties:
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `packages/actor/src/actor-prompt-builder.ts` | Assembles the epistemically-bounded actor prompt |
|
||||
| `packages/actor/src/actor.ts` | `ActorAgent` class: orchestrates prompt → LLM → decoder flow |
|
||||
| `packages/actor/src/index.ts` | Package exports |
|
||||
| `packages/core/src/world.ts:72` | `serializeSubjectiveWorldState()` |
|
||||
| `packages/intent/src/intent.ts:8` | `IntentTypeSchema` — includes `"monologue"` |
|
||||
| `packages/intent/src/intent-decoder.ts:30` | Decoder system prompt |
|
||||
| `packages/architect/src/architect.ts:35` | Monologue short-circuit |
|
||||
| `packages/architect/src/llm-validator.ts:19` | Defensive monologue guard |
|
||||
| File | Role |
|
||||
| -------------------------------------------- | ------------------------------------------------------------ |
|
||||
| `packages/actor/src/actor-prompt-builder.ts` | Assembles the epistemically-bounded actor prompt |
|
||||
| `packages/actor/src/actor.ts` | `ActorAgent` class: orchestrates prompt → LLM → decoder flow |
|
||||
| `packages/actor/src/index.ts` | Package exports |
|
||||
| `packages/core/src/world.ts:72` | `serializeSubjectiveWorldState()` |
|
||||
| `packages/intent/src/intent.ts:8` | `IntentTypeSchema` — includes `"monologue"` |
|
||||
| `packages/intent/src/intent-decoder.ts:30` | Decoder system prompt |
|
||||
| `packages/architect/src/architect.ts:35` | Monologue short-circuit |
|
||||
| `packages/architect/src/llm-validator.ts:19` | Defensive monologue guard |
|
||||
|
||||
@@ -30,20 +30,24 @@ function checkHandoffTrigger(
|
||||
entity: Entity,
|
||||
bufferEntries: BufferEntry[],
|
||||
now: Date,
|
||||
maxContext?: number
|
||||
maxContext?: number,
|
||||
): HandoffTrigger;
|
||||
```
|
||||
|
||||
### Voluntary Triggers (Soft)
|
||||
|
||||
Voluntary triggers identify natural narrative boundaries where the entity is inactive or has transitioned contexts:
|
||||
* **Scene Exit**: The entity's `locationId` changes relative to the location of the entries currently in the buffer, or the buffer contains entries spanning multiple different locations.
|
||||
* **Idle Decay**: The entity has produced no external interactions (only monologue entries or no entries) for $N$ (default: 5) consecutive turns.
|
||||
* **Attribute-Driven State**: A character attribute (e.g., `status: asleep` or `consciousness: unconscious`) signals low external activity.
|
||||
|
||||
- **Scene Exit**: The entity's `locationId` changes relative to the location of the entries currently in the buffer, or the buffer contains entries spanning multiple different locations.
|
||||
- **Idle Decay**: The entity has produced no external interactions (only monologue entries or no entries) for $N$ (default: 5) consecutive turns.
|
||||
- **Attribute-Driven State**: A character attribute (e.g., `status: asleep` or `consciousness: unconscious`) signals low external activity.
|
||||
|
||||
### Involuntary Triggers (Hard)
|
||||
|
||||
Involuntary triggers enforce context constraints before building the next prompt to prevent token limit overflows:
|
||||
* **Buffer Ceiling**: The serialized length of the working memory section (as generated by `getMemorySectionLength`) exceeds a configured fraction (default: 60%) of the provider's context window.
|
||||
* **Event Velocity**: The number of buffer entries exceeds a safe threshold (default: 20), indicating an event-heavy scene generating history faster than voluntary boundaries occur.
|
||||
|
||||
- **Buffer Ceiling**: The serialized length of the working memory section (as generated by `getMemorySectionLength`) exceeds a configured fraction (default: 60%) of the provider's context window.
|
||||
- **Event Velocity**: The number of buffer entries exceeds a safe threshold (default: 20), indicating an event-heavy scene generating history faster than voluntary boundaries occur.
|
||||
|
||||
If the buffer is empty, trigger detection exits immediately with `"none"`.
|
||||
|
||||
@@ -57,8 +61,8 @@ The watermark boundary is computed as:
|
||||
|
||||
$$\text{Watermark} = \max(K \text{ entries}, \text{entries bucketed as fresh by } \textit{naturalizeTime})$$
|
||||
|
||||
* **Hard Floor ($K = 8$)**: The last 8 entries in the buffer are always preserved.
|
||||
* **Temporal Freshness**: Any entries that `naturalizeTime` classifies within the immediate temporal horizon (`"just now"`, `"moments ago"`, `"a few minutes ago"`, or `"several minutes ago"`) are also protected.
|
||||
- **Hard Floor ($K = 8$)**: The last 8 entries in the buffer are always preserved.
|
||||
- **Temporal Freshness**: Any entries that `naturalizeTime` classifies within the immediate temporal horizon (`"just now"`, `"moments ago"`, `"a few minutes ago"`, or `"several minutes ago"`) are also protected.
|
||||
|
||||
Entries older than the watermark boundary constitute the **Candidate Pool** and are eligible for handoff processing.
|
||||
|
||||
@@ -84,16 +88,20 @@ const HandoffResultSchema = z.object({
|
||||
```
|
||||
|
||||
### LLM Processing Rules
|
||||
|
||||
The prompt instructs the model to apply the following cognitive operations:
|
||||
|
||||
1. **Clustering**: Group related consecutive candidate entries into high-level narrative beats (e.g., consolidating an entire dialogue exchange into one beat).
|
||||
2. **Third-Person Synthesis**: Write the `content` summary from a third-person narrative perspective.
|
||||
3. **Dialogue Salience**: Extract verbatim quotes of high emotional or narrative relevance.
|
||||
4. **Information Pruning (Forgetting)**: Any candidate entry ID omitted from all chunk `sourceEntryIds` is permanently deleted from the buffer and never saved to the ledger. This mirrors natural sensory attenuation.
|
||||
|
||||
### Unresolved Thread Retention (Pinning)
|
||||
When a chunk represents an unresolved high-stakes situation (e.g., an active conflict or standing threat), the LLM sets `retainInBuffer: true`.
|
||||
* The summary is committed to the long-term Ledger as normal.
|
||||
* The source buffer entries are exempted from pruning, remaining in the working memory buffer with `pinned: true` until a future handoff pass determines the thread is resolved.
|
||||
|
||||
When a chunk represents an unresolved high-stakes situation (e.g., an active conflict or standing threat), the LLM sets `retainInBuffer: true`.
|
||||
|
||||
- The summary is committed to the long-term Ledger as normal.
|
||||
- The source buffer entries are exempted from pruning, remaining in the working memory buffer with `pinned: true` until a future handoff pass determines the thread is resolved.
|
||||
|
||||
---
|
||||
|
||||
@@ -123,6 +131,6 @@ If LLM inference, Zod schema validation, or vector embedding generation fails, t
|
||||
|
||||
The handoff pipeline is exposed via `HandoffEngine` and is wired directly into the simulation loop:
|
||||
|
||||
* **Turn Hook**: `SimulationManager.step()` invokes trigger evaluation and handoff resolution for all active entities at the start of the turn, before entities act.
|
||||
* **Task Routing**: Handoff tasks are routed using the `"handoff"` routing key, enabling independent LLM provider selection and max context configuration.
|
||||
* **Buffer Garbage Collection**: By combining memory promotion with automatic pruning, the handoff engine bounds buffer memory usage over long simulation runs, eliminating the need for a separate garbage collection mechanism.
|
||||
- **Turn Hook**: `SimulationManager.step()` invokes trigger evaluation and handoff resolution for all active entities at the start of the turn, before entities act.
|
||||
- **Task Routing**: Handoff tasks are routed using the `"handoff"` routing key, enabling independent LLM provider selection and max context configuration.
|
||||
- **Buffer Garbage Collection**: By combining memory promotion with automatic pruning, the handoff engine bounds buffer memory usage over long simulation runs, eliminating the need for a separate garbage collection mechanism.
|
||||
|
||||
@@ -6,6 +6,7 @@ description: How narrative prose becomes structured, validated actions
|
||||
The simple way of understanding intents is to think of it as a proposal, not an effect.
|
||||
|
||||
Intents are:
|
||||
|
||||
- **Declarative** — they describe what the character intends, not the final outcome.
|
||||
- **High-level** — they capture the gist of an action or dialogue.
|
||||
- **Allowed to be wrong** — validation happens downstream.
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface ILLMProvider {
|
||||
```
|
||||
|
||||
The codebase provides three primary implementations:
|
||||
|
||||
1. **`GeminiProvider`:** The production provider utilizing Google's Gemini Models via the `@langchain/google-genai` SDK.
|
||||
2. **`OpenRouterProvider`:** The production provider utilizing OpenRouter via the `@langchain/openrouter` SDK, allowing routing through various third-party and local models.
|
||||
3. **`MockLLMProvider`:** A stateless, pre-programmed mock provider used for fast, deterministic unit testing and local integration tests.
|
||||
@@ -44,11 +45,12 @@ export interface LLMProviderInstance {
|
||||
```
|
||||
|
||||
Users can register multiple provider instances in the **Configuration Page** under the GUI. Each instance is given:
|
||||
* A friendly, human-readable name (e.g., `"Gemini Production Key"`, `"OpenRouter Claude Key"`).
|
||||
* A provider type (e.g., `google-genai`, `openrouter`, `mock`).
|
||||
* An API key credential.
|
||||
* A custom target model name (e.g., `gemini-2.5-flash`, `anthropic/claude-3-5-sonnet`, or local model paths).
|
||||
* An **Active** status flag (one key is marked as globally active).
|
||||
|
||||
- A friendly, human-readable name (e.g., `"Gemini Production Key"`, `"OpenRouter Claude Key"`).
|
||||
- A provider type (e.g., `google-genai`, `openrouter`, `mock`).
|
||||
- An API key credential.
|
||||
- A custom target model name (e.g., `gemini-2.5-flash`, `anthropic/claude-3-5-sonnet`, or local model paths).
|
||||
- An **Active** status flag (one key is marked as globally active).
|
||||
|
||||
Configurations are stored globally in `data/settings.db` (separated from specific simulation run databases like `data/sim-*.db` to keep key storage and audit logs isolated).
|
||||
|
||||
@@ -58,12 +60,12 @@ Configurations are stored globally in `data/settings.db` (separated from specifi
|
||||
|
||||
During a simulation run, the engine executes four distinct LLM operations. To optimize costs, latency, or model accuracy, you can route each of these tasks to different LLM provider instances:
|
||||
|
||||
| Task Name | Key ID | Description | Default Model |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Actor Prose Generation** | `actor-prose` | Generates roleplay and narrative behavioral prose for Non-Player Characters. | `gemini-2.5-flash` / `google/gemini-2.5-flash` |
|
||||
| **LLM Validator** | `llm-validator` | Arbitrates and validates proposed actions against the world state rules and constraints. | `gemini-2.5-flash` / `google/gemini-2.5-flash` |
|
||||
| **Intent Decoder** | `intent-decoder` | Parses and splits free-text actions/prose into structured intent sequences. | `gemini-2.5-flash` / `google/gemini-2.5-flash` |
|
||||
| **TimeDelta Generator** | `timedelta` | Calculates the duration of character actions to advance the game clock. | `gemini-2.5-flash` / `google/gemini-2.5-flash` |
|
||||
| Task Name | Key ID | Description | Default Model |
|
||||
| :------------------------- | :--------------- | :--------------------------------------------------------------------------------------- | :--------------------------------------------- |
|
||||
| **Actor Prose Generation** | `actor-prose` | Generates roleplay and narrative behavioral prose for Non-Player Characters. | `gemini-2.5-flash` / `google/gemini-2.5-flash` |
|
||||
| **LLM Validator** | `llm-validator` | Arbitrates and validates proposed actions against the world state rules and constraints. | `gemini-2.5-flash` / `google/gemini-2.5-flash` |
|
||||
| **Intent Decoder** | `intent-decoder` | Parses and splits free-text actions/prose into structured intent sequences. | `gemini-2.5-flash` / `google/gemini-2.5-flash` |
|
||||
| **TimeDelta Generator** | `timedelta` | Calculates the duration of character actions to advance the game clock. | `gemini-2.5-flash` / `google/gemini-2.5-flash` |
|
||||
|
||||
If no specific provider instance is mapped to a task, the task automatically routes to the globally marked **Active** provider instance.
|
||||
|
||||
|
||||
@@ -64,9 +64,10 @@ CREATE INDEX IF NOT EXISTS idx_ledger_involved_entity ON ledger_involved_entitie
|
||||
|
||||
## 3. The Handoff Pipeline
|
||||
|
||||
Working memory (Tier 1 Buffer) entries are promoted to the Ledger through the automated [Handoff Pipeline](./handoff).
|
||||
Working memory (Tier 1 Buffer) entries are promoted to the Ledger through the automated [Handoff Pipeline](./handoff).
|
||||
|
||||
During handoff:
|
||||
|
||||
1. Candidate entries are clustered into narrative beats.
|
||||
2. Ambient stage business and redundant details are pruned.
|
||||
3. Chunks are synthesized into third-person summaries and assigned importance scores.
|
||||
@@ -92,19 +93,23 @@ flowchart TD
|
||||
```
|
||||
|
||||
### Phase 1: Deterministic Heuristic Filtering
|
||||
|
||||
The primary selection uses indexes to retrieve a candidate pool (capped at 100 entries) from the database:
|
||||
|
||||
1. **Spatial Cues**: Fetch entries matching the character's current `locationId`.
|
||||
2. **Social Cues**: Fetch entries where `involvedEntityIds` intersects with entities currently inside the character's perception radius.
|
||||
3. **High Salience**: Always retrieve high-salience entries where `importance >= 8`.
|
||||
|
||||
### Phase 2: Semantic & Episodic Ranking
|
||||
|
||||
Once the candidate pool is loaded, the ranking engine evaluates entries in application memory:
|
||||
|
||||
1. **Semantic Similarity**: Cosine similarity is computed directly in JS/TS memory between the current prompt context and the candidate embeddings.
|
||||
2. **Multi-Factor Scoring**: Candidates are ranked using a weighted linear combination:
|
||||
$$\text{Score} = (\alpha \times \text{recency}) + (\beta \times \text{importance}) + (\gamma \times \text{relevance})$$
|
||||
* **Recency** is modeled via exponential decay based on elapsed simulation hours: $\text{decayRate}^{\text{hoursElapsed}}$.
|
||||
* **Importance** is the normalized salience score (1-10) assigned during handoff.
|
||||
* **Relevance** is the cosine similarity score.
|
||||
- **Recency** is modeled via exponential decay based on elapsed simulation hours: $\text{decayRate}^{\text{hoursElapsed}}$.
|
||||
- **Importance** is the normalized salience score (1-10) assigned during handoff.
|
||||
- **Relevance** is the cosine similarity score.
|
||||
3. **Chronological Associative Chaining**: When a memory is selected, the system automatically pulls in its adjacent chronological neighbors (preceding and succeeding ledger entries) to preserve episodic continuity in the prompt.
|
||||
|
||||
---
|
||||
@@ -113,11 +118,11 @@ Once the candidate pool is loaded, the ranking engine evaluates entries in appli
|
||||
|
||||
In crowded settings (e.g., a room with many characters), retrieving long-term memories for all co-located entities would exhaust the prompt context window. To prevent this, retrieval uses an **Active Focus** selection strategy:
|
||||
|
||||
* **Active Focus Set**: The prompt builder scans the last 10 entries of the character's working memory buffer. Any entity targeted by, spoken to, or mentioned in these entries is added to the Active Focus set.
|
||||
* **Dynamic Capacity Limits**:
|
||||
* If the number of co-located characters is small ($\le 3$), long-term memory is retrieved for all of them.
|
||||
* If the environment is crowded ($> 3$), long-term retrieval is strictly restricted to the top 3 characters in the Active Focus set.
|
||||
* This creates a realistic attention loop: when a new character interacts with the actor, they enter the working buffer, triggering the retrieval of their long-term history on the subsequent turn.
|
||||
- **Active Focus Set**: The prompt builder scans the last 10 entries of the character's working memory buffer. Any entity targeted by, spoken to, or mentioned in these entries is added to the Active Focus set.
|
||||
- **Dynamic Capacity Limits**:
|
||||
- If the number of co-located characters is small ($\le 3$), long-term memory is retrieved for all of them.
|
||||
- If the environment is crowded ($> 3$), long-term retrieval is strictly restricted to the top 3 characters in the Active Focus set.
|
||||
- This creates a realistic attention loop: when a new character interacts with the actor, they enter the working buffer, triggering the retrieval of their long-term history on the subsequent turn.
|
||||
|
||||
---
|
||||
|
||||
@@ -125,8 +130,8 @@ In crowded settings (e.g., a room with many characters), retrieving long-term me
|
||||
|
||||
Retrieved ledger entries are formatted into the prompt chronologically and mapped to subjective aliases. Internal metrics (e.g., importance numbers and raw system IDs) are omitted to preserve immersion.
|
||||
|
||||
* Working memory entries are injected under `=== RECENT EVENTS ===` (narrated relative to the present moment).
|
||||
* Recalled ledger entries are injected under `=== YOUR MEMORIES ===` (presented as long-term recollections).
|
||||
- Working memory entries are injected under `=== RECENT EVENTS ===` (narrated relative to the present moment).
|
||||
- Recalled ledger entries are injected under `=== YOUR MEMORIES ===` (presented as long-term recollections).
|
||||
|
||||
```text
|
||||
=== RECENT EVENTS ===
|
||||
|
||||
@@ -43,12 +43,13 @@ A subjective `BufferEntry` records a discrete event from the perspective of an e
|
||||
```typescript
|
||||
interface BufferEntry {
|
||||
id: string;
|
||||
ownerId: string; // Whose subjective memory buffer this lives in
|
||||
timestamp: string; // WorldClock.get().toISOString() at write time
|
||||
ownerId: string; // Whose subjective memory buffer this lives in
|
||||
timestamp: string; // WorldClock.get().toISOString() at write time
|
||||
locationId: string | null; // Actor's location when this happened
|
||||
|
||||
intent: Intent; // The actual dialogue/action intent, reused as-is
|
||||
outcome?: { // Present only for "action" intents
|
||||
intent: Intent; // The actual dialogue/action intent, reused as-is
|
||||
outcome?: {
|
||||
// Present only for "action" intents
|
||||
isValid: boolean;
|
||||
reason: string;
|
||||
};
|
||||
@@ -94,10 +95,9 @@ CREATE TABLE IF NOT EXISTS buffer_entries (
|
||||
|
||||
LLMs are poor at tracking quantized clock times, and real entities do not recall exact timestamps for past events. To make memories psychologically realistic, timestamps are converted into relative natural language phrases prior to prompt injection:
|
||||
|
||||
* **Utility**: `naturalizeTime(now: Date, past: Date): string` converts raw dates into subjective relative strings.
|
||||
* **Granularity Tiers**:
|
||||
* **Relative (< 6 hours)**: Returns short offsets like `"just now"`, `"moments ago"`, `"a couple hours ago"`, or `"a few hours ago"`.
|
||||
* **Same Subjective Day (6h to 18h)**: Detects waking hours (05:00 - 21:59). If both times occur within the same waking block, it returns `"earlier today, in the {period}"` (where period is `morning`, `afternoon`, or `evening`).
|
||||
* **Plausible Sleep Boundaries**: Past events from sleep hours are mapped to `"last night"`, `"around midnight"`, or `"late last night"`.
|
||||
* **Coarse (>= 48 hours)**: Returns broad descriptors like `"a couple days ago"`, `"about a week ago"`, `"a couple months ago"`, or `"years ago"`.
|
||||
|
||||
- **Utility**: `naturalizeTime(now: Date, past: Date): string` converts raw dates into subjective relative strings.
|
||||
- **Granularity Tiers**:
|
||||
- **Relative (< 6 hours)**: Returns short offsets like `"just now"`, `"moments ago"`, `"a couple hours ago"`, or `"a few hours ago"`.
|
||||
- **Same Subjective Day (6h to 18h)**: Detects waking hours (05:00 - 21:59). If both times occur within the same waking block, it returns `"earlier today, in the {period}"` (where period is `morning`, `afternoon`, or `evening`).
|
||||
- **Plausible Sleep Boundaries**: Past events from sleep hours are mapped to `"last night"`, `"around midnight"`, or `"late last night"`.
|
||||
- **Coarse (>= 48 hours)**: Returns broad descriptors like `"a couple days ago"`, `"about a week ago"`, `"a couple months ago"`, or `"years ago"`.
|
||||
|
||||
@@ -14,6 +14,7 @@ world → region → location → point of interest
|
||||
These nodes are connected by **portals** with sound and vision propagation values. When something happens, perception information bubbles outward through portals.
|
||||
|
||||
Today, actors perceive:
|
||||
|
||||
- Co-located entities
|
||||
- Their location's visible attributes
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ The central testing challenge: most of Omnia is deterministic and highly testabl
|
||||
Unit tests reside within each package's `tests/` directory. They do not use LLMs and do not perform I/O. This tier covers the majority of the codebase.
|
||||
|
||||
Examples:
|
||||
|
||||
- `hasAccess()` / ACL grant-revoke logic
|
||||
- `addAttribute` rejecting duplicate names
|
||||
- `WorldClock.advance()` / `getTimeOfDay()` boundaries
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
"name": "omnia-docs",
|
||||
"compatibility_date": "2026-07-09",
|
||||
"assets": {
|
||||
"directory": "./dist"
|
||||
"directory": "./dist",
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "omnia.adityagupta.dev/docs*",
|
||||
"zone_name": "adityagupta.dev"
|
||||
}
|
||||
]
|
||||
"zone_name": "adityagupta.dev",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export function setupCounter(element: HTMLButtonElement) {
|
||||
let counter = 0
|
||||
let counter = 0;
|
||||
const setCounter = (count: number) => {
|
||||
counter = count
|
||||
element.innerHTML = `Count is ${counter}`
|
||||
}
|
||||
element.addEventListener('click', () => setCounter(counter + 1))
|
||||
setCounter(0)
|
||||
counter = count;
|
||||
element.innerHTML = `Count is ${counter}`;
|
||||
};
|
||||
element.addEventListener("click", () => setCounter(counter + 1));
|
||||
setCounter(0);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import './style.css'
|
||||
import typescriptLogo from './assets/typescript.svg'
|
||||
import viteLogo from './assets/vite.svg'
|
||||
import heroImg from './assets/hero.png'
|
||||
import { setupCounter } from './counter.ts'
|
||||
import "./style.css";
|
||||
import typescriptLogo from "./assets/typescript.svg";
|
||||
import viteLogo from "./assets/vite.svg";
|
||||
import heroImg from "./assets/hero.png";
|
||||
import { setupCounter } from "./counter.ts";
|
||||
|
||||
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
|
||||
document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
|
||||
<section id="center">
|
||||
<div class="hero">
|
||||
<img src="${heroImg}" class="base" width="170" height="179">
|
||||
@@ -55,6 +55,6 @@ document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
|
||||
|
||||
<div class="ticks"></div>
|
||||
<section id="spacer"></section>
|
||||
`
|
||||
`;
|
||||
|
||||
setupCounter(document.querySelector<HTMLButtonElement>('#counter')!)
|
||||
setupCounter(document.querySelector<HTMLButtonElement>("#counter")!);
|
||||
|
||||
Reference in New Issue
Block a user