feat: add douyin workbench results ui
This commit is contained in:
@@ -34,6 +34,11 @@ npm run control-panel
|
||||
http://127.0.0.1:3618
|
||||
```
|
||||
|
||||
这个本地页面现在包含两部分:
|
||||
|
||||
- 上半部分是浏览器辅助采集控制台
|
||||
- 下半部分是 `Douyin Workbench`,可直接查看账号列表、Agent 分析结论、快照详情、相似账号和对标关系
|
||||
|
||||
或者继续用命令行:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -176,6 +176,7 @@ http://127.0.0.1:3618
|
||||
控制台步骤:
|
||||
|
||||
1. 填写抖音主页链接和 StoryForge 账号
|
||||
2. 如需查看采集结果,不用离开这个页面;下半部分 `Douyin Workbench` 会展示账号列表、Agent 结论、快照详情和对标结果
|
||||
2. 点击 `开始采集`
|
||||
3. 在弹出的 Chromium 里登录或通过挑战页
|
||||
4. 回到控制台点击 `已完成登录,继续采集`
|
||||
|
||||
@@ -509,8 +509,149 @@ function renderPage() {
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.subpanel {
|
||||
border: 1px solid rgba(22, 49, 61, 0.1);
|
||||
border-radius: 18px;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
.workbench-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
gap: 18px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.account-list,
|
||||
.snapshot-list,
|
||||
.similar-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.account-item,
|
||||
.snapshot-item,
|
||||
.similar-item,
|
||||
.link-item,
|
||||
.video-item,
|
||||
.report-item {
|
||||
border: 1px solid rgba(22, 49, 61, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
.account-item {
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, border-color 0.15s ease;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.account-item:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(31, 110, 95, 0.28);
|
||||
}
|
||||
.account-item.active,
|
||||
.snapshot-item.active,
|
||||
.similar-item.active {
|
||||
border-color: rgba(31, 110, 95, 0.55);
|
||||
background: rgba(31, 110, 95, 0.08);
|
||||
}
|
||||
.profile-hero {
|
||||
display: grid;
|
||||
grid-template-columns: 86px 1fr;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
.avatar {
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
border-radius: 22px;
|
||||
object-fit: cover;
|
||||
background: rgba(22, 49, 61, 0.08);
|
||||
border: 1px solid rgba(22, 49, 61, 0.12);
|
||||
}
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.metric-card {
|
||||
border: 1px solid rgba(22, 49, 61, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
}
|
||||
.metric-label {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.metric-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(22, 49, 61, 0.08);
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
}
|
||||
.two-col {
|
||||
display: grid;
|
||||
grid-template-columns: 0.9fr 1.1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
.detail-box {
|
||||
border: 1px solid rgba(22, 49, 61, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
min-height: 180px;
|
||||
}
|
||||
.inline-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mono {
|
||||
font-family: "SF Mono", ui-monospace, monospace;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
select {
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(22, 49, 61, 0.12);
|
||||
padding: 12px 14px;
|
||||
font: inherit;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
color: var(--ink);
|
||||
}
|
||||
.report-suggestion {
|
||||
border-left: 3px solid rgba(31, 110, 95, 0.55);
|
||||
padding-left: 12px;
|
||||
margin-top: 12px;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.empty-state {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background: rgba(22, 49, 61, 0.1);
|
||||
margin: 4px 0;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.grid, .row, .checks { grid-template-columns: 1fr; }
|
||||
.grid, .row, .checks, .workbench-layout, .metric-grid, .two-col { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -602,6 +743,117 @@ function renderPage() {
|
||||
</div>
|
||||
<div id="recent-runs" class="recent-list" style="margin-top: 14px;"></div>
|
||||
</section>
|
||||
|
||||
<section class="card" style="margin-top: 18px;">
|
||||
<div style="display: flex; justify-content: space-between; gap: 12px; align-items: center; flex-wrap: wrap;">
|
||||
<div>
|
||||
<h2>Douyin Workbench</h2>
|
||||
<p class="hint">采集完成后的结构化数据、Agent 结论、快照与对标结果,都在这块工作台里查看。</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="secondary" id="workbench-login-button" type="button">登录并加载</button>
|
||||
<button class="secondary" id="workbench-refresh-button" type="button">刷新工作台</button>
|
||||
<button class="secondary" id="workbench-logout-button" type="button">清除会话</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="workbench-session" class="status-box" style="margin-top: 14px;">
|
||||
<p class="hint">登录 StoryForge 后,这里会展示抖音账号列表和分析工作台。</p>
|
||||
</div>
|
||||
|
||||
<div class="workbench-layout">
|
||||
<section class="subpanel stack">
|
||||
<div style="display:flex;justify-content:space-between;gap:12px;align-items:center;">
|
||||
<h3>账号列表</h3>
|
||||
<span class="pill" id="accounts-count-pill">0 个账号</span>
|
||||
</div>
|
||||
<input id="accounts-filter" placeholder="按昵称、抖音号、标签搜索" autocomplete="off" />
|
||||
<div id="accounts-list" class="account-list">
|
||||
<p class="empty-state">登录后将显示可用账号。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stack">
|
||||
<div id="workspace-empty" class="subpanel">
|
||||
<p class="empty-state">先登录并在左侧选择一个账号,或者从“最近运行”里直接打开同步成功的账号。</p>
|
||||
</div>
|
||||
|
||||
<div id="workspace-content" class="stack" hidden>
|
||||
<section class="subpanel stack">
|
||||
<div style="display:flex;justify-content:space-between;gap:12px;align-items:center;flex-wrap:wrap;">
|
||||
<h3>账号总览</h3>
|
||||
<div class="inline-actions">
|
||||
<button class="secondary" id="reload-selected-account-button" type="button">刷新当前账号</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="account-overview"></div>
|
||||
</section>
|
||||
|
||||
<section class="subpanel stack">
|
||||
<div style="display:flex;justify-content:space-between;gap:12px;align-items:center;flex-wrap:wrap;">
|
||||
<h3>Agent 结论</h3>
|
||||
<div class="inline-actions">
|
||||
<button class="primary" id="run-analysis-button" type="button">运行分析</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>
|
||||
分析重点
|
||||
<textarea id="analysis-focus" placeholder="例如:请重点总结选题结构、钩子设计、镜头节奏和二创模板。"></textarea>
|
||||
</label>
|
||||
<div class="stack">
|
||||
<label>
|
||||
分析模型
|
||||
<select id="analysis-model-select"></select>
|
||||
</label>
|
||||
<label>
|
||||
最大视频数
|
||||
<input id="analysis-max-videos" type="number" min="1" max="20" value="12" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="analysis-feedback" class="hint">这里会显示分析执行状态和最近报告。</div>
|
||||
<div id="analysis-reports" class="stack"></div>
|
||||
</section>
|
||||
|
||||
<section class="subpanel stack">
|
||||
<h3>快照与原始采集</h3>
|
||||
<div id="snapshot-summary"></div>
|
||||
<div class="two-col">
|
||||
<div class="stack">
|
||||
<h4 style="margin:0;">快照列表</h4>
|
||||
<div id="snapshot-list" class="snapshot-list"></div>
|
||||
</div>
|
||||
<div class="stack">
|
||||
<h4 style="margin:0;">快照详情</h4>
|
||||
<div id="snapshot-detail" class="detail-box">
|
||||
<p class="empty-state">选择左侧快照后,这里会展示字段摘要和原始内容。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="subpanel stack">
|
||||
<h3>对标与相似账号</h3>
|
||||
<div id="linked-accounts" class="stack"></div>
|
||||
<div class="section-divider"></div>
|
||||
<div class="two-col">
|
||||
<div class="stack">
|
||||
<h4 style="margin:0;">最近相似搜索</h4>
|
||||
<div id="similar-search-list" class="similar-list"></div>
|
||||
</div>
|
||||
<div class="stack">
|
||||
<h4 style="margin:0;">相似搜索详情</h4>
|
||||
<div id="similar-search-detail" class="detail-box">
|
||||
<p class="empty-state">选择一次相似搜索后,这里会展示候选账号和推荐理由。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
@@ -612,7 +864,43 @@ function renderPage() {
|
||||
const refreshButton = document.getElementById("refresh-button");
|
||||
const form = document.getElementById("capture-form");
|
||||
const storageKey = "storyforge-douyin-control-panel";
|
||||
const sessionStorageKey = "storyforge-douyin-workbench-session";
|
||||
const workbenchSessionEl = document.getElementById("workbench-session");
|
||||
const accountsCountPillEl = document.getElementById("accounts-count-pill");
|
||||
const accountsListEl = document.getElementById("accounts-list");
|
||||
const accountsFilterEl = document.getElementById("accounts-filter");
|
||||
const workspaceEmptyEl = document.getElementById("workspace-empty");
|
||||
const workspaceContentEl = document.getElementById("workspace-content");
|
||||
const accountOverviewEl = document.getElementById("account-overview");
|
||||
const analysisFeedbackEl = document.getElementById("analysis-feedback");
|
||||
const analysisReportsEl = document.getElementById("analysis-reports");
|
||||
const analysisFocusEl = document.getElementById("analysis-focus");
|
||||
const analysisModelSelectEl = document.getElementById("analysis-model-select");
|
||||
const analysisMaxVideosEl = document.getElementById("analysis-max-videos");
|
||||
const snapshotSummaryEl = document.getElementById("snapshot-summary");
|
||||
const snapshotListEl = document.getElementById("snapshot-list");
|
||||
const snapshotDetailEl = document.getElementById("snapshot-detail");
|
||||
const linkedAccountsEl = document.getElementById("linked-accounts");
|
||||
const similarSearchListEl = document.getElementById("similar-search-list");
|
||||
const similarSearchDetailEl = document.getElementById("similar-search-detail");
|
||||
const workbenchLoginButton = document.getElementById("workbench-login-button");
|
||||
const workbenchRefreshButton = document.getElementById("workbench-refresh-button");
|
||||
const workbenchLogoutButton = document.getElementById("workbench-logout-button");
|
||||
const reloadSelectedAccountButton = document.getElementById("reload-selected-account-button");
|
||||
const runAnalysisButton = document.getElementById("run-analysis-button");
|
||||
let activeRunId = "";
|
||||
const workbenchState = {
|
||||
session: null,
|
||||
accounts: [],
|
||||
selectedAccountId: "",
|
||||
selectedWorkspace: null,
|
||||
snapshots: [],
|
||||
selectedSnapshotId: "",
|
||||
selectedSnapshotDetail: null,
|
||||
similarSearchDetail: null,
|
||||
loadingAccountId: "",
|
||||
lastAnalysisMessage: ""
|
||||
};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || "")
|
||||
@@ -621,6 +909,458 @@ function renderPage() {
|
||||
.replaceAll(">", ">");
|
||||
}
|
||||
|
||||
function escapeHtmlWithBreaks(value) {
|
||||
return escapeHtml(value).replaceAll("\\n", "<br>");
|
||||
}
|
||||
|
||||
function safeArray(value) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
const num = Number(value || 0);
|
||||
if (!Number.isFinite(num)) {
|
||||
return "-";
|
||||
}
|
||||
if (Math.abs(num) >= 10000) {
|
||||
return (num / 10000).toFixed(1) + "w";
|
||||
}
|
||||
return num.toLocaleString("zh-CN", { maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) {
|
||||
return "-";
|
||||
}
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return String(value);
|
||||
}
|
||||
return date.toLocaleString("zh-CN", { hour12: false });
|
||||
}
|
||||
|
||||
function normalizeBackendUrl(value) {
|
||||
let normalized = String(value || "").trim();
|
||||
while (normalized.endsWith("/")) {
|
||||
normalized = normalized.slice(0, -1);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getFormAuthPayload() {
|
||||
const backendUrl = normalizeBackendUrl(document.getElementById("backend-url").value);
|
||||
const username = document.getElementById("username").value.trim();
|
||||
const password = document.getElementById("password").value;
|
||||
const token = document.getElementById("token").value.trim();
|
||||
return {
|
||||
backendUrl,
|
||||
username,
|
||||
password,
|
||||
token
|
||||
};
|
||||
}
|
||||
|
||||
function persistWorkbenchSession(session) {
|
||||
workbenchState.session = session;
|
||||
localStorage.setItem(sessionStorageKey, JSON.stringify(session));
|
||||
renderWorkbenchSession();
|
||||
}
|
||||
|
||||
function clearWorkbenchSession() {
|
||||
workbenchState.session = null;
|
||||
workbenchState.accounts = [];
|
||||
workbenchState.selectedAccountId = "";
|
||||
workbenchState.selectedWorkspace = null;
|
||||
workbenchState.snapshots = [];
|
||||
workbenchState.selectedSnapshotId = "";
|
||||
workbenchState.selectedSnapshotDetail = null;
|
||||
workbenchState.similarSearchDetail = null;
|
||||
workbenchState.lastAnalysisMessage = "";
|
||||
localStorage.removeItem(sessionStorageKey);
|
||||
renderWorkbenchSession();
|
||||
renderAccountList();
|
||||
renderWorkspace();
|
||||
}
|
||||
|
||||
function loadWorkbenchSession() {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(sessionStorageKey) || "null");
|
||||
if (saved && saved.token && saved.backendUrl) {
|
||||
workbenchState.session = saved;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function storyforgeFetch(pathname, options = {}) {
|
||||
const session = workbenchState.session;
|
||||
const backendUrl = normalizeBackendUrl(options.backendUrl || session?.backendUrl || document.getElementById("backend-url").value);
|
||||
const token = String(options.token || session?.token || "").trim();
|
||||
const headers = {
|
||||
"content-type": "application/json"
|
||||
};
|
||||
if (token) {
|
||||
headers.Authorization = "Bearer " + token;
|
||||
}
|
||||
const response = await fetch(backendUrl + pathname, {
|
||||
method: options.method || "GET",
|
||||
headers,
|
||||
body: options.body ? JSON.stringify(options.body) : undefined
|
||||
});
|
||||
const raw = await response.text();
|
||||
let payload = null;
|
||||
try {
|
||||
payload = raw ? JSON.parse(raw) : null;
|
||||
} catch {
|
||||
payload = { raw };
|
||||
}
|
||||
if (!response.ok) {
|
||||
const detail = payload?.detail;
|
||||
const message =
|
||||
(typeof detail === "string" && detail) ||
|
||||
detail?.message ||
|
||||
payload?.message ||
|
||||
payload?.raw ||
|
||||
("HTTP " + response.status);
|
||||
throw new Error(message);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function renderWorkbenchSession() {
|
||||
const session = workbenchState.session;
|
||||
if (!session) {
|
||||
workbenchSessionEl.innerHTML = '<p class="hint">还没有 StoryForge 会话。你可以直接用上面的账号密码登录,然后加载抖音工作台。</p>';
|
||||
return;
|
||||
}
|
||||
workbenchSessionEl.innerHTML = [
|
||||
'<div class="status-line"><strong>当前账号</strong><span>' + escapeHtml(session.account?.display_name || session.account?.username || "未知账号") + '</span></div>',
|
||||
'<div class="status-line"><strong>角色</strong><span>' + escapeHtml(session.account?.role || "-") + '</span></div>',
|
||||
'<div class="status-line"><strong>后端地址</strong><span class="path">' + escapeHtml(session.backendUrl) + '</span></div>',
|
||||
'<div class="status-line"><strong>Token</strong><span class="mono">' + escapeHtml((session.token || "").slice(0, 12)) + "..." + '</span></div>'
|
||||
].join("");
|
||||
}
|
||||
|
||||
function renderAccountList() {
|
||||
const filterText = accountsFilterEl.value.trim().toLowerCase();
|
||||
const accounts = safeArray(workbenchState.accounts).filter((account) => {
|
||||
if (!filterText) {
|
||||
return true;
|
||||
}
|
||||
const haystack = [
|
||||
account.nickname,
|
||||
account.douyin_id,
|
||||
account.signature,
|
||||
...safeArray(account.tags),
|
||||
...safeArray(account.keywords)
|
||||
].join(" ").toLowerCase();
|
||||
return haystack.includes(filterText);
|
||||
});
|
||||
accountsCountPillEl.textContent = accounts.length + " 个账号";
|
||||
if (!accounts.length) {
|
||||
accountsListEl.innerHTML = '<p class="empty-state">当前没有符合条件的账号。</p>';
|
||||
return;
|
||||
}
|
||||
accountsListEl.innerHTML = accounts.map((account) => {
|
||||
const selected = workbenchState.selectedAccountId === account.id;
|
||||
return [
|
||||
'<button type="button" class="account-item ' + (selected ? "active" : "") + '" data-account-id="' + escapeHtml(account.id) + '">',
|
||||
'<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;">',
|
||||
'<strong>' + escapeHtml(account.nickname || "未命名账号") + '</strong>',
|
||||
'<span class="pill">' + escapeHtml(account.sync_status || "unknown") + '</span>',
|
||||
'</div>',
|
||||
'<p class="meta" style="margin:10px 0 0;">抖音号:' + escapeHtml(account.douyin_id || "-") + '</p>',
|
||||
'<p class="meta" style="margin:6px 0 0;">最近视频:' + escapeHtml(account.video_summary?.count ?? "-") + ' 条</p>',
|
||||
safeArray(account.tags).length ? '<div class="chips" style="margin-top:10px;">' + safeArray(account.tags).slice(0, 6).map((tag) => '<span class="chip">' + escapeHtml(tag) + '</span>').join("") + '</div>' : '',
|
||||
'</button>'
|
||||
].join("");
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function renderSnapshotDetail() {
|
||||
const detail = workbenchState.selectedSnapshotDetail;
|
||||
if (!detail) {
|
||||
snapshotDetailEl.innerHTML = '<p class="empty-state">选择左侧快照后,这里会展示字段摘要和原始内容。</p>';
|
||||
return;
|
||||
}
|
||||
const fields = safeArray(detail.fields).slice(0, 40).map((field) => {
|
||||
return '<div class="link-item"><div><strong>' + escapeHtml(field.field_path || "-") + '</strong></div><div class="meta" style="margin-top:6px;">类型:' + escapeHtml(field.field_type || "-") + '</div><div class="meta" style="margin-top:6px;">值:' + escapeHtml(String(field.field_value || "")).slice(0, 220) + '</div></div>';
|
||||
}).join("");
|
||||
snapshotDetailEl.innerHTML = [
|
||||
'<div class="stack">',
|
||||
'<div class="meta">快照类型:' + escapeHtml(detail.snapshot_type || "-") + '</div>',
|
||||
'<div class="meta">来源:' + escapeHtml(detail.source_url || "-") + '</div>',
|
||||
detail.summary ? '<pre>' + escapeHtml(JSON.stringify(detail.summary, null, 2)) + '</pre>' : '',
|
||||
fields || '<p class="empty-state">这个快照当前没有可展示的字段。</p>',
|
||||
'</div>'
|
||||
].join("");
|
||||
}
|
||||
|
||||
function renderSimilarSearchDetail() {
|
||||
const detail = workbenchState.similarSearchDetail;
|
||||
if (!detail) {
|
||||
similarSearchDetailEl.innerHTML = '<p class="empty-state">选择一次相似搜索后,这里会展示候选账号和推荐理由。</p>';
|
||||
return;
|
||||
}
|
||||
const candidates = safeArray(detail.candidates);
|
||||
similarSearchDetailEl.innerHTML = [
|
||||
'<div class="stack">',
|
||||
'<div class="meta">关键词:' + escapeHtml(safeArray(detail.keywords).join(" / ")) + '</div>',
|
||||
candidates.length ? candidates.map((candidate) => {
|
||||
return [
|
||||
'<div class="link-item">',
|
||||
'<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;">',
|
||||
'<strong>' + escapeHtml(candidate.candidate_nickname || candidate.candidate_profile_url || "候选账号") + '</strong>',
|
||||
'<span class="pill">综合 ' + escapeHtml(formatNumber(candidate.agent_score || candidate.heuristic_score)) + '</span>',
|
||||
'</div>',
|
||||
'<div class="meta" style="margin-top:8px;">' + escapeHtml(candidate.candidate_profile_url || "-") + '</div>',
|
||||
'<div class="meta" style="margin-top:8px;">' + escapeHtml(candidate.rationale_text || "暂无理由说明") + '</div>',
|
||||
'</div>'
|
||||
].join("");
|
||||
}).join("") : '<p class="empty-state">这次搜索还没有候选账号。</p>',
|
||||
'</div>'
|
||||
].join("");
|
||||
}
|
||||
|
||||
function renderWorkspace() {
|
||||
const workspace = workbenchState.selectedWorkspace;
|
||||
if (!workspace || !workspace.account) {
|
||||
workspaceEmptyEl.hidden = false;
|
||||
workspaceContentEl.hidden = true;
|
||||
accountOverviewEl.innerHTML = "";
|
||||
analysisReportsEl.innerHTML = "";
|
||||
snapshotSummaryEl.innerHTML = "";
|
||||
snapshotListEl.innerHTML = "";
|
||||
linkedAccountsEl.innerHTML = "";
|
||||
similarSearchListEl.innerHTML = "";
|
||||
renderSnapshotDetail();
|
||||
renderSimilarSearchDetail();
|
||||
return;
|
||||
}
|
||||
|
||||
const account = workspace.account;
|
||||
const videos = safeArray(account.video_summary?.videos);
|
||||
const reports = safeArray(workspace.recent_reports);
|
||||
const linkedAccounts = safeArray(workspace.linked_accounts);
|
||||
const similarSearches = safeArray(workspace.recent_similarity_searches);
|
||||
const models = safeArray(workspace.available_model_profiles);
|
||||
const syncErrors = safeArray(workspace.sync_errors);
|
||||
workspaceEmptyEl.hidden = true;
|
||||
workspaceContentEl.hidden = false;
|
||||
|
||||
if (!analysisFocusEl.value.trim() && reports[0]?.focus_text) {
|
||||
analysisFocusEl.value = reports[0].focus_text;
|
||||
}
|
||||
|
||||
const selectedModelId = analysisModelSelectEl.value;
|
||||
analysisModelSelectEl.innerHTML = models.length ? models.map((model) => {
|
||||
const selected = selectedModelId ? selectedModelId === model.id : Boolean(model.is_default);
|
||||
return '<option value="' + escapeHtml(model.id) + '"' + (selected ? " selected" : "") + '>' + escapeHtml(model.name + " / " + model.model_name) + '</option>';
|
||||
}).join("") : '<option value="">暂无可用模型</option>';
|
||||
|
||||
accountOverviewEl.innerHTML = [
|
||||
'<div class="profile-hero">',
|
||||
account.avatar_url ? '<img class="avatar" src="' + escapeHtml(account.avatar_url) + '" alt="avatar" />' : '<div class="avatar"></div>',
|
||||
'<div class="stack">',
|
||||
'<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap;">',
|
||||
'<div><h3 style="margin:0;">' + escapeHtml(account.nickname || "未命名账号") + '</h3><div class="meta" style="margin-top:8px;">抖音号:' + escapeHtml(account.douyin_id || "-") + '</div></div>',
|
||||
'<span class="pill">' + escapeHtml(account.sync_status || "unknown") + '</span>',
|
||||
'</div>',
|
||||
'<div class="meta">' + escapeHtmlWithBreaks(account.signature || "暂无签名") + '</div>',
|
||||
'<div class="path">' + escapeHtml(account.profile_url || "-") + '</div>',
|
||||
'</div>',
|
||||
'</div>',
|
||||
'<div class="metric-grid" style="margin-top: 16px;">',
|
||||
'<div class="metric-card"><div class="metric-label">作品数</div><div class="metric-value">' + escapeHtml(formatNumber(account.video_summary?.count)) + '</div></div>',
|
||||
'<div class="metric-card"><div class="metric-label">平均播放</div><div class="metric-value">' + escapeHtml(formatNumber(account.video_summary?.avg_play)) + '</div></div>',
|
||||
'<div class="metric-card"><div class="metric-label">平均点赞</div><div class="metric-value">' + escapeHtml(formatNumber(account.video_summary?.avg_like)) + '</div></div>',
|
||||
'<div class="metric-card"><div class="metric-label">平均分享</div><div class="metric-value">' + escapeHtml(formatNumber(account.video_summary?.avg_share)) + '</div></div>',
|
||||
'</div>',
|
||||
safeArray(account.tags).length ? '<div class="chips" style="margin-top:16px;">' + safeArray(account.tags).slice(0, 18).map((tag) => '<span class="chip">' + escapeHtml(tag) + '</span>').join("") + '</div>' : '',
|
||||
syncErrors.length ? '<div class="link-item" style="margin-top:16px;"><strong>同步提示</strong><div class="meta" style="margin-top:8px;">' + escapeHtml(syncErrors.join(" / ")) + '</div></div>' : '',
|
||||
videos.length ? '<div class="stack" style="margin-top:16px;"><h4 style="margin:0;">最近作品</h4>' + videos.slice(0, 8).map((video) => '<div class="video-item"><strong>' + escapeHtml(video.title || video.description || video.aweme_id) + '</strong><div class="meta" style="margin-top:8px;">发布时间:' + escapeHtml(formatDateTime(video.published_at)) + '</div><div class="meta" style="margin-top:6px;">播放 ' + escapeHtml(formatNumber(video.stats?.play)) + ' / 点赞 ' + escapeHtml(formatNumber(video.stats?.like)) + ' / 评论 ' + escapeHtml(formatNumber(video.stats?.comment)) + '</div></div>').join("") + '</div>' : ''
|
||||
].join("");
|
||||
|
||||
analysisReportsEl.innerHTML = reports.length ? reports.map((report) => {
|
||||
return [
|
||||
'<div class="report-item">',
|
||||
'<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap;">',
|
||||
'<strong>' + escapeHtml(report.focus_text || "默认分析") + '</strong>',
|
||||
'<span class="pill">' + escapeHtml(formatDateTime(report.created_at)) + '</span>',
|
||||
'</div>',
|
||||
safeArray(report.suggestions).length ? safeArray(report.suggestions).map((suggestion) => '<div class="report-suggestion"><div class="meta">' + escapeHtml(suggestion.model_label || "模型") + ' / ' + escapeHtml(suggestion.status || "-") + '</div><div style="margin-top:8px;">' + escapeHtml(suggestion.suggestion_text || "暂无结论") + '</div></div>').join("") : '<p class="empty-state" style="margin-top:10px;">这份报告还没有 suggestion。</p>',
|
||||
'</div>'
|
||||
].join("");
|
||||
}).join("") : '<p class="empty-state">这个账号还没有分析报告。你可以直接点上面的“运行分析”。</p>';
|
||||
|
||||
snapshotSummaryEl.innerHTML = [
|
||||
workspace.latest_public_snapshot ? '<div class="link-item"><strong>最新 public 快照</strong><div class="meta" style="margin-top:8px;">' + escapeHtml(workspace.latest_public_snapshot.source_url || "-") + '</div><div class="meta" style="margin-top:6px;">采集时间:' + escapeHtml(formatDateTime(workspace.latest_public_snapshot.collected_at)) + ',字段数 ' + escapeHtml(workspace.latest_public_snapshot.field_count) + '</div></div>' : '',
|
||||
workspace.latest_creator_snapshot ? '<div class="link-item"><strong>最新 creator 快照</strong><div class="meta" style="margin-top:8px;">' + escapeHtml(workspace.latest_creator_snapshot.source_url || "-") + '</div><div class="meta" style="margin-top:6px;">采集时间:' + escapeHtml(formatDateTime(workspace.latest_creator_snapshot.collected_at)) + ',字段数 ' + escapeHtml(workspace.latest_creator_snapshot.field_count) + '</div></div>' : '',
|
||||
(!workspace.latest_public_snapshot && !workspace.latest_creator_snapshot) ? '<p class="empty-state">当前没有可展示的最新快照。</p>' : ''
|
||||
].join("");
|
||||
|
||||
snapshotListEl.innerHTML = workbenchState.snapshots.length ? workbenchState.snapshots.map((snapshot) => {
|
||||
const selected = workbenchState.selectedSnapshotId === snapshot.id;
|
||||
return '<button type="button" class="snapshot-item ' + (selected ? "active" : "") + '" data-snapshot-id="' + escapeHtml(snapshot.id) + '"><strong>' + escapeHtml(snapshot.snapshot_type || "snapshot") + '</strong><div class="meta" style="margin-top:8px;">' + escapeHtml(snapshot.source_url || "-") + '</div><div class="meta" style="margin-top:6px;">字段 ' + escapeHtml(snapshot.field_count) + ' / ' + escapeHtml(formatDateTime(snapshot.collected_at)) + '</div></button>';
|
||||
}).join("") : '<p class="empty-state">这个账号当前没有快照记录。</p>';
|
||||
renderSnapshotDetail();
|
||||
|
||||
linkedAccountsEl.innerHTML = linkedAccounts.length ? [
|
||||
'<h4 style="margin:0;">已保存对标账号</h4>',
|
||||
linkedAccounts.map((link) => '<div class="link-item"><div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;"><strong>' + escapeHtml(link.target_nickname || link.target_profile_url || "未命名对标") + '</strong><span class="pill">' + escapeHtml(link.relation_type || "benchmark") + '</span></div><div class="meta" style="margin-top:8px;">' + escapeHtml(link.target_profile_url || "-") + '</div><div class="meta" style="margin-top:6px;">' + escapeHtml(link.note || "无备注") + '</div></div>').join("")
|
||||
].join("") : '<p class="empty-state">这个账号还没有保存对标关系。</p>';
|
||||
|
||||
similarSearchListEl.innerHTML = similarSearches.length ? similarSearches.map((search) => {
|
||||
const selected = workbenchState.similarSearchDetail?.id === search.id;
|
||||
return '<button type="button" class="similar-item ' + (selected ? "active" : "") + '" data-search-id="' + escapeHtml(search.id) + '"><strong>' + escapeHtml(safeArray(search.keywords).slice(0, 4).join(" / ") || search.id) + '</strong><div class="meta" style="margin-top:8px;">' + escapeHtml(formatDateTime(search.created_at)) + '</div></button>';
|
||||
}).join("") : '<p class="empty-state">这个账号还没有相似搜索记录。</p>';
|
||||
renderSimilarSearchDetail();
|
||||
}
|
||||
|
||||
async function loadSnapshotDetail(snapshotId) {
|
||||
if (!snapshotId || !workbenchState.selectedAccountId) {
|
||||
workbenchState.selectedSnapshotId = "";
|
||||
workbenchState.selectedSnapshotDetail = null;
|
||||
renderSnapshotDetail();
|
||||
return;
|
||||
}
|
||||
workbenchState.selectedSnapshotId = snapshotId;
|
||||
renderWorkspace();
|
||||
try {
|
||||
workbenchState.selectedSnapshotDetail = await storyforgeFetch("/v2/douyin/accounts/" + encodeURIComponent(workbenchState.selectedAccountId) + "/snapshots/" + encodeURIComponent(snapshotId));
|
||||
} catch (error) {
|
||||
workbenchState.selectedSnapshotDetail = {
|
||||
snapshot_type: "error",
|
||||
source_url: "",
|
||||
summary: { error: error.message },
|
||||
fields: []
|
||||
};
|
||||
}
|
||||
renderSnapshotDetail();
|
||||
renderAccountList();
|
||||
}
|
||||
|
||||
async function loadSimilarSearchDetail(searchId) {
|
||||
if (!searchId) {
|
||||
workbenchState.similarSearchDetail = null;
|
||||
renderSimilarSearchDetail();
|
||||
renderWorkspace();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
workbenchState.similarSearchDetail = await storyforgeFetch("/v2/douyin/similar-searches/" + encodeURIComponent(searchId));
|
||||
} catch (error) {
|
||||
workbenchState.similarSearchDetail = {
|
||||
id: searchId,
|
||||
keywords: [],
|
||||
candidates: [],
|
||||
context: { error: error.message }
|
||||
};
|
||||
}
|
||||
renderWorkspace();
|
||||
}
|
||||
|
||||
async function selectAccount(accountId, options = {}) {
|
||||
if (!accountId) {
|
||||
return;
|
||||
}
|
||||
const preserveFeedback = options.preserveFeedback === true;
|
||||
workbenchState.selectedAccountId = accountId;
|
||||
workbenchState.loadingAccountId = accountId;
|
||||
if (!preserveFeedback) {
|
||||
workbenchState.lastAnalysisMessage = "";
|
||||
}
|
||||
analysisFeedbackEl.textContent = "正在加载账号工作台...";
|
||||
renderAccountList();
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
storyforgeFetch("/v2/douyin/accounts/" + encodeURIComponent(accountId) + "/workspace"),
|
||||
storyforgeFetch("/v2/douyin/accounts/" + encodeURIComponent(accountId) + "/snapshots").catch(() => [])
|
||||
]);
|
||||
workbenchState.selectedWorkspace = results[0];
|
||||
workbenchState.snapshots = safeArray(results[1]);
|
||||
workbenchState.selectedSnapshotId = options.snapshotId || workbenchState.snapshots[0]?.id || "";
|
||||
workbenchState.selectedSnapshotDetail = null;
|
||||
workbenchState.similarSearchDetail = null;
|
||||
renderWorkspace();
|
||||
if (workbenchState.selectedSnapshotId) {
|
||||
await loadSnapshotDetail(workbenchState.selectedSnapshotId);
|
||||
}
|
||||
const firstSearchId = safeArray(workbenchState.selectedWorkspace?.recent_similarity_searches)[0]?.id;
|
||||
if (firstSearchId) {
|
||||
await loadSimilarSearchDetail(firstSearchId);
|
||||
}
|
||||
analysisFeedbackEl.textContent = workbenchState.lastAnalysisMessage || "工作台已加载。";
|
||||
} catch (error) {
|
||||
analysisFeedbackEl.textContent = "加载工作台失败: " + error.message;
|
||||
} finally {
|
||||
workbenchState.loadingAccountId = "";
|
||||
renderAccountList();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkbenchAccounts() {
|
||||
if (!workbenchState.session) {
|
||||
renderWorkbenchSession();
|
||||
return;
|
||||
}
|
||||
accountsCountPillEl.textContent = "加载中...";
|
||||
try {
|
||||
workbenchState.accounts = safeArray(await storyforgeFetch("/v2/douyin/accounts"));
|
||||
renderAccountList();
|
||||
const selectedExists = workbenchState.accounts.some((account) => account.id === workbenchState.selectedAccountId);
|
||||
const nextId = selectedExists ? workbenchState.selectedAccountId : workbenchState.accounts[0]?.id;
|
||||
if (nextId) {
|
||||
await selectAccount(nextId);
|
||||
} else {
|
||||
renderWorkspace();
|
||||
analysisFeedbackEl.textContent = "当前还没有抖音账号数据。";
|
||||
}
|
||||
} catch (error) {
|
||||
accountsCountPillEl.textContent = "加载失败";
|
||||
analysisFeedbackEl.textContent = "加载账号列表失败: " + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function loginWorkbench() {
|
||||
const auth = getFormAuthPayload();
|
||||
if (!auth.backendUrl) {
|
||||
alert("请先填写 StoryForge 地址。");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let session = null;
|
||||
if (auth.token) {
|
||||
const account = await storyforgeFetch("/v2/me", {
|
||||
backendUrl: auth.backendUrl,
|
||||
token: auth.token
|
||||
});
|
||||
session = { backendUrl: auth.backendUrl, token: auth.token, account };
|
||||
} else {
|
||||
if (!auth.username || !auth.password) {
|
||||
alert("登录工作台需要账号密码,或者直接提供 Token。");
|
||||
return;
|
||||
}
|
||||
const loginPayload = await storyforgeFetch("/v2/auth/login", {
|
||||
backendUrl: auth.backendUrl,
|
||||
method: "POST",
|
||||
body: {
|
||||
username: auth.username,
|
||||
password: auth.password
|
||||
}
|
||||
});
|
||||
session = {
|
||||
backendUrl: auth.backendUrl,
|
||||
token: loginPayload.token,
|
||||
account: loginPayload.account
|
||||
};
|
||||
}
|
||||
persistWorkbenchSession(session);
|
||||
await loadWorkbenchAccounts();
|
||||
} catch (error) {
|
||||
alert("工作台登录失败: " + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function renderActiveRun(run) {
|
||||
activeRunId = run?.id || "";
|
||||
continueButton.disabled = !run || run.status !== "awaiting_continue";
|
||||
@@ -651,7 +1391,8 @@ function renderPage() {
|
||||
}
|
||||
recentRunsEl.innerHTML = items.map((item) => {
|
||||
const summary = item.summary || {};
|
||||
const syncResult = summary.sync_result || {};
|
||||
const syncResult = item.syncResponse?.account || summary.sync_result || {};
|
||||
const accountId = item.syncResponse?.account?.id || summary.sync_result?.account_id || "";
|
||||
return [
|
||||
'<article class="recent-item">',
|
||||
'<div style="display:flex;justify-content:space-between;gap:12px;align-items:center;">',
|
||||
@@ -661,6 +1402,7 @@ function renderPage() {
|
||||
'<p class="meta" style="margin:10px 0 0;">作品链接 ' + escapeHtml(summary.video_link_count ?? "-") + ',creator 页面 ' + escapeHtml(summary.captured_creator_pages ?? "-") + '</p>',
|
||||
syncResult.nickname ? '<p class="meta" style="margin:8px 0 0;">同步账号:' + escapeHtml(syncResult.nickname) + '</p>' : '',
|
||||
item.outputDir ? '<div class="path" style="margin-top:8px;">' + escapeHtml(item.outputDir) + '</div>' : '',
|
||||
accountId ? '<div class="inline-actions" style="margin-top:10px;"><button class="secondary" type="button" data-open-account-id="' + escapeHtml(accountId) + '">打开工作台</button></div>' : '',
|
||||
'</article>'
|
||||
].join("");
|
||||
}).join("");
|
||||
@@ -679,6 +1421,7 @@ function renderPage() {
|
||||
if (saved.profileUrl) document.getElementById("profile-url").value = saved.profileUrl;
|
||||
if (saved.backendUrl) document.getElementById("backend-url").value = saved.backendUrl;
|
||||
if (saved.username) document.getElementById("username").value = saved.username;
|
||||
if (saved.token) document.getElementById("token").value = saved.token;
|
||||
if (saved.note) document.getElementById("note").value = saved.note;
|
||||
if (saved.maxVideos !== undefined) document.getElementById("max-videos").value = saved.maxVideos;
|
||||
if (saved.syncEnabled !== undefined) document.getElementById("sync-enabled").checked = Boolean(saved.syncEnabled);
|
||||
@@ -693,6 +1436,7 @@ function renderPage() {
|
||||
profileUrl: payload.profileUrl,
|
||||
backendUrl: payload.backendUrl,
|
||||
username: payload.username,
|
||||
token: payload.token,
|
||||
note: payload.note,
|
||||
maxVideos: payload.maxVideos,
|
||||
syncEnabled: payload.syncEnabled,
|
||||
@@ -721,6 +1465,10 @@ function renderPage() {
|
||||
skipCreatorCenter: document.getElementById("skip-creator-center").checked,
|
||||
allowCreatorCenterFallback: document.getElementById("allow-fallback").checked
|
||||
};
|
||||
if (payload.syncEnabled && !payload.token && workbenchState.session?.token && workbenchState.session.backendUrl === payload.backendUrl) {
|
||||
payload.token = workbenchState.session.token;
|
||||
payload.storyforgeToken = workbenchState.session.token;
|
||||
}
|
||||
if (payload.syncEnabled && !payload.token && (!payload.username || !payload.password)) {
|
||||
alert("当前页面没有读到 StoryForge 账号或密码。请重新输入,或直接填 Token。");
|
||||
return;
|
||||
@@ -739,6 +1487,98 @@ function renderPage() {
|
||||
await refreshStatus();
|
||||
});
|
||||
|
||||
workbenchLoginButton.addEventListener("click", loginWorkbench);
|
||||
|
||||
workbenchRefreshButton.addEventListener("click", async () => {
|
||||
if (!workbenchState.session) {
|
||||
await loginWorkbench();
|
||||
return;
|
||||
}
|
||||
await loadWorkbenchAccounts();
|
||||
});
|
||||
|
||||
workbenchLogoutButton.addEventListener("click", () => {
|
||||
clearWorkbenchSession();
|
||||
analysisFeedbackEl.textContent = "会话已清除。";
|
||||
});
|
||||
|
||||
reloadSelectedAccountButton.addEventListener("click", async () => {
|
||||
if (!workbenchState.selectedAccountId) {
|
||||
return;
|
||||
}
|
||||
await selectAccount(workbenchState.selectedAccountId);
|
||||
});
|
||||
|
||||
runAnalysisButton.addEventListener("click", async () => {
|
||||
if (!workbenchState.selectedAccountId) {
|
||||
alert("请先选择一个账号。");
|
||||
return;
|
||||
}
|
||||
const modelId = analysisModelSelectEl.value;
|
||||
analysisFeedbackEl.textContent = "正在调用 Agent 生成分析结论...";
|
||||
runAnalysisButton.disabled = true;
|
||||
try {
|
||||
const workspace = workbenchState.selectedWorkspace || {};
|
||||
const linkedIds = safeArray(workspace.linked_accounts)
|
||||
.map((item) => item.target_account_id)
|
||||
.filter(Boolean);
|
||||
const result = await storyforgeFetch("/v2/douyin/accounts/" + encodeURIComponent(workbenchState.selectedAccountId) + "/analysis", {
|
||||
method: "POST",
|
||||
body: {
|
||||
model_profile_ids: modelId ? [modelId] : [],
|
||||
linked_account_ids: linkedIds,
|
||||
include_linked_accounts: true,
|
||||
include_recent_similar_candidates: true,
|
||||
max_videos: Math.max(1, Math.min(20, Number.parseInt(analysisMaxVideosEl.value || "12", 10) || 12)),
|
||||
extra_focus: analysisFocusEl.value.trim(),
|
||||
temperature: 0.35
|
||||
}
|
||||
});
|
||||
workbenchState.lastAnalysisMessage = "分析完成,已生成 " + safeArray(result.suggestions).length + " 条建议。";
|
||||
analysisFeedbackEl.textContent = workbenchState.lastAnalysisMessage;
|
||||
await selectAccount(workbenchState.selectedAccountId, { preserveFeedback: true });
|
||||
} catch (error) {
|
||||
workbenchState.lastAnalysisMessage = "分析失败: " + error.message;
|
||||
analysisFeedbackEl.textContent = workbenchState.lastAnalysisMessage;
|
||||
} finally {
|
||||
runAnalysisButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
accountsFilterEl.addEventListener("input", renderAccountList);
|
||||
|
||||
document.addEventListener("click", async (event) => {
|
||||
const accountButton = event.target.closest("[data-account-id]");
|
||||
if (accountButton) {
|
||||
await selectAccount(accountButton.getAttribute("data-account-id"));
|
||||
return;
|
||||
}
|
||||
const snapshotButton = event.target.closest("[data-snapshot-id]");
|
||||
if (snapshotButton) {
|
||||
await loadSnapshotDetail(snapshotButton.getAttribute("data-snapshot-id"));
|
||||
return;
|
||||
}
|
||||
const searchButton = event.target.closest("[data-search-id]");
|
||||
if (searchButton) {
|
||||
await loadSimilarSearchDetail(searchButton.getAttribute("data-search-id"));
|
||||
return;
|
||||
}
|
||||
const openAccountButton = event.target.closest("[data-open-account-id]");
|
||||
if (openAccountButton) {
|
||||
if (!workbenchState.session) {
|
||||
await loginWorkbench();
|
||||
}
|
||||
const targetId = openAccountButton.getAttribute("data-open-account-id");
|
||||
if (targetId) {
|
||||
if (!workbenchState.accounts.length) {
|
||||
await loadWorkbenchAccounts();
|
||||
}
|
||||
await selectAccount(targetId);
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
continueButton.addEventListener("click", async () => {
|
||||
if (!activeRunId) {
|
||||
return;
|
||||
@@ -756,6 +1596,13 @@ function renderPage() {
|
||||
|
||||
refreshButton.addEventListener("click", refreshStatus);
|
||||
loadSavedValues();
|
||||
loadWorkbenchSession();
|
||||
renderWorkbenchSession();
|
||||
renderAccountList();
|
||||
renderWorkspace();
|
||||
if (workbenchState.session) {
|
||||
loadWorkbenchAccounts();
|
||||
}
|
||||
refreshStatus();
|
||||
setInterval(refreshStatus, 1500);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user