Compare commits
2 Commits
fc10e46ccc
...
c32fa977a8
| Author | SHA1 | Date | |
|---|---|---|---|
| c32fa977a8 | |||
| fca69d3cb5 |
@@ -262,6 +262,7 @@ export function createDatabase(config: DatabaseConfig): CharacterGardenDatabase
|
|||||||
DELETE FROM world_states;
|
DELETE FROM world_states;
|
||||||
DELETE FROM turns;
|
DELETE FROM turns;
|
||||||
DELETE FROM entities;
|
DELETE FROM entities;
|
||||||
|
DELETE FROM rulebooks;
|
||||||
`);
|
`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
6
charactergarden/app/src/tests/sqliteProbe.ts
Normal file
6
charactergarden/app/src/tests/sqliteProbe.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
|
const db = new Database(process.env.DB_PATH ?? "/var/lib/charactergarden/app.db");
|
||||||
|
db.prepare("CREATE TABLE IF NOT EXISTS probe(value TEXT)").run();
|
||||||
|
db.close();
|
||||||
|
console.log("sqlite_probe_ok");
|
||||||
34
charactergarden/app/src/tests/verifyDefaultSeed.ts
Normal file
34
charactergarden/app/src/tests/verifyDefaultSeed.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { createCharacterGardenApp } from "../app";
|
||||||
|
|
||||||
|
function run(): void {
|
||||||
|
const app = createCharacterGardenApp(process.env.DB_PATH ?? "/var/lib/charactergarden/app.db");
|
||||||
|
const snapshot = app.getSnapshot();
|
||||||
|
const rulebook = app.getRulebook();
|
||||||
|
|
||||||
|
assert.equal(snapshot.turns.length, 0, "Expected no persisted turns after database reset.");
|
||||||
|
assert.ok(snapshot.worldState.entities.player, "Expected player in default world state.");
|
||||||
|
assert.ok(snapshot.worldState.entities.room_start, "Expected room_start in default world state.");
|
||||||
|
assert.equal(rulebook.id, "rulebook_default", "Expected default rulebook to be active.");
|
||||||
|
assert.equal(rulebook.version, 1, "Expected default rulebook version to be 1.");
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
ok: true,
|
||||||
|
entityCount: Object.keys(snapshot.worldState.entities).length,
|
||||||
|
turnCount: snapshot.turns.length,
|
||||||
|
rulebookId: rulebook.id,
|
||||||
|
rulebookVersion: rulebook.version,
|
||||||
|
ruleCount: rulebook.rules.length,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
app.db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
0
charactergarden/data/sqlite/test_write_probe.txt
Normal file
0
charactergarden/data/sqlite/test_write_probe.txt
Normal file
@@ -7,10 +7,10 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- APP_PORT=3000
|
- APP_PORT=3000
|
||||||
- NODE_ENV=${NODE_ENV:-development}
|
- NODE_ENV=${NODE_ENV:-development}
|
||||||
- DB_PATH=/data/sqlite/app.db
|
- DB_PATH=/var/lib/charactergarden/app.db
|
||||||
- OLLAMA_URL=${OLLAMA_URL:-http://ollama:11434}
|
- OLLAMA_URL=${OLLAMA_URL:-http://ollama:11434}
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- sqlite_data:/var/lib/charactergarden
|
||||||
- ./app/src:/app/src
|
- ./app/src:/app/src
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
@@ -38,3 +38,4 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
ollama_data:
|
ollama_data:
|
||||||
|
sqlite_data:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FormEvent, useEffect, useState } from "react";
|
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
type Entity = {
|
type Entity = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -28,7 +28,27 @@ type Turn = {
|
|||||||
validation: ValidationResult[];
|
validation: ValidationResult[];
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
interpreter?: {
|
interpreter?: {
|
||||||
|
interpreterVersion: string;
|
||||||
|
resolutionSource: "deterministic" | "llm" | "hybrid";
|
||||||
|
minConfidence: number;
|
||||||
status: "resolved" | "needs_clarification" | "rejected";
|
status: "resolved" | "needs_clarification" | "rejected";
|
||||||
|
selectedConfidence?: number;
|
||||||
|
candidates?: Array<{
|
||||||
|
action: Action;
|
||||||
|
confidence: number;
|
||||||
|
rationale?: string;
|
||||||
|
}>;
|
||||||
|
diagnostics: string[];
|
||||||
|
clarification?: {
|
||||||
|
reasonCode: string;
|
||||||
|
question: string;
|
||||||
|
field?: string;
|
||||||
|
options?: Array<{
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,12 +71,20 @@ type ProcessTurnResponse = {
|
|||||||
worldState: WorldState;
|
worldState: WorldState;
|
||||||
interpreter: {
|
interpreter: {
|
||||||
interpreterVersion: string;
|
interpreterVersion: string;
|
||||||
|
resolutionSource: "deterministic" | "llm" | "hybrid";
|
||||||
|
minConfidence: number;
|
||||||
status: "resolved" | "needs_clarification" | "rejected";
|
status: "resolved" | "needs_clarification" | "rejected";
|
||||||
selectedConfidence?: number;
|
selectedConfidence?: number;
|
||||||
|
candidates: Array<{
|
||||||
|
action: Action;
|
||||||
|
confidence: number;
|
||||||
|
rationale?: string;
|
||||||
|
}>;
|
||||||
diagnostics: string[];
|
diagnostics: string[];
|
||||||
clarification?: {
|
clarification?: {
|
||||||
reasonCode: string;
|
reasonCode: string;
|
||||||
question: string;
|
question: string;
|
||||||
|
field?: string;
|
||||||
options?: Array<{
|
options?: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -91,14 +119,37 @@ type SceneRulebook = {
|
|||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RulebookListItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: number;
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
const starterPrompts = [
|
const starterPrompts = [
|
||||||
"look around",
|
"look around",
|
||||||
"take key",
|
"take key",
|
||||||
|
"take lantern",
|
||||||
"give key to groundskeeper",
|
"give key to groundskeeper",
|
||||||
|
"introduce jeff",
|
||||||
|
"describe groundskeeper as patient",
|
||||||
"open door",
|
"open door",
|
||||||
"move to exit",
|
"move to exit",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function formatConfidence(value: number | undefined): string {
|
||||||
|
if (typeof value !== "number") return "n/a";
|
||||||
|
return `${Math.round(value * 100)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTurnTime(epochMs: number): string {
|
||||||
|
try {
|
||||||
|
return new Date(epochMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||||
|
} catch {
|
||||||
|
return String(epochMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchJson<T>(input: RequestInfo, init?: RequestInit): Promise<T> {
|
async function fetchJson<T>(input: RequestInfo, init?: RequestInit): Promise<T> {
|
||||||
const response = await fetch(input, init);
|
const response = await fetch(input, init);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -111,7 +162,7 @@ async function fetchJson<T>(input: RequestInfo, init?: RequestInit): Promise<T>
|
|||||||
// Rulebook editor component
|
// Rulebook editor component
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function RulebookEditor() {
|
function RulebookEditor(props: { onSaved?: (rulebook: SceneRulebook) => void }) {
|
||||||
const [rulebook, setRulebook] = useState<SceneRulebook | null>(null);
|
const [rulebook, setRulebook] = useState<SceneRulebook | null>(null);
|
||||||
const [drafts, setDrafts] = useState<Record<string, string>>({});
|
const [drafts, setDrafts] = useState<Record<string, string>>({});
|
||||||
const [parseErrors, setParseErrors] = useState<Record<string, string>>({});
|
const [parseErrors, setParseErrors] = useState<Record<string, string>>({});
|
||||||
@@ -182,6 +233,7 @@ function RulebookEditor() {
|
|||||||
body: JSON.stringify({ ...rulebook, rules: updatedRules }),
|
body: JSON.stringify({ ...rulebook, rules: updatedRules }),
|
||||||
});
|
});
|
||||||
setRulebook(updated);
|
setRulebook(updated);
|
||||||
|
props.onSaved?.(updated);
|
||||||
setSaved(true);
|
setSaved(true);
|
||||||
setTimeout(() => setSaved(false), 2500);
|
setTimeout(() => setSaved(false), 2500);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -198,6 +250,9 @@ function RulebookEditor() {
|
|||||||
<div className="rulebook-header">
|
<div className="rulebook-header">
|
||||||
<div>
|
<div>
|
||||||
<p className="rulebook-name">{rulebook.name}</p>
|
<p className="rulebook-name">{rulebook.name}</p>
|
||||||
|
<p className="rulebook-desc">
|
||||||
|
Version {rulebook.version} | Updated {formatTurnTime(rulebook.updatedAt)}
|
||||||
|
</p>
|
||||||
{rulebook.description ? (
|
{rulebook.description ? (
|
||||||
<p className="rulebook-desc">{rulebook.description}</p>
|
<p className="rulebook-desc">{rulebook.description}</p>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -290,10 +345,21 @@ export default function App() {
|
|||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [tab, setTab] = useState<"world" | "rulebook">("world");
|
const [tab, setTab] = useState<"world" | "rulebook">("world");
|
||||||
|
const [activeRulebook, setActiveRulebook] = useState<SceneRulebook | null>(null);
|
||||||
|
const [rulebooks, setRulebooks] = useState<RulebookListItem[]>([]);
|
||||||
|
|
||||||
|
async function refreshRulebookState() {
|
||||||
|
const [active, list] = await Promise.all([
|
||||||
|
fetchJson<SceneRulebook>("/api/rulebook"),
|
||||||
|
fetchJson<RulebookListItem[]>("/api/rulebooks"),
|
||||||
|
]);
|
||||||
|
setActiveRulebook(active);
|
||||||
|
setRulebooks(list);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void fetchJson<AppSnapshot>("/api/state")
|
void Promise.all([fetchJson<AppSnapshot>("/api/state"), refreshRulebookState()])
|
||||||
.then((data) => {
|
.then(([data]) => {
|
||||||
setSnapshot(data);
|
setSnapshot(data);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
})
|
||||||
@@ -315,7 +381,10 @@ export default function App() {
|
|||||||
body: JSON.stringify({ input }),
|
body: JSON.stringify({ input }),
|
||||||
});
|
});
|
||||||
setLatest(result);
|
setLatest(result);
|
||||||
const nextSnapshot = await fetchJson<AppSnapshot>("/api/state");
|
const [nextSnapshot] = await Promise.all([
|
||||||
|
fetchJson<AppSnapshot>("/api/state"),
|
||||||
|
refreshRulebookState(),
|
||||||
|
]);
|
||||||
setSnapshot(nextSnapshot);
|
setSnapshot(nextSnapshot);
|
||||||
} catch (submitError) {
|
} catch (submitError) {
|
||||||
setError(submitError instanceof Error ? submitError.message : "Unknown error");
|
setError(submitError instanceof Error ? submitError.message : "Unknown error");
|
||||||
@@ -334,12 +403,22 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
setSnapshot(result);
|
setSnapshot(result);
|
||||||
setLatest(null);
|
setLatest(null);
|
||||||
|
await refreshRulebookState();
|
||||||
} catch (resetError) {
|
} catch (resetError) {
|
||||||
setError(resetError instanceof Error ? resetError.message : "Unknown error");
|
setError(resetError instanceof Error ? resetError.message : "Unknown error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const entities = snapshot ? Object.values(snapshot.worldState.entities) : [];
|
const entities = snapshot ? Object.values(snapshot.worldState.entities) : [];
|
||||||
|
const turns = useMemo(() => snapshot?.turns.slice().reverse() ?? [], [snapshot]);
|
||||||
|
|
||||||
|
function applyClarificationOption(optionValue: string) {
|
||||||
|
setInput((prev) => {
|
||||||
|
const trimmed = prev.trim();
|
||||||
|
if (!trimmed) return optionValue;
|
||||||
|
return `${trimmed} ${optionValue}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="page-shell">
|
<main className="page-shell">
|
||||||
@@ -382,20 +461,32 @@ export default function App() {
|
|||||||
<p><strong>Input:</strong> {latest.rawText}</p>
|
<p><strong>Input:</strong> {latest.rawText}</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Interpreter:</strong> {latest.interpreter.status}
|
<strong>Interpreter:</strong> {latest.interpreter.status}
|
||||||
{typeof latest.interpreter.selectedConfidence === "number"
|
{` via ${latest.interpreter.resolutionSource}`}
|
||||||
? ` (${Math.round(latest.interpreter.selectedConfidence * 100)}% confidence)`
|
{` | model threshold ${formatConfidence(latest.interpreter.minConfidence)}`}
|
||||||
: ""}
|
{` | selected ${formatConfidence(latest.interpreter.selectedConfidence)}`}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Interpreter version:</strong> {latest.interpreter.interpreterVersion}
|
||||||
</p>
|
</p>
|
||||||
{latest.interpreter.clarification ? (
|
{latest.interpreter.clarification ? (
|
||||||
<p>
|
<p>
|
||||||
<strong>Clarification:</strong> {latest.interpreter.clarification.question}
|
<strong>Clarification ({latest.interpreter.clarification.reasonCode}):</strong>{" "}
|
||||||
|
{latest.interpreter.clarification.question}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{latest.interpreter.clarification?.options?.length ? (
|
{latest.interpreter.clarification?.options?.length ? (
|
||||||
<p>
|
<div className="chips">
|
||||||
<strong>Options:</strong>{" "}
|
{latest.interpreter.clarification.options.map((o) => (
|
||||||
{latest.interpreter.clarification.options.map((o) => o.label).join(", ")}
|
<button
|
||||||
</p>
|
key={o.id}
|
||||||
|
type="button"
|
||||||
|
className="chip"
|
||||||
|
onClick={() => applyClarificationOption(o.value)}
|
||||||
|
>
|
||||||
|
clarify: {o.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<ul className="timeline-list compact">
|
<ul className="timeline-list compact">
|
||||||
{latest.validation.map((v) => (
|
{latest.validation.map((v) => (
|
||||||
@@ -417,6 +508,25 @@ export default function App() {
|
|||||||
<strong>Diagnostics:</strong> {latest.interpreter.diagnostics.join(" | ")}
|
<strong>Diagnostics:</strong> {latest.interpreter.diagnostics.join(" | ")}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
{latest.actions.length > 0 ? (
|
||||||
|
<details>
|
||||||
|
<summary>Selected actions ({latest.actions.length})</summary>
|
||||||
|
<pre>{JSON.stringify(latest.actions, null, 2)}</pre>
|
||||||
|
</details>
|
||||||
|
) : null}
|
||||||
|
{latest.interpreter.candidates.length > 0 ? (
|
||||||
|
<details>
|
||||||
|
<summary>Interpreter candidates ({latest.interpreter.candidates.length})</summary>
|
||||||
|
<ul className="timeline-list compact">
|
||||||
|
{latest.interpreter.candidates.map((candidate, index) => (
|
||||||
|
<li key={`${candidate.action.type}-${index}`}>
|
||||||
|
{candidate.action.type} at {formatConfidence(candidate.confidence)}
|
||||||
|
{candidate.rationale ? ` - ${candidate.rationale}` : ""}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -446,6 +556,14 @@ export default function App() {
|
|||||||
<article className="panel">
|
<article className="panel">
|
||||||
<h2>World state</h2>
|
<h2>World state</h2>
|
||||||
{loading && !snapshot ? <p>Loading...</p> : null}
|
{loading && !snapshot ? <p>Loading...</p> : null}
|
||||||
|
{snapshot ? (
|
||||||
|
<div className="meta-grid">
|
||||||
|
<p className="meta-kv"><strong>World ID:</strong> {snapshot.worldState.id}</p>
|
||||||
|
<p className="meta-kv"><strong>Created:</strong> {formatTurnTime(snapshot.worldState.createdAt)}</p>
|
||||||
|
<p className="meta-kv"><strong>Domain:</strong> {String(snapshot.worldState.metadata?.domain ?? "unknown")}</p>
|
||||||
|
<p className="meta-kv"><strong>World schema:</strong> {String(snapshot.worldState.metadata?.version ?? "unknown")}</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<ul className="entity-list">
|
<ul className="entity-list">
|
||||||
{entities.map((entity) => (
|
{entities.map((entity) => (
|
||||||
<li key={entity.id}>
|
<li key={entity.id}>
|
||||||
@@ -459,11 +577,25 @@ export default function App() {
|
|||||||
<article className="panel">
|
<article className="panel">
|
||||||
<h2>Turn log</h2>
|
<h2>Turn log</h2>
|
||||||
<ul className="timeline-list">
|
<ul className="timeline-list">
|
||||||
{snapshot?.turns.slice().reverse().map((turn) => (
|
{turns.map((turn) => (
|
||||||
<li key={turn.id}>
|
<li key={turn.id}>
|
||||||
<strong>{turn.rawText}</strong>
|
<strong>{turn.rawText}</strong>
|
||||||
|
<span className="turn-time"> at {formatTurnTime(turn.createdAt)}</span>
|
||||||
{turn.interpreter ? (
|
{turn.interpreter ? (
|
||||||
<span> [interp:{turn.interpreter.status}]</span>
|
<span>
|
||||||
|
{" "}[interp:{turn.interpreter.status} via {turn.interpreter.resolutionSource};
|
||||||
|
conf {formatConfidence(turn.interpreter.selectedConfidence)}]
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{turn.interpreter?.clarification ? (
|
||||||
|
<p className="parser-hint">
|
||||||
|
Clarification ({turn.interpreter.clarification.reasonCode}): {turn.interpreter.clarification.question}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{turn.interpreter?.diagnostics?.length ? (
|
||||||
|
<p className="turn-diagnostics">
|
||||||
|
Diagnostics: {turn.interpreter.diagnostics.join(" | ")}
|
||||||
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{turn.validation.map((v) => (
|
{turn.validation.map((v) => (
|
||||||
<span key={v.actionIndex}> [{v.success ? "ok" : v.reason}]</span>
|
<span key={v.actionIndex}> [{v.success ? "ok" : v.reason}]</span>
|
||||||
@@ -472,11 +604,35 @@ export default function App() {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<article className="panel">
|
||||||
|
<h2>System</h2>
|
||||||
|
{activeRulebook ? (
|
||||||
|
<div className="meta-grid">
|
||||||
|
<p className="meta-kv"><strong>Active rulebook:</strong> {activeRulebook.name}</p>
|
||||||
|
<p className="meta-kv"><strong>Rulebook ID:</strong> {activeRulebook.id}</p>
|
||||||
|
<p className="meta-kv"><strong>Rulebook version:</strong> {activeRulebook.version}</p>
|
||||||
|
<p className="meta-kv"><strong>Updated:</strong> {formatTurnTime(activeRulebook.updatedAt)}</p>
|
||||||
|
<p className="meta-kv"><strong>Saved rulebooks:</strong> {rulebooks.length}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p>Loading rulebook info...</p>
|
||||||
|
)}
|
||||||
|
{rulebooks.length > 0 ? (
|
||||||
|
<ul className="timeline-list compact">
|
||||||
|
{rulebooks.map((rb) => (
|
||||||
|
<li key={rb.id}>
|
||||||
|
<strong>{rb.name}</strong> ({rb.id}) v{rb.version} - updated {formatTurnTime(rb.updatedAt)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : null}
|
||||||
|
</article>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<article className="panel panel--full">
|
<article className="panel panel--full">
|
||||||
<h2>Rulebook editor</h2>
|
<h2>Rulebook editor</h2>
|
||||||
<RulebookEditor />
|
<RulebookEditor onSaved={(rulebook) => setActiveRulebook(rulebook)} />
|
||||||
</article>
|
</article>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -161,6 +161,19 @@ pre {
|
|||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.meta-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 6px 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-kv {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.86rem;
|
||||||
|
color: rgba(244, 239, 228, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
.error-banner {
|
.error-banner {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
color: #ffd2b8;
|
color: #ffd2b8;
|
||||||
@@ -375,4 +388,19 @@ pre {
|
|||||||
.panel {
|
.panel {
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.meta-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.turn-time {
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.turn-diagnostics {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: rgba(244, 239, 228, 0.74);
|
||||||
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user