Files
Arcrun/landing/app/mira/wiki/page.tsx
T
Leo 64193f2aa5 feat(mira): wiki listing 加 Index Entries section(CC navigation 入口)
leo 反饋:原本只看到 wiki-page 列表沒看到 per-entity index-entry,
不知道 CC 從哪入口。新增 section 列出所有 type=index-entry blocks,
標題用 entity 名稱(剝 `index-` prefix),點進去看完整 markdown 摘要。

對應 design.md §3.5.12.4.2 雙層 outliner(v1.6):
- 概覽層:index-entry markdown(含「段落 outline」/「涵蓋面向」等)
- 完整 outliner:wiki page 自身(7B.3g 已實現的樹狀渲染)

部署:arcrun-landing.pages.dev(手動 wrangler pages deploy)。
2026-05-14 18:01:55 +08:00

326 lines
11 KiB
TypeScript
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.
'use client';
// Mira Wiki 索引頁
// SDD: polaris/mira/.agents/specs/mira-app/design.md §5.2 + §3.5.10
// 對應 task: 7C.1
// 階段 7-A 已建:mira-wiki-schema、mira-wiki-index(+4 children)、mira-wiki-log(+1 child)
// 此頁列出這些 infra block 與既有 wiki-page,方便 leo 在瀏覽器確認 schema 寫得對不對
import { useEffect, useState } from 'react';
import Link from 'next/link';
import '../mira.css';
const KBDB_BASE = 'https://kbdb.finally.click';
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? 'https://cypher.arcrun.dev';
type Block = {
id: string;
page_name: string;
content: string;
type: string;
parent_id: string | null;
tags_json: string | null;
created_at: number;
};
export default function WikiIndexPage() {
const [schema, setSchema] = useState<Block | null>(null);
const [indexChildren, setIndexChildren] = useState<Block[]>([]);
const [logEntries, setLogEntries] = useState<Block[]>([]);
const [otherWikiPages, setOtherWikiPages] = useState<Block[]>([]);
const [indexEntries, setIndexEntries] = useState<Block[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
async function load() {
try {
// 先拿 ak_ partner key(同 page.tsx pattern
const meRes = await fetch(`${API_BASE}/me`, { credentials: 'include' });
if (!meRes.ok) throw new Error('未登入');
const me = (await meRes.json()) as { api_key: string };
const headers = { Authorization: `Bearer ${me.api_key}` };
// 撈所有 type=wiki-page,再 client 端過濾 tags 含 'mira-wiki'
// 原本 ?tag=mira-wiki 撞 KBDB worker D1 bugmalformed JSON),改 type filter
// 待 KBDB 修 tag filter 後可改回(SDD 待開 kbdb-tag-filter-fix
const res = await fetch(
`${KBDB_BASE}/blocks?type=wiki-page&limit=200`,
{ headers },
);
if (!res.ok) throw new Error(`KBDB ${res.status}`);
const data = await res.json();
if (cancelled) return;
const allWikiBlocks: Block[] = data.blocks ?? [];
// Client 端過濾:只留 tags 含 'mira-wiki'
const blocks: Block[] = allWikiBlocks.filter((b) => {
if (!b.tags_json) return false;
try {
const tags = JSON.parse(b.tags_json) as string[];
return tags.includes('mira-wiki');
} catch {
return false;
}
});
const tagsOf = (b: Block): string[] => {
if (!b.tags_json) return [];
try {
return JSON.parse(b.tags_json) as string[];
} catch {
return [];
}
};
const hasSubtype = (b: Block, st: string) =>
tagsOf(b).includes(`subtype:${st}`);
const hasAnyInfraSubtype = (b: Block) =>
['schema', 'index', 'index-child', 'log', 'log-child'].some((st) => hasSubtype(b, st));
const hasMetaTag = (b: Block) =>
tagsOf(b).some((t) => t === 'data-source-config' || t === 'source-skill');
setSchema(blocks.find((b) => hasSubtype(b, 'schema')) ?? null);
setIndexChildren(
blocks
.filter((b) => hasSubtype(b, 'index-child'))
.sort((a, b) => a.page_name.localeCompare(b.page_name)),
);
setLogEntries(
blocks
.filter((b) => hasSubtype(b, 'log-child'))
.sort((a, b) => b.page_name.localeCompare(a.page_name)),
);
// 真正的 wiki-page paragraphs(排除 infra 跟 meta 配置)
setOtherWikiPages(
blocks
.filter((b) => !hasAnyInfraSubtype(b) && !hasMetaTag(b))
.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)),
);
// 平行撈 index-entry blocksper-entity 摘要,CC navigation entry point
// 對應 design.md §3.5.12.4.1 / 7B.3f
const idxRes = await fetch(
`${KBDB_BASE}/blocks?type=index-entry&limit=200`,
{ headers },
);
if (idxRes.ok) {
const idxData = await idxRes.json();
if (!cancelled) {
const idxBlocks: Block[] = idxData.blocks ?? [];
setIndexEntries(
idxBlocks.sort((a, b) => (a.page_name ?? '').localeCompare(b.page_name ?? '')),
);
}
}
} catch (e: any) {
if (!cancelled) setError(e?.message ?? 'load failed');
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => {
cancelled = true;
};
}, []);
return (
<main className="mira-app">
<div className="mira-content">
<header style={{ padding: '24px 0 16px', borderBottom: '1px solid #2a2a2a' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 4 }}>
<Link
href="/mira"
style={{ color: '#888', fontSize: 14, textDecoration: 'none' }}
>
Mira 首頁
</Link>
</div>
<h1 style={{ fontSize: 28, fontWeight: 700, color: '#fff', margin: 0 }}>
📚 Mira Wiki
</h1>
<p style={{ color: '#888', fontSize: 14, marginTop: 4 }}>
leo 的個人觀點累積(Karpathy LLM Wiki 風格)
</p>
</header>
{loading && <div style={{ padding: 24, color: '#666' }}>載入中⋯</div>}
{error && (
<div style={{ padding: 24, color: '#e66' }}>讀取失敗:{error}</div>
)}
{!loading && !error && (
<>
<Section title="📋 Schema(合成規則)">
{schema ? (
<WikiCardLink page_name={schema.page_name} title="mira-wiki-schema" excerpt="ingest 規則手冊:cypher binding、17 predicates、entity normalize⋯" />
) : (
<Empty>尚未建立 schema</Empty>
)}
</Section>
<Section title="🗂 Index4 個分類)">
{indexChildren.length > 0 ? (
<div style={{ display: 'grid', gap: 8 }}>
{indexChildren.map((b) => {
const tags = b.tags_json ? (JSON.parse(b.tags_json) as string[]) : [];
const key = tags.find((t) => t.startsWith('index-key:'))?.replace('index-key:', '') ?? '?';
return (
<WikiCardLink
key={b.id}
page_name={b.page_name}
title={`${iconForKey(key)} ${key}`}
excerpt={firstLineOf(b.content)}
/>
);
})}
</div>
) : (
<Empty>index children 尚未建立</Empty>
)}
</Section>
<Section title={`🧭 Index Entries${indexEntries.length})— CC 看的 entity 摘要`}>
{indexEntries.length > 0 ? (
<div style={{ display: 'grid', gap: 8 }}>
{indexEntries.map((b) => {
const entity = (b.page_name ?? '').replace(/^index-/, '');
const firstLine = firstLineOf(b.content)
.replace(/^#+\s*/, '')
.slice(0, 80);
return (
<WikiCardLink
key={b.id}
page_name={b.page_name ?? ''}
title={entity}
excerpt={firstLine || `index for ${entity}`}
/>
);
})}
</div>
) : (
<Empty>尚未有 index-entrywiki_synthesis 跑完後自動建)</Empty>
)}
</Section>
<Section title="📜 Log(每月一筆)">
{logEntries.length > 0 ? (
<div style={{ display: 'grid', gap: 8 }}>
{logEntries.map((b) => (
<WikiCardLink
key={b.id}
page_name={b.page_name}
title={b.page_name}
excerpt={firstLineOf(b.content)}
/>
))}
</div>
) : (
<Empty>尚未有 log</Empty>
)}
</Section>
<Section title={`📖 Wiki Pages${otherWikiPages.length}`}>
{otherWikiPages.length > 0 ? (
<div style={{ display: 'grid', gap: 8 }}>
{otherWikiPages.map((p) => (
<WikiCardLink
key={p.id}
page_name={p.page_name}
title={(p.content || '').trim() || p.page_name}
excerpt={
p.created_at
? `建立 ${new Date(p.created_at * 1000).toLocaleString('zh-TW')}`
: p.page_name
}
/>
))}
</div>
) : (
<Empty>尚未有 wiki page(待 7-B ai-canon-wiki workflow 跑出第一張)</Empty>
)}
</Section>
</>
)}
</div>
</main>
);
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section style={{ padding: '20px 0', borderBottom: '1px solid #1f1f1f' }}>
<h2 style={{ fontSize: 16, fontWeight: 600, color: '#ddd', marginBottom: 12 }}>
{title}
</h2>
{children}
</section>
);
}
function Empty({ children }: { children: React.ReactNode }) {
return (
<div style={{ color: '#555', fontStyle: 'italic', fontSize: 13 }}>{children}</div>
);
}
function WikiCardLink({
page_name,
title,
excerpt,
}: {
page_name: string;
title: string;
excerpt: string;
}) {
return (
<Link
href={`/mira/wiki/${encodeURIComponent(page_name)}`}
style={{
display: 'block',
padding: '12px 14px',
background: '#1a1a1a',
border: '1px solid #2a2a2a',
borderRadius: 6,
textDecoration: 'none',
color: 'inherit',
}}
>
<div style={{ color: '#ddd', fontWeight: 500, marginBottom: 4 }}>{title}</div>
{excerpt && (
<div
style={{
color: '#888',
fontSize: 13,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{excerpt}
</div>
)}
</Link>
);
}
function iconForKey(key: string): string {
return (
{
entities: '🧩',
topics: '📂',
sources: '🔗',
stale: '⚠️',
}[key] ?? '•'
);
}
function firstLineOf(content: string): string {
if (!content) return '';
const firstNonHeader = content
.split('\n')
.map((l) => l.trim())
.find((l) => l && !l.startsWith('#') && !l.startsWith('>'));
return firstNonHeader ?? '';
}