Skip to content

Commit 18c87fd

Browse files
authored
feat: add workshop page (#166)
* feat: add workshop page * feat: update workshop page
1 parent abe782f commit 18c87fd

File tree

10 files changed

+1253
-0
lines changed

10 files changed

+1253
-0
lines changed

app/[lang]/workshop/page.tsx

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import { CheckCircle2, ExternalLink, Sparkles } from 'lucide-react'
2+
import type { Metadata } from 'next'
3+
import type { ReactNode } from 'react'
4+
5+
type LinkItem = {
6+
label: string
7+
href: string
8+
}
9+
10+
type PrerequisiteItem = {
11+
title: string
12+
description: string
13+
link: LinkItem
14+
}
15+
16+
type StepItem = {
17+
title: string
18+
description: string
19+
link?: LinkItem
20+
extra?: ReactNode
21+
}
22+
23+
const prerequisites: PrerequisiteItem[] = [
24+
{
25+
title: '准备一台电脑',
26+
description:
27+
'TEN Workshop 全程在浏览器中进行,建议使用稳定的网络与最新版 Chrome 或 Edge。',
28+
link: {
29+
label: '查看 Codespaces 环境要求',
30+
href: 'https://docs.github.com/zh/codespaces/overview#system-requirements'
31+
}
32+
},
33+
{
34+
title: '注册 GitHub 账户',
35+
description:
36+
'所有操作都在 GitHub 上完成,如果你还没有账户,请先注册并完成邮箱验证。',
37+
link: {
38+
label: '快速注册 GitHub',
39+
href: 'https://github.com/signup'
40+
}
41+
},
42+
{
43+
title: '申请 ElevenLabs API Key',
44+
description:
45+
'工作坊需要实时语音能力,请提前在 ElevenLabs 官方申请个人 API Key,并妥善保管。',
46+
link: {
47+
label: '申请 ElevenLabs Key',
48+
href: 'https://elevenlabs.io/app/speech-synthesis'
49+
}
50+
}
51+
]
52+
53+
const envSnippet = [
54+
'AGORA_APP_ID=d83b679bc7b3406c83f63864cb74aa99',
55+
'DEEPSEEK_API_KEY=sk-61a43a61c84e45078a1ade4ef5113af0',
56+
'DEEPGRAM_API_KEY=92366fdabbcc9da1013a3ed5ead4e6aa0c31173',
57+
'ELEVENLABS_API_KEY=<请填入你申请的 Key>'
58+
].join('\n')
59+
60+
const workspaceCommands = [
61+
'cd agents/examples',
62+
'task install',
63+
'task run'
64+
].join('\n')
65+
66+
const steps: StepItem[] = [
67+
{
68+
title: 'Fork TEN 代码仓库',
69+
description:
70+
'打开 TEN Framework 的 GitHub 仓库,点击右上角的 Fork,将项目复制到你的个人空间,方便后续使用 Codespaces。',
71+
link: {
72+
label: '打开 TEN Framework 仓库',
73+
href: 'https://github.com/TEN-framework/TEN'
74+
}
75+
},
76+
{
77+
title: '创建 Codespace 在线开发环境',
78+
description:
79+
"在你的 Fork 页面点击 'Code' → 'Create codespace on main',等待几分钟即可在浏览器内打开开发环境。",
80+
link: {
81+
label: '了解 Codespaces 创建流程',
82+
href: 'https://docs.github.com/zh/codespaces/getting-started/quickstart'
83+
}
84+
},
85+
{
86+
title: '配置环境变量',
87+
description:
88+
'在 Codespaces 根目录创建 `.env` 文件,将以下密钥贴入。前三个值已为你准备好,ElevenLabs 需要使用你刚申请的 Key。',
89+
extra: (
90+
<pre className='mt-4 overflow-auto rounded-xl bg-slate-900/60 p-4 text-slate-100 text-sm shadow-inner dark:bg-slate-900'>
91+
<code className='whitespace-pre'>{envSnippet}</code>
92+
</pre>
93+
)
94+
},
95+
{
96+
title: '安装依赖并启动示例',
97+
description:
98+
'密钥准备完毕后,进入 `agents/examples` 目录,依次执行 `task install` 与 `task run`,即可在终端看到工作坊示例运行日志。',
99+
extra: (
100+
<pre className='mt-4 overflow-auto rounded-xl bg-slate-100 p-4 text-slate-900 text-sm shadow-inner dark:bg-slate-800 dark:text-slate-100'>
101+
<code className='whitespace-pre'>{workspaceCommands}</code>
102+
</pre>
103+
)
104+
},
105+
{
106+
title: '完结撒花',
107+
description:
108+
'示例运行成功后,你已经完成 TEN Workshop 的所有准备。接下来可以根据导师指引继续扩展、调试或部署自己的智能体。'
109+
}
110+
]
111+
112+
export const metadata: Metadata = {
113+
title: 'TEN Workshop 全流程指南',
114+
description:
115+
'使用 GitHub Codespaces 一站式完成 TEN Framework 工作坊准备,从 Fork 仓库、配置密钥到运行示例。'
116+
}
117+
118+
export default function WorkshopPage() {
119+
return (
120+
<div className='flex flex-col'>
121+
<section className='relative overflow-hidden bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-white'>
122+
<div className='-top-24 -left-24 absolute size-[420px] rounded-full bg-sky-500/10 blur-3xl' />
123+
<div className='-bottom-32 -right-6 absolute size-[520px] rounded-full bg-emerald-400/10 blur-3xl' />
124+
125+
<div className='relative mx-auto w-full max-w-5xl px-6 py-20 sm:py-24'>
126+
<div className='inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-1 text-sm text-white/80 backdrop-blur'>
127+
<Sparkles className='size-4' />
128+
TEN Workshop 指南
129+
</div>
130+
<h1 className='mt-6 font-semibold text-4xl leading-tight sm:text-5xl lg:text-6xl'>
131+
TEN Framework 工作坊
132+
</h1>
133+
<p className='mt-6 max-w-2xl text-lg text-white/80'>
134+
按照下方步骤操作,从 Fork 仓库、配置 Codespaces,到填入所需密钥,
135+
让我们一起快速进入 AI 智能体的实战环节。
136+
</p>
137+
<div className='mt-10 flex flex-wrap gap-4'>
138+
<a
139+
href='https://github.com/TEN-framework/TEN'
140+
target='_blank'
141+
rel='noreferrer'
142+
className='inline-flex items-center gap-2 rounded-full bg-white px-6 py-3 font-semibold text-slate-900 text-sm shadow-sm transition hover:bg-slate-100'
143+
>
144+
前往 GitHub 仓库
145+
<ExternalLink className='size-4' />
146+
</a>
147+
<a
148+
href='https://discord.gg/tenframework'
149+
target='_blank'
150+
rel='noreferrer'
151+
className='inline-flex items-center gap-2 rounded-full border border-white/40 px-6 py-3 font-semibold text-sm text-white transition hover:border-white hover:bg-white/10'
152+
>
153+
加入 TEN Discord
154+
<ExternalLink className='size-4' />
155+
</a>
156+
</div>
157+
</div>
158+
</section>
159+
160+
<section className='bg-white py-16 dark:bg-slate-950/90 dark:text-white'>
161+
<div className='mx-auto w-full max-w-5xl px-6'>
162+
<div className='max-w-3xl'>
163+
<h2 className='font-semibold text-3xl'>
164+
前置条件:准备好你的工具箱
165+
</h2>
166+
<p className='mt-3 text-slate-600 dark:text-slate-200'>
167+
在正式开工前,请先确认以下准备事项都已完成。每一项都附上了便捷链接,方便你快速跳转查看详情。
168+
</p>
169+
</div>
170+
171+
<div className='mt-10 grid gap-6 md:grid-cols-3'>
172+
{prerequisites.map((item) => (
173+
<div
174+
key={item.title}
175+
className='hover:-translate-y-1 flex h-full flex-col rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition hover:shadow-lg dark:border-slate-800 dark:bg-slate-900'
176+
>
177+
<div className='inline-flex size-10 items-center justify-center rounded-full bg-emerald-100 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-200'>
178+
<CheckCircle2 className='size-5' />
179+
</div>
180+
<h3 className='mt-6 font-semibold text-lg text-slate-900 dark:text-white'>
181+
{item.title}
182+
</h3>
183+
<p className='mt-3 flex-1 text-slate-600 text-sm dark:text-slate-300'>
184+
{item.description}
185+
</p>
186+
<a
187+
href={item.link.href}
188+
target='_blank'
189+
rel='noreferrer'
190+
className='mt-4 inline-flex items-center gap-2 font-semibold text-emerald-600 text-sm hover:text-emerald-500 dark:text-emerald-300 dark:hover:text-emerald-200'
191+
>
192+
{item.link.label}
193+
<ExternalLink className='size-4' />
194+
</a>
195+
</div>
196+
))}
197+
</div>
198+
</div>
199+
</section>
200+
201+
<section className='bg-slate-950 py-16 text-white'>
202+
<div className='mx-auto w-full max-w-5xl px-6'>
203+
<div className='max-w-3xl'>
204+
<h2 className='font-semibold text-3xl'>正式开工:跟着步骤动手做</h2>
205+
<p className='mt-3 text-white/70'>
206+
按顺序完成以下五个步骤,你就能在浏览器内跑起来 TEN Framework
207+
的示例智能体,并在工作坊中与导师保持同频。
208+
</p>
209+
</div>
210+
211+
<ol className='mt-10 space-y-8'>
212+
{steps.map((step, index) => (
213+
<li
214+
key={step.title}
215+
className='rounded-3xl border border-white/10 bg-slate-900/60 p-6 backdrop-blur'
216+
>
217+
<div className='flex flex-col gap-4 sm:flex-row sm:items-start'>
218+
<span className='inline-flex size-10 items-center justify-center rounded-full bg-emerald-500/20 font-semibold text-emerald-200 text-lg sm:mt-1'>
219+
{index + 1}
220+
</span>
221+
<div className='flex-1'>
222+
<h3 className='font-semibold text-xl'>{step.title}</h3>
223+
<p className='mt-3 text-sm text-white/80'>
224+
{step.description}
225+
</p>
226+
{step.link ? (
227+
<a
228+
href={step.link.href}
229+
target='_blank'
230+
rel='noreferrer'
231+
className='mt-4 inline-flex items-center gap-2 font-semibold text-emerald-300 text-sm hover:text-emerald-200'
232+
>
233+
{step.link.label}
234+
<ExternalLink className='size-4' />
235+
</a>
236+
) : null}
237+
{step.extra}
238+
</div>
239+
</div>
240+
</li>
241+
))}
242+
</ol>
243+
</div>
244+
</section>
245+
</div>
246+
)
247+
}

components/workshop/archive.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
'use client'
2+
3+
import { ArrowUpRight } from 'lucide-react'
4+
import { useMemo, useState } from 'react'
5+
6+
import type { Workshop, WorkshopCategory } from '@/constants'
7+
import { formatWorkshopDateRange } from '@/lib/date'
8+
import { cn } from '@/lib/utils'
9+
10+
interface WorkshopArchiveProps {
11+
workshops: Workshop[]
12+
categories: { id: WorkshopCategory; label: string }[]
13+
}
14+
15+
export function WorkshopArchive({
16+
workshops,
17+
categories,
18+
}: WorkshopArchiveProps) {
19+
const [activeCategory, setActiveCategory] = useState<WorkshopCategory | 'all'>(
20+
'all'
21+
)
22+
23+
const filtered = useMemo(() => {
24+
if (activeCategory === 'all') return workshops
25+
return workshops.filter((workshop) => workshop.category === activeCategory)
26+
}, [activeCategory, workshops])
27+
28+
return (
29+
<section className="border-t border-slate-200 bg-white py-16 dark:border-slate-800 dark:bg-slate-950">
30+
<div className="mx-auto max-w-5xl px-6">
31+
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
32+
<div>
33+
<h2 className="text-3xl font-semibold tracking-tight text-slate-900 dark:text-white">
34+
Past workshops
35+
</h2>
36+
<p className="mt-2 text-slate-600 dark:text-slate-300">
37+
Catch up on previous labs and deep dives. Replays unlock 48 hours
38+
after we wrap.
39+
</p>
40+
</div>
41+
42+
<div className="flex flex-wrap gap-2">
43+
<button
44+
type="button"
45+
onClick={() => setActiveCategory('all')}
46+
className={cn(
47+
'rounded-full border px-4 py-2 text-sm font-medium transition',
48+
activeCategory === 'all'
49+
? 'border-emerald-400 bg-emerald-50 text-emerald-700 dark:border-emerald-400/80 dark:bg-emerald-500/10 dark:text-emerald-200'
50+
: 'border-slate-200 bg-white text-slate-600 hover:border-emerald-200 hover:text-emerald-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-emerald-400/40 dark:hover:text-emerald-200'
51+
)}
52+
>
53+
All
54+
</button>
55+
{categories.map((category) => (
56+
<button
57+
key={category.id}
58+
type="button"
59+
onClick={() => setActiveCategory(category.id)}
60+
className={cn(
61+
'rounded-full border px-4 py-2 text-sm font-medium transition',
62+
activeCategory === category.id
63+
? 'border-emerald-400 bg-emerald-50 text-emerald-700 dark:border-emerald-400/80 dark:bg-emerald-500/10 dark:text-emerald-200'
64+
: 'border-slate-200 bg-white text-slate-600 hover:border-emerald-200 hover:text-emerald-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-emerald-400/40 dark:hover:text-emerald-200'
65+
)}
66+
>
67+
{category.label}
68+
</button>
69+
))}
70+
</div>
71+
</div>
72+
73+
{filtered.length ? (
74+
<div className="grid gap-6 md:grid-cols-2">
75+
{filtered.map((workshop) => {
76+
const { dateLabel, timeLabel } = formatWorkshopDateRange(
77+
workshop.start,
78+
workshop.end,
79+
workshop.timezone
80+
)
81+
82+
return (
83+
<article
84+
key={workshop.slug}
85+
className="flex h-full flex-col rounded-3xl border border-slate-200 bg-white p-6 shadow-sm transition hover:border-emerald-200 hover:shadow-md dark:border-slate-800 dark:bg-slate-900 dark:hover:border-emerald-400/40"
86+
>
87+
<span className="inline-flex w-fit rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-slate-600 dark:bg-slate-800 dark:text-slate-300">
88+
{workshop.status === 'completed'
89+
? 'Completed'
90+
: 'Upcoming'}
91+
</span>
92+
<h3 className="mt-4 text-xl font-semibold text-slate-900 dark:text-white">
93+
{workshop.title}
94+
</h3>
95+
<p className="mt-2 text-sm text-slate-600 dark:text-slate-300">
96+
{workshop.description}
97+
</p>
98+
<div className="mt-4 text-sm text-slate-500 dark:text-slate-400">
99+
<p>{dateLabel}</p>
100+
<p>{timeLabel}</p>
101+
</div>
102+
<div className="mt-auto pt-5">
103+
<a
104+
href={workshop.registrationUrl}
105+
target="_blank"
106+
rel="noreferrer"
107+
className="inline-flex items-center gap-2 text-sm font-semibold text-emerald-600 transition hover:text-emerald-500 dark:text-emerald-300 dark:hover:text-emerald-200"
108+
>
109+
View details
110+
<ArrowUpRight className="size-4" />
111+
</a>
112+
</div>
113+
</article>
114+
)
115+
})}
116+
</div>
117+
) : (
118+
<div className="rounded-3xl border border-dashed border-slate-300 bg-slate-50 p-10 text-center dark:border-slate-700 dark:bg-slate-900/40">
119+
<p className="text-lg font-medium text-slate-700 dark:text-slate-200">
120+
No archived workshops yet
121+
</p>
122+
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
123+
This is our inaugural session—check back soon for replays and new
124+
categories.
125+
</p>
126+
</div>
127+
)}
128+
</div>
129+
</section>
130+
)
131+
}

0 commit comments

Comments
 (0)