Next.js+Ollama本地聊天模型应用!

今天我学习了Next.js + Ollama + TypeScript,收获真的超级多!没想到一天时间就能做出一个真正能用的AI聊天应用,并且是将模型下载到本地,减少了我们平常调用API的花费,让我兴奋得简直想马上分享给大家。下面我就来详细回顾一下今天学到的内容,保证通俗易懂,就算是前端小白也能看懂!

老规矩,先来看一下具体效果:

准备工作

首先我们得下载 Ollama,下载完成后,可以通过命令行来看看是否下载成功:ollama --version

然后选择模型,这里我选择得是一个比较小得模型,太大了下载太慢了,这本来就是一个学习怎么使用,所以我就下载得是比较小得模型:

ollama pull deepseek-r1:1.5b 在命令行输入,然后下载所选模型,

ollama run deepseek-r1:1.5b 测试是否能够运行

我们页可以查看本地的11434端口查看是否运行成功

项目整体架构

首先,我得搞清楚这个项目是做什么的。简单来说,我构建了一个可以在浏览器里和AI模型聊天的应用!用户在页面上输入消息,消息会发送到我们写的API接口,然后API去调用本地运行的Ollama服务,最后把AI的回复展示给用户。

这涉及到几个关键部分:前端页面(page.tsx)、API接口(route.ts)和数据类型定义(chat.ts)。它们各司其职,配合得天衣无缝!

数据类型定义:打好基础

先从最简单的开始看------数据类型定义。在chat.ts文件中,我定义了聊天功能需要的各种类型:

typescript 复制代码
export type Message = {
    role: 'user' | 'assistant' | 'system' | 'tool';
    content: string
}

export type ChatResponse = {
    model: string;                  // 使用的模型名称
    created_at: string;             // 响应生成的时间戳
    message: Message;               // 核心:模型返回的消息内容
    done: boolean;                  // 流式传输结束标志
    total_duration: number;         // 整个请求的总耗时(纳秒)
    load_duration: number;          // 模型加载耗时(纳秒)
    prompt_eval_count: number;      // 提示词(prompt)处理的 token 数量
    prompt_eval_duration: number;   // 处理提示词的耗时(纳秒)
    eval_count: number;             // 生成回复的 token 数量
    eval_duration: number;          // 生成回复的耗时(纳秒)
}

export type ChatRequest = {
    model: string;
    messages: Message[]
    stream?: boolean; // ? 可选的 默认是false
}

为什么要先定义类型呢?这就好比建房子要先画图纸一样。TypeScript是类型安全的,提前定义好数据结构能让代码更加可靠,减少错误。

Message类型表示一条聊天消息,它有rolecontent两个字段。role可以是'user'、'assistant'、'system'或'tool',表示消息的发送者身份;content就是消息的实际内容。

ChatRequest是我们要发送给Ollama API的请求结构,包含要使用的模型名称和消息列表。这里的stream?后面的问号表示这个字段是可选的,如果不传,默认就是false。

ChatResponse是Ollama API返回的响应结构,包含了很多有用的信息!不只是AI回复的消息内容,还有各种性能指标,比如处理耗时、token数量等。这些信息对于优化应用性能很有帮助。

前端页面:用户看到的界面

接下来看看用户直接交互的页面部分,在page.tsx文件中:

typescript 复制代码
'use client'; // 组件在客户端执行
import { useState } from 'react';
import type { Message } from '@/types/chat';

export default function Home() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState<string>("")
  const [isLoading, setIsLoading] = useState<boolean>(false);
  
  // ...其余代码
}

第一行的'use client'指令很重要,它告诉Next.js这个组件是在客户端(也就是浏览器中)执行的,而不是在服务器端。这是因为我们需要处理用户的交互和状态管理。

然后我导入了useState钩子和Message类型。useState是React中管理状态的神器!它让函数组件也能拥有状态管理能力。

在Home组件中,我定义了三个状态:

  • messages:存储所有聊天消息的数组
  • input:存储用户当前输入的文本
  • isLoading:表示是否正在等待AI回复的标志

useState的用法是const [变量名, 设置函数名] = useState<类型>(初始值)。比如useState<Message[]>([])表示初始值为空数组,数组中的每个元素都是Message类型。

javascript 复制代码
const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  if (!input.trim || isLoading) return;
  
  const userMessage: Message = {
    role: 'user',
    content: input,
  }
  
  setMessages((prev) => [...prev, userMessage])
  setInput('')
  setIsLoading(true)
  
  try {
    const res = await fetch('/api/chat', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ messages: [...messages, userMessage] })
    })
    
    const data = await res.json();
    const assistantMessage: Message = data.message;
    setMessages((prev) => [...prev, assistantMessage])
    
  } catch (err) {
    // 错误处理
  } finally {
    setIsLoading(false)
  }
}

handleSubmit函数处理表单提交事件。当用户点击发送按钮或按回车时,这个函数就会被调用。

首先调用e.preventDefault()阻止表单的默认提交行为(防止页面刷新)。然后检查输入是否为空或是否正在加载中,如果是就直接返回,不做任何处理。

接着创建用户消息对象,并将其添加到消息列表中。这里用了setMessages((prev) => [...prev, userMessage]),意思是"取之前的所有消息,再加上新的用户消息"。

[...messages, userMessage]这种写法叫做扩展运算符(spread operator),它相当于创建了一个新数组,包含原数组的所有元素和新元素。

然后清空输入框并将加载状态设为true,这样界面上就会显示"正在加载"的提示。

最关键的部分是使用fetchAPI向我们的后端接口发送请求:await fetch('/api/chat', {...})。这里用了async/await语法,让异步代码看起来像同步代码一样清晰。

请求成功后,从响应中获取AI回复的消息,并将其添加到消息列表中。无论成功还是失败,最后都要将加载状态设回false。

ini 复制代码
return (
  <div className="flex flex-col h-screen bg-gray-100">
    <div className="flex-1 overflow-y-auto p-4 space-y-4">
      {
        messages.length === 0 ? (
          <p className="text-center text-gray-500 mt-10">
            开始与deepseek模型聊天吧
          </p>
        ) : (
          messages.map((msg, idx) => (
            <div
              key={msg.content}
              className={
                `flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`
              }
            >
              <div
                className={`max-w-xs lg:max-w-md px-4 py-2 rounded-lg 
                  ${msg.role === 'user' ? 'bg-blue-500 text-white'
                    : 'bg-white text-gray-800 shadow'
                  }
                `}
              >
                <p>{msg.content}</p>
              </div>
            </div>
          ))
        )
      }
      {isLoading && (
        <div className="flex justify-start">
          <div className="bg-white text-gray-800 shadow px-4 py-2 
            rounded-lg max-w-xs lg:max-w-md
          ">
            <p>DeepSeek正在思考....</p>
          </div>
        </div>
      )}
    </div>
    <div className="p-4 bg-white border-t">
      <form onSubmit={handleSubmit} className='flex space-x-2'>
        <input
          type='text'
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder='输入你的消息'
          className='flex-1 p-2 border rounded-lg focus:outline-none
          focus:ring-2 focus:ring-blue-500
          '
        />
        <button
          type='submit'
          disabled={isLoading}
          className='px-4 py-2 bg-blue-500 text-white rounded-lg
          hover:bg-blue-600 disabled:bg-blue-300 disabled:cursor-not-allowed
          '
        >
          {isLoading ? '发送中...' : '发送'}
        </button>
      </form>
    </div>
  </div>
)

页面的UI部分使用了Tailwind CSS来样式化。整个页面是一个垂直flex容器,占据整个屏幕高度。

消息列表部分会根据消息数量显示不同的内容:如果没有消息,显示提示文本;如果有消息,就用map函数遍历所有消息并渲染出来。

每条消息根据发送者的不同有不同的样式:用户消息靠右显示,蓝色背景;AI消息靠左显示,白色背景。这种条件样式通过模板字符串和三元表达式实现。

加载状态时显示"DeepSeek正在思考...."的提示。

底部是输入表单,包含一个文本输入框和一个发送按钮。输入框的值绑定到input状态, onChange事件更新这个状态。按钮在加载状态下会禁用,并显示不同的文本。

API路由:连接前端和Ollama的桥梁

现在来看看后端的API路由,在route.ts文件中:

javascript 复制代码
import type { NextResponse } from "next/server";
import {
    Message,
    ChatResponse,
    ChatRequest,
} from "@/types/chat";

const oLLAMA_API_URL = "http://localhost:11434/api/chat";
const MODEL_NAME = 'deepseek-r1:1.5b';

export async function POST(request: NextResponse) {
    try {
        const body: { messages: Message[] } = await request.json();

        const ollamaRequestBody: ChatRequest = {
            model: MODEL_NAME,
            messages: body.messages,
            stream: false
        }
        
        const response = await fetch(oLLAMA_API_URL, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(ollamaRequestBody)
        })
        
        if (!response.ok) {
            const errorText = await response.text
            return Response.json(
                {
                    error: `Ollama API Error:${errorText}`
                }
            )
        }

        const ollamaData: ChatResponse = await response.json()
        return Response.json(ollamaData)
        
    } catch (err) {
        console.log('Chat API Error:', err);
        return Response.json(
            {
                status: 500
            }
        )
    }
}

这个文件定义了一个POST接口,它充当了前端和Ollama服务之间的代理。

首先定义了Ollama API的URL和要使用的模型名称。Ollama默认在本地11434端口提供服务,所以我们连接到http://localhost:11434/api/chat

在POST函数中,首先从请求体中获取前端发送的消息列表。然后构建要发送给Ollama的请求体,包括模型名称、消息列表和stream选项(这里设为false,表示不使用流式传输)。

接下来使用fetch向Ollama API发送请求。如果请求失败,返回错误信息;如果成功,将Ollama的响应直接返回给前端。

整个流程用try-catch包裹,确保任何错误都能被捕获并处理,返回500状态码。

总结与思考

通过今天的学习,我深刻理解了Next.js全栈开发的魅力!前端、API路由、类型定义各司其职,构成了一个完整的应用。

前端页面负责用户交互和状态管理,使用React的useState钩子来管理聊天消息、用户输入和加载状态。

API路由作为中间层,接收前端请求,转发给Ollama服务,再将响应返回给前端。这种架构的好处是前端不需要直接连接Ollama,提高了安全性和灵活性。

类型定义确保了整个应用的数据结构一致性,减少了潜在的错误。

最让我兴奋的是,这个应用虽然代码量不大,但涵盖了现代Web开发的很多核心概念:React状态管理、API调用、异步处理、错误处理、TypeScript类型系统等。

而且这个应用是真正可用的!我可以在浏览器里和本地的AI模型聊天,看到消息一来一回,那种成就感真的无法用言语形容。

相关推荐
袁煦丞18 分钟前
Redis内存闪电侠:cpolar内网穿透第614个成功挑战
前端·程序员·远程工作
BillKu23 分钟前
Vue3组件加载顺序
前端·javascript·vue.js
IT_陈寒31 分钟前
Python性能优化必知必会:7个让代码快3倍的底层技巧与实战案例
前端·人工智能·后端
暖木生晖43 分钟前
引入资源即针对于不同的屏幕尺寸,调用不同的css文件
前端·css·媒体查询
袁煦丞1 小时前
DS file文件管家远程自由:cpolar内网穿透实验室第492个成功挑战
前端·程序员·远程工作
用户013741284371 小时前
九个鲜为人知却极具威力的 CSS 功能:提升前端开发体验的隐藏技巧
前端
永远不打烊1 小时前
Window环境 WebRTC demo 运行
前端
风舞1 小时前
一文搞定JS所有类型判断最佳实践
前端·javascript
coding随想1 小时前
哈希值变化的魔法:深入解析HTML5 hashchange事件的奥秘与实战
前端
一树山茶1 小时前
uniapp在微信小程序中实现 SSE进行通信
前端·javascript