t193 三修:每庫一頁+狀態時間軸+預設淺色(leo 六點回饋)
## ① logo 尺寸不對導致白邊
真兇:我寫死 height:22px **沒給 width:auto** ⇒ 容器一壓就變形、露出底板邊緣。
修:照 portal 側欄同一條 clamp(22px,5vw,28px) + width:auto + max-width:100%,
並依 CIS「clear space = one chevron height」在四邊留白。
實測:logo 底板與品牌區落差 **0**(原本是明顯方塊)。
## ② 深色跟網頁不同/「它有淺色佈景?」
是——**portal 預設淺色**,深色由使用者切換並存 localStorage。
我卻硬跟系統走 ⇒ 兩邊當然不同。
修:改成與 portal 同一套(預設淺色+切換鈕+localStorage),
按鈕規格也逐字對齊 portal 的 .btn/.btn3(padding/font-size/radius/border)。
實測淺色:品牌區(253,252,251)=Paper、側欄(242,241,237)=Canvas,與 CIS 一致。
## ③ 「開啟知識庫網頁」開到錯的庫
真兇:它在首頁、寫死取 accounts[0] ⇒ 有兩個庫必然開錯。
修:移到**各庫頁的上方**,開的就是那個庫。
## ④⑤ 30 個資料夾放不下/「加入資料夾」會加到哪個帳號?
修:**每個知識庫一個獨立分頁**(側邊欄動態列出,附資料夾數)。
「加入資料夾」在庫頁裡,**作用對象就是那個庫**,不會加錯。
## ⑥ 首頁要顯示 status(看守/發現變化/萃取/上傳)
修:首頁改成**狀態時間軸**四步,進行中那步的圓點會呼吸。
誠實邊界:collector 目前回報「整輪」而非逐檔階段,
所以同步中時後三步一起標進行中,**不假裝有更細的進度**。
兩支機械閘全過(check-cis.sh 14 項/check-render.sh 深淺色皆驗)。
style.css 仍是 0 個 hex(顏色全走 var)。
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
// Arcrun 桌面 App — 前端(t193)
|
||||
//
|
||||
// 版面照 leo 08-04 指定(附 Google Drive 截圖):
|
||||
// 「可以完全模仿 gdrive,**左方側邊欄,在右方替換頁面**」
|
||||
// ⇒ 設定不再開 popup,全部是右側的頁;只有「確認刪除」才用覆蓋層。
|
||||
// 版面(leo 08-04 指定,附 Google Drive 截圖):左側邊欄、右側換頁。
|
||||
// 🔴 08-04 二輪回饋(本次處理):
|
||||
// ③「開啟知識庫網頁」不該在首頁(會開到錯的庫)⇒ 移到**各庫頁的上方**
|
||||
// ④ 一個庫 30 個資料夾放不下 ⇒ **每個知識庫一個獨立分頁**
|
||||
// ⑤「加入資料夾」會加到哪個帳號?⇒ 在庫頁裡加,**作用對象就是那個庫**,不會加錯
|
||||
// ⑥ 首頁要顯示 status:看守/發現變化/萃取/上傳 ⇒ **狀態時間軸**
|
||||
import './arcrun-cis.css'; // 共用底層(色票/字體/紋理)——唯一真相源在 arcrun-cis/css/
|
||||
import './style.css'; // 本 App 的版面
|
||||
|
||||
@@ -11,73 +14,96 @@ const $ = (id) => document.getElementById(id);
|
||||
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
||||
({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
|
||||
// 跟隨系統深淺色(CIS 兩套色票都在 arcrun-cis.css 定義好了)
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const applyTheme = () =>
|
||||
document.documentElement.setAttribute('data-theme', mq.matches ? 'dark' : 'light');
|
||||
mq.addEventListener('change', applyTheme);
|
||||
applyTheme();
|
||||
// 主題:**與 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';
|
||||
let updateInfo = null; // { current, latest, available, notes, staged }
|
||||
let page = 'home'; // 'home' | 'ai' | 'update' | 'lib:<idx>'
|
||||
let updateInfo = null;
|
||||
|
||||
// ── 覆蓋層(只給必須打斷的確認)──
|
||||
function openSheet(html, wire) {
|
||||
$('sheet').innerHTML = html; $('overlay').classList.add('on'); if (wire) wire();
|
||||
}
|
||||
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(); });
|
||||
|
||||
// ── 側邊欄換頁 ──
|
||||
document.querySelectorAll('#side .nav').forEach((el) => {
|
||||
el.onclick = () => {
|
||||
page = el.dataset.page;
|
||||
document.querySelectorAll('#side .nav').forEach((n) => n.classList.toggle('on', n === el));
|
||||
renderPage();
|
||||
};
|
||||
});
|
||||
// ── 側邊欄:每個知識庫一項(leo:「每個帳號有獨立的一個頁面」)──
|
||||
function renderNav() {
|
||||
const accs = (state && state.accounts) || [];
|
||||
$('nav').innerHTML = `
|
||||
<div class="nav" data-p="home"><span class="ic">◫</span><span class="nm">首頁</span></div>
|
||||
${accs.length ? `<div class="sec">知識庫</div>` : ''}
|
||||
${accs.map((a, i) => `
|
||||
<div class="nav" data-p="lib:${i}">
|
||||
<span class="ic">▤</span><span class="nm">${esc(a.name)}</span>
|
||||
<span class="cnt">${(a.folders || []).length}</span>
|
||||
</div>`).join('')}
|
||||
<div class="sec">設定</div>
|
||||
<div class="nav" data-p="ai"><span class="ic">✧</span><span class="nm">AI 設定</span></div>
|
||||
<div class="nav" data-p="update"><span class="ic">↧</span><span class="nm">版本與更新</span></div>`;
|
||||
$('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) {
|
||||
const folders = (s.accounts || []).reduce((n, a) => n + (a.folders || []).length, 0);
|
||||
if (!s.accounts || !s.accounts.length) return onboarding();
|
||||
const st = s.steps || [];
|
||||
return `
|
||||
<div class="card">
|
||||
<h3>目前狀況</h3>
|
||||
<div class="kv" style="margin-top:10px">
|
||||
<div><div class="big-num">${s.accounts.length}</div><div class="k">個知識庫</div></div>
|
||||
<div><div class="big-num">${folders}</div><div class="k">個資料夾在看守</div></div>
|
||||
<div><div class="big-num">${s.extractedOK || 0}</div><div class="k">份已整理</div></div>
|
||||
<h3>現在的狀態</h3>
|
||||
<div class="steps">
|
||||
${st.map((x) => `
|
||||
<div class="step ${esc(x.state)}">
|
||||
<span class="dot"></span>
|
||||
<span class="t">${esc(x.title)}</span>
|
||||
<span class="m">${esc(x.meta || '')}</span>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>快速動作</h3>
|
||||
<div class="acts">
|
||||
<button id="qAdd">加入資料夾</button>
|
||||
<button id="qPortal">開啟知識庫網頁</button>
|
||||
<button id="qDocs">使用說明</button>
|
||||
<h3>總計</h3>
|
||||
<div class="kv" style="margin-top:10px">
|
||||
<div><div class="big-num">${s.accounts.length}</div><div class="k">個知識庫</div></div>
|
||||
<div><div class="big-num">${s.accounts.reduce((n,a)=>n+(a.folders||[]).length,0)}</div><div class="k">個資料夾在看守</div></div>
|
||||
<div><div class="big-num">${s.extractedOK || 0}</div><div class="k">份已整理</div></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function pageFolders(s) {
|
||||
if (!s.accounts || !s.accounts.length) return onboarding();
|
||||
return (s.accounts).map((a) => `
|
||||
<div class="acct"><span class="name">${esc(a.name)}</span><span class="host">${esc(a.host)}</span></div>
|
||||
// ── 各庫頁:動作全部作用在這個庫(不會加錯帳號)──
|
||||
function pageLib(s, idx) {
|
||||
const a = s.accounts[idx];
|
||||
if (!a) return `<div class="empty"><div class="t">找不到這個知識庫</div></div>`;
|
||||
const portal = 'https://' + a.host.replace('arcrun-cypher-executor.', 'arcrun-rag-ui.') + '/portal/';
|
||||
return `
|
||||
<div class="libhead">
|
||||
<div class="g">
|
||||
<div class="nm">${esc(a.name)}</div>
|
||||
<div class="host">${esc(a.host)}</div>
|
||||
</div>
|
||||
<button data-portal="${esc(portal)}">開啟知識庫網頁</button>
|
||||
<button class="primary" data-addto="${idx}">加入資料夾</button>
|
||||
</div>
|
||||
${(a.folders || []).map((f) => `
|
||||
<div class="folder">
|
||||
<span class="path" title="${esc(f.path)}">${esc(f.path)}</span>
|
||||
<span class="tag">自動同步中</span>
|
||||
<button class="ghost" data-rm="${esc(f.path)}" data-acc="${f.accIdx}">移除</button>
|
||||
</div>`).join('')
|
||||
|| `<div class="folder"><span class="path dim">還沒有資料夾</span></div>`}
|
||||
`).join('') + `
|
||||
<div class="acts">
|
||||
<button class="primary" id="fAdd">加入資料夾</button>
|
||||
<button id="fAcct">新增知識庫帳號</button>
|
||||
</div>`;
|
||||
|| `<div class="empty"><div class="t">這個知識庫還沒有資料夾</div>
|
||||
<div class="d">按右上的「加入資料夾」,選一個要自動整理的資料夾。</div></div>`}`;
|
||||
}
|
||||
|
||||
function pageAI(s) {
|
||||
@@ -100,11 +126,9 @@ function pageAI(s) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// leo:「檢查更新連到 docs 的檢查更新,不正確,**是直接在這裡進行檢查更新**,
|
||||
// 應該是跳到一個頁面顯示**現在版本與最新版本**,然後**下載可以進行安裝**」
|
||||
function pageUpdate(s) {
|
||||
const u = updateInfo;
|
||||
const latest = u ? (u.latest || '查詢中…') : '查詢中…';
|
||||
const latest = u ? (u.latest || '查詢中…') : '按「檢查更新」查詢';
|
||||
let action = `<button class="primary" id="uCheck">檢查更新</button>`;
|
||||
let note = '';
|
||||
if (u && u.staged) {
|
||||
@@ -149,32 +173,30 @@ function onboarding() {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── 渲染 ──
|
||||
function renderPage() {
|
||||
if (!state) return;
|
||||
const el = $('page');
|
||||
el.innerHTML = page === 'folders' ? pageFolders(state)
|
||||
: page === 'ai' ? pageAI(state)
|
||||
: page === 'update' ? pageUpdate(state)
|
||||
: pageHome(state);
|
||||
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 += `<div class="acts"><button id="hAcct">新增知識庫帳號</button></div>`;
|
||||
}
|
||||
$('page').innerHTML = html;
|
||||
wire();
|
||||
}
|
||||
|
||||
function wire() {
|
||||
const on = (id, fn) => { const e = $(id); if (e) e.onclick = fn; };
|
||||
on('qAdd', addFolder); on('fAdd', addFolder);
|
||||
on('qDocs', () => go.OpenURL('https://rag.arcrun.dev/docs/'));
|
||||
on('uDocs', () => go.OpenURL('https://rag.arcrun.dev/docs/use/update/'));
|
||||
on('qPortal', () => {
|
||||
const a = state.accounts && state.accounts[0];
|
||||
if (a) go.OpenURL('https://' + a.host.replace('arcrun-cypher-executor.', 'arcrun-rag-ui.') + '/portal/');
|
||||
});
|
||||
on('fAcct', showConnect); on('obConnect', showConnect);
|
||||
on('uDocs', () => go.OpenURL('https://rag.arcrun.dev/docs/'));
|
||||
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);
|
||||
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);
|
||||
});
|
||||
@@ -182,28 +204,27 @@ function wire() {
|
||||
|
||||
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 ? s.version : '';
|
||||
$('ver').textContent = s.version || '';
|
||||
$('statusBig').textContent = s.statusBig;
|
||||
$('statusBig').classList.toggle('syncing', !!s.syncing);
|
||||
$('statusSub').textContent = s.statusSub;
|
||||
// 只有狀態列每秒更新;頁面內容不重繪,避免打斷輸入
|
||||
if (first) renderPage();
|
||||
if (first || navChanged) { renderNav(); renderPage(); }
|
||||
else if (page === 'home') renderPage(); // 首頁的狀態時間軸要跟著跳
|
||||
}
|
||||
|
||||
async function tick() {
|
||||
try { render(await go.GetState()); } catch (e) { /* 後端還沒起來 */ }
|
||||
}
|
||||
async function tick() { try { render(await go.GetState()); } catch (e) {} }
|
||||
|
||||
// ── 動作 ──
|
||||
$('btnSync').onclick = async () => { await go.SyncNow(); tick(); };
|
||||
|
||||
async function addFolder() {
|
||||
if (!state || !state.accounts.length) { showConnect(); return; }
|
||||
async function addFolder(accIdx) {
|
||||
const p = await go.PickFolder();
|
||||
if (!p) return;
|
||||
await go.AddFolder(0, p);
|
||||
state = await go.GetState(); renderPage();
|
||||
await go.AddFolder(accIdx, p);
|
||||
state = await go.GetState(); renderNav(); renderPage();
|
||||
}
|
||||
|
||||
function confirmRemove(accIdx, path) {
|
||||
@@ -215,7 +236,7 @@ function confirmRemove(accIdx, path) {
|
||||
$('c1').onclick = closeSheet;
|
||||
$('c2').onclick = async () => {
|
||||
await go.RemoveFolder(accIdx, path); closeSheet();
|
||||
state = await go.GetState(); renderPage();
|
||||
state = await go.GetState(); renderNav(); renderPage();
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -236,7 +257,7 @@ function showConnect() {
|
||||
$('c2').onclick = async () => {
|
||||
try {
|
||||
await go.Connect($('u').value.trim(), $('e').value.trim(), $('p').value);
|
||||
closeSheet(); state = await go.GetState(); renderPage();
|
||||
closeSheet(); state = await go.GetState(); renderNav(); renderPage();
|
||||
} catch (ex) { $('err').textContent = String(ex); $('err').style.display = 'block'; }
|
||||
};
|
||||
});
|
||||
@@ -250,7 +271,6 @@ async function saveAI() {
|
||||
} catch (ex) { $('aiErr').textContent = String(ex); $('aiErr').style.display = 'block'; }
|
||||
}
|
||||
|
||||
// 更新:**在 App 裡完成**(leo 指定),不是丟去 docs
|
||||
async function checkUpdate() {
|
||||
updateInfo = { latest: '查詢中…' }; renderPage();
|
||||
try { updateInfo = await go.CheckUpdate(); } catch (ex) { updateInfo = { err: String(ex) }; }
|
||||
|
||||
Reference in New Issue
Block a user