Micro Dao Orchestrator Ui — React Layout (shell)· typescript
import React from "react";
\
\
\
\Private\
\DAO\
\Public\
\
\
\
\
\
{netOnline ? 'Online' : 'Offline'}
\
\
\
Orchestrator {orchOk ? 'ok' : 'unreachable'}
\
\\Налаштування\
\
\
);
}
function HealthGrid() {
const items \= \[
{ title: "Messenger", ok: true },
{ title: "Parser", ok: false },
{ title: "KB Core", ok: true },
{ title: "RAG", ok: true },
{ title: "Wallet", ok: true },
{ title: "DAO", ok: true },
\];
return (
\
{items.map((x) \=\> (
\
\
\
{x.ok ? (
\
) : (
\
)}
{x.title}
\
\
\
\{x.ok ? "Працює стабільно" : "Черга задач \> p95"}\
\
\
))}
\
);
}
function OrchestratorChat() {
return (
\
\
\Чат з Оркестратором\
\
\
\
\
Вітаю. Чим допомогти? Наприклад: "Розбери PDF та створй короткий бріф у Проєктах".\
\
\
\
\
\
);
}
function ActivityFeed() {
const rows \= \[
{ t: "ingest.completed", d: "USDO готово", ts: "09:15" },
{ t: "message.created", d: "\#general", ts: "09:12" },
{ t: "vote.finalized", d: "Постанова \#12", ts: "08:55" },
{ t: "payment.sent", d: "Reward 2.5 μUTIL", ts: "08:40" },
\];
return (
\
\\Стрічка подій\ \
\
\
{rows.map((r, i) \=\> (
\
\
\{r.t}\
\— {r.d}\
\{r.ts}\
\
))}
Зміни потрібні. Я спробував застосувати правки в Canvas автоматично, але інструмент не знайшов кілька точних збігів у вашій версії файлу і відхилив патч.
Щоб не блокуватися, ось мінімальні дифи для ручної правки у вашому поточному файлі **“Micro Dao Orchestrator Ui — React Layout (shell)”**. Вони точно відповідають його актуальному вмісту вище.
# **1\) Topbar: 3-статус оркестратора**
**A. Сигнатура:**
`- function Topbar({ netOnline, orchOk }: { netOnline: boolean; orchOk: boolean }) {`
`+ function Topbar({ netOnline, orchStatus }: { netOnline: boolean; orchStatus: 'ok'|'down'|'unknown' }) {`
**B. Бейдж Orchestrator:**
``- ``
`- Orchestrator {orchOk ? 'ok' : 'unreachable'}`
``+ ``
`+ Orchestrator {orchStatus === 'ok' ? 'ok' : orchStatus === 'down' ? 'down' : 'unknown (CORS?)'}`
**C. Виклик Topbar у return:**
`- `
`+ `
**D. Стейт у OrchestratorLayout():**
`- const [orchOk, setOrchOk] = React.useState(true);`
`+ const [orchStatus, setOrchStatus] = React.useState<'ok'|'down'|'unknown'>('unknown');`
# **2\) Блок helpers: /healthz з конфігом, рекомендація профілю, оцінка індексу**
Замініть усе від рядка `// --- helpers ---` до початку `export default function OrchestratorLayout()` на це:
`// --- helpers ---`
`const CONFIG = {`
`HEALTHZ_URL: (typeof import !== 'undefined' && (import.meta as any)?.env?.VITE_HEALTHZ_URL) || '/healthz',`
`};`
`async function pingHealthz(url: string, timeoutMs = 3000): Promise<'ok'|'down'|'unknown'> {`
`try {`
`const ctrl = new AbortController();`
`const t = setTimeout(() => ctrl.abort(), timeoutMs);`
`const res = await fetch(url, { signal: ctrl.signal, headers: { accept: 'application/json,text/plain,*/*' } });`
`clearTimeout(t);`
`return res.ok ? 'ok' : 'down';`
`} catch {`
`const online = typeof navigator !== 'undefined' ? navigator.onLine : false;`
`return online ? 'unknown' : 'down';`
`}`
`}`
`async function recommendModelProfile(): Promise<'Lite'|'Base'|'Plus'|'Pro'> {`
`// Heuristics: deviceMemory (GB), CPU cores, simple UA.`
`// @ts-ignore`
`const dm = (navigator as any).deviceMemory || 4;`
`const cores = navigator.hardwareConcurrency || 4;`
`const isMobile = /iPhone|Android/i.test(navigator.userAgent);`
`if (dm >= 24 && cores >= 8 && !isMobile) return 'Pro';`
`if (dm >= 12 && cores >= 8) return 'Plus';`
`if (dm >= 6) return 'Base';`
`return 'Lite';`
`}`
`function profileSizeMB(p: 'Lite'|'Base'|'Plus'|'Pro'): number {`
`return p === 'Lite' ? 300 : p === 'Base' ? 1000 : p === 'Plus' ? 4000 : 7000;`
`}`
`function parseIndexSizeMB(label: string): number | null {`
`if (!label) return null;`
`if (label.startsWith('custom:')) return null;`
`if (label.toUpperCase().endsWith('GB')) return parseInt(label) * 1024;`
`if (label.toUpperCase().endsWith('MB')) return parseInt(label);`
`return null;`
`}`
`// ~2KB/chunk → ≈512 чанків/MB`
`function estimateChunksInt8(sizeMB: number): number { return Math.floor(sizeMB * 512); }`
# **3\) OrchestratorLayout(): стейти, useEffect, інсталятор, пропси StartScreen**
**A. Додайте/оновіть стейти:**
`const [modelProfile, setModelProfile] = React.useState('Base');`
`const [recommendedProfile, setRecommendedProfile] = React.useState('Base');`
`const [indexSize, setIndexSize] = React.useState('500MB');`
`+ const [estChunks, setEstChunks] = React.useState('');`
`const [netOnline, setNetOnline] = React.useState(typeof navigator !== 'undefined' ? navigator.onLine : true);`
`- const [orchOk, setOrchOk] = React.useState(true);`
`+ const [orchStatus, setOrchStatus] = React.useState<'ok'|'down'|'unknown'>('unknown');`
**B. Замініть увесь `useEffect` на:**
`React.useEffect(() => {`
`const on = () => setNetOnline(true);`
`const off = () => setNetOnline(false);`
`window.addEventListener('online', on);`
`window.addEventListener('offline', off);`
`let mounted = true;`
`// real /healthz ping every 10s with config + CORS aware`
`const tick = async () => {`
`const st = await pingHealthz(CONFIG.HEALTHZ_URL);`
`if (mounted) setOrchStatus(st);`
`};`
`tick();`
`const id = setInterval(tick, 10000);`
`// model profile recommendation`
`recommendModelProfile().then(p => { if (mounted) { setRecommendedProfile(p); setModelProfile(p); } });`
`// restore custom index path if saved`
`const savedPath = localStorage.getItem('microdao.indexPath');`
``if (savedPath) setIndexSize(`custom:${savedPath}`);``
`// estimate chunks for selected index size`
`const estId = setInterval(() => {`
`const mb = parseIndexSizeMB(indexSize);`
``if (mb) setEstChunks(`≈ ${Math.round(estimateChunksInt8(mb)/1000)} тис.`);``
`else setEstChunks('');`
`}, 300);`
`return () => { mounted = false; window.removeEventListener('online', on); window.removeEventListener('offline', off); clearInterval(id); clearInterval(estId); };`
`}, [indexSize]);`
**C. Інсталятор ваг, прив’язаний до профілю/індексу:**
`const handleInstallAgent = () => {`
`if (installing || agentReady) return;`
`setInstalling(true);`
`setProgress(0);`
`- const timer = setInterval(() => {`
`- setProgress((p) => {`
`- const next = Math.min(100, p + Math.random() * 18 + 5);`
`- if (next >= 100) {`
`- clearInterval(timer);`
`- setInstalling(false);`
`- setAgentReady(true);`
`- }`
`- return next;`
`- });`
`- }, 400);`
`+ localStorage.setItem('microdao.modelProfile', modelProfile);`
`+ localStorage.setItem('microdao.indexSize', indexSize);`
`+ const modelMB = profileSizeMB(modelProfile as any);`
`+ const idxMB = parseIndexSizeMB(indexSize) || 0;`
`+ const totalMB = modelMB + idxMB;`
`+ const estTimeMs = Math.min(120000, 1500 * (totalMB / 50)); // ~1.5s per 50MB, cap 2min`
`+ const started = Date.now();`
`+ const timer = setInterval(() => {`
`+ const elapsed = Date.now() - started;`
`+ let pct = Math.min(100, (elapsed / estTimeMs) * 100);`
`+ pct = pct < 100 ? (pct * 0.85) : 100; // ease-out`
`+ setProgress(pct);`
`+ if (pct >= 99.5) {`
`+ clearInterval(timer);`
`+ setInstalling(false);`
`+ setAgentReady(true);`
`+ }`
`+ }, 300);`
`};`
**D. Пропси для `StartScreen` у `content()` (внизу):**
` `
# **4\) StartScreen: додайте проп і підказку**
**A. Сигнатура:**
`- function StartScreen({ onCreateDAO, onJoinDAO, onSolo, agentReady, onInstallAgent, installing, progress, modelProfile, setModelProfile, indexSize, setIndexSize, recommendedProfile, onPickCustomIndex }:`
`+ function StartScreen({ onCreateDAO, onJoinDAO, onSolo, agentReady, onInstallAgent, installing, progress, modelProfile, setModelProfile, indexSize, setIndexSize, recommendedProfile, onPickCustomIndex, estChunks }:`
…та в типі параметрів додайте `estChunks: string;`.
**B. Підказка під індексом:**
`- int8 квантування заощаджує ×3–4 місця.
`
`+ `
`+ int8 квантування заощаджує ×3–4 місця. {estChunks && Орієнтовно: {estChunks} чанків (int8). }`
`+
`
Після цих замін у вас буде:
* `/healthz` з URL із конфіга і CORS-статусом `unknown`.
* Автовибір профілю моделі та збереження вибору.
* Оцінка місткості індексу в чанках (int8).
* Тристатусний бейдж оркестратора.