// Arcrun 桌面 App — 前端(t193) // // 版面(leo 08-04 指定,附 Google Drive 截圖):左側邊欄、右側換頁。 // 🔴 08-04 二輪回饋(本次處理): // ③「開啟知識庫網頁」不該在首頁(會開到錯的庫)⇒ 移到**各庫頁的上方** // ④ 一個庫 30 個資料夾放不下 ⇒ **每個知識庫一個獨立分頁** // ⑤「加入資料夾」會加到哪個帳號?⇒ 在庫頁裡加,**作用對象就是那個庫**,不會加錯 // ⑥ 首頁要顯示 status:看守/發現變化/萃取/上傳 ⇒ **狀態時間軸** import './arcrun-cis.css'; // 共用底層(色票/字體/紋理)——唯一真相源在 arcrun-cis/css/ import './style.css'; // 本 App 的版面 const go = window.go.main.App; const $ = (id) => document.getElementById(id); const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); // 主題:**與 portal 同一套規則**——預設淺色,使用者切換後存 localStorage // (leo 問「它有淺色佈景?」⇒ 是,portal 預設就是淺色) const THEME_KEY = 'arcrun_app_theme'; function applyTheme(t) { document.documentElement.setAttribute('data-theme', t); try { localStorage.setItem(THEME_KEY, t); } catch (e) {} const b = $('themeBtn'); if (b) b.textContent = t === 'dark' ? '☀' : '☾'; } applyTheme((() => { try { return localStorage.getItem(THEME_KEY) === 'dark' ? 'dark' : 'light'; } catch (e) { return 'light'; } })()); $('themeBtn').onclick = () => applyTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'); let state = null; let page = 'home'; // 'home' | 'ai' | 'update' | 'lib:' let updateInfo = null; // ── 覆蓋層(只給必須打斷的確認)── function openSheet(html, wire) { $('sheet').innerHTML = html; $('overlay').classList.add('on'); if (wire) wire(); } function closeSheet() { $('overlay').classList.remove('on'); } $('overlay').addEventListener('click', (e) => { if (e.target.id === 'overlay') closeSheet(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeSheet(); }); // ── 側邊欄:每個知識庫一項(leo:「每個帳號有獨立的一個頁面」)── function renderNav() { const accs = (state && state.accounts) || []; $('nav').innerHTML = ` ${accs.length ? `
知識庫
` : ''} ${accs.map((a, i) => ` `).join('')}
設定
`; $('nav').querySelectorAll('.nav').forEach((el) => { el.classList.toggle('on', el.dataset.p === page); el.onclick = () => { page = el.dataset.p; renderNav(); renderPage(); }; }); } // ── 首頁:狀態時間軸(leo:「看守、發現變化、萃取、上傳… 不同 status 在哪裡顯示?」)── function pageHome(s) { if (!s.accounts || !s.accounts.length) return onboarding(); const st = s.steps || []; return `

現在的狀態

${st.map((x) => `
${esc(x.title)} ${esc(x.meta || '')}
`).join('')}
${cardSkipped(s.skipped)}

總計

${s.accounts.length}
個知識庫
${s.accounts.reduce((n,a)=>n+(a.folders||[]).length,0)}
個資料夾在看守
${s.extractedOK || 0}
份已整理
`; } // 🔴 G-6.2「不准安靜地略過」(2026-08-06)——J-1/S6 考題的後半句: // 「Then 我一樣找得到——**或當場被告知這種檔案還不支援**」 // 以前 .doc/.pages 這類檔在 collector 掃描時就被丟掉,畫面上一個字都沒有, // 使用者只能得到「我丟了檔,然後什麼都沒發生」這個結論。 // 這張卡就是那句話該出現的地方——**首頁**,他每次打開 App 一定會看到。 // 沒有東西被略過時後端回 null ⇒ 這裡回空字串,畫面保持乾淨(沒事不佔版面)。 function cardSkipped(k) { if (!k) return ''; return `

${esc(k.title)}

${esc(k.note)}
${(k.files || []).length ? ` ` : ''} ${k.other ? `
${esc(k.other)}
` : ''}
`; } // ── 各庫頁:動作全部作用在這個庫(不會加錯帳號)── function pageLib(s, idx) { const a = s.accounts[idx]; if (!a) return `
找不到這個知識庫
`; const portal = 'https://' + a.host.replace('arcrun-cypher-executor.', 'arcrun-rag-ui.') + '/portal/'; return `
${esc(a.name)}
${esc(a.host)}
${(a.folders || []).map((f) => `
${esc(f.path)} 自動同步中
`).join('') || `
這個知識庫還沒有資料夾
按右上的「加入資料夾」,選一個要自動整理的資料夾。
`}`; } function pageAI(s) { const gem = s.engine === 'gemma'; return `

用哪個 AI 幫你整理文件?

Gemini 金鑰${s.geminiKey ? '(目前已設定,清空並儲存即可刪除)' : ''}
`; } function pageUpdate(s) { const u = updateInfo; const latest = u ? (u.latest || '查詢中…') : '按「檢查更新」查詢'; let action = ``; let note = ''; if (u && u.staged) { action = ``; note = `
新版 ${esc(u.latest)} 已下載完成,重新啟動就會套用。
`; } else if (u && u.available) { action = ``; note = u.notes ? `
${esc(u.notes)}
` : ''; } else if (u && u.err) { note = `
${esc(u.err)}
`; } else if (u) { note = `
你已經是最新版本。
`; } return `

版本與更新

${esc(s.version || '—')}
目前版本
${esc(latest)}
最新版本
${note}
${action}

需要協助?

安裝、更新、把知識庫接到你的 AI,說明文件都寫在這裡。
`; } function onboarding() { return `
先連上你的知識庫
Arcrun 會看守你指定的資料夾,
自動把文件整理成 AI 查得到的知識卡。
還沒有知識庫?先到 install.arcrun.dev 裝一個(免費)。
`; } function renderPage() { if (!state) return; let html; if (page.startsWith('lib:')) html = pageLib(state, Number(page.slice(4))); else if (page === 'ai') html = pageAI(state); else if (page === 'update') html = pageUpdate(state); else html = pageHome(state); // 每個庫頁底下都給「新增知識庫帳號」入口 if (page === 'home' && state.accounts && state.accounts.length) { html += `
`; } $('page').innerHTML = html; wire(); } function wire() { const on = (id, fn) => { const e = $(id); if (e) e.onclick = fn; }; on('uDocs', () => go.OpenURL('https://rag.arcrun.dev/docs/')); on('hLogs', () => go.OpenLogFolder()); on('hAcct', showConnect); on('obConnect', showConnect); on('obInstall', () => go.OpenURL('https://install.arcrun.dev/')); on('aiSave', saveAI); on('uCheck', checkUpdate); on('uDownload', downloadUpdate); on('uApply', applyUpdate); document.querySelectorAll('[data-portal]').forEach((b) => { b.onclick = () => go.OpenURL(b.dataset.portal); }); document.querySelectorAll('[data-addto]').forEach((b) => { b.onclick = () => addFolder(Number(b.dataset.addto)); }); document.querySelectorAll('[data-rm]').forEach((b) => { b.onclick = () => confirmRemove(Number(b.dataset.acc), b.dataset.rm); }); } function render(s) { const first = !state; const navChanged = state && JSON.stringify((state.accounts||[]).map(a=>[a.name,(a.folders||[]).length])) !== JSON.stringify((s.accounts||[]).map(a=>[a.name,(a.folders||[]).length])); state = s; $('ver').textContent = s.version || ''; $('statusBig').textContent = s.statusBig; $('statusBig').classList.toggle('syncing', !!s.syncing); $('statusSub').textContent = s.statusSub; if (first || navChanged) { renderNav(); renderPage(); } else if (page === 'home') renderPage(); // 首頁的狀態時間軸要跟著跳 } async function tick() { try { render(await go.GetState()); } catch (e) {} } // ── 動作 ── $('btnSync').onclick = async () => { await go.SyncNow(); tick(); }; async function addFolder(accIdx) { const p = await go.PickFolder(); if (!p) return; await go.AddFolder(accIdx, p); state = await go.GetState(); renderNav(); renderPage(); } function confirmRemove(accIdx, path) { openSheet(`

移除這個資料夾?

「${esc(path)}」不再自動同步。
已經上傳的知識卡不會被刪除。

`, () => { $('c1').onclick = closeSheet; $('c2').onclick = async () => { await go.RemoveFolder(accIdx, path); closeSheet(); state = await go.GetState(); renderNav(); renderPage(); }; }); } function showConnect() { openSheet(`

連上你的知識庫

貼上你的知識庫網址,再輸入你在網站上設定的帳號密碼。

知識庫網址
帳號(Email)
密碼
`, () => { $('c1').onclick = closeSheet; $('c2').onclick = async () => { try { await go.Connect($('u').value.trim(), $('e').value.trim(), $('p').value); closeSheet(); state = await go.GetState(); renderNav(); renderPage(); } catch (ex) { $('err').textContent = String(ex); $('err').style.display = 'block'; } }; }); } async function saveAI() { const useGemini = document.querySelector('input[name=ai]:checked').value === 'gemini'; try { await go.SetAI(useGemini, $('k').value.trim()); state = await go.GetState(); renderPage(); } catch (ex) { $('aiErr').textContent = String(ex); $('aiErr').style.display = 'block'; } } async function checkUpdate() { updateInfo = { latest: '查詢中…' }; renderPage(); try { updateInfo = await go.CheckUpdate(); } catch (ex) { updateInfo = { err: String(ex) }; } renderPage(); } async function downloadUpdate() { updateInfo = Object.assign({}, updateInfo, { notes: '下載中…請稍候' }); renderPage(); try { updateInfo = await go.DownloadUpdate(); } catch (ex) { updateInfo = { err: String(ex) }; } renderPage(); } async function applyUpdate() { try { await go.ApplyUpdate(); } catch (ex) { updateInfo = { err: String(ex) }; renderPage(); } } tick(); setInterval(tick, 1000);