diff --git a/apps/waterfall-wiki/src/pages/index.tsx b/apps/waterfall-wiki/src/pages/index.tsx index 200d64f..b3f7981 100644 --- a/apps/waterfall-wiki/src/pages/index.tsx +++ b/apps/waterfall-wiki/src/pages/index.tsx @@ -1,7 +1,75 @@ import Head from 'next/head'; import Link from 'next/link'; -import {useEffect,useState} from 'react'; +import {useRouter} from 'next/router'; +import {useEffect,useState,type FormEvent} from 'react'; import GuidanceShell from '@/components/GuidanceShell'; import {guides} from '@/content/guides/catalog'; import s from '@/styles/guidance.module.css'; -export default function Home(){const [published,setPublished]=useState<{id:string;revision:number;title:string;summary:string;kind:string}[]>([]),[loadError,setLoadError]=useState('');useEffect(()=>{fetch('/api/documents').then(async r=>{const b=await r.json();if(!r.ok)throw Error(b.error);setPublished(b.documents);}).catch(e=>setLoadError(e.message));},[]);const[q,setQ]=useState(''),[kind,setKind]=useState('all');const items=guides.filter(g=>(kind==='all'||g.kind===kind)&&`${g.title} ${g.summary} ${g.tags.join(' ')}`.toLowerCase().includes(q.toLowerCase()));return 织页 · 为 Agent 找到下一页
A WEB OF USEFUL KNOWLEDGE

一个起点,
连接下一种可能。

每一篇文档,都能独立帮助你的 Agent。
从当前问题出发,沿着有用的链接继续探索。

{['代码审查','规划','验证'].map(t=>)}

用户发布

发布一篇文档 ↗
{loadError&&

{loadError}

}
{published.filter(g=>(kind==='all'||g.kind===kind)&&`${g.title} ${g.summary}`.toLowerCase().includes(q.toLowerCase())).map(g=>
↗
{g.id} · {g.kind} · v{g.revision}

{g.title}

{g.summary}

)}

从这里开始 {items.length} 篇

协议样例 · v0.1
{items.map(g=>
{g.kind==='guide'?'↗':'≋'}
织页 / protocol-examples · r{g.revision}

{g.title}

{g.summary}

{g.tags.map(t=>{t})}↗ {g.links.length} 个关联
)}{!items.length&&

没有匹配文档,请尝试其他关键词或分类。

}
} + +type PublishedDocument={id:string;revision:number;title:string;summary:string;kind:string}; +const categories=[['all','全部文档'],['guide','操作指导'],['context','背景知识'],['evidence','事实依据']]; +export default function Home(){ + const router=useRouter(); + const [published,setPublished]=useState([]); + const [loadError,setLoadError]=useState(''); + const [loading,setLoading]=useState(true); + const [q,setQ]=useState(''); + const [kind,setKind]=useState('all'); + useEffect(()=>{ + let active=true; + fetch('/api/documents').then(async r=>{ + const b=await r.json();if(!r.ok)throw Error(b.error); + if(active)setPublished(b.documents); + }).catch(e=>{if(active)setLoadError(e.message);}).finally(()=>{if(active)setLoading(false);}); + return()=>{active=false;}; + },[]); + useEffect(()=>{ + if(!router.isReady)return; + setQ(typeof router.query.q==='string'?router.query.q:''); + setKind(typeof router.query.kind==='string'&&categories.some(([key])=>key===router.query.kind)?router.query.kind:'all'); + },[router.isReady,router.query.q,router.query.kind]); + function rememberFilters(query:string,category:string){ + const next:{q?:string;kind?:string}={}; + if(query)next.q=query;if(category!=='all')next.kind=category; + void router.push({pathname:'/',query:next},undefined,{shallow:true,scroll:false}); + } + function search(e:FormEvent){e.preventDefault();rememberFilters(q,kind);} + function chooseKind(value:string){setKind(value);rememberFilters(q,value);} + function clearFilters(){setQ('');setKind('all');rememberFilters('','all');} + const matches=(g:{title:string;summary:string;kind:string;tags?:string[]})=>(kind==='all'||g.kind===kind)&&`${g.title} ${g.summary} ${g.tags?.join(' ')||''}`.toLowerCase().includes(q.trim().toLowerCase()); + const items=guides.filter(matches),userItems=published.filter(matches); + const total=items.length+userItems.length; + return + 织页 · 为 Agent 找到下一页 +
+ A WEB OF USEFUL KNOWLEDGE +

一个起点,
连接下一种可能。

+

每一篇文档,都能独立帮助你的 Agent。
从当前问题出发,沿着有用的链接继续探索。

+
+ setQ(e.target.value)}/> +
+
{['代码审查','规划','验证'].map(t=>)}
+
+
+ +
+

{loading?'正在加载用户文档…':loadError?`协议样例 ${items.length} 篇;用户文档加载失败`:`找到 ${total} 篇文档`}

{(q||kind!=='all')&&}
+ {loadError&&

用户文档暂未加载:{loadError}。下方仍可阅读协议样例。

} + {!loading&&!loadError&&total===0&&

没有匹配文档,请尝试其他关键词或清除筛选。

} +

用户发布 {userItems.length} 篇

发布一篇文档 ↗
+
{userItems.map(g=>
↗
{g.id} · {g.kind} · v{g.revision}

{g.title}

{g.summary}

)}
+ {!loading&&!loadError&&!published.length&&

还没有用户文档。你可以发布第一篇,或先阅读下方样例。

} +

从这里开始 {items.length} 篇

协议样例 · v0.1
+
{items.map(g=>
{g.kind==='guide'?'↗':'≋'}
织页 / protocol-examples · r{g.revision}

{g.title}

{g.summary}

{g.tags.map(t=>{t})}↗ {g.links.length} 个关联
)}
+
+ +
+
; +} diff --git a/apps/waterfall-wiki/src/styles/guidance.module.css b/apps/waterfall-wiki/src/styles/guidance.module.css index 29bd39e..684cafd 100644 --- a/apps/waterfall-wiki/src/styles/guidance.module.css +++ b/apps/waterfall-wiki/src/styles/guidance.module.css @@ -1 +1,2 @@ .site{background:#fff;color:#233026;min-height:100vh;font:14px Arial,"PingFang SC",sans-serif}.site a{color:inherit;text-decoration:none}.site button,.site input{font:inherit}.header{height:80px;display:flex;align-items:center;gap:60px;border-bottom:1px solid #e7ebe5;padding:0 5%}.brand{font-size:28px;font-weight:700}.brand small{font-size:10px;border:1px solid #d5e3d1;padding:3px 5px;border-radius:4px;color:#71856c}.header nav{display:flex;gap:30px}.account{margin-left:auto;border:1px solid #dce2d8;border-radius:6px;padding:10px 14px}.hero{padding:65px 6% 45px;background:radial-gradient(ellipse at 90% 10%,#eaf3e4,transparent 55%),#f8faf6;border-bottom:1px solid #e2e8dd}.eyebrow{font-size:10px;letter-spacing:2px;font-weight:700;color:#748b70}.hero h1{font-size:52px;letter-spacing:-2px;line-height:1.25;font-weight:600;margin:22px 0}.hero p{font-size:16px;color:#788675;line-height:1.9}.search{display:flex;align-items:center;gap:14px;background:#fff;border:1px solid #d6e0cf;border-radius:8px;max-width:620px;padding:15px 18px;margin-top:26px;box-shadow:0 5px 16px #35442b09}.search input{border:0;outline:0;flex:1;min-width:0;background:transparent}.search:focus-within{outline:2px solid #7b9b71}.search kbd{font-size:10px;color:#929e89;border:1px solid #e0e6da;padding:3px}.topics{display:flex;gap:12px;margin-top:15px}.topics button{border:0;background:transparent;color:#7b8b73;font-size:12px;cursor:pointer}.layout{display:grid;grid-template-columns:170px minmax(0,1fr) 250px;gap:35px;padding:40px 5% 80px;max-width:1500px;margin:auto}.sidebar h2{font-size:12px;color:#85917f;margin-bottom:18px}.sidebar button{display:flex;justify-content:space-between;width:100%;padding:12px;border:0;border-radius:5px;background:transparent;color:#74816e;cursor:pointer;margin-bottom:5px}.sidebar button[aria-pressed=true]{background:#edf3e8;color:#44673c;font-weight:600}.sidebar button span{font-size:11px}.sidebar p{font-size:12px;line-height:1.9;color:#919d88;border-top:1px solid #e7ebe2;padding-top:25px;margin-top:30px}.sectionTitle{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.sectionTitle h2{font-size:18px;margin:0}.sectionTitle small{font-size:11px;color:#91a086;font-weight:400;margin-left:10px}.sectionTitle>span{font-size:10px;color:#949e8d}.list{border-top:1px solid #e4eadd}.row{display:flex;gap:16px;padding:25px 0;border-bottom:1px solid #e4eadd}.icon{display:grid;place-items:center;flex-shrink:0;width:36px;height:42px;background:#f0f4eb;border:1px solid #e0e7d7;border-radius:5px;color:#7c9671;font-size:24px}.meta{font-size:10px;color:#949e8e;line-height:1.9}.row h3{font-size:17px;margin:8px 0;font-weight:600}.row p{font-size:12px;line-height:1.8;color:#84917b;margin:8px 0 13px}.tags{display:flex;gap:7px;flex-wrap:wrap}.tags span{font-size:10px;border:1px solid #e1e8d8;border-radius:4px;padding:3px 7px;color:#86977b}.manifest{padding:25px;background:#f0f4e7;border:1px solid #e1e8d5;border-radius:8px}.manifest h2{font-size:29px;line-height:1.5;font-weight:500;margin:20px 0}.manifest p{font-size:12px;line-height:1.9;color:#8c9c7c}.manifest a{display:block;border-top:1px solid #dce4d0;padding-top:16px;margin-top:25px;font-size:12px}.note{padding:24px 0;font-size:12px;line-height:1.9;color:#87957c}.note h3{font-size:13px;color:#53684a;margin:20px 0 8px}.footer{display:flex;justify-content:space-between;gap:20px;border-top:1px solid #e5ebdf;padding:25px 5%;font-size:11px;color:#95a08c}.detail{max-width:1120px;margin:auto;padding:40px 5% 80px}.detail>h1{font-size:38px;font-weight:600}.detail>.eyebrow{display:block;margin-top:30px}.detail>p{color:#84967a}.tabs{display:flex;gap:24px;align-items:center;border-bottom:1px solid #dfe6d8;margin-top:32px}.tabs button{border:0;border-bottom:2px solid transparent;padding:15px 0;background:transparent;cursor:pointer;color:#84967a}.tabs button[aria-pressed=true]{border-color:#79976b;color:#456b3a}.tabs a{margin-left:auto;font-size:12px}.detailGrid{display:grid;grid-template-columns:minmax(0,1fr) 230px;gap:50px}.prose{font-size:15px;line-height:2;color:#617656}.prose h2{font-size:20px;color:#344a2c;margin:30px 0 12px}.prose li{padding:5px 0}.prose pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px;background:#f4f7ef;border:1px solid #e2e9d7;border-radius:6px;padding:22px;line-height:1.9;color:#6c865f}.related{display:block;border:1px solid #dfe8d4;border-radius:6px;padding:15px 18px;margin:12px 0}.related strong{font-size:14px}.related span{display:block;font-size:11px;color:#96a48b}.site a:hover{color:#71975c}.site button:focus-visible,.site a:focus-visible{outline:2px solid #7c9b6c;outline-offset:4px}@media(max-width:1050px){.layout{grid-template-columns:140px minmax(0,1fr);gap:25px}.right{display:none}.hero h1{font-size:44px}.header{gap:30px}}@media(max-width:650px){.header{height:70px;padding:0 20px;gap:18px}.header nav{gap:12px;font-size:12px}.brand{font-size:22px}.brand small{display:none}.account{font-size:11px;padding:8px}.hero{padding:42px 23px 32px}.hero h1{font-size:38px}.hero p{font-size:14px}.layout{display:block;padding:25px 22px 50px}.sidebar{display:flex;gap:4px;overflow:auto;margin-bottom:25px}.sidebar h2,.sidebar p{display:none}.sidebar button{white-space:nowrap;width:auto;gap:10px;font-size:12px}.sectionTitle>span{display:none}.footer{flex-direction:column}.detail{padding:30px 22px}.detail>h1{font-size:30px}.detailGrid{display:block}.tabs{gap:16px}.tabs a{font-size:10px}.prose pre{padding:15px}.icon{display:none}} +.search button,.sectionTitle>button{border:1px solid #d5e1cc;border-radius:5px;background:#f0f5eb;color:#456b3a;padding:6px 10px;cursor:pointer;white-space:nowrap}.sectionTitle>p{font-size:12px;color:#687d5d}.sectionTitle{gap:12px}.list+.sectionTitle{margin-top:30px} diff --git a/apps/waterfall-wiki/tests/guidance-integration.ts b/apps/waterfall-wiki/tests/guidance-integration.ts index b7bf9e6..66b5091 100644 --- a/apps/waterfall-wiki/tests/guidance-integration.ts +++ b/apps/waterfall-wiki/tests/guidance-integration.ts @@ -36,6 +36,22 @@ export async function verifyGuidancePublishing(base:string){ const link=`a[href="/documents/${httpDoc.id}?revision=1"]`; await page.goto(base+'/',{waitUntil:'domcontentloaded'}); await page.locator(link).waitFor({state:'visible'});assert.equal(await page.locator(link).textContent(),httpDoc.title); + // Published matches must not inherit the examples' empty state; submitted filters survive reload. + const search=page.getByRole('textbox',{name:'搜索文档'}); + await search.fill(httpDoc.title);await search.press('Enter'); + await page.waitForURL(url=>url.searchParams.get('q')===httpDoc.title); + await page.getByRole('status').filter({hasText:'找到 2 篇文档'}).waitFor(); + assert.equal(await page.getByText('没有匹配文档',{exact:false}).count(),0); + await page.reload({waitUntil:'domcontentloaded'}); + await page.getByRole('status').filter({hasText:'找到 2 篇文档'}).waitFor(); + assert.equal(await search.inputValue(),httpDoc.title); + await page.getByRole('button',{name:'事实依据'}).click(); + await page.waitForURL(url=>url.searchParams.get('kind')==='evidence'); + await page.getByText('没有匹配文档,请尝试其他关键词或清除筛选。',{exact:true}).waitFor(); + await page.getByRole('button',{name:'清除所有筛选'}).click(); + await page.waitForURL(url=>url.search===''); + await page.locator(link).waitFor({state:'visible'}); + assert.equal(await search.inputValue(),''); await page.locator('header a[href="/documents"]').click();await page.locator(link).waitFor({state:'visible'}); await page.locator(link).click();await page.locator('h1').waitFor();assert.equal(await page.locator('h1').textContent(),httpDoc.title); await page.goto(base+'/write',{waitUntil:'domcontentloaded'});assert.equal(await page.locator('h1').textContent(),'发布独立文档');