欢迎光临
我们一直在努力

Next.js + AI 实战:从零做一个极简智能摘要工具,三天上线 Product Hunt

Next.js + AI 实战:从零做一个极简智能摘要工具,三天上线 Product Hunt

文章总体概览信息图

前言

上个月我做了一个小工具:粘贴一篇长文,AI 帮你生成三句话摘要。

没有复杂的后台管理系统,没有用户体系,没有付费墙。就一个输入框,一个按钮,一段漂亮的输出。

三天开发,第四天上线 Product Hunt。当天拿了 Top 5。

这篇文章完整记录技术实现过程。代码不多,但每一行都是我反复删减后留下来的。

一、产品设计:少即是多

1.1 功能清单

整个产品只有三个功能:

  • 粘贴或输入长文本
  • 点击按钮,AI 生成三句话摘要
  • 一键复制摘要
  • 没了。就这些。

    graph LR
    A["用户输入长文"] –> B["调用 AI API"]
    B –> C["流式输出摘要"]
    C –> D["一键复制"]

    style B fill:#8b5cf6,color:#fff

    1.2 技术选型

    层选择理由
    框架 Next.js 14 (App Router) 前后端一体,Route Handler 直接当 API
    样式 CSS Modules 不引入额外依赖,够用就好
    AI OpenAI gpt-4o-mini 便宜、快、摘要质量够
    部署 Vercel 零配置,推完代码自动上线

    💡 独立开发的原则:能少引一个依赖就少引一个。每多一个 npm install,就多一份未来的维护债。

    二、核心实现

    2.1 项目结构

    summarizer/
    ├── app/
    │ ├── layout.tsx # 全局布局
    │ ├── page.tsx # 首页(唯一页面)
    │ ├── page.module.css # 首页样式
    │ └── api/
    │ └── summarize/
    │ └── route.ts # AI 摘要接口
    ├── components/
    │ ├── TextInput.tsx # 输入框组件
    │ ├── SummaryOutput.tsx # 摘要输出组件
    │ └── CopyButton.tsx # 复制按钮
    ├── lib/
    │ └── openai.ts # OpenAI 封装
    └── package.json

    一共 8 个文件。够了。

    2.2 后端:流式 AI 接口

    // app/api/summarize/route.ts
    import { NextRequest } from 'next/server';

    export async function POST(req: NextRequest) {
    const { text } = await req.json();

    // 输入校验:不废话,直接拦
    if (!text || text.length < 50) {
    return new Response(
    JSON.stringify({ error: '文本太短,至少 50 个字' }),
    { status: 400 }
    );
    }

    if (text.length > 10000) {
    return new Response(
    JSON.stringify({ error: '文本太长,最多 10000 个字' }),
    { status: 400 }
    );
    }

    // 调用 OpenAI 流式接口
    const response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
    },
    body: JSON.stringify({
    model: 'gpt-4o-mini',
    stream: true,
    temperature: 0.3, // 摘要场景用低温度,输出更稳定
    messages: [
    {
    role: 'system',
    content: '你是一个专业的文本摘要助手。请用三句话概括用户提供的文章核心内容。要求:第一句点明主题,第二句总结关键论点,第三句给出结论或启发。语言精炼,不要套话。',
    },
    {
    role: 'user',
    content: `请摘要以下文章:\\n\\n${text}`,
    },
    ],
    }),
    });

    // 直接透传 SSE 流给前端
    return new Response(response.body, {
    headers: {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    },
    });
    }

    ⚠️ 这里直接把 OpenAI 的 SSE 流透传给前端。不做中间缓存,不做二次封装。数据流向越短,延迟越低。

    2.3 前端:流式渲染

    // components/SummaryOutput.tsx
    'use client';

    import { useState } from 'react';
    import styles from './SummaryOutput.module.css';

    interface Props {
    text: string;
    onComplete: (summary: string) => void;
    }

    export default function SummaryOutput({ text, onComplete }: Props) {
    const [summary, setSummary] = useState('');
    const [loading, setLoading] = useState(false);

    const handleSummarize = async () => {
    setLoading(true);
    setSummary('');

    const response = await fetch('/api/summarize', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text }),
    });

    if (!response.ok || !response.body) {
    setSummary('摘要生成失败,请重试');
    setLoading(false);
    return;
    }

    // 流式读取 SSE
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    let fullText = '';

    while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\\n');
    buffer = lines.pop() || ''; // 最后一行可能不完整,留到下次

    for (const line of lines) {
    if (line === 'data: [DONE]') continue;
    if (!line.startsWith('data: ')) continue;

    try {
    const json = JSON.parse(line.slice(6));
    const content = json.choices?.[0]?.delta?.content || '';
    fullText += content;
    setSummary(fullText); // 逐字更新,打字机效果
    } catch {
    // 跳过解析失败的行
    }
    }
    }

    setLoading(false);
    onComplete(fullText);
    };

    return (
    <div className={styles.container}>
    <button
    className={styles.button}
    onClick={handleSummarize}
    disabled={loading || !text}
    >
    {loading ? '正在思考…' : '生成摘要'}
    </button>
    {summary && (
    <div className={styles.output}>
    <p>{summary}</p>
    {!loading && <span className={styles.cursor} />}
    </div>
    )}
    </div>
    );
    }

    2.4 一键复制

    // components/CopyButton.tsx
    'use client';

    import { useState } from 'react';
    import styles from './CopyButton.module.css';

    export default function CopyButton({ text }: { text: string }) {
    const [copied, setCopied] = useState(false);

    const handleCopy = async () => {
    try {
    await navigator.clipboard.writeText(text);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
    } catch {
    // 降级方案:用 textarea 选中复制
    const textarea = document.createElement('textarea');
    textarea.value = text;
    document.body.appendChild(textarea);
    textarea.select();
    document.execCommand('copy');
    document.body.removeChild(textarea);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
    }
    };

    if (!text) return null;

    return (
    <button className={styles.copyBtn} onClick={handleCopy}>
    {copied ? '✅ 已复制' : '📋 复制摘要'}
    </button>
    );
    }

    三、样式:克制的美

    3.1 设计原则

    整个 UI 只有黑、白、一个强调色。不用渐变,不用阴影堆砌。

    /* app/page.module.css */
    .main {
    max-width: 640px;
    margin: 0 auto;
    padding: 4rem 1.5rem;
    font-family: -apple-system, 'Noto Sans SC', sans-serif;
    }

    .title {
    font-size: 1.5rem;
    font-weight: 600;
    color: #1a1a1a;
    margin-bottom: 0.5rem;
    }

    .subtitle {
    font-size: 0.875rem;
    color: #888;
    margin-bottom: 2rem;
    }

    .textarea {
    width: 100%;
    min-height: 200px;
    padding: 1rem;
    border: 1px solid #e5e5e5;
    border-radius: 8px;
    font-size: 0.9375rem;
    line-height: 1.6;
    resize: vertical;
    transition: border-color 0.2s;
    font-family: inherit;
    }

    .textarea:focus {
    outline: none;
    border-color: #8b5cf6;
    }

    /* 输出区域:打字机闪烁光标 */
    .output {
    margin-top: 1.5rem;
    padding: 1.25rem;
    background: #fafafa;
    border-radius: 8px;
    line-height: 1.8;
    color: #333;
    }

    .cursor {
    display: inline-block;
    width: 2px;
    height: 1em;
    background: #8b5cf6;
    animation: blink 0.8s infinite;
    vertical-align: text-bottom;
    margin-left: 2px;
    }

    @keyframes blink {
    0%, 50% { opacity: 1; }
    51%, 100% { opacity: 0; }
    }

    一个紫色。一个闪烁光标。够了。

    四、部署:三条命令上线

    # 第一步:环境变量
    echo "OPENAI_API_KEY=sk-你的密钥" > .env.local

    # 第二步:推到 GitHub
    git add . && git commit -m "feat: 极简摘要工具" && git push

    # 第三步:在 Vercel 导入项目
    # 打开 vercel.com -> Import -> 选择仓库 -> 填入环境变量 -> Deploy
    # 完事。

    Vercel 会自动检测 Next.js 项目,零配置部署。自带 HTTPS、CDN、Edge Runtime。

    五、避坑指南

    5.1 流式透传的陷阱

    ⚠️ Vercel 的 Edge Runtime 和 Node.js Runtime 对流式的支持不一样:

    // 如果用 Edge Runtime(推荐,更快)
    export const runtime = 'edge';

    // 如果用 Node.js Runtime,需要用 ReadableStream 手动包一层
    // Edge Runtime 可以直接透传 response.body

    5.2 成本控制

    GPT-4o-mini 很便宜,但不限制的话还是会被刷。

    // 简单的速率限制(基于 IP)
    const rateLimitMap = new Map<string, number[]>();

    function checkRateLimit(ip: string): boolean {
    const now = Date.now();
    const windowMs = 60 * 1000; // 1 分钟窗口
    const maxRequests = 10; // 每分钟最多 10 次

    const timestamps = rateLimitMap.get(ip) || [];
    const recent = timestamps.filter(t => now – t < windowMs);

    if (recent.length >= maxRequests) {
    return false; // 超限
    }

    recent.push(now);
    rateLimitMap.set(ip, recent);
    return true;
    }

    5.3 SEO 基础

    // app/layout.tsx
    export const metadata = {
    title: '三句话摘要 – AI 智能文本摘要工具',
    description: '粘贴任意长文,AI 帮你用三句话精准概括核心内容。免费、快速、无需注册。',
    };

    六、总结

    三天,8 个文件,一个能用的产品。

    技术栈越简单,迭代速度越快。不要在 Day 1 就引入 Redis、数据库、用户系统。先把核心价值跑通,让用户用起来,再按需加东西。

    好的代码,是删出来的。

    赞(0)
    未经允许不得转载:171主机测评 » Next.js + AI 实战:从零做一个极简智能摘要工具,三天上线 Product Hunt
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址