优化情报分栏和日报展示
This commit is contained in:
+188
-69
@@ -77,9 +77,7 @@ HOME_PAGE_HTML = """
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.brand span,
|
||||
.nav-note,
|
||||
.auto-card span {
|
||||
.brand span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
@@ -110,20 +108,6 @@ HOME_PAGE_HTML = """
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.auto-card {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-top: 24px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: oklch(99% 0.006 180 / 0.78);
|
||||
}
|
||||
|
||||
.auto-card strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 28px;
|
||||
display: grid;
|
||||
@@ -155,7 +139,7 @@ HOME_PAGE_HTML = """
|
||||
}
|
||||
|
||||
.pulse {
|
||||
min-width: 184px;
|
||||
min-width: 220px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
@@ -172,6 +156,11 @@ HOME_PAGE_HTML = """
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.pulse span {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr);
|
||||
@@ -189,7 +178,7 @@ HOME_PAGE_HTML = """
|
||||
}
|
||||
|
||||
.view[data-view="signals"].active {
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr);
|
||||
grid-template-columns: minmax(0, 1.05fr) minmax(360px, 0.95fr);
|
||||
}
|
||||
|
||||
.panel {
|
||||
@@ -282,7 +271,70 @@ HOME_PAGE_HTML = """
|
||||
padding: 18px;
|
||||
min-height: 520px;
|
||||
line-height: 1.75;
|
||||
white-space: pre-wrap;
|
||||
color: oklch(25% 0.028 210);
|
||||
}
|
||||
|
||||
.report-doc {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
.report-doc h2,
|
||||
.report-doc h3,
|
||||
.report-doc h4,
|
||||
.report-doc p,
|
||||
.report-doc ul,
|
||||
.report-doc ol {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.report-doc h2 {
|
||||
font-size: 24px;
|
||||
line-height: 1.25;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.report-doc h3 {
|
||||
margin-top: 8px;
|
||||
font-size: 18px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.report-doc h4 {
|
||||
color: oklch(34% 0.035 190);
|
||||
font-size: 15px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.report-doc p {
|
||||
color: oklch(30% 0.025 210);
|
||||
}
|
||||
|
||||
.report-doc ul,
|
||||
.report-doc ol {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.report-doc li {
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
.report-doc a {
|
||||
color: oklch(42% 0.12 174);
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.report-doc code {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 1px 5px;
|
||||
background: oklch(96% 0.012 180);
|
||||
font-size: 0.92em;
|
||||
}
|
||||
|
||||
.runs { display: grid; }
|
||||
@@ -342,57 +394,52 @@ HOME_PAGE_HTML = """
|
||||
<span>AI 情报采集与研判工作台</span>
|
||||
</div>
|
||||
<nav class="nav" aria-label="主导航">
|
||||
<button class="active" type="button" data-view-target="signals">情报</button>
|
||||
<button class="active" type="button" data-view-target="signals">信号</button>
|
||||
<button type="button" data-view-target="report">日报</button>
|
||||
<button type="button" data-view-target="runs">记录</button>
|
||||
</nav>
|
||||
<div class="auto-card">
|
||||
<strong>自动整理</strong>
|
||||
<span>约每 30 分钟自动整理一次。</span>
|
||||
</div>
|
||||
<p class="nav-note">聚合 AI 新闻、模型发布、Agent 工具和 GitHub 热门项目。</p>
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
<section class="topbar">
|
||||
<div>
|
||||
<h1>今日 AI 情报</h1>
|
||||
<p class="subtitle">Agent 自动筛选、去重和生成日报;页面会持续更新最新结果。</p>
|
||||
<h1>今日 AI 信号</h1>
|
||||
<p class="subtitle">新闻动态与 GitHub 项目分开阅读,日报保留适合快速复盘的重点判断。</p>
|
||||
</div>
|
||||
<div class="pulse">
|
||||
<strong id="lastRunText">等待运行</strong>
|
||||
<span id="lastRunDetail">正在读取运行记录。</span>
|
||||
<strong id="todayCountText">读取中</strong>
|
||||
<span id="todayCountDetail">正在整理新闻与项目。</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view active" data-view="signals">
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>情报信号</h2>
|
||||
<span class="count" id="signalCount">0 条</span>
|
||||
<h2>AI 新闻</h2>
|
||||
<span class="count" id="newsCount">0 条</span>
|
||||
</div>
|
||||
<div class="signals" id="signalList">
|
||||
<div class="empty">正在读取情报。</div>
|
||||
<div class="signals" id="newsList">
|
||||
<div class="empty">正在读取新闻。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stack">
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>最新日报</h2>
|
||||
<span class="count" id="reportTime">待生成</span>
|
||||
<h2>GitHub 热门项目</h2>
|
||||
<span class="count" id="githubCount">0 条</span>
|
||||
</div>
|
||||
<div class="signals" id="githubList">
|
||||
<div class="empty">正在读取项目。</div>
|
||||
</div>
|
||||
<div class="report" id="reportPreview">正在读取日报。</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>自动记录</h2>
|
||||
<span class="count" id="runCount">0 次</span>
|
||||
</div>
|
||||
<div class="runs" id="runPreview">
|
||||
<div class="empty">正在读取运行记录。</div>
|
||||
<h2>最新日报</h2>
|
||||
<span class="count" id="reportTime">待生成</span>
|
||||
</div>
|
||||
<div class="report" id="reportPreview">正在读取日报。</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -410,7 +457,7 @@ HOME_PAGE_HTML = """
|
||||
<section class="view" data-view="runs">
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>自动记录</h2>
|
||||
<h2>运行记录</h2>
|
||||
<span class="count" id="runCountFull">0 次</span>
|
||||
</div>
|
||||
<div class="runs" id="runList">
|
||||
@@ -424,18 +471,18 @@ HOME_PAGE_HTML = """
|
||||
<script>
|
||||
const navButtons = Array.from(document.querySelectorAll("[data-view-target]"));
|
||||
const views = Array.from(document.querySelectorAll("[data-view]"));
|
||||
const signalList = document.querySelector("#signalList");
|
||||
const signalCount = document.querySelector("#signalCount");
|
||||
const newsList = document.querySelector("#newsList");
|
||||
const newsCount = document.querySelector("#newsCount");
|
||||
const githubList = document.querySelector("#githubList");
|
||||
const githubCount = document.querySelector("#githubCount");
|
||||
const reportPreview = document.querySelector("#reportPreview");
|
||||
const reportBody = document.querySelector("#reportBody");
|
||||
const reportTime = document.querySelector("#reportTime");
|
||||
const reportTimeFull = document.querySelector("#reportTimeFull");
|
||||
const runPreview = document.querySelector("#runPreview");
|
||||
const runList = document.querySelector("#runList");
|
||||
const runCount = document.querySelector("#runCount");
|
||||
const runCountFull = document.querySelector("#runCountFull");
|
||||
const lastRunText = document.querySelector("#lastRunText");
|
||||
const lastRunDetail = document.querySelector("#lastRunDetail");
|
||||
const todayCountText = document.querySelector("#todayCountText");
|
||||
const todayCountDetail = document.querySelector("#todayCountDetail");
|
||||
|
||||
const stateText = {
|
||||
completed: "完成",
|
||||
@@ -452,6 +499,67 @@ HOME_PAGE_HTML = """
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function renderInlineMarkdown(value) {
|
||||
return escapeHtml(value)
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||
.replace(/\\*\\*([^*]+)\\*\\*/g, "<strong>$1</strong>")
|
||||
.replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)]+)\\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>');
|
||||
}
|
||||
|
||||
function renderMarkdownReport(markdown) {
|
||||
const lines = String(markdown || "").split(/\\r?\\n/);
|
||||
const blocks = [];
|
||||
let paragraph = [];
|
||||
let listItems = [];
|
||||
let listTag = "ul";
|
||||
|
||||
const flushParagraph = () => {
|
||||
if (!paragraph.length) return;
|
||||
blocks.push(`<p>${renderInlineMarkdown(paragraph.join(" "))}</p>`);
|
||||
paragraph = [];
|
||||
};
|
||||
|
||||
const flushList = () => {
|
||||
if (!listItems.length) return;
|
||||
blocks.push(`<${listTag}>${listItems.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join("")}</${listTag}>`);
|
||||
listItems = [];
|
||||
};
|
||||
|
||||
lines.forEach((line) => {
|
||||
const trimmed = line.trim();
|
||||
const heading = /^(#{1,4})\\s+(.+)$/.exec(trimmed);
|
||||
const bullet = /^[-*]\\s+(.+)$/.exec(trimmed);
|
||||
const ordered = /^\\d+[.)]\\s+(.+)$/.exec(trimmed);
|
||||
|
||||
if (!trimmed) {
|
||||
flushParagraph();
|
||||
flushList();
|
||||
return;
|
||||
}
|
||||
if (heading) {
|
||||
flushParagraph();
|
||||
flushList();
|
||||
const level = Math.min(heading[1].length + 1, 4);
|
||||
blocks.push(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`);
|
||||
return;
|
||||
}
|
||||
if (bullet || ordered) {
|
||||
flushParagraph();
|
||||
const nextTag = ordered ? "ol" : "ul";
|
||||
if (listItems.length && listTag !== nextTag) flushList();
|
||||
listTag = nextTag;
|
||||
listItems.push((bullet || ordered)[1]);
|
||||
return;
|
||||
}
|
||||
flushList();
|
||||
paragraph.push(trimmed);
|
||||
});
|
||||
|
||||
flushParagraph();
|
||||
flushList();
|
||||
return `<div class="report-doc">${blocks.join("")}</div>`;
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return "未标注时间";
|
||||
const text = String(value);
|
||||
@@ -473,13 +581,13 @@ HOME_PAGE_HTML = """
|
||||
if (updateHash) history.replaceState(null, "", `#${name}`);
|
||||
}
|
||||
|
||||
function renderSignals(signals) {
|
||||
signalCount.textContent = `${signals.length} 条`;
|
||||
function renderSignalGroup(signals, target, counter, emptyText) {
|
||||
counter.textContent = `${signals.length} 条`;
|
||||
if (!signals.length) {
|
||||
signalList.innerHTML = '<div class="empty">Agent 正在按固定节奏自动整理,完成后这里会出现情报信号。</div>';
|
||||
target.innerHTML = `<div class="empty">${escapeHtml(emptyText)}</div>`;
|
||||
return;
|
||||
}
|
||||
signalList.innerHTML = signals.map((item) => `
|
||||
target.innerHTML = signals.map((item) => `
|
||||
<article class="signal-row">
|
||||
<div class="signal-meta">
|
||||
<span class="pill">${escapeHtml(item.topic)}</span>
|
||||
@@ -500,16 +608,16 @@ HOME_PAGE_HTML = """
|
||||
if (!report) {
|
||||
reportTime.textContent = "待生成";
|
||||
reportTimeFull.textContent = "待生成";
|
||||
reportPreview.textContent = "Agent 正在自动整理,日报生成后会显示在这里。";
|
||||
reportBody.textContent = "Agent 正在自动整理,日报生成后会显示在这里。";
|
||||
reportPreview.textContent = "日报生成后会显示在这里。";
|
||||
reportBody.textContent = "日报生成后会显示在这里。";
|
||||
return;
|
||||
}
|
||||
const timeText = formatDate(report.created_at);
|
||||
const content = report.content_md || "日报内容为空。";
|
||||
reportTime.textContent = timeText;
|
||||
reportTimeFull.textContent = timeText;
|
||||
reportPreview.textContent = content;
|
||||
reportBody.textContent = content;
|
||||
reportPreview.innerHTML = renderMarkdownReport(content);
|
||||
reportBody.innerHTML = renderMarkdownReport(content);
|
||||
}
|
||||
|
||||
function runRows(runs) {
|
||||
@@ -522,27 +630,38 @@ HOME_PAGE_HTML = """
|
||||
}
|
||||
|
||||
function renderRuns(runs) {
|
||||
runCount.textContent = `${runs.length} 次`;
|
||||
runCountFull.textContent = `${runs.length} 次`;
|
||||
if (!runs.length) {
|
||||
runPreview.innerHTML = '<div class="empty">Agent 会按固定节奏自动整理。</div>';
|
||||
runList.innerHTML = '<div class="empty">Agent 会按固定节奏自动整理。</div>';
|
||||
lastRunText.textContent = "等待记录";
|
||||
lastRunDetail.textContent = "约每 30 分钟自动整理一次。";
|
||||
runList.innerHTML = '<div class="empty">暂无运行记录。</div>';
|
||||
return;
|
||||
}
|
||||
runPreview.innerHTML = runRows(runs.slice(0, 4));
|
||||
runList.innerHTML = runRows(runs);
|
||||
const latest = runs[0];
|
||||
lastRunText.textContent = stateText[latest.status] || latest.status;
|
||||
lastRunDetail.textContent = `最近一次:${formatDate(latest.started_at)}`;
|
||||
}
|
||||
|
||||
function renderSignalSummary(newsSignals, githubSignals) {
|
||||
const newsTotal = newsSignals.length;
|
||||
const githubTotal = githubSignals.length;
|
||||
todayCountText.textContent = `${newsTotal + githubTotal} 条信号`;
|
||||
todayCountDetail.textContent = `新闻 ${newsTotal} · GitHub ${githubTotal}`;
|
||||
}
|
||||
|
||||
async function loadDashboard() {
|
||||
const response = await fetch("/dashboard", { headers: { Accept: "application/json" } });
|
||||
if (!response.ok) throw new Error("读取情报失败");
|
||||
const data = await response.json();
|
||||
renderSignals(data.signals || []);
|
||||
renderSignalGroup(
|
||||
data.news_signals || [],
|
||||
newsList,
|
||||
newsCount,
|
||||
"Agent 正在整理 AI 新闻,完成后这里会出现新闻信号。"
|
||||
);
|
||||
renderSignalGroup(
|
||||
data.github_signals || [],
|
||||
githubList,
|
||||
githubCount,
|
||||
"Agent 正在整理 GitHub 热门项目,完成后这里会出现项目线索。"
|
||||
);
|
||||
renderSignalSummary(data.news_signals || [], data.github_signals || []);
|
||||
renderReport(data.latest_report);
|
||||
renderRuns(data.runs || []);
|
||||
}
|
||||
@@ -557,10 +676,10 @@ HOME_PAGE_HTML = """
|
||||
}
|
||||
|
||||
loadDashboard().catch(() => {
|
||||
signalList.innerHTML = '<div class="empty">情报读取失败,请稍后再试。</div>';
|
||||
newsList.innerHTML = '<div class="empty">新闻读取失败,请稍后再试。</div>';
|
||||
githubList.innerHTML = '<div class="empty">项目读取失败,请稍后再试。</div>';
|
||||
reportPreview.textContent = "日报读取失败。";
|
||||
reportBody.textContent = "日报读取失败。";
|
||||
runPreview.innerHTML = '<div class="empty">运行记录读取失败。</div>';
|
||||
runList.innerHTML = '<div class="empty">运行记录读取失败。</div>';
|
||||
});
|
||||
window.setInterval(loadDashboard, 60000);
|
||||
|
||||
Reference in New Issue
Block a user