Next.js助你5分钟搭建AI聊天室

ChatAI展示

Next.js是什么

是什么不重要,重要的是它能干什么!

  • 包含前后端(这不是就全栈吗,听起来可怕,不用担心,就算是小白,只要会CV大法就行)
  • 急速开发(会点开发的同学,在敲代码提示到时蛮多的,用习惯了之后开发效率起飞)

为什么要选next.js开发AI

因为在众多 Web 框架中,Next.js 是最接近 AI "原生友好"的。

CV三部曲

1. 安装环境

  • 安装nodejs环境,版本去官网上去安装最新的稳定版本。
  • 安装nextjs脚手架,创建文件夹,npx create-next-app@latest,配置全部选择默认。
  • 去DeepSeek买API或者OpenAI买API额度,下图是DeepSeek,支持下国产。创建一个API keys,记得保存key,因为只能创建的时候复制Key。

2. 创建文件,复制代码

  • app目录下创建api文件夹,在api目录下创建chat目录,在chat目录创建两个文件,key.tsroute.ts
  • key.ts
js 复制代码
// 替换为你在DeepSeek申请的key
export const DEEPSEEK_API_KEY = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
  • route.ts
js 复制代码
import { NextRequest } from "next/server";
import { streamText,convertToModelMessages } from 'ai'
import { createDeepSeek } from "@ai-sdk/deepseek";
import { DEEPSEEK_API_KEY } from "./key"
const deepSeek = createDeepSeek({
    apiKey: DEEPSEEK_API_KEY, //设置API密钥
});
export async function POST(req: NextRequest) {
    const { messages } = await req.json(); //获取请求体
    //这里为什么接受messages 因为我们使用前端的useChat 他会自动注入这个参数,所有可以直接读取
    const result = streamText({
        model: deepSeek('deepseek-chat'), //使用deepseek-chat模型
        messages:convertToModelMessages(messages), //转换为模型消息
        //前端传过来的额messages不符合sdk格式所以需要convertToModelMessages转换一下
        //转换之后的格式:
        system: '新手小白', //系统提示词
    });
   
    return result.toUIMessageStreamResponse() //返回流式响应
}
  • page.tsx
js 复制代码
'use client';
import { useState, useRef, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { useChat } from '@ai-sdk/react';

export default function HomePage() {
    const [input, setInput] = useState(''); //输入框的值
    const messagesEndRef = useRef<HTMLDivElement>(null); //获取消息结束的ref
    //useChat 内部封装了流式响应 默认会向/api/chat 发送请求
    const { messages, sendMessage } = useChat({
        onFinish: () => {
            setInput('');
        }
    });
    // 自动滚动到底部
    useEffect(() => {
        messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
    }, [messages]);
    //回车发送消息
    const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
        if (e.key === 'Enter' && !e.shiftKey) {
            e.preventDefault();
            if (input.trim()) {
                sendMessage({ text: input });
            }
        }
    };

    return (
        <div className='flex flex-col h-screen bg-linear-to-br from-blue-50 via-white to-purple-50'>
            {/* 头部标题 */}
            <div className='bg-white/80 backdrop-blur-sm shadow-sm border-b border-gray-200'>
                <div className='max-w-4xl mx-auto px-6 py-4'>
                    <h1 className='text-2xl font-bold bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent'>
                        AI 智能助手
                    </h1>
                    <p className='text-sm text-gray-500 mt-1'>随时为您解答问题</p>
                </div>
            </div>

            {/* 消息区域 */}
            <div className='flex-1 overflow-y-auto px-4 py-6'>
                <div className='max-w-4xl mx-auto space-y-4'>
                    {messages.length === 0 ? (
                        <div className='flex flex-col items-center justify-center h-full text-center py-20'>
                            <div className='bg-linear-to-br from-blue-100 to-purple-100 rounded-full p-6 mb-4'>
                                <svg className='w-12 h-12 text-blue-600' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
                                    <path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z' />
                                </svg>
                            </div>
                            <h2 className='text-xl font-semibold text-gray-700 mb-2'>开始对话</h2>
                            <p className='text-gray-500'>输入您的问题,我会尽力帮助您</p>
                        </div>
                    ) : (
                        messages.map((message) => (
                            <div
                                key={message.id}
                                className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'} animate-in fade-in slide-in-from-bottom-4 duration-500`}
                            >
                                <div className={`flex gap-3 max-w-[80%] ${message.role === 'user' ? 'flex-row-reverse' : 'flex-row'}`}>
                                    {/* 头像 */}
                                    <div className={`shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-white font-semibold ${
                                        message.role === 'user' 
                                            ? 'bg-linear-to-br from-blue-500 to-blue-600' 
                                            : 'bg-linear-to-br from-purple-500 to-purple-600'
                                    }`}>
                                        {message.role === 'user' ? '你' : 'AI'}
                                    </div>
                                    
                                    {/* 消息内容 */}
                                    <div className={`flex flex-col ${message.role === 'user' ? 'items-end' : 'items-start'}`}>
                                        <div className={`rounded-2xl px-4 py-3 shadow-sm ${
                                            message.role === 'user'
                                                ? 'bg-linear-to-br from-blue-500 to-blue-600 text-white'
                                                : 'bg-white border border-gray-200 text-gray-800'
                                        }`}>
                                            {message.parts.map((part, index) => {
                                                switch (part.type) {
                                                    case 'text':
                                                        return (
                                                            <div key={message.id + index} className='whitespace-pre-wrap wrap-break-word'>
                                                                {part.text}
                                                            </div>
                                                        );
                                                }
                                            })}
                                        </div>
                                    </div>
                                </div>
                            </div>
                        ))
                    )}
                    <div ref={messagesEndRef} />
                </div>
            </div>

            {/* 输入区域 */}
            <div className='bg-white/80 backdrop-blur-sm border-t border-gray-200 shadow-lg'>
                <div className='max-w-4xl mx-auto px-4 py-4'>
                    <div className='flex gap-3 items-end'>
                        <div className='flex-1 relative'>
                            <Textarea
                                value={input}
                                onChange={(e:any) => setInput(e.target.value)}
                                onKeyDown={handleKeyDown}
                                placeholder='请输入你的问题... (按 Enter 发送,Shift + Enter 换行)'
                                className='min-h-[60px] max-h-[200px] resize-none rounded-xl border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all shadow-sm'
                            />
                        </div>
                        <Button
                            onClick={() => {
                                if (input.trim()) {
                                    sendMessage({ text: input });
                                }
                            }}
                            disabled={!input.trim()}
                            className='h-[60px] px-6 rounded-xl bg-linear-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 transition-all shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed'
                        >
                            <svg className='w-5 h-5' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
                                <path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M12 19l9 2-9-18-9 18 9-2zm0 0v-8' />
                            </svg>
                        </Button>
                    </div>
                </div>
            </div>
        </div>
    );
}
  • 安装UI组件库,会生成components.json文件 和 components文件夹
    • npx shadcn@latest
    • npx shadcn@latest add button
    • npx shadcn@latest add textarea

3. 运行项目

在当前项目运行npm run dev,然后用浏览器开打http://localhost:3000/ ,就可以正常测试了。

总结

此Demo可以快速嵌套很多小型项目,譬如公司AI智能聊天软件等,其本质是调用大模型厂商提供的模型,对于要求性价比高且无保密性要求的个人和公司很实用。如果你想做出更酷炫的产品,可以进一步了解nextjs

相关推荐
鹏多多2 小时前
前端组件二次封装实战:Vue+React基于Element UI/AntD的高效封装策略
前端·vue.js·react.js
随风一样自由3 小时前
React中实现iframe嵌套登录页面:跨域与状态同步解决方案探讨
javascript·react.js·ecmascript
黛色正浓3 小时前
【React】ReactRouter记账本案例实现
前端·react.js·前端框架
梓沂3 小时前
playEdu自定义接口需要满足的格式
前端·javascript·react.js
nunumaymax3 小时前
第一章-React入门
前端·react.js
技术钱3 小时前
react可视化标尺@scena/react-ruler使用
前端·react.js·前端框架
土豆125010 小时前
React-Draggable 快速上手指南
react.js
用户479492835691513 小时前
Claude 总是泛泛而谈?试试给它装个"技能包",用 Skills 沉淀团队最佳实践
aigc·ai编程·claude