fix: optimize visualization hot paths and refine bgp brief workspace
This commit is contained in:
@@ -76,6 +76,11 @@ export default function MarkdownRenderer({ markdown, className }: MarkdownRender
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('<!--') && trimmed.endsWith('-->')) {
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('```')) {
|
||||
const codeLines: string[] = []
|
||||
index += 1
|
||||
@@ -94,6 +99,12 @@ export default function MarkdownRenderer({ markdown, className }: MarkdownRender
|
||||
continue
|
||||
}
|
||||
|
||||
if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
||||
nodes.push(<hr key={`block-${index}`} />)
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/)
|
||||
if (headingMatch) {
|
||||
const level = headingMatch[1].length
|
||||
@@ -155,6 +166,50 @@ export default function MarkdownRenderer({ markdown, className }: MarkdownRender
|
||||
continue
|
||||
}
|
||||
|
||||
const tableHeaderCells = parseTableRow(trimmed)
|
||||
const tableDividerCells = lines[index + 1] ? parseTableDivider(lines[index + 1].trim()) : null
|
||||
if (
|
||||
tableHeaderCells &&
|
||||
tableHeaderCells.length > 0 &&
|
||||
tableDividerCells &&
|
||||
tableDividerCells.length === tableHeaderCells.length
|
||||
) {
|
||||
const bodyRows: string[][] = []
|
||||
index += 2
|
||||
while (index < lines.length) {
|
||||
const rowCells = parseTableRow(lines[index].trim())
|
||||
if (!rowCells || rowCells.length !== tableHeaderCells.length) {
|
||||
break
|
||||
}
|
||||
bodyRows.push(rowCells)
|
||||
index += 1
|
||||
}
|
||||
|
||||
nodes.push(
|
||||
<div key={`block-${index}`} className="markdown-renderer__table-wrap">
|
||||
<table className="markdown-renderer__table">
|
||||
<thead>
|
||||
<tr>
|
||||
{tableHeaderCells.map((cell, cellIndex) => (
|
||||
<th key={`head-${cellIndex}`}>{renderInlineMarkdown(cell)}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bodyRows.map((row, rowIndex) => (
|
||||
<tr key={`row-${rowIndex}`}>
|
||||
{row.map((cell, cellIndex) => (
|
||||
<td key={`cell-${rowIndex}-${cellIndex}`}>{renderInlineMarkdown(cell)}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
const paragraphLines: string[] = []
|
||||
while (index < lines.length && lines[index].trim()) {
|
||||
paragraphLines.push(lines[index].trim())
|
||||
@@ -165,3 +220,23 @@ export default function MarkdownRenderer({ markdown, className }: MarkdownRender
|
||||
|
||||
return <div className={className ? `markdown-renderer ${className}` : 'markdown-renderer'}>{nodes}</div>
|
||||
}
|
||||
|
||||
function parseTableRow(line: string): string[] | null {
|
||||
if (!line.includes('|')) {
|
||||
return null
|
||||
}
|
||||
|
||||
const normalized = line.replace(/^\|/, '').replace(/\|$/, '')
|
||||
const cells = normalized.split('|').map((cell) => cell.trim())
|
||||
return cells.length > 0 ? cells : null
|
||||
}
|
||||
|
||||
function parseTableDivider(line: string): string[] | null {
|
||||
const cells = parseTableRow(line)
|
||||
if (!cells || cells.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isDivider = cells.every((cell) => /^:?-{3,}:?$/.test(cell))
|
||||
return isDivider ? cells : null
|
||||
}
|
||||
|
||||
@@ -1006,11 +1006,45 @@ body {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.bgp-page__brief-tabpane {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 4px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
|
||||
}
|
||||
|
||||
.bgp-page__brief-tabpane::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.bgp-page__brief-tabpane::-webkit-scrollbar-thumb {
|
||||
border-radius: 999px;
|
||||
background: rgba(148, 163, 184, 0.88);
|
||||
}
|
||||
|
||||
.bgp-page__brief-tabpane::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.bgp-page__brief-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
min-height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bgp-page__brief-body {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bgp-page__brief-content {
|
||||
min-height: 360px;
|
||||
overflow: visible;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(246, 248, 251, 0.98));
|
||||
border: 1px solid rgba(5, 5, 5, 0.08);
|
||||
}
|
||||
|
||||
.bgp-page__brief-head {
|
||||
@@ -1045,24 +1079,16 @@ body {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bgp-page__brief-card > * + * {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.bgp-page__brief-meta .ant-descriptions-view {
|
||||
background: #f7f8fa;
|
||||
border-radius: 12px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.bgp-page__brief-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(246, 248, 251, 0.98));
|
||||
border: 1px solid rgba(5, 5, 5, 0.08);
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
|
||||
}
|
||||
|
||||
.markdown-renderer {
|
||||
color: #262626;
|
||||
font-size: 13px;
|
||||
@@ -1103,7 +1129,9 @@ body {
|
||||
.markdown-renderer ul,
|
||||
.markdown-renderer ol,
|
||||
.markdown-renderer blockquote,
|
||||
.markdown-renderer pre {
|
||||
.markdown-renderer pre,
|
||||
.markdown-renderer hr,
|
||||
.markdown-renderer__table-wrap {
|
||||
margin: 0 0 0.9em;
|
||||
}
|
||||
|
||||
@@ -1132,6 +1160,43 @@ body {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.markdown-renderer hr {
|
||||
border: 0;
|
||||
border-top: 1px solid rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.markdown-renderer__table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.markdown-renderer__table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 520px;
|
||||
font-size: 13px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.markdown-renderer__table th,
|
||||
.markdown-renderer__table td {
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.markdown-renderer__table th {
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
background: rgba(248, 250, 252, 0.95);
|
||||
}
|
||||
|
||||
.markdown-renderer__table tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-renderer code {
|
||||
padding: 0.08em 0.32em;
|
||||
border-radius: 6px;
|
||||
|
||||
@@ -519,60 +519,62 @@ function BGP() {
|
||||
}
|
||||
|
||||
const briefTabContent = (
|
||||
<div className="bgp-page__brief-card">
|
||||
<div className="bgp-page__brief-head">
|
||||
<div>
|
||||
<Text strong>BGP AI 简报</Text>
|
||||
<div className="bgp-page__brief-subtitle">
|
||||
每次生成都会保存为 Markdown。打开页面时默认展示最近一次简报,也可以从历史列表切换查看。
|
||||
<div className="bgp-page__brief-tabpane">
|
||||
<div className="bgp-page__brief-card">
|
||||
<div className="bgp-page__brief-head">
|
||||
<div>
|
||||
<Text strong>BGP AI 简报</Text>
|
||||
<div className="bgp-page__brief-subtitle">
|
||||
每次生成都会保存为 Markdown。打开页面时默认展示最近一次简报,也可以从历史列表切换查看。
|
||||
</div>
|
||||
</div>
|
||||
<div className="bgp-page__brief-actions">
|
||||
<Select
|
||||
className="bgp-page__brief-select"
|
||||
placeholder="选择历史简报"
|
||||
value={selectedBriefId || undefined}
|
||||
options={formatBriefOptions(briefOptions)}
|
||||
onChange={(value) => void handleBriefSelectionChange(value)}
|
||||
disabled={briefLoading || briefDetailLoading || briefOptions.length === 0}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={briefLoading}
|
||||
onClick={() => void handleGenerateBrief()}
|
||||
>
|
||||
生成 AI 简报
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bgp-page__brief-actions">
|
||||
<Select
|
||||
className="bgp-page__brief-select"
|
||||
placeholder="选择历史简报"
|
||||
value={selectedBriefId || undefined}
|
||||
options={formatBriefOptions(briefOptions)}
|
||||
onChange={(value) => void handleBriefSelectionChange(value)}
|
||||
disabled={briefLoading || briefDetailLoading || briefOptions.length === 0}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={briefLoading}
|
||||
onClick={() => void handleGenerateBrief()}
|
||||
>
|
||||
生成 AI 简报
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{briefLoading || briefDetailLoading ? (
|
||||
<div className="bgp-page__brief-loading">
|
||||
<Spin />
|
||||
<Text type="secondary">
|
||||
{briefLoading ? '正在整理 BGP 事实并生成简报...' : '正在加载已保存的 BGP 简报...'}
|
||||
</Text>
|
||||
</div>
|
||||
) : brief ? (
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Descriptions size="small" column={compactViewport ? 1 : 3} className="bgp-page__brief-meta">
|
||||
<Descriptions.Item label="Provider">{brief.provider || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="模型">{brief.model || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="请求ID">{brief.request_id || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="生成时间" span={compactViewport ? 1 : 3}>
|
||||
{formatDateTimeZhCN(brief.generated_at)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div className="bgp-page__brief-content">
|
||||
<MarkdownRenderer markdown={brief.content_markdown} />
|
||||
{briefLoading || briefDetailLoading ? (
|
||||
<div className="bgp-page__brief-loading">
|
||||
<Spin />
|
||||
<Text type="secondary">
|
||||
{briefLoading ? '正在整理 BGP 事实并生成简报...' : '正在加载已保存的 BGP 简报...'}
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
) : (
|
||||
<div className="bgp-page__brief-empty">
|
||||
<Text type="secondary">还没有生成简报。点击右侧按钮,用当前 BGP 真实数据生成并保存第一份 Markdown 简报。</Text>
|
||||
</div>
|
||||
)}
|
||||
) : brief ? (
|
||||
<div className="bgp-page__brief-body">
|
||||
<Descriptions size="small" column={compactViewport ? 1 : 3} className="bgp-page__brief-meta">
|
||||
<Descriptions.Item label="Provider">{brief.provider || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="模型">{brief.model || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="请求ID">{brief.request_id || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="生成时间" span={compactViewport ? 1 : 3}>
|
||||
{formatDateTimeZhCN(brief.generated_at)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div className="bgp-page__brief-content">
|
||||
<MarkdownRenderer markdown={brief.content_markdown} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bgp-page__brief-empty">
|
||||
<Text type="secondary">还没有生成简报。点击右侧按钮,用当前 BGP 真实数据生成并保存第一份 Markdown 简报。</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user