Files
J-Wash/ui/src/App.jsx
T
2026-07-13 22:26:50 +02:00

1731 lines
76 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useRef, useState } from 'react'
import { marked } from 'marked'
import DOMPurify from 'dompurify'
import LensView from './LensView.jsx'
import LensDiff from './Diff.jsx'
import Editor from './Editor.jsx'
import { fmtTok } from './tok'
const GB = 2 ** 30
marked.setOptions({ breaks: true, gfm: true })
// Markdown bubble content: parsed + sanitized (a local model can still emit
// arbitrary HTML — never inject it raw).
function Md({ text }) {
const html = useMemo(() => DOMPurify.sanitize(marked.parse(text || '')), [text])
return <div className="md" dangerouslySetInnerHTML={{ __html: html }} />
}
// Polluter tokens excluded by default from the Frequencies token cloud (compared
// on the form without edge spaces). Managed at the bottom of the Chat tab.
const DEFAULT_HIDDEN = [
'热门推荐', '阅读全文', '网友评论', '点击查看答案', '查看全文', '最新发布', '展开全文',
'相关问答', '続きを読む', 'Читать', 'Weiterlesen', 'スポンサーリンク',
]
const trimTok = (s) => (s || '').replace(/^\s+|\s+$/g, '')
const LENS_PRESETS = {
'Qwen/Qwen3.5-4B': {
repo_id: 'neuronpedia/jacobian-lens',
revision: 'qwen-n1000',
filename: 'qwen3.5-4b/jlens/Salesforce-wikitext/Qwen3.5-4B_jacobian_lens_n1000.pt',
},
'Qwen/Qwen3-4B': {
repo_id: 'neuronpedia/jacobian-lens',
revision: 'main',
filename: 'qwen3-4b/jlens/Salesforce-wikitext/Qwen3-4B_jacobian_lens.pt',
},
}
async function jsonFetch(url, options) {
const res = await fetch(url, options)
const body = await res.json().catch(() => ({}))
if (!res.ok) throw new Error(body.detail || res.statusText)
return body
}
const SAMPLING_DEFAULT = { temperature: 0.7, top_p: 0.95, top_k: 40, max_tokens: 512, seed: -1 }
// Human-readable name of the loaded lens (local path or Hub file) for "which lens do I have?".
function lensName(meta) {
if (!meta) return ''
if (meta.path) return meta.path.replace(/[/\\]+$/, '').split(/[/\\]/).pop()
if (meta.filename) {
const stem = meta.filename.split('/').pop().replace('_jacobian_lens', '').replace('.pt', '')
return meta.revision ? `${stem} @${meta.revision}` : stem
}
return meta.repo_id || 'lens'
}
// Note about the chat template fetched when loading a base model.
function templateNote(meta) {
const src = meta.chat_template_source
if (!src) return { suffix: '', warn: false } // the model has its own template
if (src === 'generic') {
return { suffix: ' — no chat template found on the Hub: generic User:/Assistant: template (limited results on a base model)', warn: true }
}
return { suffix: ` — chat template fetched from ${src}`, warn: false }
}
function TreeNode({ node, childs, depth, activeIds, onSelect }) {
return (
<>
<div
className={`tree-node ${activeIds.has(node.id) ? 'on-path' : ''}`}
style={{ paddingLeft: 8 + depth * 14 }}
onClick={() => onSelect(node.id)}
>
<span className={`tn-role ${node.role}`}>{node.role[0].toUpperCase()}</span>
<span className="tn-text" title={node.content || '(empty)'}>#{node.id} {node.content.slice(0, 70) || '(empty)'}</span>
{node.has_frames && <span className="tn-frames"></span>}
</div>
{(childs[node.id] || []).map((c) => (
<TreeNode key={c.id} node={c} childs={childs} depth={depth + 1} activeIds={activeIds} onSelect={onSelect} />
))}
</>
)
}
export default function App() {
const [models, setModels] = useState([])
const [status, setStatus] = useState(null)
const [selected, setSelected] = useState(null)
const [dtype, setDtype] = useState('bf16')
const [quant, setQuant] = useState('')
const [device, setDevice] = useState('cuda:0')
const [notice, setNotice] = useState(null)
const [downloadRepo, setDownloadRepo] = useState('')
const [browseOpen, setBrowseOpen] = useState(false)
const [browse, setBrowse] = useState(null)
const [lensForm, setLensForm] = useState({ repo_id: 'neuronpedia/jacobian-lens', revision: '', filename: '', path: '', k: 8 })
const [reg, setReg] = useState(null)
// model being loaded right now (id requested) + lens queued to chain-load
const [loadingId, setLoadingId] = useState(null)
const queuedLensRef = useRef(null)
const [queuedLensName, setQueuedLensName] = useState(null)
const [lensOn, setLensOn] = useState(true)
const [framesCount, setFramesCount] = useState(0)
const [selectedIdx, setSelectedIdx] = useState(null)
const [diffSel, setDiffSel] = useState([])
const [tab, setTab] = useState('chat')
const [fitName, setFitName] = useState('')
const [hidden, setHidden] = useState(() => {
try {
const saved = JSON.parse(localStorage.getItem('jlens_hidden_tokens') || 'null')
return new Set(Array.isArray(saved) ? saved : DEFAULT_HIDDEN)
} catch { return new Set(DEFAULT_HIDDEN) }
})
const [hideInput, setHideInput] = useState('')
// chat display: markdown rendering of assistant replies (default ON)
const [chatMd, setChatMd] = useState(() => localStorage.getItem('jlens_chat_md') !== '0')
useEffect(() => { localStorage.setItem('jlens_chat_md', chatMd ? '1' : '0') }, [chatMd])
// server-side settings (Options tab)
const [settings, setSettings] = useState(null)
useEffect(() => {
jsonFetch('/api/settings').then((s) => {
setSettings(s)
setChatMd(!!s.chat_markdown)
if (s.default_quant != null) setQuant(s.default_quant)
}).catch(() => {})
}, [])
async function patchSettings(patch) {
try {
const s = await jsonFetch('/api/settings', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
})
setSettings(s)
if (patch.chat_markdown != null) setChatMd(!!s.chat_markdown)
if (patch.default_quant != null) setQuant(s.default_quant)
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
}
}
// manual height of the lens view (drag handle above it; null = auto 46vh)
const [lensViewH, setLensViewH] = useState(() => {
const v = +localStorage.getItem('jlens_lensview_h')
return v >= 80 ? v : null
})
useEffect(() => {
if (lensViewH) localStorage.setItem('jlens_lensview_h', String(Math.round(lensViewH)))
else localStorage.removeItem('jlens_lensview_h')
}, [lensViewH])
function startLensResize(e) {
e.preventDefault()
const el = document.querySelector('.lensview')
const startH = lensViewH ?? el?.getBoundingClientRect().height ?? 300
const startY = e.clientY
const move = (ev) => setLensViewH(
Math.max(80, Math.min(window.innerHeight * 0.85, startH + (startY - ev.clientY))),
)
const up = () => {
window.removeEventListener('mousemove', move)
window.removeEventListener('mouseup', up)
}
window.addEventListener('mousemove', move)
window.addEventListener('mouseup', up)
}
// reply being edited in place: { idx, text } | null
const [editingMsg, setEditingMsg] = useState(null)
// assistant message being CONTINUED (ref: onFrame is a stale closure)
const continuingIdRef = useRef(null)
const [continuingId, setContinuingId] = useState(null)
const [ivRules, setIvRules] = useState([])
const [ivScale, setIvScale] = useState(1)
const [ivMode, setIvMode] = useState('standard')
const [editorOpen, setEditorOpen] = useState(false)
const [editorPrefill, setEditorPrefill] = useState(null)
const [capLayers, setCapLayers] = useState('')
const [capK, setCapK] = useState('')
const [convs, setConvs] = useState([])
const [convQuery, setConvQuery] = useState('')
const [conv, setConv] = useState(null)
const [tree, setTree] = useState([])
const [showTree, setShowTree] = useState(false)
const [editParent, setEditParent] = useState(undefined)
const [fitModel, setFitModel] = useState('')
const [fitN, setFitN] = useState(100)
const [fitDataset, setFitDataset] = useState('Salesforce/wikitext-103-raw-v1')
const [fitQuant, setFitQuant] = useState('')
const [fitDevices, setFitDevices] = useState([])
const [fitDimBatch, setFitDimBatch] = useState('')
const [fitMaxSeq, setFitMaxSeq] = useState(128)
const [fitLayers, setFitLayers] = useState('')
const [fitContinue, setFitContinue] = useState('')
const [localLenses, setLocalLenses] = useState([])
const [fitAdvanced, setFitAdvanced] = useState(false)
const [system, setSystem] = useState('')
const [messages, setMessages] = useState([])
const [draft, setDraft] = useState(null)
const [input, setInput] = useState('')
const [sampling, setSampling] = useState(() => {
// restore sampling params after a refresh (temperature, etc.)
try {
const saved = JSON.parse(localStorage.getItem('jlens_sampling') || 'null')
return saved && typeof saved === 'object' ? { ...SAMPLING_DEFAULT, ...saved } : SAMPLING_DEFAULT
} catch { return SAMPLING_DEFAULT }
})
const wsRef = useRef(null)
const draftRef = useRef('')
const framesRef = useRef([])
const messagesRef = useRef(null)
const lastPaintRef = useRef(0)
const convRef = useRef(null)
convRef.current = conv
const refreshModels = () =>
jsonFetch('/api/models').then((b) => setModels(b.models)).catch(() => {})
const refreshConvs = (query = convQuery) =>
jsonFetch(`/api/conversations${query ? `?query=${encodeURIComponent(query)}` : ''}`)
.then((b) => setConvs(b.conversations))
.catch(() => {})
useEffect(() => {
refreshModels()
refreshConvs()
// restore the current conversation after a page reload
const saved = Number(localStorage.getItem('jlens_conv'))
if (saved) {
jsonFetch(`/api/conversations/${saved}`).then((body) => {
setTree(body.messages)
setConv({ id: body.id, title: body.title, tags: body.tags })
if (body.messages.length) {
applyPath(Math.max(...body.messages.map((m) => m.id)), body.messages)
}
}).catch(() => localStorage.removeItem('jlens_conv'))
}
const tick = () => jsonFetch('/api/status').then(setStatus).catch(() => {})
tick()
const id = setInterval(tick, 2000)
return () => clearInterval(id)
}, [])
useEffect(() => {
if (conv?.id) localStorage.setItem('jlens_conv', String(conv.id))
}, [conv?.id])
useEffect(() => {
localStorage.setItem('jlens_sampling', JSON.stringify(sampling))
}, [sampling])
useEffect(() => {
localStorage.setItem('jlens_hidden_tokens', JSON.stringify([...hidden]))
}, [hidden])
function hideToken(s) {
const clean = trimTok(s)
if (!clean) return
setHidden((prev) => new Set(prev).add(clean))
}
function unhideToken(s) {
setHidden((prev) => { const n = new Set(prev); n.delete(s); return n })
}
// fit devices follow the GPUs actually present: drop the missing ones,
// default to all of them when nothing (valid) is selected
useEffect(() => {
const avail = (status?.gpus || []).map((g) => `cuda:${g.index}`)
if (!avail.length) return
setFitDevices((prev) => {
const filtered = prev.filter((d) => avail.includes(d))
return filtered.length ? filtered : avail
})
}, [status?.gpus?.length])
useEffect(() => { refreshConvs() }, [convQuery])
// Auto-scroll to the bottom while a reply streams in — but only if the user
// is already near the bottom, so scrolling up to read is never hijacked.
useEffect(() => {
const el = messagesRef.current
if (!el || draft === null) return
if (el.scrollHeight - el.scrollTop - el.clientHeight < 160) {
el.scrollTop = el.scrollHeight
}
}, [draft])
// After a refresh / branch change, messages are restored without their frames
// (applyPath sets frames: undefined) → the lens view won't show. We reload the
// frames of the last message that has some (they exist server-side), to get the
// heatmap/frequencies back without having to regenerate.
const framesTriedRef = useRef(new Set())
useEffect(() => {
// draft !== null == streaming (const "streaming" declared below → avoid the TDZ)
if (draft !== null || messages.some((m) => m.frames?.length)) return
let lastIdx = -1
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].has_frames && messages[i].id != null) { lastIdx = i; break }
}
if (lastIdx < 0) return
const mid = messages[lastIdx].id
if (framesTriedRef.current.has(mid)) return
framesTriedRef.current.add(mid)
jsonFetch(`/api/messages/${mid}/frames`)
.then((body) => {
if (body.frames?.length) {
setMessages((prev) => prev.map((x) => (x.id === mid ? { ...x, frames: body.frames } : x)))
}
})
.catch(() => {})
}, [messages, draft])
useEffect(() => {
if (tab === 'fit') jsonFetch('/api/registry/local').then((b) => setLocalLenses(b.lenses)).catch(() => {})
}, [tab, status?.fit?.state])
useEffect(() => {
if (!status) return
setIvRules(status.interventions || [])
if (status.interventions_scale != null) setIvScale(status.interventions_scale)
if (status.interventions_mode != null) setIvMode(status.interventions_mode)
}, [status])
// auto-open the token editor when a lens just got loaded
// (status?.lens, NOT lensMeta: that const is declared further down — TDZ)
const hadLensRef = useRef(false)
useEffect(() => {
const has = !!status?.lens
if (has && !hadLensRef.current) setEditorOpen(true)
hadLensRef.current = has
}, [status?.lens])
const loadedId = status?.loaded?.model_id
const lensMeta = status?.lens
const busy = status?.busy
const streaming = draft !== null
useEffect(() => {
// the registry is queryable for the model being LOADED too: pick a lens
// while the weights stream in, it chain-loads once the model is ready
const target = loadedId || loadingId
const preset = LENS_PRESETS[target]
if (preset) setLensForm((f) => ({ ...f, ...preset }))
if (!target) { setReg(null); return }
const rev = loadedId ? status?.loaded?.revision : null
jsonFetch(`/api/registry/for-model?model_id=${encodeURIComponent(target)}${rev ? `&revision=${rev}` : ''}`)
.then(setReg)
.catch(() => setReg(null))
}, [loadedId, loadingId])
function ensureWs() {
return new Promise((resolve, reject) => {
const cur = wsRef.current
if (cur && cur.readyState === WebSocket.OPEN) return resolve(cur)
const proto = location.protocol === 'https:' ? 'wss' : 'ws'
const ws = new WebSocket(`${proto}://${location.host}/ws`)
ws.onopen = () => resolve(ws)
ws.onerror = () => reject(new Error('websocket unavailable'))
ws.onmessage = (ev) => onFrame(JSON.parse(ev.data))
ws.onclose = () => { wsRef.current = null }
wsRef.current = ws
})
}
async function fetchTree(cid, { keepPath = true } = {}) {
try {
const body = await jsonFetch(`/api/conversations/${cid}`)
setTree(body.messages)
setConv({ id: body.id, title: body.title, tags: body.tags })
return body
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
return null
}
}
function onFrame(frame) {
if (frame.type === 'token') {
draftRef.current += frame.text
setDraft(draftRef.current)
} else if (frame.type === 'frame') {
framesRef.current.push(frame)
const now = performance.now()
if (now - lastPaintRef.current > 60) {
lastPaintRef.current = now
setFramesCount(framesRef.current.length)
}
} else if (frame.type === 'persisted') {
if (!convRef.current) setConv({ id: frame.conversation_id, title: '', tags: [] })
setMessages((prev) => {
const copy = [...prev]
for (let i = copy.length - 1; i >= 0; i--) {
if (copy[i].role === 'user' && copy[i].id == null) {
copy[i] = { ...copy[i], id: frame.user_message_id }
break
}
}
return copy
})
} else if (frame.type === 'done') {
const frames = framesRef.current
framesRef.current = []
setFramesCount(0)
setSelectedIdx(null)
if (frame.continued && frame.message_id != null) {
// continuation: update the extended reply in place (frame.text is the
// FULL new content) and reload its merged frames blob
continuingIdRef.current = null
setContinuingId(null)
setMessages((prev) => prev.map((m) => (m.id === frame.message_id
? {
...m, content: frame.text, stats: frame.stats,
gen_id: frame.gen_id, has_frames: m.has_frames || frames.length > 0,
frames: undefined,
}
: m)))
framesTriedRef.current.delete(frame.message_id)
jsonFetch(`/api/messages/${frame.message_id}/frames`)
.then((body) => {
if (body.frames?.length) {
setMessages((prev) => prev.map((x) => (x.id === frame.message_id
? { ...x, frames: body.frames } : x)))
}
})
.catch(() => {})
} else {
setMessages((prev) => [...prev, {
id: frame.message_id ?? null,
role: 'assistant',
content: frame.text,
meta: frame.meta,
stats: frame.stats,
gen_id: frame.gen_id,
has_frames: frames.length > 0,
frames,
}])
}
draftRef.current = ''
setDraft(null)
if (!frame.text && !frame.continued) {
setNotice({
kind: 'err',
text: 'the model emitted end-of-turn immediately (0 tokens) — strong '
+ 'global rules can do this on short prompts: soften the factor/scale '
+ 'or rephrase',
})
}
if (frame.conversation_id) {
fetchTree(frame.conversation_id)
refreshConvs()
}
} else if (frame.type === 'error') {
setNotice({ kind: 'err', text: frame.message })
framesRef.current = []
draftRef.current = ''
setDraft(null)
continuingIdRef.current = null
setContinuingId(null)
}
}
function lastPathId() {
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].id != null) return messages[i].id
}
const root = tree.find((m) => m.role === 'system')
return root ? root.id : null
}
async function sendChat({ content, parentId }) {
try {
const ws = await ensureWs()
draftRef.current = ''
framesRef.current = []
setFramesCount(0)
setDraft('')
ws.send(JSON.stringify({
type: 'chat',
conversation_id: conv?.id ?? null,
parent_id: parentId,
content,
system: conv ? undefined : (system.trim() || undefined),
sampling,
lens: lensOn && !!lensMeta,
}))
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
setDraft(null)
}
}
function onSend() {
const text = input.trim()
if (!text || streaming) return
const parentId = editParent !== undefined ? editParent : lastPathId()
setMessages((prev) => [...prev, { id: null, role: 'user', content: text }])
setInput('')
setEditParent(undefined)
sendChat({ content: text, parentId })
}
function onStop() {
wsRef.current?.send(JSON.stringify({ type: 'stop' }))
}
function onRegenerate() {
if (streaming) return
const history = [...messages]
while (history.length && history[history.length - 1].role === 'assistant') history.pop()
const lastUser = [...history].reverse().find((m) => m.role === 'user')
if (!lastUser || lastUser.id == null) return
setMessages(history)
sendChat({ content: null, parentId: lastUser.id })
}
// Extend the last assistant reply: the server generates with the turn left
// open and appends to the stored message (frames merged too).
async function onContinue() {
if (streaming) return
const last = messages[messages.length - 1]
if (!last || last.role !== 'assistant' || last.id == null) return
continuingIdRef.current = last.id
setContinuingId(last.id)
try {
const ws = await ensureWs()
draftRef.current = ''
framesRef.current = []
setFramesCount(0)
setDraft('')
ws.send(JSON.stringify({
type: 'chat',
continue_message_id: last.id,
sampling,
lens: lensOn && !!lensMeta,
}))
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
setDraft(null)
continuingIdRef.current = null
setContinuingId(null)
}
}
async function saveEditedMsg() {
if (!editingMsg) return
const m = messages[editingMsg.idx]
if (!m || m.id == null) { setEditingMsg(null); return }
try {
await jsonFetch(`/api/messages/${m.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: editingMsg.text }),
})
setMessages((prev) => prev.map((x, i) => (i === editingMsg.idx ? { ...x, content: editingMsg.text } : x)))
setEditingMsg(null)
setNotice({ kind: 'ok', text: 'reply updated — later turns will use the edited text' })
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
}
}
function onEditLast() {
if (streaming) return
const history = [...messages]
while (history.length && history[history.length - 1].role === 'assistant') history.pop()
const last = history.pop()
if (!last) return
setMessages(history)
setInput(last.content)
const parent = tree.find((m) => m.id === last.id)?.parent_id
setEditParent(parent ?? null)
}
function pathFor(nodeId, msgs) {
const byId = Object.fromEntries(msgs.map((m) => [m.id, m]))
const path = []
let cur = byId[nodeId]
while (cur) {
path.unshift(cur)
cur = cur.parent_id != null ? byId[cur.parent_id] : null
}
return path
}
function applyPath(nodeId, msgs) {
const path = pathFor(nodeId, msgs)
const sys = path.find((m) => m.role === 'system')
setSystem(sys ? sys.content : '')
setMessages(path.filter((m) => m.role !== 'system').map((m) => ({ ...m, frames: undefined })))
setSelectedIdx(null)
setEditParent(undefined)
}
async function openConv(cid) {
const body = await fetchTree(cid)
if (!body) return
const maxId = Math.max(...body.messages.map((m) => m.id))
applyPath(maxId, body.messages)
}
function newConv() {
localStorage.removeItem('jlens_conv')
setConv(null)
setTree([])
setMessages([])
setSystem('')
setSelectedIdx(null)
setEditParent(undefined)
}
async function deleteConv(cid, ev) {
ev.stopPropagation()
if (!window.confirm(`Delete conversation ${cid}?`)) return
await jsonFetch(`/api/conversations/${cid}`, { method: 'DELETE' }).catch(() => {})
if (conv?.id === cid) newConv()
refreshConvs()
}
async function patchConv(fields) {
if (!conv?.id) return
await jsonFetch(`/api/conversations/${conv.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fields),
}).catch(() => {})
refreshConvs()
}
async function selectMessage(i) {
const m = messages[i]
if (m.has_frames && !m.frames?.length && m.id != null) {
try {
const body = await jsonFetch(`/api/messages/${m.id}/frames`)
setMessages((prev) => prev.map((x, j) => (j === i ? { ...x, frames: body.frames } : x)))
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
return
}
}
setSelectedIdx(i)
}
// Consume the lens queued during the model load (chain-load), if any.
function flushQueuedLens() {
const q = queuedLensRef.current
queuedLensRef.current = null
setQueuedLensName(null)
if (q) lensLoadBy(q)
}
async function onLoad() {
if (!selected) return
setNotice({ kind: 'ok', text: `loading ${selected}...` })
setLoadingId(selected)
try {
const meta = await jsonFetch('/api/load', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model_id: selected, dtype, quant: quant || null, device }),
})
const note = templateNote(meta)
setNotice({
kind: note.warn ? 'err' : 'ok',
text: `loaded in ${meta.load_seconds}s (${meta.n_layers} layers, d=${meta.d_model})` + note.suffix,
})
flushQueuedLens()
} catch (err) {
queuedLensRef.current = null
setQueuedLensName(null)
setNotice({ kind: 'err', text: String(err.message || err) })
} finally {
setLoadingId(null)
}
}
async function onUnload() {
try {
const r = await jsonFetch('/api/unload', { method: 'POST' })
let text = 'nothing to unload'
if (r.unloaded) {
// VRAM actually returned = allocated before reserved after (the rest = CUDA context)
const before = Object.values(r.vram_allocated_before || {}).reduce((a, b) => a + b, 0)
const reserved = Object.values(r.vram_reserved_after || {}).reduce((a, b) => a + b, 0)
text = `model unloaded · ${(before / GB).toFixed(1)} GB returned`
+ (reserved > 64 * 2 ** 20 ? ` (${(reserved / GB).toFixed(1)} GB still reserved)` : ' (VRAM returned to the driver)')
}
setNotice({ kind: 'ok', text })
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
}
}
async function lensLoadBy(payload) {
setNotice({ kind: 'ok', text: 'loading the lens...' })
// form top-k applied to ALL load paths (registry included)
if (payload.k == null && +lensForm.k > 0) payload = { ...payload, k: +lensForm.k }
try {
const meta = await jsonFetch('/api/lens/load', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ k: +lensForm.k || 8, ...payload }),
})
const warn = (meta.warnings || []).join(' ; ')
setNotice({ kind: warn ? 'err' : 'ok', text: warn || `lens loaded (layers ${meta.tapped_layers.join(', ')})` })
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
}
}
function onLensLoad() {
if (lensForm.path.trim()) {
lensClick({ path: lensForm.path.trim() })
return
}
lensClick({
repo_id: lensForm.repo_id || null,
revision: lensForm.revision || null,
filename: lensForm.filename,
})
}
// While a model is loading, clicking a lens QUEUES it (chain-load).
const modelLoading = !!loadingId && busy === 'loading'
function lensClick(payload) {
if (modelLoading) {
queuedLensRef.current = payload
setQueuedLensName(payload.path || payload.filename?.split('/').pop() || 'lens')
setNotice({ kind: 'ok', text: 'lens queued — it loads as soon as the model is ready' })
return
}
lensLoadBy(payload)
}
function renderHubLens(h) {
const nMatch = h.filename.match(/_n(\d+)/)
const stem = h.filename.split('/').pop().replace('_jacobian_lens', '').replace('.pt', '')
const isBase = h.via === 'base-model' || h.via === 'base-guess'
return (
<div key={`${h.repo_id}/${h.filename}`} className={`reg-item2 ${h.compatible === false ? 'reg-warn' : ''}`}>
<div className="reg-main">
<div className="reg-name" title={`${h.filename || stem}`}>
{stem} <span className="reg-tag hub">Hub</span>
{isBase && <span className="reg-tag base" title={h.reason}>base model</span>}
</div>
<div className="src">
{nMatch ? `${nMatch[1]} prompts` : 'n unspecified (repo default fit)'}
{h.base_model ? ` · base ${h.base_model}` : ''}
{h.cached ? ' · ✓ cached' : ' · to download'}
</div>
<div className="src">{h.repo_id} @{h.revision}</div>
{h.reason && <div className="src reg-reason"> {h.reason}</div>}
</div>
<button disabled={h.compatible === false || (!!busy && !modelLoading)}
title={modelLoading ? 'queue: loads right after the model'
: h.cached ? 'load (already cached)' : 'download and load'}
onClick={() => lensClick({ repo_id: h.repo_id, filename: h.filename, revision: h.revision })}>
{modelLoading ? '⏲' : h.cached ? '▶' : '⬇'}</button>
</div>
)
}
async function onLensUnload() {
await jsonFetch('/api/lens/unload', { method: 'POST' }).catch(() => {})
}
function openEditorWith(prefill) {
setEditorOpen(true)
if (prefill) setEditorPrefill(prefill)
}
// "Visualized" generation: the live stream, else the selected message if it has
// frames, else the last message with frames. Shared between the J-lens and the
// editor (auto-selecting the peak layer of an added token).
function currentGenView() {
const live = streaming && framesRef.current.length ? framesRef.current : null
let idx = selectedIdx
if (idx == null || !messages[idx]?.frames?.length) {
idx = -1
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].frames?.length) { idx = i; break }
}
}
// gen_id is only known for messages generated THIS page session; after a
// reload, fall back to the id carried by the persisted frames themselves —
// the server-side residual store survives a page refresh.
const msgGen = idx >= 0
? messages[idx].gen_id ?? messages[idx].frames[messages[idx].frames.length - 1]?.gen ?? null
: null
const genId = live ? live[live.length - 1]?.gen ?? null : msgGen
return { live, idx, genId }
}
async function applyCapture() {
const list = []
for (const part of capLayers.split(',')) {
const m = part.trim().match(/^(\d+)\s*-\s*(\d+)$/)
if (m) for (let i = +m[1]; i <= +m[2]; i++) list.push(i)
else if (part.trim()) list.push(+part.trim())
}
const layers = list.filter((x) => !isNaN(x))
try {
const body = await jsonFetch('/api/lens/layers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
// empty layers field = keep the current layers (changing k only)
layers: layers.length ? layers : lensMeta.tapped_layers,
k: capK ? +capK : null,
}),
})
setCapLayers('')
setCapK('')
setNotice({ kind: 'ok', text: `capture: layers ${body.tapped_layers.join(', ')} · k=${body.k}` })
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
}
}
async function openBrowse(path) {
try {
const body = await jsonFetch(`/api/browse${path ? `?path=${encodeURIComponent(path)}` : ''}`)
setBrowse(body)
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
}
}
// NATIVE folder picker (server-side tkinter dialog — the server is local, and
// tkinter works on Windows/Linux/macOS alike). Returns the chosen path.
async function pickFolder() {
try {
const body = await jsonFetch('/api/pick-path', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kind: 'dir' }),
})
return body.path || null
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
return null
}
}
async function pickNative() {
const path = await pickFolder()
if (path) await registerPath(path)
}
// Browse result: the folder is ADDED to the model list (nothing is loaded
// yet) — load it from the list with the dtype/quant/device of your choice.
async function registerPath(path) {
try {
const r = await jsonFetch('/api/models/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path }),
})
setSelected(r.registered)
setNotice({ kind: 'ok', text: `added to the list: ${r.registered} — pick dtype/quant/device and Load` })
refreshModels()
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
}
}
async function unregisterPath(m, ev) {
ev.stopPropagation()
try {
await jsonFetch('/api/models/unregister', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: m.path }),
})
setNotice({ kind: 'ok', text: `removed from the list (files untouched): ${m.path}` })
if (selected === m.id) setSelected(null)
refreshModels()
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
}
}
async function deleteModel(m, ev) {
ev.stopPropagation()
const size = m.size_bytes ? ` (${(m.size_bytes / GB).toFixed(1)} GB)` : ''
if (!window.confirm(`Permanently delete ${m.id}${size}?\nThis erases the files from disk.`)) return
try {
const r = await jsonFetch('/api/models/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model_id: m.id }),
})
const freed = r.freed_bytes ? ` · ${(r.freed_bytes / GB).toFixed(1)} GB freed` : ''
setNotice({ kind: 'ok', text: `deleted: ${m.id}${freed}` })
if (selected === m.id) setSelected(null)
refreshModels()
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
}
}
async function convertBf16(path) {
try {
await jsonFetch('/api/convert-bf16', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path }),
})
setNotice({ kind: 'ok', text: 'bf16 conversion started (see status below)' })
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
}
}
async function onDownload() {
if (!downloadRepo.trim()) return
try {
await jsonFetch('/api/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ repo_id: downloadRepo.trim() }),
})
setNotice({ kind: 'ok', text: `download of ${downloadRepo} started` })
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
}
}
const doneDownloads = (status?.downloads || []).filter((d) => d.state === 'done').length
useEffect(() => {
if (doneDownloads > 0) refreshModels()
}, [doneDownloads])
useEffect(() => {
if (status?.convert?.state === 'done') refreshModels()
}, [status?.convert?.state])
const childs = {}
tree.forEach((m) => { (childs[m.parent_id] ??= []).push(m) })
const activeIds = new Set(messages.map((m) => m.id).filter((x) => x != null))
tree.filter((m) => m.role === 'system' && activeIds.size).forEach((m) => activeIds.add(m.id))
return (
<>
<div className="sidebar">
<h1>J-Wash</h1>
<h2>GPU</h2>
<div>
{(status?.gpus || []).map((g) => (
<div className="gpu" key={g.index}>
<div className="name">
<span>cuda:{g.index} · {g.name.replace('NVIDIA GeForce ', '')}</span>
<span>{(g.vram_used / GB).toFixed(1)} / {(g.vram_total / GB).toFixed(0)} GB</span>
</div>
<div className="bar"><div className="fill" style={{ width: `${(100 * g.vram_used) / g.vram_total}%` }} /></div>
</div>
))}
</div>
<div className="tabs">
{[['chat', 'Chat'], ['model', 'Model'], ['lens', 'Lens'], ['fit', 'Fit'], ['options', 'Options']].map(([id, label]) => (
<button key={id} className={tab === id ? 'tab-on' : ''} onClick={() => setTab(id)}>{label}</button>
))}
</div>
{tab === 'chat' && (<>
<h2>Conversations <button style={{ float: 'right', padding: '1px 7px' }} onClick={newConv}>+ new</button></h2>
<input type="text" placeholder="full-text search..." value={convQuery} onChange={(e) => setConvQuery(e.target.value)} />
<div className="conv-list">
{convs.map((c) => (
<div key={c.id} className={`conv-item ${conv?.id === c.id ? 'selected' : ''}`} onClick={() => openConv(c.id)}>
<div className="conv-title" title={c.title || `conversation ${c.id}`}>
{c.title || `conversation ${c.id}`}
<button className="conv-del" onClick={(e) => deleteConv(c.id, e)}></button>
</div>
<div className="src">
{c.n_messages} msgs{c.tags?.length ? ` · ${c.tags.join(', ')}` : ''}
{c.snippet ? <div className="conv-snippet">{c.snippet}</div> : null}
</div>
</div>
))}
</div>
<h2>Ignored tokens{hidden.size ? ` (${hidden.size})` : ''}</h2>
<input type="text" placeholder="hide a token…" value={hideInput}
onChange={(e) => setHideInput(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') { hideToken(hideInput); setHideInput('') } }} />
{hidden.size > 0 ? (
<div className="lv-hidden-row">
{[...hidden].map((h) => (
<span key={h} className="lv-hidden-chip" title="click to show again"
onClick={() => unhideToken(h)}>{fmtTok(h)} </span>
))}
</div>
) : (
<div className="src">tokens excluded from the Frequencies view (right-click a token there to add one)</div>
)}
</>)}
{tab === 'model' && (<>
<h2>Models <button style={{ float: 'right', padding: '1px 7px' }} onClick={refreshModels}></button></h2>
<div className="model-list">
{models.map((m) => {
const isFp32 = m.dtype === 'float32' || m.dtype === 'fp32'
const isRegistered = m.source === 'registered'
const label = isRegistered ? (m.path.split(/[\\/]/).pop() || m.id) : m.id
return (
<div
key={m.id}
className={`model-item ${selected === m.id ? 'selected' : ''} ${loadedId === m.id ? 'loaded' : ''}`}
onClick={() => setSelected(m.id)}
>
<div className="model-item-head">
<span title={m.path || m.id}>{label} {loadedId === m.id ? '●' : ''}{m.missing ? ' ⚠ folder missing' : ''}</span>
{isRegistered ? (
<button className="model-del model-unreg"
title="remove this entry from the list — the model files are NOT touched"
disabled={loadedId === m.id || !!busy}
onClick={(e) => unregisterPath(m, e)}>
<svg viewBox="0 0 14 14" width="13" height="13" aria-hidden="true">
<path d="M2 3.5h6M2 7h6M2 10.5h4M9.5 8.5l3 3m0-3l-3 3"
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" fill="none" />
</svg>
</button>
) : (
<button className="model-del model-trash"
title={loadedId === m.id ? 'unload it first' : 'PERMANENTLY delete the model files from disk'}
disabled={loadedId === m.id || !!busy}
onClick={(e) => deleteModel(m, e)}>
<svg viewBox="0 0 14 14" width="13" height="13" aria-hidden="true">
<path d="M2 3.5h10M5.5 3.5V2.2c0-.4.3-.7.7-.7h1.6c.4 0 .7.3.7.7v1.3M3.2 3.5l.6 8.1c0 .5.4.9.9.9h4.6c.5 0 .9-.4.9-.9l.6-8.1M5.6 6v4M8.4 6v4"
stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" fill="none" />
</svg>
</button>
)}
</div>
<div className="src">
{m.source} · {(m.size_bytes / GB).toFixed(1)} GB
{m.n_layers != null ? ` · ${m.n_layers} layers` : ''}
{m.dtype ? ` · ${m.dtype}` : ''}
</div>
{isFp32 && (
<div className="src">
fp32 <button className="linkbtn"
onClick={(e) => { e.stopPropagation(); convertBf16(m.path) }}
disabled={status?.convert?.state === 'running'}>convert to bf16 (÷2 space)</button>
</div>
)}
</div>
)
})}
</div>
<div className="row"><label>dtype</label>
<select value={dtype} onChange={(e) => setDtype(e.target.value)}>
<option value="bf16">bf16</option>
<option value="fp16">fp16</option>
</select>
</div>
<div className="row"><label>quant</label>
<select value={quant} onChange={(e) => setQuant(e.target.value)}>
<option value="">none</option>
<option value="int8">int8</option>
<option value="nf4">nf4</option>
</select>
</div>
<div className="row"><label>device</label>
<select value={device} onChange={(e) => setDevice(e.target.value)}>
{(status?.gpus || []).map((g) => (
<option key={g.index} value={`cuda:${g.index}`}>
cuda:{g.index} · {g.name.replace('NVIDIA GeForce ', '')}
</option>
))}
{(status?.gpus?.length || 0) > 1 && <option value="auto">auto (all GPUs)</option>}
</select>
</div>
<div className="row">
<button className="primary" style={{ flex: 1 }} disabled={!selected || !!busy} onClick={onLoad}>Load</button>
<button className="danger" disabled={!loadedId || !!busy} onClick={onUnload}>Unload</button>
</div>
</>)}
{tab === 'lens' && (<>
<h2>J-Lens</h2>
{lensMeta ? (
<div className="lens-status">
<div className="status-line ok" title={lensMeta.path || `${lensMeta.repo_id || ''} ${lensMeta.filename || ''}`.trim()}>
<b>{lensName(lensMeta)}</b>
</div>
<div className="status-line">
layers {lensMeta.tapped_layers[0]}{lensMeta.tapped_layers[lensMeta.tapped_layers.length - 1]} ({lensMeta.tapped_layers.length}) · k={lensMeta.k} · n={lensMeta.n_prompts}
</div>
{(lensMeta.warnings || []).map((w, i) => <div key={i} className="status-line err"> {w}</div>)}
<div className="row" style={{ marginTop: 6 }}>
<label style={{ width: 'auto' }}>
<input type="checkbox" checked={lensOn} onChange={(e) => setLensOn(e.target.checked)} /> active in chat
</label>
<button className="danger" onClick={onLensUnload}>Remove</button>
</div>
<div className="row" style={{ marginTop: 6 }}>
<label title="layers read by the lens on the next generations (fitted: see the lens)">capture</label>
<input type="text" value={capLayers}
placeholder={`${lensMeta.tapped_layers[0]}-${lensMeta.tapped_layers[lensMeta.tapped_layers.length - 1]}`}
onChange={(e) => setCapLayers(e.target.value)} />
<label title="number of tokens read per cell (top-k) — applies to the next generations" style={{ width: 'auto' }}>k</label>
<input type="number" min="1" max="32" value={capK} placeholder={lensMeta.k}
onChange={(e) => setCapK(e.target.value)} style={{ width: 52, flexShrink: 0 }} />
<button disabled={(!capLayers.trim() && !capK) || !!busy} onClick={applyCapture}>OK</button>
</div>
<div className="status-line">captured: {lensMeta.tapped_layers.join(', ')} · k={lensMeta.k} applies to the next generations</div>
</div>
) : (
<>
{modelLoading && (
<div className="status-line ok">
{loadingId} is loading pick a lens now, it will load automatically
once the model is ready{queuedLensName ? ` · queued: ${queuedLensName}` : ''}.
</div>
)}
{reg && (reg.local.length + reg.hub.length > 0) && (
<div className="reg-list">
{reg.local.map((l) => {
const m = l.meta || {}
const sl = m.source_layers
return (
<div key={l.path} className={`reg-item2 ${l.compatible === false ? 'reg-warn' : ''}`}>
<div className="reg-main">
<div className="reg-name" title={l.name}>{l.name} <span className="reg-tag">local</span>{l.compatible === false ? ' ⚠' : ''}</div>
<div className="src">
{m.n_prompts != null ? `${m.n_prompts} prompts` : 'n unknown'}
{sl ? ` · layers ${sl[0]}${sl[1]}` : ''}
{m.dtype ? ` · ${m.quant || m.dtype}` : ''}
{m.d_model ? ` · d=${m.d_model}` : ''}
</div>
{m.created_at && <div className="src">fit {m.created_at.slice(0, 10)}{m.fit_seconds ? ` · ${Math.round(m.fit_seconds / 60)} min` : ''}{m.corpus ? ` · ${m.corpus.split(' ')[0]}` : ''}</div>}
{l.reason && <div className="src reg-reason"> {l.reason}</div>}
</div>
<button disabled={l.compatible === false || (!!busy && !modelLoading)}
onClick={() => lensClick({ path: l.path })}>{modelLoading ? '⏲' : 'Load'}</button>
</div>
)
})}
{reg.hub.map(renderHubLens)}
{reg.hub_error && <div className="status-line err">{reg.hub_error}</div>}
</div>
)}
{reg && reg.local.length + reg.hub.length === 0 && (loadedId || loadingId) && (
<div className="status-line">no known lens for this model fit one (Fit tab), or
load a lens fitted for a compatible model below</div>
)}
{reg && (reg.other?.length || 0) > 0 && (
<details>
<summary style={{ fontSize: 12, color: 'var(--muted)', cursor: 'pointer' }}>
lenses fitted for other models ({reg.other.length})
</summary>
<div className="src" style={{ margin: '6px 0' }}>
For your own finetune or merge without a matching lens: a lens fitted
on a compatible model of the SAME architecture can work (d_model and
layer count are checked at load). Readouts drift with the distance
between the weights treat them as approximate.
</div>
<div className="reg-list">{reg.other.map(renderHubLens)}</div>
</details>
)}
<details>
<summary style={{ fontSize: 12, color: 'var(--muted)', cursor: 'pointer' }}>manual load</summary>
<div className="src" style={{ margin: '6px 0' }}>
Load any Jacobian-lens <code>.pt</code>: either a Hugging Face repo + the
file path inside it (mirror the entries above), or the local path of a
lens you fitted (Fit tab writes <code>lenses/&lt;name&gt;/lens.pt</code>).
</div>
<div className="row" style={{ marginTop: 6 }}><label>repo</label>
<input type="text" value={lensForm.repo_id} placeholder="neuronpedia/jacobian-lens"
onChange={(e) => setLensForm({ ...lensForm, repo_id: e.target.value })} />
</div>
<div className="row"><label>file</label>
<input type="text" value={lensForm.filename} placeholder="gemma-3-1b-it/jlens/…/gemma-3-1b-it_jacobian_lens.pt"
onChange={(e) => setLensForm({ ...lensForm, filename: e.target.value })} />
</div>
<div className="row"><label>revision</label>
<input type="text" value={lensForm.revision} placeholder="main (default)"
onChange={(e) => setLensForm({ ...lensForm, revision: e.target.value })} />
</div>
<div className="row"><label>or path</label>
<input type="text" value={lensForm.path} placeholder="lenses/my-fit/lens.pt (local file)"
onChange={(e) => setLensForm({ ...lensForm, path: e.target.value })} />
</div>
<div className="row"><label>top-k</label>
<input type="number" min="1" max="32" value={lensForm.k} onChange={(e) => setLensForm({ ...lensForm, k: e.target.value })} />
</div>
<button className="primary"
disabled={(!loadedId && !modelLoading) || (!lensForm.filename && !lensForm.path.trim()) || (!!busy && !modelLoading)}
onClick={onLensLoad}>{modelLoading ? 'Queue lens' : 'Load lens'}</button>
</details>
</>
)}
{lensMeta && (
<>
<h2>Token editing </h2>
<button className="primary" onClick={() => openEditorWith(null)}>
Open editor{ivRules.length ? ` (${ivRules.length} rule${ivRules.length > 1 ? 's' : ''})` : ''}
</button>
{ivScale !== 1 && <div className="status-line">global multiplier: ×{(+ivScale).toFixed(2)}</div>}
</>
)}
</>)}
{tab === 'fit' && (<>
<h2>Fitting</h2>
{(() => {
const fit = status?.fit
if (fit?.state === 'running' || fit?.state === 'stopping') {
return (
<div>
<div className="status-line">
{fit.name} · {fit.phase} · {fit.done}/{fit.total}
{fit.eta_seconds ? ` · ETA ${Math.max(1, Math.round(fit.eta_seconds / 60))} min` : ''}
</div>
{(fit.workers || []).map((w, i) => (
<div className="gpu" key={i}>
<div className="name"><span>{w.device} · {w.state}</span><span>{w.done}/{w.total}</span></div>
<div className="bar"><div className="fill" style={{ width: `${(100 * w.done) / Math.max(1, w.total)}%` }} /></div>
</div>
))}
<button className="danger" onClick={() => jsonFetch('/api/fit/stop', { method: 'POST' }).catch(() => {})}>
Stop (resumable)
</button>
</div>
)
}
return (
<>
{fit?.state === 'done' && (
<div className="status-line ok">
fit done: {fit.name} ({Math.round((fit.meta?.fit_seconds || 0) / 60)} min) reload the model then{' '}
<button onClick={async () => {
try {
await jsonFetch('/api/lens/load', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: fit.lens_path }),
})
setNotice({ kind: 'ok', text: `lens ${fit.name} loaded` })
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
}
}}>load lens</button>
</div>
)}
{fit?.state === 'error' && <div className="status-line err">fit error: {fit.error}</div>}
{fit?.state === 'stopped' && <div className="status-line">fit stopped (checkpoints kept, restart = resume)</div>}
<div className="row"><label>continue</label>
<select value={fitContinue} onChange={(e) => {
setFitContinue(e.target.value)
const l = localLenses.find((x) => x.path === e.target.value)
if (l?.meta?.model_id) setFitModel(l.meta.model_id)
}}>
<option value=""> new lens </option>
{localLenses.map((l) => (
<option key={l.path} value={l.path}>{l.name} (n={l.meta?.n_prompts ?? '?'})</option>
))}
</select>
</div>
{fitContinue && (
<div className="status-line">resume: {fitN} prompts are added to the {localLenses.find((l) => l.path === fitContinue)?.meta?.n_prompts ?? '?'} existing ones (weighted average = fit over the union); source layers inherited</div>
)}
<div className="row"><label>model</label>
<select value={fitModel} onChange={(e) => setFitModel(e.target.value)} disabled={!!fitContinue}>
<option value=""></option>
{models.map((m) => <option key={m.id} value={m.id}>{m.id}{m.n_layers != null ? ` (${m.n_layers} l.)` : ''}</option>)}
</select>
</div>
<div className="row"><label>name</label>
<input type="text" placeholder="(auto: model_nN)" value={fitName} onChange={(e) => setFitName(e.target.value)} />
</div>
<div className="row"><label title="number of corpus prompts (existing lenses were made with n=100 unless marked _nNNN)">prompts</label>
<input type="number" min="4" step="1" value={fitN} onChange={(e) => setFitN(e.target.value)} />
</div>
<div className="row"><label title="fit corpus. mixed = both datasets in equal parts (rounded to the nearest prompt), shuffled">dataset</label>
<select value={fitDataset} onChange={(e) => setFitDataset(e.target.value)}>
<option value="Salesforce/wikitext-103-raw-v1">Salesforce/wikitext-103-raw-v1</option>
<option value="heretic-org/Semantic-Harmless">heretic-org/Semantic-Harmless</option>
<option value="mixed">mixed (50/50)</option>
</select>
</div>
<div className="row"><label>quant</label>
<select value={fitQuant} onChange={(e) => setFitQuant(e.target.value)} disabled={!!fitContinue}>
<option value="">none (bf16)</option>
<option value="int8">int8</option>
<option value="nf4">nf4</option>
</select>
</div>
<div className="row">
<button className="linkbtn" onClick={() => setFitAdvanced(!fitAdvanced)}>
{fitAdvanced ? '▾' : '▸'} advanced settings
</button>
</div>
{fitAdvanced && (
<div className="fit-adv">
<div className="row"><label title="GPUs used (several = corpus split across them)">devices</label>
<span style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
{(status?.gpus || []).map((g) => {
const d = `cuda:${g.index}`
return (
<label key={d} style={{ width: 'auto' }} title={g.name}>
<input type="checkbox" checked={fitDevices.includes(d)}
onChange={(e) => setFitDevices(e.target.checked
? [...fitDevices, d].sort()
: fitDevices.filter((x) => x !== d))} /> {d}
</label>
)
})}
</span>
</div>
<div className="row"><label title="default: 8 on ≥16 GB GPUs, 4 below (bf16 4B). Too high = OOM">dim_batch</label>
<input type="number" min="1" step="1" placeholder="auto" value={fitDimBatch}
onChange={(e) => setFitDimBatch(e.target.value)} />
</div>
<div className="row"><label>max_seq_len</label>
<input type="number" min="8" step="8" value={fitMaxSeq} onChange={(e) => setFitMaxSeq(e.target.value)} />
</div>
<div className="row"><label title="layers to fit, e.g. 3-30 or 5,10,15; empty = library default">layers</label>
<input type="text" placeholder="(default)" value={fitLayers} disabled={!!fitContinue}
onChange={(e) => setFitLayers(e.target.value)} />
</div>
<div className="status-line">
max = second-to-last layer: the last one is the lens TARGET
(its readout is already exact, J = I, nothing to fit)
</div>
<div className="status-line">resume: restarting with the same name picks up from the checkpoints in data/fits/&lt;name&gt;/</div>
</div>
)}
<button
className="primary"
disabled={(!fitModel && !fitContinue) || !fitDevices.length || !!loadedId || !!busy}
onClick={async () => {
try {
const layers = []
for (const part of fitLayers.split(',')) {
const mm = part.trim().match(/^(\d+)\s*-\s*(\d+)$/)
if (mm) for (let i = +mm[1]; i <= +mm[2]; i++) layers.push(i)
else if (part.trim() && !isNaN(+part.trim())) layers.push(+part.trim())
}
await jsonFetch('/api/fit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_id: fitModel, n_prompts: +fitN, quant: fitQuant || null,
dataset: fitDataset,
name: fitName.trim() || null, devices: fitDevices,
dim_batch: fitDimBatch ? +fitDimBatch : null,
max_seq_len: +fitMaxSeq,
source_layers: layers.length ? layers : null,
continue_from: fitContinue || null,
}),
})
setNotice({ kind: 'ok', text: `fitting started on ${fitDevices.join(', ')}` })
} catch (err) {
setNotice({ kind: 'err', text: String(err.message || err) })
}
}}
>{fitContinue ? 'Continue fit' : 'Start fit'}</button>
{loadedId && <div className="status-line">unload the model first (VRAM required)</div>}
</>
)
})()}
</>)}
{tab === 'model' && (<>
<h2>Download</h2>
<div className="row">
<input type="text" placeholder="org/repo" value={downloadRepo} onChange={(e) => setDownloadRepo(e.target.value)} />
<button onClick={onDownload}
disabled={(status?.downloads || []).some((d) => d.repo_id === downloadRepo.trim() && d.state === 'running')}></button>
</div>
{(status?.downloads || []).map((d) => {
const planTxt = d.plan
? ` (${(d.plan.size_bytes / GB).toFixed(1)} GB${d.plan.kind === 'fallback' ? ' · full repo' : ''})`
: ''
const pr = d.progress
const pct = pr && pr.total ? Math.min(100, (100 * pr.done) / pr.total) : null
return (
<div key={d.repo_id} className={`status-line ${d.state === 'error' ? 'err' : d.state === 'done' ? 'ok' : ''}`}>
{d.state === 'running' && `downloading: ${d.repo_id}${planTxt}${pct != null ? ` — ${pct.toFixed(0)} %` : '...'}`}
{d.state === 'converting' && `auto bf16 conversion: ${d.repo_id} (fp32 detected)...`}
{d.state === 'done' && `downloaded: ${d.repo_id}${d.converted ? ` → converted: ${d.converted}` : ''}`}
{d.state === 'error' && `failed ${d.repo_id}: ${d.error}`}
{(d.state === 'done' || d.state === 'error') && (
<span style={{ cursor: 'pointer', marginLeft: 6 }}
onClick={() => jsonFetch(`/api/download/${encodeURIComponent(d.repo_id)}`, { method: 'DELETE' }).catch(() => {})}> </span>
)}
{d.state === 'running' && pct != null && (
<div className="dl-bar"><div className="dl-fill" style={{ width: `${pct}%` }} /></div>
)}
</div>
)
})}
{status?.convert?.state === 'running' && (
<div className="status-line">bf16 conversion in progress: {status.convert.path}...</div>
)}
{status?.convert?.state === 'done' && (
<div className="status-line ok">converted to bf16: {status.convert.result?.id}</div>
)}
{status?.convert?.state === 'error' && (
<div className="status-line err">conversion failed: {status.convert.error}</div>
)}
<h2>Browse <button style={{ float: 'right', padding: '1px 7px' }}
onClick={() => { const n = !browseOpen; setBrowseOpen(n); if (n && !browse) openBrowse(null) }}>
{browseOpen ? 'hide' : 'open'}</button></h2>
<div className="row">
<button disabled={!!busy} onClick={() => pickNative('dir')}>🗂 Browse (folder)</button>
</div>
{browseOpen && browse && (
<div className="browser">
<div className="browser-path">
{browse.parent != null && <button className="linkbtn" onClick={() => openBrowse(browse.parent)}> parent</button>}
<button className="linkbtn" onClick={() => openBrowse(null)}>drives</button>
<span className="src"> {browse.path || '(drives)'}</span>
</div>
{browse.is_model && (
<div className="reg-item2" style={{ marginBottom: 4 }}>
<div className="reg-main"><div className="reg-name">📦 this folder is a model</div></div>
<button title="add to the model list (nothing is loaded or copied)"
onClick={() => registerPath(browse.path)}>+ Add</button>
</div>
)}
<div className="browser-list">
{browse.dirs.map((d) => (
<div key={d.path} className={`browser-row ${d.is_model ? 'is-model' : ''}`}>
<span className="browser-name" title={d.path} onClick={() => openBrowse(d.path)}>
{d.is_model ? '📦' : '📁'} {d.name}
</span>
{d.is_model && <button title="add to the model list (nothing is loaded or copied)"
onClick={() => registerPath(d.path)}>+ Add</button>}
</div>
))}
{!browse.dirs.length && <div className="src">(empty)</div>}
</div>
</div>
)}
</>)}
{tab === 'options' && (<>
<h2>Defaults</h2>
<div className="row"><label title="quantization preselected in the Model tab">quant</label>
<select value={settings?.default_quant ?? ''}
onChange={(e) => patchSettings({ default_quant: e.target.value })}>
<option value="">none</option>
<option value="int8">int8</option>
<option value="nf4">nf4</option>
</select>
</div>
<div className="row">
<label title="editor: when a token is added, its layers are auto-selected around the most-relevant one (peak ± this radius). Wider = more robust edit, narrower = more surgical.">auto layers ±</label>
<input type="number" min="0" max="8" step="1" value={settings?.auto_layer_radius ?? 2}
onChange={(e) => patchSettings({ auto_layer_radius: Math.max(0, Math.min(8, Math.trunc(+e.target.value) || 0)) })}
style={{ width: 64, flex: 'none' }} />
</div>
<div className="row"><label>chat</label>
<label style={{ width: 'auto' }}>
<input type="checkbox" checked={settings ? !!settings.chat_markdown : chatMd}
onChange={(e) => patchSettings({ chat_markdown: e.target.checked })} /> render replies as markdown
</label>
</div>
<h2>Paths</h2>
<div className="row">
<label title="Hugging Face cache directory (downloaded models and lenses). Applied at the next server start; run.py --hf-cache overrides it.">HF cache</label>
<input type="text" value={settings?.hf_cache ?? ''} placeholder="(shared HF cache)"
onChange={(e) => setSettings((s) => ({ ...s, hf_cache: e.target.value }))}
onBlur={(e) => patchSettings({ hf_cache: e.target.value.trim() })} />
<button title="browse folders"
onClick={async () => { const p = await pickFolder(); if (p) patchSettings({ hf_cache: p }) }}>🔍</button>
</div>
<div className="row">
<label title="llama.cpp folder — needs convert_hf_to_gguf.py (and llama-quantize for quantized outputs). Setting it enables the direct GGUF export in the token editor.">llama.cpp</label>
<input type="text" value={settings?.llamacpp_dir ?? ''} placeholder="(disables GGUF export)"
onChange={(e) => setSettings((s) => ({ ...s, llamacpp_dir: e.target.value }))}
onBlur={(e) => patchSettings({ llamacpp_dir: e.target.value.trim() })} />
<button title="browse folders"
onClick={async () => { const p = await pickFolder(); if (p) patchSettings({ llamacpp_dir: p }) }}>🔍</button>
</div>
<div className="src" style={{ marginTop: 4 }}>
HF cache applies at the next server start. llama.cpp enables the direct
GGUF export (editor Export).
</div>
</>)}
<div className="status-line">
{busy && <span>({busy}) </span>}
{/* no duplicate "busy: generating" when busy already shows the state */}
{notice && !(busy && /^busy\s*:/.test(notice.text)) && <span className={notice.kind}>{notice.text}</span>}
</div>
</div>
<div className="main">
{conv?.id && (
<div className="conv-header">
<input
className="conv-title-input"
defaultValue={conv.title}
key={`t${conv.id}`}
onBlur={(e) => e.target.value !== conv.title && patchConv({ title: e.target.value })}
/>
<input
className="conv-tags-input"
placeholder="tags (comma-separated)"
defaultValue={conv.tags?.join(', ')}
key={`g${conv.id}`}
onBlur={(e) => patchConv({ tags: e.target.value.split(',').map((t) => t.trim()).filter(Boolean) })}
/>
<button onClick={() => setShowTree(!showTree)}>{showTree ? 'Hide' : 'Tree'}</button>
<a href={`/api/conversations/${conv.id}/export?format=json&frames=1`} download>JSON</a>
<a href={`/api/conversations/${conv.id}/export?format=markdown`} download>MD</a>
</div>
)}
{showTree && conv?.id && (
<div className="tree-panel">
{(childs[null] || []).map((root) => (
<TreeNode
key={root.id}
node={root}
childs={childs}
depth={0}
activeIds={activeIds}
onSelect={(nodeId) => applyPath(nodeId, tree)}
/>
))}
<div className="tree-hint">click a node = resume/branch from there</div>
</div>
)}
<details className="sys">
<summary>System prompt {system.trim() ? '●' : ''} {conv?.id ? '(set at creation)' : ''}</summary>
<textarea value={system} onChange={(e) => setSystem(e.target.value)} placeholder="(none)" disabled={!!conv?.id} />
</details>
<div className="messages" ref={messagesRef}>
{messages.map((m, i) => (
<div
key={m.id ?? `tmp${i}`}
className={`msg ${m.role} ${m.has_frames || m.frames?.length ? 'has-frames' : ''} ${selectedIdx === i ? 'msg-selected' : ''}`}
onClick={() => { if (m.has_frames || m.frames?.length) selectMessage(i) }}
>
{editingMsg?.idx === i ? (
<div className="msg-edit" onClick={(e) => e.stopPropagation()}>
<textarea
value={editingMsg.text}
onChange={(e) => setEditingMsg({ idx: i, text: e.target.value })}
rows={Math.min(16, Math.max(4, editingMsg.text.split('\n').length + 1))}
/>
<div className="msg-edit-actions">
<button className="primary" onClick={saveEditedMsg}>Save</button>
<button onClick={() => setEditingMsg(null)}>Cancel</button>
</div>
</div>
) : (() => {
const text = continuingId != null && m.id === continuingId && draft !== null
? m.content + draft
: m.content
return m.role === 'assistant' && chatMd ? <Md text={text} /> : text
})()}
{m.meta && (
<div className="msgmeta">
{m.meta.model_id} · {m.meta.quant || m.meta.dtype} · {m.meta.device} · T={m.meta.sampling?.temperature ?? '?'}
{m.meta.sampling?.seed >= 0 ? ` · seed=${m.meta.sampling.seed}` : ''} · {(m.stats || m.meta.stats)?.tok_per_s} tok/s
{m.meta.interventions?.length ? ` · ☢ ${m.meta.interventions.length} intervention(s)` : ''}
{m.has_frames || m.frames?.length ? ' · lens frames ◈' : ''}
{m.role === 'assistant' && m.id != null && (
<button
className="diff-btn"
title="edit this reply (later turns will use the edited text)"
onClick={(e) => { e.stopPropagation(); setEditingMsg({ idx: i, text: m.content }) }}
></button>
)}
{(m.has_frames || m.frames?.length) ? (
<button
className="diff-btn"
title="A/B lens diff — pick TWO replies to compare their lens frames side by side"
onClick={async (e) => {
e.stopPropagation()
const key = m.id ?? `tmp${i}`
if (diffSel.find((x) => x.key === key)) {
setDiffSel((prev) => prev.filter((x) => x.key !== key))
return
}
let frames = m.frames
if (!frames?.length && m.id != null) {
try {
frames = (await jsonFetch(`/api/messages/${m.id}/frames`)).frames
} catch { return }
}
if (!frames?.length) return
setDiffSel((prev) => [...prev, { key, frames }].slice(-2))
}}
>{diffSel.find((x) => x.key === (m.id ?? `tmp${i}`)) ? `◧ ${diffSel.findIndex((x) => x.key === (m.id ?? `tmp${i}`)) === 0 ? 'A' : 'B'}` : '◧'}</button>
) : null}
</div>
)}
</div>
))}
{draft !== null && continuingId == null && (
<div className="msg assistant">{chatMd && draft ? <Md text={draft} /> : (draft || '…')}</div>
)}
</div>
{diffSel.length === 2 ? (
<LensDiff
framesA={diffSel[0].frames}
framesB={diffSel[1].frames}
labelA={`#${diffSel[0].key}`}
labelB={`#${diffSel[1].key}`}
onClose={() => setDiffSel([])}
/>
) : lensMeta && lensOn || messages.some((m) => m.frames?.length) ? (() => {
const { live, idx, genId: viewGen } = currentGenView()
const viewFrames = live || (idx >= 0 ? messages[idx].frames : [])
// keep LensView MOUNTED even with no frames (it renders an empty
// shell): unmounting here would wipe the pinned tokens, e.g. while
// regenerating the first reply of a conversation
return (
<>
{viewFrames.length > 0 && (
<div className="v-resizer" title="drag to set the lens view height — double-click for auto"
onMouseDown={startLensResize} onDoubleClick={() => setLensViewH(null)} />
)}
<LensView
frames={viewFrames}
tick={`${framesCount}-${messages.length}-${idx}`}
genId={viewGen ?? null}
lensMeta={lensMeta}
hidden={hidden}
onHideToken={hideToken}
onNotice={(text, kind) => setNotice({ kind: kind || 'err', text })}
onEditToken={(id, str, layer) => openEditorWith({ id, str, layer })}
maxH={lensViewH}
editorOpen={editorOpen}
streaming={streaming}
/>
</>
)
})() : null}
<div className="composer">
<div className="controls">
<label>temp <input type="number" step="0.05" min="0" max="2" value={sampling.temperature}
onChange={(e) => setSampling({ ...sampling, temperature: +e.target.value })} /></label>
<label>top_p <input type="number" step="0.01" min="0" max="1" value={sampling.top_p}
onChange={(e) => setSampling({ ...sampling, top_p: +e.target.value })} /></label>
<label>top_k <input type="number" step="1" min="0" value={sampling.top_k}
onChange={(e) => setSampling({ ...sampling, top_k: +e.target.value })} /></label>
<label>max <input type="number" step="16" min="1" value={sampling.max_tokens}
onChange={(e) => setSampling({ ...sampling, max_tokens: +e.target.value })} /></label>
<label title="-1 = random; ≥ 0 = reproducible sampling">seed <input type="number" step="1" min="-1" value={sampling.seed}
onChange={(e) => setSampling({ ...sampling, seed: Math.trunc(+e.target.value) })} /></label>
<button onClick={onRegenerate} disabled={streaming || !messages.some((m) => m.role === 'assistant')}>Regenerate</button>
<button onClick={onContinue}
title="extend the last reply: the model picks up exactly where it stopped"
disabled={streaming || messages[messages.length - 1]?.role !== 'assistant' || messages[messages.length - 1]?.id == null}>
Continue</button>
<button onClick={onEditLast} disabled={streaming || !messages.some((m) => m.role === 'user')}>Edit last</button>
<label title="render assistant replies as markdown">
<input type="checkbox" checked={chatMd} onChange={(e) => setChatMd(e.target.checked)} /> md
</label>
{lensMeta && (
<button
className={editorOpen ? 'ed-toggle-on' : ''}
style={{ marginLeft: 'auto' }}
onClick={() => setEditorOpen(!editorOpen)}
> Editor{ivRules.length ? ` (${ivRules.length})` : ''}{ivScale !== 1 ? ` ×${(+ivScale).toFixed(2)}` : ''}</button>
)}
{editParent !== undefined && <span className="fork-note">branch from #{editParent ?? 'root'}</span>}
</div>
<div className="inputrow">
<textarea
value={input}
placeholder={loadedId ? 'Message... (Enter to send, Shift+Enter for a new line)' : 'Load a model to chat'}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onSend() }
}}
/>
{streaming
? <button className="danger" onClick={onStop}>Stop</button>
: <button className="primary" onClick={onSend} disabled={!loadedId || !input.trim()}>Send</button>}
</div>
</div>
</div>
<Editor
open={editorOpen}
onClose={() => setEditorOpen(false)}
rules={ivRules}
scale={ivScale}
mode={ivMode}
lensMeta={lensMeta}
nLayers={status?.loaded?.n_layers}
genId={currentGenView().genId}
busy={busy}
prefill={editorPrefill}
onPrefillConsumed={() => setEditorPrefill(null)}
onRules={setIvRules}
onScale={setIvScale}
onMode={setIvMode}
onNotice={(text, kind) => setNotice({ kind: kind || 'err', text })}
rebaseSupported={status?.loaded?.rebase_supported}
autoLayerRadius={settings?.auto_layer_radius}
llamaCppSet={!!settings?.llamacpp_dir}
ggufState={status?.gguf}
/>
</>
)
}