Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68155fc0f7 |
@@ -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 <GuidanceShell><Head><title>织页 · 为 Agent 找到下一页</title></Head><section className={s.hero}><span className={s.eyebrow}>A WEB OF USEFUL KNOWLEDGE</span><h1>一个起点,<br/>连接下一种可能。</h1><p>每一篇文档,都能独立帮助你的 Agent。<br/>从当前问题出发,沿着有用的链接继续探索。</p><label className={s.search}>⌕ <input aria-label="搜索文档" placeholder="搜索文档、任务或一种想法…" value={q} onChange={e=>setQ(e.target.value)}/><kbd>探索</kbd></label><div className={s.topics}>{['代码审查','规划','验证'].map(t=><button key={t} onClick={()=>setQ(t)}>{t} ↗</button>)}</div></section><main className={s.layout}><aside className={s.sidebar}><h2>文档目录</h2>{[['all','全部文档'],['guide','操作指导'],['context','背景知识'],['evidence','事实依据']].map(([key,label])=><button key={key} aria-pressed={kind===key} onClick={()=>setKind(key)}>{label}<span>{guides.filter(g=>key==='all'||g.kind===key).length}</span></button>)}<p>一个文档就是一个入口。<br/>集合是组织方式,<br/>无需整套安装。</p></aside><section><div className={s.sectionTitle}><h2>用户发布</h2><Link href="/write">发布一篇文档 ↗</Link></div>{loadError&&<p>{loadError}</p>}<div className={s.list}>{published.filter(g=>(kind==='all'||g.kind===kind)&&`${g.title} ${g.summary}`.toLowerCase().includes(q.toLowerCase())).map(g=><article className={s.row} key={g.id}><div className={s.icon}>↗</div><div><div className={s.meta}>{g.id} · {g.kind} · v{g.revision}</div><h3><Link href={`/documents/${g.id}?revision=${g.revision}`}>{g.title}</Link></h3><p>{g.summary}</p></div></article>)}</div><div className={s.sectionTitle}><h2>从这里开始 <small>{items.length} 篇</small></h2><span>协议样例 · v0.1</span></div><div className={s.list}>{items.map(g=><article className={s.row} key={g.id}><div className={s.icon}>{g.kind==='guide'?'↗':'≋'}</div><div><div className={s.meta}>织页 / protocol-examples · r{g.revision}</div><h3><Link href={`/guide/${g.id}`}>{g.title}</Link></h3><p>{g.summary}</p><div className={s.tags}>{g.tags.map(t=><span key={t}>{t}</span>)}<span>↗ {g.links.length} 个关联</span></div></div></article>)}{!items.length&&<p>没有匹配文档,请尝试其他关键词或分类。</p>}</div></section><aside className={s.right}><div className={s.manifest}><span className={s.eyebrow}>DOCUMENTS, CONNECTED.</span><h2>知识不必<br/>打包带走。</h2><p>读一页,解决眼前的问题。<br/>需要更多时,再打开下一页。</p><Link href="/protocol">了解文档协议 ↗</Link></div><div className={s.note}><h3>为两种 Agent 而写</h3><p>Coding agent 的工作方法,Personal agent 的日常指导,都可以从一篇文档开始。</p><h3>这是一个早期原型</h3><p>已验证邮箱用户可以发布独立文档,更新会创建不可变版本。下方协议样例与真实用户文档分别展示。</p></div></aside></main></GuidanceShell>}
|
||||
|
||||
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<PublishedDocument[]>([]);
|
||||
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 <GuidanceShell>
|
||||
<Head><title>织页 · 为 Agent 找到下一页</title></Head>
|
||||
<section className={s.hero}>
|
||||
<span className={s.eyebrow}>A WEB OF USEFUL KNOWLEDGE</span>
|
||||
<h1>一个起点,<br/>连接下一种可能。</h1>
|
||||
<p>每一篇文档,都能独立帮助你的 Agent。<br/>从当前问题出发,沿着有用的链接继续探索。</p>
|
||||
<form className={s.search} role="search" onSubmit={search}>
|
||||
<span aria-hidden="true">⌕</span><input aria-label="搜索文档" placeholder="搜索文档、任务或一种想法…" value={q} onChange={e=>setQ(e.target.value)}/><button type="submit">搜索</button>
|
||||
</form>
|
||||
<div className={s.topics}>{['代码审查','规划','验证'].map(t=><button key={t} onClick={()=>{setQ(t);rememberFilters(t,kind);}}>{t} ↗</button>)}</div>
|
||||
</section>
|
||||
<main className={s.layout}>
|
||||
<aside className={s.sidebar}>
|
||||
<h2>文档目录</h2>
|
||||
{categories.map(([key,label])=><button key={key} aria-pressed={kind===key} onClick={()=>chooseKind(key)}>{label}<span>{[...guides,...published].filter(g=>key==='all'||g.kind===key).length}</span></button>)}
|
||||
<p>一个文档就是一个入口。<br/>集合是组织方式,<br/>无需整套安装。</p>
|
||||
</aside>
|
||||
<section aria-label="文档搜索结果">
|
||||
<div className={s.sectionTitle}><p role="status">{loading?'正在加载用户文档…':loadError?`协议样例 ${items.length} 篇;用户文档加载失败`:`找到 ${total} 篇文档`}</p>{(q||kind!=='all')&&<button type="button" onClick={clearFilters}>清除所有筛选</button>}</div>
|
||||
{loadError&&<p role="alert">用户文档暂未加载:{loadError}。下方仍可阅读协议样例。</p>}
|
||||
{!loading&&!loadError&&total===0&&<p>没有匹配文档,请尝试其他关键词或清除筛选。</p>}
|
||||
<div className={s.sectionTitle}><h2>用户发布 <small>{userItems.length} 篇</small></h2><Link href="/write">发布一篇文档 ↗</Link></div>
|
||||
<div className={s.list}>{userItems.map(g=><article className={s.row} key={g.id}><div className={s.icon}>↗</div><div><div className={s.meta}>{g.id} · {g.kind} · v{g.revision}</div><h3><Link href={`/documents/${g.id}?revision=${g.revision}`}>{g.title}</Link></h3><p>{g.summary}</p></div></article>)}</div>
|
||||
{!loading&&!loadError&&!published.length&&<p>还没有用户文档。你可以发布第一篇,或先阅读下方样例。</p>}
|
||||
<div className={s.sectionTitle}><h2>从这里开始 <small>{items.length} 篇</small></h2><span>协议样例 · v0.1</span></div>
|
||||
<div className={s.list}>{items.map(g=><article className={s.row} key={g.id}><div className={s.icon}>{g.kind==='guide'?'↗':'≋'}</div><div><div className={s.meta}>织页 / protocol-examples · r{g.revision}</div><h3><Link href={`/guide/${g.id}`}>{g.title}</Link></h3><p>{g.summary}</p><div className={s.tags}>{g.tags.map(t=><span key={t}>{t}</span>)}<span>↗ {g.links.length} 个关联</span></div></div></article>)}</div>
|
||||
</section>
|
||||
<aside className={s.right}>
|
||||
<div className={s.manifest}><span className={s.eyebrow}>DOCUMENTS, CONNECTED.</span><h2>知识不必<br/>打包带走。</h2><p>读一页,解决眼前的问题。<br/>需要更多时,再打开下一页。</p><Link href="/protocol">了解文档协议 ↗</Link></div>
|
||||
<div className={s.note}><h3>为两种 Agent 而写</h3><p>Coding agent 的工作方法,Personal agent 的日常指导,都可以从一篇文档开始。</p><h3>这是一个早期原型</h3><p>已验证邮箱用户可以发布独立文档,更新会创建不可变版本。下方协议样例与真实用户文档分别展示。</p></div>
|
||||
</aside>
|
||||
</main>
|
||||
</GuidanceShell>;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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(),'发布独立文档');
|
||||
|
||||
Reference in New Issue
Block a user