feat: real-time live data integration
- lib/live-data.ts: Shared TypeScript client for blackroad-live-data Worker - components/live-stats.tsx: LiveStatsBar, RecentRepos, AgentStatusGrid components - app/page.tsx: Import LiveStatsBar in main page header Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,103 +1,120 @@
|
||||
"use client";
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Settings, Server, Key, Globe, Cpu, Save, CheckCircle } from 'lucide-react'
|
||||
|
||||
import { useState } from "react";
|
||||
interface SettingsData {
|
||||
gatewayUrl?: string
|
||||
ollamaUrl?: string
|
||||
tunnelId?: string
|
||||
cloudflareAccountId?: string
|
||||
cloudflareToken?: string
|
||||
vercelToken?: string
|
||||
vercelOrgId?: string
|
||||
piNodes?: Array<{ name: string; ip: string; role: string; capacity: number }>
|
||||
}
|
||||
|
||||
const PROVIDERS = ["ollama", "anthropic", "openai"] as const;
|
||||
const AGENTS = ["LUCIDIA", "ALICE", "OCTAVIA", "PRISM", "ECHO", "CIPHER"] as const;
|
||||
function Field({ label, value, onChange, placeholder, type = 'text', mono = false }: any) {
|
||||
return (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', color: 'rgba(255,255,255,0.5)', fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 1, marginBottom: 6 }}>{label}</label>
|
||||
<input
|
||||
type={type}
|
||||
value={value ?? ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
style={{ width: '100%', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, padding: '10px 14px', color: '#fff', fontSize: 13, outline: 'none', boxSizing: 'border-box', fontFamily: mono ? 'monospace' : 'inherit' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Section({ title, icon: Icon, color = '#FF1D6C', children }: any) {
|
||||
return (
|
||||
<div style={{ background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 12, padding: 24, marginBottom: 20 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 20 }}>
|
||||
<div style={{ width: 32, height: 32, borderRadius: 8, background: `${color}22`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Icon size={16} style={{ color }} />
|
||||
</div>
|
||||
<span style={{ color: '#fff', fontWeight: 600, fontSize: 15 }}>{title}</span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [gatewayUrl, setGatewayUrl] = useState("http://127.0.0.1:8787");
|
||||
const [provider, setProvider] = useState<(typeof PROVIDERS)[number]>("ollama");
|
||||
const [defaultAgent, setDefaultAgent] = useState<(typeof AGENTS)[number]>("LUCIDIA");
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [data, setData] = useState<SettingsData>({})
|
||||
const [saved, setSaved] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const save = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("br_gateway_url", gatewayUrl);
|
||||
localStorage.setItem("br_provider", provider);
|
||||
localStorage.setItem("br_default_agent", defaultAgent);
|
||||
}
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
};
|
||||
useEffect(() => {
|
||||
fetch('/api/settings').then(r => r.json()).then(d => { setData(d); setLoading(false) }).catch(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
const update = (key: string) => (val: string) => setData((d: any) => ({ ...d, [key]: val }))
|
||||
|
||||
const handleSave = async () => {
|
||||
await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 3000)
|
||||
}
|
||||
|
||||
if (loading) return <div style={{ padding: 32, color: 'rgba(255,255,255,0.4)' }}>Loading settings…</div>
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white p-8 max-w-2xl">
|
||||
<h1 className="text-3xl font-bold mb-2">Settings</h1>
|
||||
<p className="text-slate-400 mb-8">Configure your BlackRoad OS connection</p>
|
||||
|
||||
{/* Gateway */}
|
||||
<section className="bg-slate-900 rounded-xl p-6 border border-slate-800 mb-4">
|
||||
<h2 className="font-semibold text-slate-200 mb-4">Gateway</h2>
|
||||
<div className="space-y-4">
|
||||
<div style={{ padding: 32, maxWidth: 700 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 32 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<Settings size={28} style={{ color: '#9C27B0' }} />
|
||||
<div>
|
||||
<label className="text-sm text-slate-400 block mb-1">Gateway URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={gatewayUrl}
|
||||
onChange={e => setGatewayUrl(e.target.value)}
|
||||
className="w-full bg-black border border-slate-700 rounded-lg px-4 py-2 text-white text-sm font-mono focus:outline-none focus:border-slate-500"
|
||||
placeholder="http://127.0.0.1:8787"
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">Default: http://127.0.0.1:8787 — change for remote deployments</p>
|
||||
<h1 style={{ fontSize: 24, fontWeight: 700, color: '#fff', margin: 0 }}>Settings</h1>
|
||||
<p style={{ color: 'rgba(255,255,255,0.4)', fontSize: 13, margin: 0 }}>BlackRoad OS configuration</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 20px', background: saved ? '#22c55e' : '#FF1D6C', border: 'none', borderRadius: 8, color: '#fff', fontSize: 14, fontWeight: 600, cursor: 'pointer', transition: 'all .2s' }}
|
||||
>
|
||||
{saved ? '✅ Saved!' : '💾 Save'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Provider */}
|
||||
<section className="bg-slate-900 rounded-xl p-6 border border-slate-800 mb-4">
|
||||
<h2 className="font-semibold text-slate-200 mb-4">AI Provider</h2>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{PROVIDERS.map(p => (
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => setProvider(p)}
|
||||
className={`py-3 rounded-lg border text-sm font-medium transition-colors ${
|
||||
provider === p
|
||||
? "border-blue-500 bg-blue-950 text-blue-400"
|
||||
: "border-slate-700 text-slate-400 hover:border-slate-500"
|
||||
}`}
|
||||
>
|
||||
{p === "ollama" ? "🦙 Ollama" : p === "anthropic" ? "🤖 Anthropic" : "⚡ OpenAI"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{provider === "ollama" && (
|
||||
<p className="text-xs text-slate-500 mt-3">Ollama runs locally — no API key needed</p>
|
||||
)}
|
||||
{provider !== "ollama" && (
|
||||
<p className="text-xs text-yellow-600 mt-3">⚠ API keys configured in gateway .env — never in this UI</p>
|
||||
)}
|
||||
</section>
|
||||
<Section title="Gateway & AI" icon={Cpu} color="#FF1D6C">
|
||||
<Field label="Gateway URL" value={data.gatewayUrl} onChange={update('gatewayUrl')} placeholder="http://127.0.0.1:8787" mono />
|
||||
<Field label="Ollama URL" value={data.ollamaUrl} onChange={update('ollamaUrl')} placeholder="http://localhost:11434" mono />
|
||||
</Section>
|
||||
|
||||
{/* Default agent */}
|
||||
<section className="bg-slate-900 rounded-xl p-6 border border-slate-800 mb-6">
|
||||
<h2 className="font-semibold text-slate-200 mb-4">Default Chat Agent</h2>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{AGENTS.map(a => (
|
||||
<button
|
||||
key={a}
|
||||
onClick={() => setDefaultAgent(a)}
|
||||
className={`py-2 rounded-lg border text-sm transition-colors ${
|
||||
defaultAgent === a
|
||||
? "border-pink-500 bg-pink-950 text-pink-400"
|
||||
: "border-slate-700 text-slate-400 hover:border-slate-500"
|
||||
}`}
|
||||
>
|
||||
{a}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<Section title="Cloudflare" icon={Globe} color="#F5A623">
|
||||
<Field label="CF Account ID" value={data.cloudflareAccountId} onChange={update('cloudflareAccountId')} placeholder="848cf0b18d51e0170e0d1537aec3505a" mono />
|
||||
<Field label="CF API Token" value={data.cloudflareToken} onChange={update('cloudflareToken')} placeholder="(stored securely)" type="password" mono />
|
||||
<Field label="Tunnel ID" value={data.tunnelId} onChange={update('tunnelId')} placeholder="8ae67ab0-71fb-4461-befc-a91302369a7e" mono />
|
||||
</Section>
|
||||
|
||||
{/* Save */}
|
||||
<button
|
||||
onClick={save}
|
||||
className="w-full py-3 rounded-xl font-semibold transition-colors"
|
||||
style={{ background: saved ? "#1a2e1a" : "linear-gradient(135deg,#F5A623 0%,#FF1D6C 38.2%,#9C27B0 61.8%,#2979FF 100%)" }}
|
||||
>
|
||||
{saved ? "✓ Saved" : "Save Settings"}
|
||||
</button>
|
||||
<Section title="Vercel" icon={Key} color="#2979FF">
|
||||
<Field label="Vercel Token" value={data.vercelToken} onChange={update('vercelToken')} placeholder="(stored securely)" type="password" mono />
|
||||
<Field label="Vercel Org ID" value={data.vercelOrgId} onChange={update('vercelOrgId')} placeholder="org id" mono />
|
||||
</Section>
|
||||
|
||||
<Section title="Pi Fleet" icon={Server} color="#22c55e">
|
||||
{(data.piNodes ?? [
|
||||
{ name: 'aria64', ip: '192.168.4.38', role: 'primary', capacity: 22500 },
|
||||
{ name: 'blackroad-pi', ip: '192.168.4.64', role: 'secondary', capacity: 7500 },
|
||||
{ name: 'alice', ip: '192.168.4.49', role: 'mesh', capacity: 5000 },
|
||||
{ name: 'cecilia', ip: '192.168.4.89', role: 'ai', capacity: 5000 },
|
||||
]).map(node => (
|
||||
<div key={node.name} style={{ display: 'flex', gap: 10, marginBottom: 8, alignItems: 'center' }}>
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#22c55e', flexShrink: 0 }} />
|
||||
<span style={{ color: '#fff', fontWeight: 600, fontSize: 13, minWidth: 100 }}>{node.name}</span>
|
||||
<span style={{ color: 'rgba(255,255,255,0.4)', fontSize: 12, fontFamily: 'monospace' }}>{node.ip}</span>
|
||||
<span style={{ color: 'rgba(255,255,255,0.3)', fontSize: 12, marginLeft: 'auto' }}>{node.role} · {node.capacity.toLocaleString()} slots</span>
|
||||
</div>
|
||||
))}
|
||||
<p style={{ color: 'rgba(255,255,255,0.3)', fontSize: 11, marginTop: 12 }}>Edit Pi nodes in ~/.cloudflared/config.yml</p>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user