sync: update from blackroad-operator 2026-03-14
Some checks failed
Autonomous Repo Agent / autonomous-build (push) Has been cancelled
BlackRoad AI Agents / agent-response (push) Has been cancelled
🔍 BlackRoad CodeQL Security Analysis / CodeQL Analysis (javascript) (push) Has been cancelled
🔍 BlackRoad CodeQL Security Analysis / CodeQL Analysis (python) (push) Has been cancelled
CI / Test (push) Has been cancelled
Deploy to Cloudflare Pages / Deploy to Cloudflare Pages (push) Has been cancelled
Trinity Compliance Check / check-compliance (push) Has been cancelled
Some checks failed
Autonomous Repo Agent / autonomous-build (push) Has been cancelled
BlackRoad AI Agents / agent-response (push) Has been cancelled
🔍 BlackRoad CodeQL Security Analysis / CodeQL Analysis (javascript) (push) Has been cancelled
🔍 BlackRoad CodeQL Security Analysis / CodeQL Analysis (python) (push) Has been cancelled
CI / Test (push) Has been cancelled
Deploy to Cloudflare Pages / Deploy to Cloudflare Pages (push) Has been cancelled
Trinity Compliance Check / check-compliance (push) Has been cancelled
Synced from BlackRoad-OS-Inc/blackroad-operator/orgs/core/blackroad-os-web BlackRoad OS — Pave Tomorrow. RoadChain-SHA2048: 13032509284e1f6c RoadChain-Identity: alexa@sovereign RoadChain-Full: 13032509284e1f6ca60f7004aa28e90fdc0fdae165e934d79f9ee91ee80caa9c42b57ad6c0ed9c400d303a39716259ad59602b6bc19ba3ea0720412c7957b64908250e99db1c5debc19331e7d473bb26d0c501cf1f02155ec53315372f62c0a36ca9d67d033e42c4d9683c2220eda4b4f4487eff9e474726e279d738e8a613870d38f5197ee4504b40c95ce73a1df4eb837b18bfce046609b29fbb4a7bdb83501806d25bfaa79be4f46f31b9616511733690a6b2a6257084c264223462161aca13e0608a59f5a0cc55f9835d640a1dde518b15c019a4ba62e8513cbbd58fd436d9e401fa12a1a8c82908b4688359b829c90e76067668e4793638a8d33fb9a77c
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="animate-pulse space-y-4 p-6">
|
||||
<div className="h-8 bg-gray-200 rounded w-48" />
|
||||
<div className="h-32 bg-gray-200 rounded" />
|
||||
<div className="space-y-2">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="h-6 bg-gray-200 rounded" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Globe, Scroll, Code2, Rss, Sparkles, RefreshCw, ExternalLink } from 'lucide-react';
|
||||
|
||||
interface WorldArtifact {
|
||||
id: string;
|
||||
@@ -12,15 +11,15 @@ interface WorldArtifact {
|
||||
link: string;
|
||||
}
|
||||
|
||||
const TYPE_META: Record<string, { color: string; icon: any; label: string }> = {
|
||||
world: { color: '#2979FF', icon: Globe, label: 'World' },
|
||||
lore: { color: '#9C27B0', icon: Scroll, label: 'Lore' },
|
||||
code: { color: '#FF1D6C', icon: Code2, label: 'Code' },
|
||||
const TYPE_COLORS: Record<string, string> = {
|
||||
world: '#2979FF',
|
||||
lore: '#9C27B0',
|
||||
code: '#FF1D6C',
|
||||
};
|
||||
|
||||
const NODE_DOT: Record<string, string> = {
|
||||
aria64: '#FF1D6C',
|
||||
alice: '#2979FF',
|
||||
const NODE_EMOJI: Record<string, string> = {
|
||||
aria64: '🔴',
|
||||
alice: '🔵',
|
||||
};
|
||||
|
||||
export default function WorldsPage() {
|
||||
@@ -28,185 +27,103 @@ export default function WorldsPage() {
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filter, setFilter] = useState<string>('all');
|
||||
const [generating, setGenerating] = useState(false);
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const res = await fetch('/api/worlds?limit=60');
|
||||
const data = await res.json();
|
||||
setWorlds(data.worlds || []);
|
||||
setTotal(data.total || 0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
try {
|
||||
const res = await fetch('/api/worlds?limit=60');
|
||||
const data = await res.json();
|
||||
setWorlds(data.worlds || []);
|
||||
setTotal(data.total || 0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
load();
|
||||
const interval = setInterval(load, 60000);
|
||||
const interval = setInterval(load, 60000); // refresh every 60s
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const filtered = filter === 'all' ? worlds : worlds.filter(w => w.type === filter || w.node === filter);
|
||||
|
||||
const generateWorld = async () => {
|
||||
setGenerating(true);
|
||||
try {
|
||||
// Ask the chat API to generate a world
|
||||
const chatRes = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message: 'Generate a new BlackRoad world artifact. Return ONLY valid JSON with fields: name (string), type (one of: world|lore|code|artifact), description (2 sentences), lore (one evocative sentence), tags (array of 3 strings).',
|
||||
}),
|
||||
});
|
||||
|
||||
if (chatRes.ok) {
|
||||
const chatData = await chatRes.json();
|
||||
// Try to parse JSON from the response text
|
||||
const text: string = chatData.text || chatData.message || '';
|
||||
const match = text.match(/\{[\s\S]*\}/);
|
||||
if (match) {
|
||||
try {
|
||||
const parsed = JSON.parse(match[0]);
|
||||
// POST to /api/worlds to persist it
|
||||
await fetch('/api/worlds', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...parsed, agent: 'lucidia' }),
|
||||
});
|
||||
} catch { /* JSON parse failed — still reload */ }
|
||||
}
|
||||
}
|
||||
|
||||
await load();
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="p-6 max-w-5xl mx-auto">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white flex items-center gap-3">
|
||||
<Globe className="h-7 w-7 text-[#2979FF]" />
|
||||
World Artifacts
|
||||
</h1>
|
||||
<h1 className="text-2xl font-bold text-white">🌍 World Artifacts</h1>
|
||||
<p className="text-gray-400 text-sm mt-1">
|
||||
{total} artifacts generated by the Pi fleet · auto-refreshes every 60s
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={generateWorld}
|
||||
disabled={generating}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-[#FF1D6C] to-violet-600 text-white text-sm font-medium rounded-xl hover:opacity-90 disabled:opacity-50 transition-opacity"
|
||||
>
|
||||
{generating ? <RefreshCw className="h-4 w-4 animate-spin" /> : <Sparkles className="h-4 w-4" />}
|
||||
{generating ? 'Generating…' : 'Generate World'}
|
||||
</button>
|
||||
<a
|
||||
href="https://worlds.blackroad.io/rss"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-1.5 px-3 py-2 bg-white/5 border border-white/10 rounded-xl text-gray-400 text-sm hover:text-white hover:bg-white/10 transition-all"
|
||||
>
|
||||
<Rss className="h-4 w-4" />
|
||||
RSS
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
href="https://worlds.blackroad.io/rss"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-xs px-3 py-1 rounded border border-gray-600 text-gray-400 hover:text-white hover:border-gray-400 transition-colors"
|
||||
>
|
||||
RSS Feed ↗
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{[
|
||||
{ label: 'Worlds', count: worlds.filter(w => w.type === 'world').length, color: '#2979FF', icon: Globe },
|
||||
{ label: 'Lore', count: worlds.filter(w => w.type === 'lore').length, color: '#9C27B0', icon: Scroll },
|
||||
{ label: 'Code', count: worlds.filter(w => w.type === 'code').length, color: '#FF1D6C', icon: Code2 },
|
||||
].map(s => (
|
||||
<div key={s.label} className="bg-white/5 border border-white/10 rounded-xl p-4 flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center" style={{ background: s.color + '20' }}>
|
||||
<s.icon className="h-4 w-4" style={{ color: s.color }} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-bold text-white">{s.count || '—'}</div>
|
||||
<div className="text-xs text-gray-500">{s.label} artifacts</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Filter Pills */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{/* Filter pills */}
|
||||
<div className="flex gap-2 mb-4 flex-wrap">
|
||||
{['all', 'world', 'lore', 'code', 'aria64', 'alice'].map(f => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`px-4 py-1.5 rounded-xl text-xs font-medium transition-all capitalize ${
|
||||
filter === f ? 'bg-white/10 text-white' : 'text-gray-400 hover:text-white bg-white/5 hover:bg-white/8'
|
||||
className={`px-3 py-1 rounded-full text-xs font-medium transition-colors ${
|
||||
filter === f
|
||||
? 'bg-white text-black'
|
||||
: 'bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{f === 'aria64' ? '🔴 aria64' : f === 'alice' ? '🔵 alice' : f}
|
||||
{f === 'all' ? 'All' : f}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Artifacts */}
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-3 text-gray-400">
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
Loading worlds…
|
||||
</div>
|
||||
<div className="text-gray-500 text-sm">Loading worlds…</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{filtered.map(w => {
|
||||
const meta = TYPE_META[w.type] || TYPE_META.world;
|
||||
const Icon = meta.icon;
|
||||
return (
|
||||
<a
|
||||
key={w.id}
|
||||
href={w.link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-3 p-4 rounded-xl bg-white/5 border border-white/10 hover:bg-white/8 hover:border-white/20 transition-all group"
|
||||
<div className="grid gap-2">
|
||||
{filtered.map(w => (
|
||||
<a
|
||||
key={w.id}
|
||||
href={w.link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-gray-900 border border-gray-800 hover:border-gray-600 transition-colors group"
|
||||
>
|
||||
<span
|
||||
className="w-2 h-2 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: TYPE_COLORS[w.type] || '#666' }}
|
||||
/>
|
||||
<span className="text-white text-sm font-medium flex-1 group-hover:text-blue-300 transition-colors">
|
||||
{w.title}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{NODE_EMOJI[w.node] || '⚪'} {w.node}
|
||||
</span>
|
||||
<span
|
||||
className="text-xs px-2 py-0.5 rounded"
|
||||
style={{
|
||||
backgroundColor: (TYPE_COLORS[w.type] || '#666') + '22',
|
||||
color: TYPE_COLORS[w.type] || '#aaa',
|
||||
}}
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0" style={{ background: meta.color + '20' }}>
|
||||
<Icon className="h-4 w-4" style={{ color: meta.color }} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-white text-sm font-medium group-hover:text-[#2979FF] transition-colors truncate">
|
||||
{w.title}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-xs text-gray-500 font-mono">{w.node}</span>
|
||||
<span className="text-gray-700">·</span>
|
||||
<span className="text-xs text-gray-500">{new Date(w.timestamp).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className="text-xs px-2 py-0.5 rounded-md font-medium" style={{ background: meta.color + '15', color: meta.color }}>
|
||||
{meta.label}
|
||||
</span>
|
||||
<ExternalLink className="h-3.5 w-3.5 text-gray-600 group-hover:text-gray-400 transition-colors" />
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
{w.type}
|
||||
</span>
|
||||
<span className="text-xs text-gray-600 font-mono">
|
||||
{new Date(w.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && filtered.length === 0 && (
|
||||
<div className="text-center py-16 text-gray-500">
|
||||
<Globe className="h-12 w-12 mx-auto mb-3 opacity-20" />
|
||||
<div>No artifacts match this filter.</div>
|
||||
<button onClick={generateWorld} className="mt-4 text-[#FF1D6C] text-sm hover:underline">
|
||||
Generate the first one →
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-gray-500 text-sm text-center py-12">No artifacts match this filter.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user