热门搜索:和平精英 原神 街篮2 

您的位置:首页 > > 教程攻略 > ai教程 >Next.js + AI 实战:从零做一个极简智能摘要工具,三天上线 Product Hunt

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

来源:互联网 更新时间:2026-06-09 07:32

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

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

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

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

一、产品设计:少即是多

1.1 功能清单

整个产品只有三个功能:

  1. 粘贴或输入长文本
  2. 点击按钮,AI 生成三句话摘要
  3. 一键复制摘要

没了。就这些。

graph LRA["用户输入长文"] --> 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.tsimport { 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: `请摘要以下文章:${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 (    
{summary && (

{summary}

{!loading && }
)}
);}

流式读取 SSE 并逐字更新,打造打字机效果,让用户实时看到 AI 在“思考输出”。

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 na vigator.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 (    

手机号码测吉凶
本站所有软件,都由网友上传,如有侵犯你的版权,请发邮件haolingcc@hotmail.com 联系删除。 版权所有 Copyright@2012-2013 haoling.cc