Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 247 additions & 0 deletions app/[lang]/workshop/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import { CheckCircle2, ExternalLink, Sparkles } from 'lucide-react'
import type { Metadata } from 'next'
import type { ReactNode } from 'react'

type LinkItem = {
label: string
href: string
}

type PrerequisiteItem = {
title: string
description: string
link: LinkItem
}

type StepItem = {
title: string
description: string
link?: LinkItem
extra?: ReactNode
}

const prerequisites: PrerequisiteItem[] = [
{
title: '准备一台电脑',
description:
'TEN Workshop 全程在浏览器中进行,建议使用稳定的网络与最新版 Chrome 或 Edge。',
link: {
label: '查看 Codespaces 环境要求',
href: 'https://docs.github.com/zh/codespaces/overview#system-requirements'
}
},
{
title: '注册 GitHub 账户',
description:
'所有操作都在 GitHub 上完成,如果你还没有账户,请先注册并完成邮箱验证。',
link: {
label: '快速注册 GitHub',
href: 'https://github.com/signup'
}
},
{
title: '申请 ElevenLabs API Key',
description:
'工作坊需要实时语音能力,请提前在 ElevenLabs 官方申请个人 API Key,并妥善保管。',
link: {
label: '申请 ElevenLabs Key',
href: 'https://elevenlabs.io/app/speech-synthesis'
}
}
]

const envSnippet = [
'AGORA_APP_ID=d83b679bc7b3406c83f63864cb74aa99',
'DEEPSEEK_API_KEY=sk-61a43a61c84e45078a1ade4ef5113af0',
'DEEPGRAM_API_KEY=92366fdabbcc9da1013a3ed5ead4e6aa0c31173',
Comment on lines +53 to +56

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Do not publish real API keys in tutorial snippet

The new workshop page embeds concrete values for AGORA_APP_ID, DEEPSEEK_API_KEY and DEEPGRAM_API_KEY directly in the environment setup block. Because the page is rendered to end users, these tokens become public and can be abused or rotated unexpectedly, breaking the tutorial for everyone. These should be placeholders instructing attendees to supply their own credentials, not actual secrets checked into the repo.

Useful? React with 👍 / 👎.

'ELEVENLABS_API_KEY=<请填入你申请的 Key>'
].join('\n')

const workspaceCommands = [
'cd agents/examples',
'task install',
'task run'
].join('\n')

const steps: StepItem[] = [
{
title: 'Fork TEN 代码仓库',
description:
'打开 TEN Framework 的 GitHub 仓库,点击右上角的 Fork,将项目复制到你的个人空间,方便后续使用 Codespaces。',
link: {
label: '打开 TEN Framework 仓库',
href: 'https://github.com/TEN-framework/TEN'
}
},
{
title: '创建 Codespace 在线开发环境',
description:
"在你的 Fork 页面点击 'Code' → 'Create codespace on main',等待几分钟即可在浏览器内打开开发环境。",
link: {
label: '了解 Codespaces 创建流程',
href: 'https://docs.github.com/zh/codespaces/getting-started/quickstart'
}
},
{
title: '配置环境变量',
description:
'在 Codespaces 根目录创建 `.env` 文件,将以下密钥贴入。前三个值已为你准备好,ElevenLabs 需要使用你刚申请的 Key。',
extra: (
<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'>
<code className='whitespace-pre'>{envSnippet}</code>
</pre>
)
},
{
title: '安装依赖并启动示例',
description:
'密钥准备完毕后,进入 `agents/examples` 目录,依次执行 `task install` 与 `task run`,即可在终端看到工作坊示例运行日志。',
extra: (
<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'>
<code className='whitespace-pre'>{workspaceCommands}</code>
</pre>
)
},
{
title: '完结撒花',
description:
'示例运行成功后,你已经完成 TEN Workshop 的所有准备。接下来可以根据导师指引继续扩展、调试或部署自己的智能体。'
}
]

export const metadata: Metadata = {
title: 'TEN Workshop 全流程指南',
description:
'使用 GitHub Codespaces 一站式完成 TEN Framework 工作坊准备,从 Fork 仓库、配置密钥到运行示例。'
}

export default function WorkshopPage() {
return (
<div className='flex flex-col'>
<section className='relative overflow-hidden bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-white'>
<div className='-top-24 -left-24 absolute size-[420px] rounded-full bg-sky-500/10 blur-3xl' />
<div className='-bottom-32 -right-6 absolute size-[520px] rounded-full bg-emerald-400/10 blur-3xl' />

<div className='relative mx-auto w-full max-w-5xl px-6 py-20 sm:py-24'>
<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'>
<Sparkles className='size-4' />
TEN Workshop 指南
</div>
<h1 className='mt-6 font-semibold text-4xl leading-tight sm:text-5xl lg:text-6xl'>
TEN Framework 工作坊
</h1>
<p className='mt-6 max-w-2xl text-lg text-white/80'>
按照下方步骤操作,从 Fork 仓库、配置 Codespaces,到填入所需密钥,
让我们一起快速进入 AI 智能体的实战环节。
</p>
<div className='mt-10 flex flex-wrap gap-4'>
<a
href='https://github.com/TEN-framework/TEN'
target='_blank'
rel='noreferrer'
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'
>
前往 GitHub 仓库
<ExternalLink className='size-4' />
</a>
<a
href='https://discord.gg/tenframework'
target='_blank'
rel='noreferrer'
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'
>
加入 TEN Discord
<ExternalLink className='size-4' />
</a>
</div>
</div>
</section>

<section className='bg-white py-16 dark:bg-slate-950/90 dark:text-white'>
<div className='mx-auto w-full max-w-5xl px-6'>
<div className='max-w-3xl'>
<h2 className='font-semibold text-3xl'>
前置条件:准备好你的工具箱
</h2>
<p className='mt-3 text-slate-600 dark:text-slate-200'>
在正式开工前,请先确认以下准备事项都已完成。每一项都附上了便捷链接,方便你快速跳转查看详情。
</p>
</div>

<div className='mt-10 grid gap-6 md:grid-cols-3'>
{prerequisites.map((item) => (
<div
key={item.title}
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'
>
<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'>
<CheckCircle2 className='size-5' />
</div>
<h3 className='mt-6 font-semibold text-lg text-slate-900 dark:text-white'>
{item.title}
</h3>
<p className='mt-3 flex-1 text-slate-600 text-sm dark:text-slate-300'>
{item.description}
</p>
<a
href={item.link.href}
target='_blank'
rel='noreferrer'
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'
>
{item.link.label}
<ExternalLink className='size-4' />
</a>
</div>
))}
</div>
</div>
</section>

<section className='bg-slate-950 py-16 text-white'>
<div className='mx-auto w-full max-w-5xl px-6'>
<div className='max-w-3xl'>
<h2 className='font-semibold text-3xl'>正式开工:跟着步骤动手做</h2>
<p className='mt-3 text-white/70'>
按顺序完成以下五个步骤,你就能在浏览器内跑起来 TEN Framework
的示例智能体,并在工作坊中与导师保持同频。
</p>
</div>

<ol className='mt-10 space-y-8'>
{steps.map((step, index) => (
<li
key={step.title}
className='rounded-3xl border border-white/10 bg-slate-900/60 p-6 backdrop-blur'
>
<div className='flex flex-col gap-4 sm:flex-row sm:items-start'>
<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'>
{index + 1}
</span>
<div className='flex-1'>
<h3 className='font-semibold text-xl'>{step.title}</h3>
<p className='mt-3 text-sm text-white/80'>
{step.description}
</p>
{step.link ? (
<a
href={step.link.href}
target='_blank'
rel='noreferrer'
className='mt-4 inline-flex items-center gap-2 font-semibold text-emerald-300 text-sm hover:text-emerald-200'
>
{step.link.label}
<ExternalLink className='size-4' />
</a>
) : null}
{step.extra}
</div>
</div>
</li>
))}
</ol>
</div>
</section>
</div>
)
}
131 changes: 131 additions & 0 deletions components/workshop/archive.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
'use client'

import { ArrowUpRight } from 'lucide-react'
import { useMemo, useState } from 'react'

import type { Workshop, WorkshopCategory } from '@/constants'
import { formatWorkshopDateRange } from '@/lib/date'
import { cn } from '@/lib/utils'

interface WorkshopArchiveProps {
workshops: Workshop[]
categories: { id: WorkshopCategory; label: string }[]
}

export function WorkshopArchive({
workshops,
categories,
}: WorkshopArchiveProps) {
const [activeCategory, setActiveCategory] = useState<WorkshopCategory | 'all'>(
'all'
)

const filtered = useMemo(() => {
if (activeCategory === 'all') return workshops
return workshops.filter((workshop) => workshop.category === activeCategory)
}, [activeCategory, workshops])

return (
<section className="border-t border-slate-200 bg-white py-16 dark:border-slate-800 dark:bg-slate-950">
<div className="mx-auto max-w-5xl px-6">
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<h2 className="text-3xl font-semibold tracking-tight text-slate-900 dark:text-white">
Past workshops
</h2>
<p className="mt-2 text-slate-600 dark:text-slate-300">
Catch up on previous labs and deep dives. Replays unlock 48 hours
after we wrap.
</p>
</div>

<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => setActiveCategory('all')}
className={cn(
'rounded-full border px-4 py-2 text-sm font-medium transition',
activeCategory === 'all'
? 'border-emerald-400 bg-emerald-50 text-emerald-700 dark:border-emerald-400/80 dark:bg-emerald-500/10 dark:text-emerald-200'
: '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'
)}
>
All
</button>
{categories.map((category) => (
<button
key={category.id}
type="button"
onClick={() => setActiveCategory(category.id)}
className={cn(
'rounded-full border px-4 py-2 text-sm font-medium transition',
activeCategory === category.id
? 'border-emerald-400 bg-emerald-50 text-emerald-700 dark:border-emerald-400/80 dark:bg-emerald-500/10 dark:text-emerald-200'
: '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'
)}
>
{category.label}
</button>
))}
</div>
</div>

{filtered.length ? (
<div className="grid gap-6 md:grid-cols-2">
{filtered.map((workshop) => {
const { dateLabel, timeLabel } = formatWorkshopDateRange(
workshop.start,
workshop.end,
workshop.timezone
)

return (
<article
key={workshop.slug}
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"
>
<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">
{workshop.status === 'completed'
? 'Completed'
: 'Upcoming'}
</span>
<h3 className="mt-4 text-xl font-semibold text-slate-900 dark:text-white">
{workshop.title}
</h3>
<p className="mt-2 text-sm text-slate-600 dark:text-slate-300">
{workshop.description}
</p>
<div className="mt-4 text-sm text-slate-500 dark:text-slate-400">
<p>{dateLabel}</p>
<p>{timeLabel}</p>
</div>
<div className="mt-auto pt-5">
<a
href={workshop.registrationUrl}
target="_blank"
rel="noreferrer"
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"
>
View details
<ArrowUpRight className="size-4" />
</a>
</div>
</article>
)
})}
</div>
) : (
<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">
<p className="text-lg font-medium text-slate-700 dark:text-slate-200">
No archived workshops yet
</p>
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
This is our inaugural session—check back soon for replays and new
categories.
</p>
</div>
)}
</div>
</section>
)
}
Loading