AI
Vercel提供了AI SDK,可以让我们在Next.js中轻松集成AI功能。AI SDK 官网
安装AI-SDK
sh
npm i ai @ai-sdk/deepseek @ai-sdk/react
这儿我们使用deepseek作为AI模型,@ai-sdk/react封装了流式输出和上下文管理hook,可以让我们在Next.js中轻松集成AI功能。如果你要安装其他模型,只需要将deepseek替换为其他模型即可。
例如:安装openai模型
sh
npm i ai @ai-sdk/openai @ai-sdk/react
为什么使用
deepseek模型?因为deepseek比较便宜,充10元可以测试很久(非广告)。
获取deepSeek API Key


然后把生成的API Key复制一下保存起来。
编写API接口
src/app/api/chat/route.ts
ts
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转换一下
//转换之后的格式:
//[
//{ role: 'user', content: [ [Object] ] },
//{ role: 'assistant', content: [ [Object] ] },
//{ role: 'user', content: [ [Object] ] },
//{ role: 'assistant', content: [ [Object] ] },
//{ role: 'user', content: [ [Object] ] },
//{ role: 'assistant', content: [ [Object] ] },
//{ role: 'user', content: [ [Object] ] }
//]
system: '你是一个高级程序员,请根据用户的问题给出回答', //系统提示词
});
return result.toUIMessageStreamResponse() //返回流式响应
}
src/app/page.tsx
我们在前端使用useChat组件来实现AI对话,这个组件内部封装了流式响应,默认会向/api/chat发送请求。
messages: 消息列表,包含用户和AI的对话内容sendMessage: 发送消息的函数,参数为消息内容onFinish: 消息发送完成后回调函数,可以在这里进行一些操作,例如清空输入框
messages:数据结构解析
ts
[
{
"parts": [
{
"type": "text", //文本类型
"text": "你知道 api router 吗"
}
],
"id": "FPHwY1udRrkEoYgR", //消息ID
"role": "user" //用户角色
},
{
"id": "qno6vcWcwFM4Yc8J", //消息ID
"role": "assistant", //AI角色
"parts": [
{
"type": "step-start" //步骤开始
},
{
"type": "text", //文本类型
"text": "是的,我知道 **API Router**。", //文本内容
"state": "done" //步骤完成
}
]
}
]
tsx
'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) => 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>
);
}
