Files
momentry_core/docs_v1.0/doc_wasm/index.html
2026-05-22 10:18:44 +08:00

356 lines
15 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Momentry API Docs</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<script>mermaid.initialize({startOnLoad:false,theme:'base',themeVariables:{primaryColor:'#e8f4fd',primaryBorderColor:'#4a90d9',primaryTextColor:'#333',lineColor:'#4a90d9',secondaryColor:'#fef3e2',secondaryBorderColor:'#d9a84a',tertiaryColor:'#e8f8e8'}});</script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
#app { display: flex; min-height: 100vh; }
html, body { height: 100%; }
.sidebar { width: 260px; height: 100vh; position: sticky; top: 0; overflow-y: auto; background: #fff; border-right: 1px solid #ddd; padding: 20px; display: flex; flex-direction: column; }
.sidebar h1 { font-size: 18px; margin-bottom: 16px; }
.sidebar a { display: block; padding: 6px 0; color: #0066cc; text-decoration: none; font-size: 14px; cursor: pointer; }
.sidebar a:hover { color: #003d80; }
.sidebar .active { font-weight: 600; color: #003d80; }
.sidebar .logout { margin-top: auto; padding-top: 16px; border-top: 1px solid #eee; position: sticky; bottom: 20px; }
.sidebar .logout a { font-size: 13px; color: #cc0000; cursor: pointer; font-weight: 600; }
.sidebar .logout a:hover { color: #990000; text-decoration: underline; }
.content { flex: 1; padding: 40px; max-width: 960px; }
.content table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
.content th, .content td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
.content th { background: #f0f0f0; font-weight: 600; }
.content code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
.content pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
.content pre code { background: none; padding: 0; }
.content h1 { font-size: 24px; margin: 24px 0 12px; }
.content h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
.content h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
.content p { line-height: 1.6; margin: 8px 0; }
.content a { color: #0066cc; }
</style>
</head>
<body>
<div id="app">
<div class="sidebar" id="sidebar">
<h1>Momentry Docs</h1>
<div style="position:relative">
<input id="search" type="text" placeholder="搜尋模組..." style="width:100%;padding:8px;margin-bottom:12px;border:1px solid #ddd;border-radius:6px;font-size:13px;box-sizing:border-box">
<span id="search-clear" style="display:none;position:absolute;right:8px;top:6px;cursor:pointer;font-size:16px;color:#999;line-height:1" onclick="clearSearch()">×</span>
</div>
<div id="module-list"></div>
<div id="search-results" style="display:none;margin-top:8px"></div>
<div class="logout">
<a id="logout-btn">Logout</a>
</div>
</div>
<div class="content" id="content">
<p>Loading...</p>
</div>
</div>
<script>
const MODULES = [
["01_auth","安全認證","Authentication"],
["02_health","健康檢查","Health"],
["03_register","檔案註冊","File Registration"],
["04_lookup","檔案屬性查詢","File Lookup"],
["05_process","處理流程","Processing"],
["06_search","搜尋功能","Search"],
["07_identity","身份識別","Identity"],
["08_identity_agent","智能身份綁定","Smart Identity Binding"],
["08_media","串流與截圖","Streaming & Thumbnails"],
["09_tmdb","TMDb 整合","TMDb Integration"],
["10_pipeline","生產線","Pipeline"],
["12_agent","智慧代理","AI Agents"],
["13_config","系統設定","System Config"],
];
const el = document.getElementById('content');
let wasm_render = null;
let wasm_exports = null;
async function initWasm() {
const resp = await fetch('/doc-wasm/pkg/md_wasm_bg.wasm');
if (!resp.ok) throw new Error('WASM fetch failed: ' + resp.status);
const bytes = await resp.arrayBuffer();
// Import object: __wbindgen_init_externref_table will be called from __wbindgen_start
// after instantiation, so we close over a variable that will be set later
let exports = null;
const importObj = {
'./md_wasm_bg.js': {
__wbindgen_init_externref_table: function() {
const table = exports.__wbindgen_externrefs;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
table.set(offset + 1, null);
table.set(offset + 2, true);
table.set(offset + 3, false);
},
}
};
const wasm = await WebAssembly.instantiate(bytes, importObj);
exports = wasm.instance.exports;
wasm_exports = exports;
// Call start function to initialize (triggers __wbindgen_init_externref_table)
if (exports.__wbindgen_start) {
exports.__wbindgen_start();
}
}
function md2html(md) {
if (!wasm_exports) return '<p>WASM not loaded</p>';
const encoder = new TextEncoder();
const buf = encoder.encode(md);
const malloc = wasm_exports.__wbindgen_malloc;
const free = wasm_exports.__wbindgen_free;
const memory = wasm_exports.memory;
const ptr = malloc(buf.length, 1);
const mem = new Uint8Array(memory.buffer);
mem.set(buf, ptr);
const result = wasm_exports.render(ptr, buf.length);
const rptr = result[0];
const rlen = result[1];
const ret = new TextDecoder().decode(new Uint8Array(memory.buffer, rptr, rlen));
free(rptr, rlen, 1);
// Convert mermaid code blocks: <pre><code class="language-mermaid">...</code></pre> → <pre class="mermaid">...</pre>
var h = ret.replace(/<table>/g, '<table class="table">');
h = h.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, '<pre class="mermaid">$1</pre>');
return h;
}
async function loadDoc(name, highlightQ) {
el.innerHTML = '<p>Loading...</p>';
try {
const resp = await fetch('/doc-wasm/modules/' + name + '.md');
if (!resp.ok) throw new Error('HTTP ' + resp.status + ' fetching /doc-wasm/modules/' + name + '.md');
const md = await resp.text();
if (!wasm_exports) throw new Error('WASM not loaded');
var dlHtml = '<div style="float:right;margin-bottom:8px"><a href="/doc-wasm/modules/' + name + '.md" download="' + name + '.md" style="text-decoration:none;font-size:13px;color:#0066cc;border:1px solid #0066cc;border-radius:4px;padding:4px 10px">↓ Download .md</a></div>';
el.innerHTML = dlHtml + md2html(md);
if (typeof mermaid !== 'undefined') mermaid.run({nodes:[el.querySelector('.mermaid')].filter(Boolean)});
document.querySelectorAll('.sidebar a.module-link').forEach(function(a) { a.classList.remove('active'); });
var link = document.querySelector('.sidebar a[data-module="' + name + '"]');
if (link) link.classList.add('active');
history.pushState(null, '', '#' + name);
// Scroll to first highlighted match if highlightQ provided
if (highlightQ) {
var re = new RegExp(highlightQ.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
var node, match;
while (node = walker.nextNode()) {
if (re.test(node.textContent)) {
match = node;
break;
}
}
if (match) {
re.lastIndex = 0;
var idx = match.textContent.search(re);
if (idx >= 0) {
var range = document.createRange();
range.setStart(match, idx);
range.setEnd(match, idx + highlightQ.length);
var mark = document.createElement('mark');
mark.style.background = '#ffeb3b';
mark.style.color = '#000';
mark.style.padding = '0 2px';
mark.style.borderRadius = '2px';
range.surroundContents(mark);
mark.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}
} catch(e) {
el.innerHTML = '<p style="color:red">Error: ' + e.message + '</p><pre>'+e.stack+'</pre>';
}
}
function escapeHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function highlightText(text, query) {
var escaped = escapeHtml(text);
var re = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
return escaped.replace(re, '<mark style="background:#ffeb3b;color:#000;padding:0 2px;border-radius:2px">$1</mark>');
}
async function fulltextSearch(q) {
var results = [];
for (var i = 0; i < MODULES.length; i++) {
var m = MODULES[i];
try {
var resp = await fetch('/doc-wasm/modules/' + m[0] + '.md');
if (!resp.ok) continue;
var md = await resp.text();
var lines = md.split('\n');
for (var li = 0; li < lines.length; li++) {
if (lines[li].toLowerCase().indexOf(q) >= 0) {
results.push({ module: m[0], title: m[1] + ' / ' + m[2], line: li + 1, text: lines[li].trim() });
if (results.length > 50) break;
}
}
} catch(e) {}
}
var target = document.getElementById('search-results');
if (!target) return;
if (results.length === 0) {
target.innerHTML = '<p style="font-size:13px;color:#888">No results for <strong>' + escapeHtml(q) + '</strong></p>';
el.innerHTML = '<p>No search results</p>';
return;
}
var html = '<p style="font-size:13px;font-weight:600;color:#333;margin-bottom:8px">' + results.length + ' result(s) for <strong>' + escapeHtml(q) + '</strong></p>';
for (var r of results) {
var safeQ = q.replace(/'/g, "\\'");
html += '<div style="margin-bottom:8px;padding:6px;border-radius:4px;background:#f9f9f9;border:1px solid #eee;cursor:pointer" onclick="showSearchResult(\'' + r.module + '\',\'' + safeQ + '\');return false">'
+ '<span style="font-size:12px;font-weight:600;color:#0066cc">' + escapeHtml(r.module) + ' ' + escapeHtml(r.title) + '</span>'
+ ' <span style="color:#888;font-size:11px">line ' + r.line + '</span><br>'
+ '<span style="font-size:11px;color:#555;display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'
+ highlightText(r.text, q) + '</span></div>';
}
target.innerHTML = html;
el.innerHTML = '<p style="color:#888">' + results.length + ' result(s) — click a result in the sidebar to open</p>';
}
function showSearchResult(module, q) {
loadDoc(module, q);
}
function clearSearch() {
var el = document.getElementById('search');
el.value = '';
el.focus();
// Simulate input event to trigger the handler
var evt = new Event('input', { bubbles: true });
el.dispatchEvent(evt);
}
async function loginUser(user, pass) {
var resp = await fetch('/api/v1/auth/login', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({username:user, password:pass})
});
return resp.ok;
}
function showLoginForm() {
el.innerHTML = '<div style="max-width:360px;margin:80px auto;background:#fff;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,0.08);padding:40px;text-align:center">' +
'<h1 style="font-size:24px;margin-bottom:24px">Momentry Docs</h1>' +
'<form onsubmit="doLogin();return false">' +
'<input type="text" id="login-user" placeholder="Username" value="demo" style="width:100%;padding:10px;margin-bottom:12px;border:1px solid #ddd;border-radius:6px;font-size:14px">' +
'<input type="password" id="login-pass" placeholder="Password" value="" style="width:100%;padding:10px;margin-bottom:12px;border:1px solid #ddd;border-radius:6px;font-size:14px">' +
'<div id="login-err" style="color:#cc0000;font-size:13px;margin-bottom:12px;display:none">Invalid credentials</div>' +
'<button type="submit" style="width:100%;padding:10px;background:#0066cc;color:#fff;border:none;border-radius:6px;font-size:16px;cursor:pointer">Login</button>' +
'</form>' +
'</div>';
}
window.doLogin = async function() {
var u = document.getElementById('login-user').value;
var p = document.getElementById('login-pass').value;
if (await loginUser(u, p)) {
document.getElementById('sidebar').style.display = 'flex';
initApp();
} else {
document.getElementById('login-err').style.display = 'block';
}
};
async function initApp() {
try {
el.innerHTML = '<p>Loading WASM...</p>';
await initWasm();
el.innerHTML = '<p>Building modules...</p>';
var listEl = document.getElementById('module-list');
listEl.innerHTML = '';
MODULES.forEach(function(m) {
var a = document.createElement('a');
a.className = 'module-link';
a.setAttribute('data-module', m[0]);
a.textContent = m[0] + ' ' + m[1];
a.onclick = function(e) { e.preventDefault(); loadDoc(m[0]); };
listEl.appendChild(a);
});
// Search — full text across all modules
var searchTimer = null;
var searchEl = document.getElementById('search');
var searchResultsEl = document.getElementById('search-results');
var moduleListEl = document.getElementById('module-list');
if (searchEl) {
var clearEl = document.getElementById('search-clear');
searchEl.oninput = function() {
var q = this.value.toLowerCase().trim();
clearEl.style.display = q ? 'block' : 'none';
if (q.length < 2) {
moduleListEl.style.display = '';
searchResultsEl.style.display = 'none';
document.querySelectorAll('#module-list a').forEach(function(a) {
a.style.display = a.textContent.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
});
if (searchTimer) clearTimeout(searchTimer);
if (!q) loadDoc(location.hash.slice(1) || '01_auth');
return;
}
moduleListEl.style.display = 'none';
searchResultsEl.style.display = 'block';
searchResultsEl.innerHTML = '<p style="font-size:13px;color:#888">Searching...</p>';
if (searchTimer) clearTimeout(searchTimer);
searchTimer = setTimeout(function() { fulltextSearch(q); }, 300);
};
}
// Watch for logout disappearing
var lo = document.getElementById('logout-btn');
if (lo) {
var loObs = new MutationObserver(function() {
if (!document.getElementById('logout-btn')) {
document.getElementById('content').innerHTML += '<p style="color:orange">⚠️ logout removed from DOM</p>';
}
});
loObs.observe(document.body, {childList: true, subtree: true});
}
document.getElementById('sidebar').style.display = 'flex';
document.getElementById('logout-btn').onclick = async function(e) {
e.preventDefault();
e.stopPropagation();
try {
await fetch('/api/v1/auth/logout', {method:'POST', credentials:'include'});
} catch(_) {}
// Clear client-side cookies
document.cookie.split(';').forEach(function(c) {
document.cookie = c.trim().split('=')[0] + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
});
document.getElementById('sidebar').style.display = 'none';
showLoginForm();
};
// Load initial module from hash or default
var hash = location.hash.slice(1);
await loadDoc(hash || '01_auth');
} catch(e) {
el.innerHTML = '<p style="color:red">Init error: ' + e.message + '</p><pre>'+e.stack+'</pre>';
}
}
async function init() {
// Hide sidebar until authenticated
document.getElementById('sidebar').style.display = 'none';
showLoginForm();
}
init();
</script>
</body>
</html>