Spaces:
Runtime error
Runtime error
Upload 33 files
Browse files- automation.py +25 -0
- server.py +21 -0
- ui_kits/chan-compass/api.js +2 -0
- ui_kits/chan-compass/app.jsx +17 -3
- ui_kits/chan-compass/views.jsx +80 -22
automation.py
CHANGED
|
@@ -20,11 +20,14 @@ schedule fires unattended.
|
|
| 20 |
"""
|
| 21 |
from __future__ import annotations
|
| 22 |
|
|
|
|
| 23 |
import datetime as dt
|
| 24 |
import threading
|
| 25 |
import traceback
|
| 26 |
from zoneinfo import ZoneInfo
|
| 27 |
|
|
|
|
|
|
|
| 28 |
NY = ZoneInfo("America/New_York")
|
| 29 |
RUN_HOUR, RUN_MINUTE = 18, 10
|
| 30 |
|
|
@@ -41,6 +44,27 @@ STATE = {
|
|
| 41 |
}
|
| 42 |
_lock = threading.Lock()
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
def _log(msg: str):
|
| 46 |
stamp = dt.datetime.now(NY).strftime("%m-%d %H:%M:%S ET")
|
|
@@ -109,6 +133,7 @@ def run_pipeline(tickers=None, force: bool = True) -> str:
|
|
| 109 |
_log(f"Auto-research skipped: {e}")
|
| 110 |
|
| 111 |
STATE["last_run"] = dt.datetime.now(NY)
|
|
|
|
| 112 |
_log("Pipeline finished.")
|
| 113 |
return f"Done. {summary}"
|
| 114 |
except Exception as e:
|
|
|
|
| 20 |
"""
|
| 21 |
from __future__ import annotations
|
| 22 |
|
| 23 |
+
import os
|
| 24 |
import datetime as dt
|
| 25 |
import threading
|
| 26 |
import traceback
|
| 27 |
from zoneinfo import ZoneInfo
|
| 28 |
|
| 29 |
+
import paths
|
| 30 |
+
|
| 31 |
NY = ZoneInfo("America/New_York")
|
| 32 |
RUN_HOUR, RUN_MINUTE = 18, 10
|
| 33 |
|
|
|
|
| 44 |
}
|
| 45 |
_lock = threading.Lock()
|
| 46 |
|
| 47 |
+
_LAST_RUN_FILE = os.path.join(paths.OUTPUT_DIR, "last_run.txt")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _save_last_run(when: dt.datetime):
|
| 51 |
+
try:
|
| 52 |
+
with open(_LAST_RUN_FILE, "w", encoding="utf-8") as f:
|
| 53 |
+
f.write(when.isoformat())
|
| 54 |
+
except Exception:
|
| 55 |
+
pass
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _load_last_run():
|
| 59 |
+
try:
|
| 60 |
+
with open(_LAST_RUN_FILE, encoding="utf-8") as f:
|
| 61 |
+
STATE["last_run"] = dt.datetime.fromisoformat(f.read().strip())
|
| 62 |
+
except Exception:
|
| 63 |
+
STATE["last_run"] = None
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
_load_last_run()
|
| 67 |
+
|
| 68 |
|
| 69 |
def _log(msg: str):
|
| 70 |
stamp = dt.datetime.now(NY).strftime("%m-%d %H:%M:%S ET")
|
|
|
|
| 133 |
_log(f"Auto-research skipped: {e}")
|
| 134 |
|
| 135 |
STATE["last_run"] = dt.datetime.now(NY)
|
| 136 |
+
_save_last_run(STATE["last_run"])
|
| 137 |
_log("Pipeline finished.")
|
| 138 |
return f"Done. {summary}"
|
| 139 |
except Exception as e:
|
server.py
CHANGED
|
@@ -237,6 +237,27 @@ async def market_status():
|
|
| 237 |
return JSONResponse({"open": is_open, "label": label, "variant": variant})
|
| 238 |
|
| 239 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
@app.get("/api/model/status")
|
| 241 |
async def model_status():
|
| 242 |
return JSONResponse({
|
|
|
|
| 237 |
return JSONResponse({"open": is_open, "label": label, "variant": variant})
|
| 238 |
|
| 239 |
|
| 240 |
+
@app.get("/api/model/list")
|
| 241 |
+
async def model_list():
|
| 242 |
+
return JSONResponse({
|
| 243 |
+
"models": list(llm_local.MODEL_ZOO.keys()),
|
| 244 |
+
"analyst": llm_local.WORKERS["analyst"]["model"],
|
| 245 |
+
"analyst_ready": llm_local.WORKERS["analyst"]["llm"] is not None,
|
| 246 |
+
})
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
@app.post("/api/model/load")
|
| 250 |
+
async def model_load(req: Request):
|
| 251 |
+
body = await req.json()
|
| 252 |
+
name = body.get("model", "")
|
| 253 |
+
if name not in llm_local.MODEL_ZOO:
|
| 254 |
+
return JSONResponse({"status": "⚠️ Unknown model."})
|
| 255 |
+
import threading
|
| 256 |
+
threading.Thread(target=lambda: llm_local.load_model(name, worker="analyst"),
|
| 257 |
+
daemon=True).start()
|
| 258 |
+
return JSONResponse({"status": f"⏳ Loading {name} onto the Analyst sub-agent…"})
|
| 259 |
+
|
| 260 |
+
|
| 261 |
@app.get("/api/model/status")
|
| 262 |
async def model_status():
|
| 263 |
return JSONResponse({
|
ui_kits/chan-compass/api.js
CHANGED
|
@@ -55,6 +55,8 @@ window.CCApi = (function () {
|
|
| 55 |
autoState: () => jget("/api/automation/state"),
|
| 56 |
publishTraces: (repo) => jpost("/api/automation/publish-traces", { repo }),
|
| 57 |
marketStatus: () => jget("/api/market/status"),
|
|
|
|
|
|
|
| 58 |
modelStatus: () => jget("/api/model/status"),
|
| 59 |
modelTest: () => jpost("/api/model/test", {}),
|
| 60 |
modelTestStatus: () => jget("/api/model/test-status"),
|
|
|
|
| 55 |
autoState: () => jget("/api/automation/state"),
|
| 56 |
publishTraces: (repo) => jpost("/api/automation/publish-traces", { repo }),
|
| 57 |
marketStatus: () => jget("/api/market/status"),
|
| 58 |
+
modelList: () => jget("/api/model/list"),
|
| 59 |
+
modelLoad: (model) => jpost("/api/model/load", { model }),
|
| 60 |
modelStatus: () => jget("/api/model/status"),
|
| 61 |
modelTest: () => jpost("/api/model/test", {}),
|
| 62 |
modelTestStatus: () => jget("/api/model/test-status"),
|
ui_kits/chan-compass/app.jsx
CHANGED
|
@@ -9,7 +9,8 @@ function _pascalG(name) {
|
|
| 9 |
function _glyphChildren(node) {
|
| 10 |
if (!node) return [];
|
| 11 |
if (Array.isArray(node)) {
|
| 12 |
-
if (node.
|
|
|
|
| 13 |
return node;
|
| 14 |
}
|
| 15 |
return node.tags || node.children || [];
|
|
@@ -29,6 +30,16 @@ function _glyphSvg(name, size, color) {
|
|
| 29 |
+ `stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`;
|
| 30 |
}
|
| 31 |
function Glyph({ name, size = 18, color, style }) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
const html = _glyphSvg(name, size, color);
|
| 33 |
return <span style={{ width:size, height:size, display:'inline-flex',
|
| 34 |
alignItems:'center', justifyContent:'center', ...style }}
|
|
@@ -109,11 +120,14 @@ function App() {
|
|
| 109 |
</main>
|
| 110 |
|
| 111 |
<footer className="cc-footer">
|
| 112 |
-
<span>Chan Compass
|
|
|
|
|
|
|
|
|
|
| 113 |
<span className="cc-foot-sep">·</span>
|
| 114 |
<span>data: Yahoo Finance</span>
|
| 115 |
<span className="cc-foot-sep">·</span>
|
| 116 |
-
<span>design
|
| 117 |
</footer>
|
| 118 |
</div>
|
| 119 |
);
|
|
|
|
| 9 |
function _glyphChildren(node) {
|
| 10 |
if (!node) return [];
|
| 11 |
if (Array.isArray(node)) {
|
| 12 |
+
if (typeof node[0] === "string" && node[0].toLowerCase() === "svg" && Array.isArray(node[2]))
|
| 13 |
+
return node[2];
|
| 14 |
return node;
|
| 15 |
}
|
| 16 |
return node.tags || node.children || [];
|
|
|
|
| 30 |
+ `stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`;
|
| 31 |
}
|
| 32 |
function Glyph({ name, size = 18, color, style }) {
|
| 33 |
+
const [, force] = useS(0);
|
| 34 |
+
useE(() => {
|
| 35 |
+
if (window.lucide && window.lucide.icons) return;
|
| 36 |
+
let n = 0;
|
| 37 |
+
const id = setInterval(() => {
|
| 38 |
+
n++;
|
| 39 |
+
if ((window.lucide && window.lucide.icons) || n > 40) { clearInterval(id); force(x=>x+1); }
|
| 40 |
+
}, 100);
|
| 41 |
+
return () => clearInterval(id);
|
| 42 |
+
}, []);
|
| 43 |
const html = _glyphSvg(name, size, color);
|
| 44 |
return <span style={{ width:size, height:size, display:'inline-flex',
|
| 45 |
alignItems:'center', justifyContent:'center', ...style }}
|
|
|
|
| 120 |
</main>
|
| 121 |
|
| 122 |
<footer className="cc-footer">
|
| 123 |
+
<span>Chan Compass</span>
|
| 124 |
+
<span className="cc-foot-sep">·</span>
|
| 125 |
+
<span>by <a href="https://huggingface.co/ranranrunforit" target="_blank" rel="noreferrer"
|
| 126 |
+
style={{color:"var(--accent)", textDecoration:"none"}}>@ranranrunforit</a>, made with care for my family</span>
|
| 127 |
<span className="cc-foot-sep">·</span>
|
| 128 |
<span>data: Yahoo Finance</span>
|
| 129 |
<span className="cc-foot-sep">·</span>
|
| 130 |
+
<span>design: Adobe Spectrum 2</span>
|
| 131 |
</footer>
|
| 132 |
</div>
|
| 133 |
);
|
ui_kits/chan-compass/views.jsx
CHANGED
|
@@ -5,6 +5,22 @@ const { Button, Field, Checkbox, Switch, Card, InlineAlert, Badge, StatusLight,
|
|
| 5 |
const { useState, useEffect, useRef } = React;
|
| 6 |
const API = window.CCApi;
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
// kebab-case (data-lucide name) → PascalCase (lucide.icons key)
|
| 9 |
function _pascal(name) {
|
| 10 |
return String(name).split(/[-_]/).map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join("");
|
|
@@ -12,8 +28,10 @@ function _pascal(name) {
|
|
| 12 |
function _lucideChildren(node) {
|
| 13 |
if (!node) return [];
|
| 14 |
if (Array.isArray(node)) {
|
| 15 |
-
//
|
| 16 |
-
if (node.
|
|
|
|
|
|
|
| 17 |
return node;
|
| 18 |
}
|
| 19 |
return node.tags || node.children || [];
|
|
@@ -33,6 +51,16 @@ function _lucideSvg(name, size, color) {
|
|
| 33 |
+ `stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`;
|
| 34 |
}
|
| 35 |
function Icon({ name, size = 18, color, style }) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
const html = _lucideSvg(name, size, color);
|
| 37 |
return <span style={{ width:size, height:size, display:"inline-flex",
|
| 38 |
alignItems:"center", justifyContent:"center", ...style }}
|
|
@@ -84,13 +112,13 @@ function Markdown({ text }) {
|
|
| 84 |
|
| 85 |
/* ─────────── Signals ─────────── */
|
| 86 |
function SignalsView() {
|
| 87 |
-
const [pool, setPool] =
|
| 88 |
const [force, setForce] = useState(false);
|
| 89 |
const [running, setRunning] = useState(false);
|
| 90 |
-
const [rows, setRows] =
|
| 91 |
-
const [summary, setSummary] =
|
| 92 |
-
const [sel, setSel] =
|
| 93 |
-
const [aiText, setAiText] =
|
| 94 |
const [aiBusy, setAiBusy] = useState(false);
|
| 95 |
|
| 96 |
const run = async () => {
|
|
@@ -175,11 +203,11 @@ function SignalsView() {
|
|
| 175 |
|
| 176 |
/* ─────────── Rotation ─────────── */
|
| 177 |
function RotationView() {
|
| 178 |
-
const [d1, setD1] =
|
| 179 |
-
const [d5, setD5] =
|
| 180 |
-
const [d20, setD20] =
|
| 181 |
-
const [asof, setAsof] =
|
| 182 |
-
const [ai, setAi] =
|
| 183 |
const [loading, setLoading] = useState(false);
|
| 184 |
const refresh = async () => {
|
| 185 |
setLoading(true);
|
|
@@ -232,11 +260,11 @@ function RotationView() {
|
|
| 232 |
|
| 233 |
/* ─────────── News ─────────── */
|
| 234 |
function NewsView() {
|
| 235 |
-
const [hold, setHold] =
|
| 236 |
-
const [out, setOut] =
|
| 237 |
const [busy, setBusy] = useState(false);
|
| 238 |
const [saved, setSaved] = useState("");
|
| 239 |
-
useEffect(()=>{ API.holdings().then(r=>setHold((r.holdings||[]).join(", "))).catch(()=>{}); },[]);
|
| 240 |
const save = async () => {
|
| 241 |
setSaved("Saving…");
|
| 242 |
try { const r = await API.saveHoldings(hold);
|
|
@@ -267,12 +295,12 @@ function NewsView() {
|
|
| 267 |
|
| 268 |
/* ─────────── Research ─────────── */
|
| 269 |
function ResearchView() {
|
| 270 |
-
const [tic, setTic] =
|
| 271 |
-
const [progress, setProgress] =
|
| 272 |
-
const [report, setReport] =
|
| 273 |
-
const [reports, setReports] =
|
| 274 |
const [busy, setBusy] = useState(false);
|
| 275 |
-
const [sel, setSel] =
|
| 276 |
useEffect(()=>{ API.reports().then(r=>setReports(r.reports||[])).catch(()=>{}); },[]);
|
| 277 |
const run = async () => {
|
| 278 |
if (!tic) return;
|
|
@@ -362,10 +390,16 @@ function ModelView() {
|
|
| 362 |
const [workers, setWorkers] = useState({});
|
| 363 |
const [test, setTest] = useState("");
|
| 364 |
const [ft, setFt] = useState(""); const [dl, setDl] = useState("");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
useEffect(()=>{
|
| 366 |
const tick = ()=> API.modelStatus().then(r=>{setStatus(r.status); setWorkers(r.workers||{});}).catch(()=>{});
|
| 367 |
tick(); const id=setInterval(tick, 3000); return ()=>clearInterval(id);
|
| 368 |
},[]);
|
|
|
|
|
|
|
| 369 |
useEffect(()=>{ API.finetuneStatus().then(r=>setFt(r.status)).catch(()=>{}); },[]);
|
| 370 |
const runTest = async () => {
|
| 371 |
setTest("⏳ Testing sub-agents… (this can take a moment on CPU)");
|
|
@@ -377,11 +411,17 @@ function ModelView() {
|
|
| 377 |
} catch (_) { clearInterval(poll); }
|
| 378 |
}, 1500);
|
| 379 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
const [dlUrl, setDlUrl] = useState("");
|
| 381 |
const exportDs = async () => {
|
| 382 |
const r = await API.exportDataset();
|
| 383 |
if (r.download) { setDl("Exported " + r.count + " pairs."); setDlUrl(r.download); }
|
| 384 |
-
else { setDl("No pairs captured yet — run a few Signals AI
|
| 385 |
};
|
| 386 |
return (
|
| 387 |
<div style={{display:"flex", flexDirection:"column", gap:"var(--space-300)"}}>
|
|
@@ -401,7 +441,25 @@ function ModelView() {
|
|
| 401 |
</div>
|
| 402 |
{test && <div className="cc-llm-out" style={{marginTop:12}}><Markdown text={test}/></div>}
|
| 403 |
</Card>
|
| 404 |
-
<Card title="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
<div className="dim" style={{marginBottom:10}}><Markdown text={ft}/></div>
|
| 406 |
<Button variant="accent" onClick={exportDs}><Icon name="download" size={15}/> Export dataset (JSONL)</Button>
|
| 407 |
{dl && <div className="dim" style={{marginTop:8, fontSize:13}}>{dl}{dlUrl &&
|
|
|
|
| 5 |
const { useState, useEffect, useRef } = React;
|
| 6 |
const API = window.CCApi;
|
| 7 |
|
| 8 |
+
/* Persistent store — survives tab unmount/remount so data isn't lost when the
|
| 9 |
+
user switches tabs and comes back. Backed by a module-level object. */
|
| 10 |
+
window.CCStore = window.CCStore || {};
|
| 11 |
+
function usePersistent(key, initial) {
|
| 12 |
+
const [v, setV] = useState(() => (key in window.CCStore ? window.CCStore[key] : initial));
|
| 13 |
+
const set = (nv) => {
|
| 14 |
+
setV(prev => {
|
| 15 |
+
const next = typeof nv === "function" ? nv(prev) : nv;
|
| 16 |
+
window.CCStore[key] = next;
|
| 17 |
+
return next;
|
| 18 |
+
});
|
| 19 |
+
};
|
| 20 |
+
return [v, set];
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
// kebab-case (data-lucide name) → PascalCase (lucide.icons key)
|
| 25 |
function _pascal(name) {
|
| 26 |
return String(name).split(/[-_]/).map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join("");
|
|
|
|
| 28 |
function _lucideChildren(node) {
|
| 29 |
if (!node) return [];
|
| 30 |
if (Array.isArray(node)) {
|
| 31 |
+
// wrapped form: ["svg", attrs, [children]] → children at [2]
|
| 32 |
+
if (typeof node[0] === "string" && node[0].toLowerCase() === "svg" && Array.isArray(node[2]))
|
| 33 |
+
return node[2];
|
| 34 |
+
// bare iconNode: [ [tag, attrs], [tag, attrs], ... ]
|
| 35 |
return node;
|
| 36 |
}
|
| 37 |
return node.tags || node.children || [];
|
|
|
|
| 51 |
+ `stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`;
|
| 52 |
}
|
| 53 |
function Icon({ name, size = 18, color, style }) {
|
| 54 |
+
const [, force] = useState(0);
|
| 55 |
+
useEffect(() => {
|
| 56 |
+
if (window.lucide && window.lucide.icons) return;
|
| 57 |
+
let n = 0;
|
| 58 |
+
const id = setInterval(() => {
|
| 59 |
+
n++;
|
| 60 |
+
if ((window.lucide && window.lucide.icons) || n > 40) { clearInterval(id); force(x=>x+1); }
|
| 61 |
+
}, 100);
|
| 62 |
+
return () => clearInterval(id);
|
| 63 |
+
}, []);
|
| 64 |
const html = _lucideSvg(name, size, color);
|
| 65 |
return <span style={{ width:size, height:size, display:"inline-flex",
|
| 66 |
alignItems:"center", justifyContent:"center", ...style }}
|
|
|
|
| 112 |
|
| 113 |
/* ─────────── Signals ─────────── */
|
| 114 |
function SignalsView() {
|
| 115 |
+
const [pool, setPool] = usePersistent("sig.pool", "AAPL, MSFT, NVDA, TSLA, AMZN, GOOGL, META, AMD");
|
| 116 |
const [force, setForce] = useState(false);
|
| 117 |
const [running, setRunning] = useState(false);
|
| 118 |
+
const [rows, setRows] = usePersistent("sig.rows", []);
|
| 119 |
+
const [summary, setSummary] = usePersistent("sig.summary", "");
|
| 120 |
+
const [sel, setSel] = usePersistent("sig.sel", null);
|
| 121 |
+
const [aiText, setAiText] = usePersistent("sig.ai", "");
|
| 122 |
const [aiBusy, setAiBusy] = useState(false);
|
| 123 |
|
| 124 |
const run = async () => {
|
|
|
|
| 203 |
|
| 204 |
/* ─────────── Rotation ─────────── */
|
| 205 |
function RotationView() {
|
| 206 |
+
const [d1, setD1] = usePersistent("rot.d1", []);
|
| 207 |
+
const [d5, setD5] = usePersistent("rot.d5", []);
|
| 208 |
+
const [d20, setD20] = usePersistent("rot.d20", []);
|
| 209 |
+
const [asof, setAsof] = usePersistent("rot.asof", "");
|
| 210 |
+
const [ai, setAi] = usePersistent("rot.ai", ""); const [busy, setBusy] = useState(false);
|
| 211 |
const [loading, setLoading] = useState(false);
|
| 212 |
const refresh = async () => {
|
| 213 |
setLoading(true);
|
|
|
|
| 260 |
|
| 261 |
/* ─────────── News ─────────── */
|
| 262 |
function NewsView() {
|
| 263 |
+
const [hold, setHold] = usePersistent("news.hold", "");
|
| 264 |
+
const [out, setOut] = usePersistent("news.out", "");
|
| 265 |
const [busy, setBusy] = useState(false);
|
| 266 |
const [saved, setSaved] = useState("");
|
| 267 |
+
useEffect(()=>{ if(!("news.hold" in window.CCStore)) API.holdings().then(r=>setHold((r.holdings||[]).join(", "))).catch(()=>{}); },[]);
|
| 268 |
const save = async () => {
|
| 269 |
setSaved("Saving…");
|
| 270 |
try { const r = await API.saveHoldings(hold);
|
|
|
|
| 295 |
|
| 296 |
/* ─────────── Research ─────────── */
|
| 297 |
function ResearchView() {
|
| 298 |
+
const [tic, setTic] = usePersistent("res.tic", "");
|
| 299 |
+
const [progress, setProgress] = usePersistent("res.progress", "");
|
| 300 |
+
const [report, setReport] = usePersistent("res.report", "");
|
| 301 |
+
const [reports, setReports] = usePersistent("res.reports", []);
|
| 302 |
const [busy, setBusy] = useState(false);
|
| 303 |
+
const [sel, setSel] = usePersistent("res.sel", null);
|
| 304 |
useEffect(()=>{ API.reports().then(r=>setReports(r.reports||[])).catch(()=>{}); },[]);
|
| 305 |
const run = async () => {
|
| 306 |
if (!tic) return;
|
|
|
|
| 390 |
const [workers, setWorkers] = useState({});
|
| 391 |
const [test, setTest] = useState("");
|
| 392 |
const [ft, setFt] = useState(""); const [dl, setDl] = useState("");
|
| 393 |
+
const [models, setModels] = useState([]);
|
| 394 |
+
const [analyst, setAnalyst] = useState("");
|
| 395 |
+
const [pick, setPick] = useState("");
|
| 396 |
+
const [loadMsg, setLoadMsg] = useState("");
|
| 397 |
useEffect(()=>{
|
| 398 |
const tick = ()=> API.modelStatus().then(r=>{setStatus(r.status); setWorkers(r.workers||{});}).catch(()=>{});
|
| 399 |
tick(); const id=setInterval(tick, 3000); return ()=>clearInterval(id);
|
| 400 |
},[]);
|
| 401 |
+
useEffect(()=>{ API.modelList().then(r=>{ setModels(r.models||[]); setAnalyst(r.analyst||"");
|
| 402 |
+
setPick(p=>p||r.analyst||""); }).catch(()=>{}); },[]);
|
| 403 |
useEffect(()=>{ API.finetuneStatus().then(r=>setFt(r.status)).catch(()=>{}); },[]);
|
| 404 |
const runTest = async () => {
|
| 405 |
setTest("⏳ Testing sub-agents… (this can take a moment on CPU)");
|
|
|
|
| 411 |
} catch (_) { clearInterval(poll); }
|
| 412 |
}, 1500);
|
| 413 |
};
|
| 414 |
+
const loadModel = async () => {
|
| 415 |
+
if (!pick) return;
|
| 416 |
+
setLoadMsg("⏳ Loading…");
|
| 417 |
+
try { const r = await API.modelLoad(pick); setLoadMsg(r.status); }
|
| 418 |
+
catch(e){ setLoadMsg("❌ " + e.message); }
|
| 419 |
+
};
|
| 420 |
const [dlUrl, setDlUrl] = useState("");
|
| 421 |
const exportDs = async () => {
|
| 422 |
const r = await API.exportDataset();
|
| 423 |
if (r.download) { setDl("Exported " + r.count + " pairs."); setDlUrl(r.download); }
|
| 424 |
+
else { setDl("No pairs captured yet — run a few Signals AI interprets first."); setDlUrl(""); }
|
| 425 |
};
|
| 426 |
return (
|
| 427 |
<div style={{display:"flex", flexDirection:"column", gap:"var(--space-300)"}}>
|
|
|
|
| 441 |
</div>
|
| 442 |
{test && <div className="cc-llm-out" style={{marginTop:12}}><Markdown text={test}/></div>}
|
| 443 |
</Card>
|
| 444 |
+
<Card title="Analyst model" subtitle="Pick the model for the Auto Research analyst (all ≤32B). The 1.7B sub-agents are fixed.">
|
| 445 |
+
<div className="cc-radios">
|
| 446 |
+
{models.map(m=>(
|
| 447 |
+
<label key={m} className={"cc-radio" + (pick===m?" sel":"")} onClick={()=>setPick(m)}
|
| 448 |
+
style={{cursor:"pointer"}}>
|
| 449 |
+
<span className="cc-radio-dot"></span>
|
| 450 |
+
<span className="mono">{m}</span>
|
| 451 |
+
{analyst===m && <Badge variant="positive">current</Badge>}
|
| 452 |
+
</label>
|
| 453 |
+
))}
|
| 454 |
+
</div>
|
| 455 |
+
<div style={{display:"flex", gap:"var(--space-150)", marginTop:"var(--space-200)", alignItems:"center"}}>
|
| 456 |
+
<Button variant="accent" onClick={loadModel} disabled={!pick || pick===analyst}>
|
| 457 |
+
<Icon name="download" size={15}/> Load model
|
| 458 |
+
</Button>
|
| 459 |
+
{loadMsg && <span className="dim" style={{fontSize:13}}>{loadMsg}</span>}
|
| 460 |
+
</div>
|
| 461 |
+
</Card>
|
| 462 |
+
<Card title="Fine-tuning dataset" subtitle="Every Signals AI interpret is captured as a training pair on /data">
|
| 463 |
<div className="dim" style={{marginBottom:10}}><Markdown text={ft}/></div>
|
| 464 |
<Button variant="accent" onClick={exportDs}><Icon name="download" size={15}/> Export dataset (JSONL)</Button>
|
| 465 |
{dl && <div className="dim" style={{marginTop:8, fontSize:13}}>{dl}{dlUrl &&
|