t193:daemon UI 換 Wails——CIS 是硬要求,fyne 做不到
leo 2026-08-04 看過 v0.16.0 畫面後:
「功能都有了,但美感非常糟糕⋯⋯跟 CIS 完全無關,每個功能都開一個小小的 popup 視窗,
非常缺乏整體感,這要理解的是原始的技術選擇是否出錯?」
「我的要求是符合 CIS,在風格上跟 portal 一樣」
「CIS 已經提供規範,你做的連 Logo 都沒放上去,這跟美不美有關係嗎?
要求放進 CIS 是硬要求,你做為檢查有嗎?沒有怎麼交貨?」
## 我的兩個錯
1. **動工前沒提醒做不到**——decisions-summary.md:314 的 D-daemon-UI
是我 07-27 自己寫的調研(結論:fyne 全自繪、CSS 套不進去),寫完就沒再看。
今天動工前該查它卻直接開寫,浪費 leo 的時間與 token。
2. **沒做 CIS 檢查就交貨**——連 logo 都沒放。
## 換 Wails(WebView 殼)
前端就是 HTML/CSS ⇒ 可以直接用 portal 那份色票與 lockup。
- style.css 的色票/字體/紙張紋理**逐字取自** portal 的 :root
(matrix/arcrun:console-ui/public/portal/index.html),不自創任何顏色
- CIS lockup 官方 PNG,淺色 ink 版/深色 paper 版,切換規則同 portal
(避開 08-01 作廢的自產 SVG——字腔缺失)
- 對話框改**內嵌覆蓋層**,不再每個功能開一個小 popup
- 原生資料夾選擇器(macOS powerbox 會自動授予該資料夾存取權,
fyne 自繪 picker 拿不到;未來上 Mac App Store 是硬需求)
- 同步中的圓點會呼吸 ⇒ 看得出來在動(issue #17)
- 無帳號時是 onboarding 引導,不是空白面板
## 連線邏輯逐字沿用,不重寫
connect.go 的 normalizePortalURL/fetchConfigByLogin 取自 arcrun-tray/main.go,
含 t86 個資外洩事故的防線(不同實例=新增帳號,絕不覆蓋舊帳號的資料夾)。
今天已犯過一次「重寫別人修好的東西還修得更差」,不再犯第二次。
## 新增 check-cis.sh(交貨前必跑)
機械檢查六類:官方色票逐個到齊/**不准出現非 CIS 色**/logo 真的放進去/
不可用作廢 SVG/深色模式/字體與紙張紋理同 portal/app icon。
實跑 14 項全過。以後「沒跑過就不准交」。
⚠️ 未驗:實際畫面要 leo 開來看(我開不了視窗)。
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
// Arcrun 桌面 App — 前端(t193)
|
||||
//
|
||||
// 全部畫面都在**同一個視窗**裡:狀態、清單、設定都用內嵌覆蓋層,
|
||||
// 不再像 fyne 版那樣「每個功能開一個小小的 popup」(leo 08-04 點破)。
|
||||
import './style.css';
|
||||
|
||||
const go = window.go.main.App;
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
// 跟隨系統深淺色——CIS 兩套色票都定義好了,這裡只負責切 data-theme
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const applyTheme = () =>
|
||||
document.documentElement.setAttribute('data-theme', mq.matches ? 'dark' : 'light');
|
||||
mq.addEventListener('change', applyTheme);
|
||||
applyTheme();
|
||||
|
||||
// ── 覆蓋層(取代 popup 視窗)──
|
||||
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(); });
|
||||
|
||||
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
||||
({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
|
||||
// ── 主畫面渲染 ──
|
||||
let state = null;
|
||||
|
||||
function render(s) {
|
||||
state = s;
|
||||
$('ver').textContent = s.version ? '版本 ' + s.version : '';
|
||||
$('statusBig').textContent = s.statusBig;
|
||||
$('statusBig').classList.toggle('syncing', !!s.syncing);
|
||||
$('statusSub').textContent = s.statusSub;
|
||||
|
||||
const list = $('list');
|
||||
if (!s.accounts || s.accounts.length === 0) {
|
||||
// 空狀態=onboarding,不是空白面板
|
||||
list.innerHTML = `
|
||||
<div class="empty">
|
||||
<div class="t">先連上你的知識庫</div>
|
||||
<div class="d">
|
||||
Arcrun 會看守你指定的資料夾,<br/>
|
||||
自動把文件整理成 AI 查得到的知識卡。<br/>
|
||||
還沒有知識庫?先到 install.arcrun.dev 裝一個(免費)。
|
||||
</div>
|
||||
<button class="primary" id="obConnect">新增知識庫帳號</button>
|
||||
<button id="obInstall">我還沒有知識庫</button>
|
||||
</div>`;
|
||||
$('obConnect').onclick = showConnect;
|
||||
$('obInstall').onclick = () => go.OpenURL('https://install.arcrun.dev/');
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = s.accounts.map((a) => `
|
||||
<div class="acct"><span class="name">${esc(a.name)}</span><span class="host">${esc(a.host)}</span></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('');
|
||||
|
||||
list.querySelectorAll('[data-rm]').forEach((b) => {
|
||||
b.onclick = () => confirmRemove(Number(b.dataset.acc), b.dataset.rm);
|
||||
});
|
||||
}
|
||||
|
||||
async function tick() {
|
||||
try { render(await go.GetState()); } catch (e) { /* 後端還沒起來,下一輪再試 */ }
|
||||
}
|
||||
|
||||
// ── 動作 ──
|
||||
$('btnSync').onclick = async () => { await go.SyncNow(); tick(); };
|
||||
|
||||
$('btnAddFolder').onclick = async () => {
|
||||
if (!state || !state.accounts.length) { showConnect(); return; }
|
||||
const p = await go.PickFolder(); // 系統原生面板(macOS powerbox 會授權)
|
||||
if (!p) return;
|
||||
await go.AddFolder(0, p);
|
||||
tick();
|
||||
};
|
||||
|
||||
function confirmRemove(accIdx, path) {
|
||||
openSheet(`
|
||||
<h2>移除這個資料夾?</h2>
|
||||
<p>「${esc(path)}」不再自動同步。<br/>已經上傳的知識卡不會被刪除。</p>
|
||||
<div class="acts">
|
||||
<button id="c1">取消</button>
|
||||
<button class="primary" id="c2">移除</button>
|
||||
</div>`, () => {
|
||||
$('c1').onclick = closeSheet;
|
||||
$('c2').onclick = async () => { await go.RemoveFolder(accIdx, path); closeSheet(); tick(); };
|
||||
});
|
||||
}
|
||||
|
||||
function showConnect() {
|
||||
openSheet(`
|
||||
<h2>連上你的知識庫</h2>
|
||||
<p>貼上你的知識庫網址,再輸入你在網站上設定的帳號密碼。</p>
|
||||
<label>知識庫網址</label>
|
||||
<input type="text" id="u" placeholder="https://arcrun-cypher-executor.xxxx.workers.dev"/>
|
||||
<label>帳號(Email)</label>
|
||||
<input type="text" id="e" placeholder="you@example.com"/>
|
||||
<label>密碼</label>
|
||||
<input type="password" id="p"/>
|
||||
<div class="err" id="err" style="display:none"></div>
|
||||
<div class="acts">
|
||||
<button id="c1">取消</button>
|
||||
<button class="primary" id="c2">連線</button>
|
||||
</div>`, () => {
|
||||
$('c1').onclick = closeSheet;
|
||||
$('c2').onclick = async () => {
|
||||
const err = $('err');
|
||||
try {
|
||||
await go.Connect($('u').value.trim(), $('e').value.trim(), $('p').value);
|
||||
closeSheet(); tick();
|
||||
} catch (ex) {
|
||||
err.textContent = String(ex); err.style.display = 'block';
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
$('btnAddAccount').onclick = showConnect;
|
||||
|
||||
$('btnAI').onclick = () => {
|
||||
const gem = state && state.engine === 'gemma';
|
||||
openSheet(`
|
||||
<h2>用哪個 AI 幫你整理文件?</h2>
|
||||
<label class="radio"><input type="radio" name="ai" value="cloud" ${gem ? '' : 'checked'}/>
|
||||
<span><b>雲端 AI</b>(推薦・不必申請任何金鑰)<br/>
|
||||
<span class="muted">用你自己 Cloudflare 帳號內建的 AI,不需要任何金鑰。</span></span></label>
|
||||
<label class="radio"><input type="radio" name="ai" value="gemini" ${gem ? 'checked' : ''}/>
|
||||
<span><b>Google Gemini</b>(需要自己申請金鑰)</span></label>
|
||||
<label>Gemini 金鑰${state && state.geminiKey ? '(目前已設定,清空即可刪除)' : ''}</label>
|
||||
<input type="text" id="k" placeholder="貼上你的 Gemini API Key"/>
|
||||
<div class="err" id="err" style="display:none"></div>
|
||||
<div class="acts">
|
||||
<button id="c1">取消</button>
|
||||
<button class="primary" id="c2">儲存</button>
|
||||
</div>`, () => {
|
||||
$('c1').onclick = closeSheet;
|
||||
$('c2').onclick = async () => {
|
||||
const useGemini = document.querySelector('input[name=ai]:checked').value === 'gemini';
|
||||
const err = $('err');
|
||||
try {
|
||||
await go.SetAI(useGemini, $('k').value.trim());
|
||||
closeSheet(); tick();
|
||||
} catch (ex) { err.textContent = String(ex); err.style.display = 'block'; }
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
$('btnUpdate').onclick = () => go.OpenURL('https://rag.arcrun.dev/docs/use/update/');
|
||||
|
||||
tick();
|
||||
setInterval(tick, 1000); // 每秒刷新 ⇒ 同步中看得到在動
|
||||
Reference in New Issue
Block a user