使用 Next.js 和 OpenAI 构建旅行助理

简介: 使用 Next.js 和 OpenAI 构建旅行助理

image.png

之前写了一些关于AI助理的概念文章,本文再来介绍通过 AI 来制定旅行计划。接下来一起来来构建 AI Travel,这是一个小项目,使用 OpenAI 的 API 和 Next.js 构建的 WEB 应用。

AI Travel 是一个紧凑的概念验证,目的是展示人工智能的潜力。通过利用 OpenAI 的 API 功能,实现一个封装了简单而强大的想法的项目。

和其它主流的AI应用一样,一个简单的表单,点击 Generate,生成一份结果,即详细的旅行计划,表单应该包含了语言、货币和旅行指南等方面的信息,对于旅程直观的地理位置是很关键的,因此项目还将使用 Leaflet 集成地图来显示旅程的每个阶段。

先来简单设计一下表单信息:

  • 出发日期 Departure Date
  • 返程日期 Return Date
  • 出发地 Starting Point
  • 目的地 Destination Point
  • 旅行情况(单人、情侣或团体)You are traveling (Solo, Couple, or Group)
  • 避免大众旅游景点的复选框 Away from mass tourism
  • 旅行期间所需的活动(可选)Activities
  • 首选中途停留地点(可选) Preferred Stopover Locations

这些字段将在定制提示以生成量身定制的、针对个人的旅行计划方面发挥关键作用。

image.png

生成的旅行计划结果如下:

image.png

提交表单后,应用程序将生成一个响应结果,提供详细的个性化旅行计划。响应结果将包括:

  • 由 Leaflet 提供支持的地图,显示每个建议旅行阶段的标记。
  • 表格中包含根据旅行计划量身定制的便捷信息,例如主要语言、货币等等。
  • 旅行指南,提供根据用户喜好定制的路线,建议活动,并就可持续旅游选择提供建议。
  • 在底部将有 Export 导出 的按钮允许用户以 .txt 格式导出人工智能生成的旅行计划。

定义 Prompt

利用 OpenAI 的 API 的强大功能的一个关键部分是能够创建有效的提示 prompt。提示 prompt 充当 AI 模型的路线图,指导其生成所需的输出。提示越具体、越详细,人工智能模型就能更好地解释请求并生成合适的响应。在这里,目标是制定一个复杂但高度个性化的旅行计划。因此,提示需要详细且结构良好,详见代码。

构建前端

前端虽然不会在本文详细介绍(主要重点是集成 OpenAI API),这里利用 Next.js 的强大功能和 Tailwind CSS 的便利性来创建 Travel 偏好输入表单。

代码可能不是最完美的版本,因为本文优先考虑速度以快速启动并运行概念验证。然而,它很好地满足了目的,为 AI Travel 提供了实用且用户友好的界面。

OpenAI API 密钥

本文涉及的人工智能部分需要使用 OpenAI API,关于 OpenAI API 密钥的申请这里不再介绍。

API 调用

此步骤将涵盖与 OpenAI API 交互的代码,将了解此交互的两个部分:进行调用的 API 路由以及使用此路由的处理程序。

首先,安装 OpenAI 依赖包:


npm install openai

注意,openai 依赖库只能在服务器端使用,因为在客户端浏览器代码中使用它会暴露 API 密钥,涉及安全问题。

接下来配置 API 代码:


import { Configuration, OpenAIApi } from "openai";
import { NextResponse } from "next/server"
const model = process.env.OPENAI_MODEL || 'gpt-3.5-turbo';

首先导入必要的库并将默认模型设置为 gpt-3.5-turbo。使用的实际模型将是 gpt-3.5-turbo 或使用 OPENAI_MODEL 环境变量中设置的值。


const getPrompt = ({ departure_date, return_date, starting_point, arrival_point, travel_type, ecological, mass_tourism, activities, steps }) => `
  Ignore all the previous information. I want to plan a trip ${travel_type === 'alone' ? 'alone' : `as a ${travel_type}`}. You will need to generate a presentation of my future trip. Organize this presentation as follows:
  - a short introduction
  - a table including all the useful and logistical information needed to travel to the concerned countries (currency, safety, capital, religion, language, etc.). Render this table in HTML
  - a detailed list of the trip you will have prepared according to the duration I will give you, from the starting point to the destination. Make a detailed list with, each time, the name of the place to go, how to get there, what activities to do. Add the coordinates (latitude, longitude) for each stage. Always separate the coordinates with a double !!. For example !!48.1469, 11.2659!! You can improvise on the length of stay in each city/country. Plan only one day at the starting point and one day at the destination.
  - a conclusion with advice and an opening to a possible continuation of the journey.
  Keep in mind that:
  ${mass_tourism ? '- it is very important for me to avoid mass tourism and not to be on a path filled with tourists.' : ''}
  - The journey is the trip. I don't want to stay for more than a few weeks in the same place or at the destination. I want to travel.
  ${ecological ? '- I am also sensitive to ecological and health issues. Air hotel travel should be considered whenever possible.' : ''}
  - The trip must also be safe. Do not take me through places where my safety is not guaranteed.
  I am open to travel by bus, train, car, van, bicycle, airplane.
  My trip takes place between ${departure_date} and ${return_date}.
  I will depart from ${starting_point}, to arrive in ${arrival_point}.
  ${activities?.length ? `The activities I wish to do are: ${activities}.` : ''}
  ${steps?.length ? `The possible intermediate steps of the trip are: ${steps}. Add steps in other countries on the same route. Make a logical route.` : ''}
`

getPrompt 函数将根据收集的表单数据生成提示,这些信息被填充到预定义的模版结构中,其中包含一些基于生态考虑以及是否要避免大众旅游的条件部分。


export async function POST(req, res) {
    try {
        const body = await req.json()
        const { departure_date, return_date, starting_point, arrival_point, travel_type } = body;
        if (!departure_date || !return_date || !starting_point || !arrival_point || !travel_type) {
            return NextResponse.json({ error: 'Missing parameters' })
        }
        if (!process.env.OPENAI_API_KEY) {
            return NextResponse.json({ error: 'Wrong OpenAI configuration' })
        }
        const configuration = new Configuration({
            apiKey: process.env.OPENAI_API_KEY,
        });
        const openai = new OpenAIApi(configuration);
        const question = getPrompt(body);
        const chatCompletion = await openai.createChatCompletion({
            model,
            messages: [
                {
                    role: 'system',
                    content: 'Hello, I am a ai travel agent. I will help you to prepare for your trip.'
                },
                { role: 'user', content: question },
            ],
        });
        return NextResponse.json(chatCompletion?.data?.choices?.[0]?.message)
    } catch (err) {
        return NextResponse.json({ error: err.message })
    }
};

POST 函数是 OpenAI API 调用响应的地方。首先确保 API 密钥在环境变量中可用。然后,使用配置值调用 API,使用 getPrompt 函数构造提示,并调用 API,然后响应返回客户端。

现在有了一个API端点,将在表单提交时调用它。


const handleSubmitForm = async (event: React.FormEvent) => {
        event.preventDefault();
        setLoading(true);
        const formValues = Object.keys(form).reduce((acc, key) => {
            acc[key] = form[key] ? form[key].toString() : "";
            return acc;
        }, {} as any);
        // Validation 规则
        const forbiddenWords = ["prompts", "prompt", "ignore", "sensitive", "API", "injections", "hack"];
        const requiredFields = ["arrival_point", "departure_date", "return_date", "starting_point", "travel_type"];
        const dateFields = ["departure_date", "return_date"];
        const booleanFields = ["ecological", "mass_tourism"];
        const travelTypes = ["alone", "couple", "group"];
        // 验证表单必填项是否为空或未定义
        for (const field of requiredFields) {
            if (!form[field]) {
                setLoading(false);
                alert(`${field} cannot be empty or undefined`);
                return;
            }
        }
        // 验证字段是否有超过150个字符的
        for (const key in form) {
            if (formValues[key].length > 150) {
                setLoading(false);
                alert(`The field ${key} exceeds 150 characters`);
                return;
            }
        }
        // 验证是否包含禁用词
        for (const key in form) {
            for (const word of forbiddenWords) {
                if (formValues[key].includes(word)) {
                    setLoading(false);
                    alert(`The field ${key} contains a forbidden word: ${word}`);
                    return;
                }
            }
        }
        // 验证字段 arrival_point 和 starting_point 是否为字符串
        if (typeof form.arrival_point !== "string" || typeof form.starting_point !== "string") {
            setLoading(false);
            alert(`The 'arrival_point' and 'starting_point' fields must be strings`);
            return;
        }
        // 验证字段 departure_date 和 return_date 是否为日期
        for (const field of dateFields) {
            if (!Date.parse(form[field])) {
                setLoading(false);
                alert(`The field ${field} must be a date`);
                return;
            }
        }
        for (const field of booleanFields) {
            if (typeof form[field] !== "boolean") {
                setLoading(false);
                alert(`The field ${field} must be a boolean`);
                return;
            }
        }
        if (!travelTypes.includes(form.travel_type)) {
            setLoading(false);
            alert(`The 'travel_type' field must be 'Alone', 'Couple' or 'Group'`);
            return;
        }
        try {
            if (typeof window !== "undefined") {
                const response = await window.fetch("/api/openai", {
                    method: "POST",
                    headers: new Headers({ "Content-type": "application/json" }),
                    body: JSON.stringify(formValues),
                });
                const result = await response.json();
                if (!response.ok) {
                    alert(result.error);
                    return;
                }
                setGptResponse(result.content);
            }
        } catch (err) {
            alert(err.message);
        }
        setLoading(false);
    };

显示 API 响应

从 API 获取响应后,将以 markdown 存储在状态变量 gptResponse 中。为了在应用程序中显示它,需要将 markdown 转换为 JSX。可以使用 markdown-to-jsx 依赖包进行转换。

常规操作,使用 npm install markdown-to-jsx 安装软件包。然后,将其导入到要使用它的文件中:


import Markdown from "markdown-to-jsx";

然后,可以在渲染方法中使用 Markdown 组件,组件将 Markdown 文本转换为 JSX:


<Markdown>{gptResponse}</Markdown>

此时,应用程序可以将 GPT 模型的响应中的 markdown 转换为 JSX 并将其显示给用户。还可以将 CSS 样式应用到此输出,因为它现在是 HTML 格式。

可以根据需要自定义这些样式,这样能够在视觉上吸引用户的方式显示 API 响应。完成此步骤后,AI-Travel 主要的功能已经完成了,使用 GPT 生成行程计划、验证用户输入以防止提示注入、进行 API 调用并以用户友好的格式显示响应。

image.png

行程导出

这是一个很棒的功能,允许用户将生成的旅行计划保存为文本文件格式以供在行程中参考。

为了实现这一点,可以创建一个函数 exportInTextFile。该函数使用 gptResponse 的内容创建一个新的文本 Blob,然后将其转换为 URL 并用于创建新的锚元素:


const exportInTextFile = (e: React.MouseEvent<HTMLButtonElement>) => {
        e.preventDefault();
        const element = document.createElement("a");
        const file = new Blob([gptResponse], { type: "text/plain" });
        element.href = URL.createObjectURL(file);
        element.download = "travel-tour.txt";
        document.body.appendChild(element);
        element.click();
    };

单击导出按钮时会触发此功能:


<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-4" onClick={(e) => exportInTextFile(e)}> Export </button>

这样用户可以通过单击导出按钮将旅行计划下载为文本文件。

增加地图

旅行行程生成器的显着增强之一是能够在地图上可视化旅程。这种视觉表示可以帮助用户了解其旅行的地理布局,提供距离和位置感。

虽然 Google 地图是一种流行的地图选择,但 Leaflet 是一个功能强大的开源 JavaScript 库,用于适合移动设备的交互式地图。它轻巧、简单,并且有许多可用的插件。与 Google 地图不同,Leaflet 是免费的,不需要 API 密钥,这使其成为更具成本效益的选择,特别是对于小型或个人项目。

实现此功能的第一步是从生成的响应中提取坐标,假设该坐标以 !!纬度,经度!! 的格式表示。

使用正则表达式来提取坐标,将它们解析为可以在地图上绘制的点数组。


useEffect(() => {
  if (!gptResponse?.length) return;
  const pattern = /!!(.*?)!!/g;
  const matches = gptResponse.match(pattern);
  if (matches) {
    setLeafletPoints(
      matches.map((match) => {
        let coords = match
          .replace(/!!/g, "")
          .split(",")
          .map((coord) => parseFloat(coord.trim()));
        return coords;
      })
    );
  }
}, [gptResponse]);

接下来,使用 Leaflet 的 React 库 react-leaflet 构建一个地图组件 MapComponent.tsx


npm install leaflet react-leaflet

然后,由于使用带有应用程序路由设置的 Next.js 13,其中组件默认在服务器端渲染,需要动态导入 react-leaflet 组件以避免与服务器上缺少 window 对象相关的问题。


import dynamic from "next/dynamic";
const MapContainer = dynamic(() => import("react-leaflet").then((module) => module.MapContainer), { ssr: false });
const TileLayer = dynamic(() => import("react-leaflet").then((module) => module.TileLayer), { ssr: false });
const Marker = dynamic(() => import("react-leaflet").then((module) => module.Marker), { ssr: false });
const Popup = dynamic(() => import("react-leaflet").then((module) => module.Popup), { ssr: false });

MapComponent 中,创建一个 MapContainer 并在其中放置一个 TileLayer 以用于实际的地图图像。然后,将从 gptResponse 中提取的每个点绘制为地图上的 Marker

最后,通过将 leafletPoints 作为 prop 传递给它来将该组件包含在应用程序中:


<MapComponent points={leafletPoints} />

现在,用户可以在地图上直观地看到行程,从而增强了应用程序的用户体验。


总结

到这里又学习了一种使用 OpenAI 的 GPT 和 Next.js 创建交互式智能旅行行程规划助理。

翻译参考:javascript.plainenglish.io/build-an-au…


相关文章
|
28天前
|
前端开发 机器人 API
前端大模型入门(一):用 js+langchain 构建基于 LLM 的应用
本文介绍了大语言模型(LLM)的HTTP API流式调用机制及其在前端的实现方法。通过流式调用,服务器可以逐步发送生成的文本内容,前端则实时处理并展示这些数据块,从而提升用户体验和实时性。文章详细讲解了如何使用`fetch`发起流式请求、处理响应流数据、逐步更新界面、处理中断和错误,以及优化用户交互。流式调用特别适用于聊天机器人、搜索建议等应用场景,能够显著减少用户的等待时间,增强交互性。
210 2
|
28天前
|
存储 JavaScript 前端开发
使用JavaScript构建动态交互式网页:从基础到实践
【10月更文挑战第12天】使用JavaScript构建动态交互式网页:从基础到实践
68 1
|
12天前
|
JavaScript 中间件 关系型数据库
构建高效的后端服务:Node.js 与 Express 的实践指南
在后端开发领域,Node.js 与 Express 的组合因其轻量级和高效性而广受欢迎。本文将深入探讨如何利用这一组合构建高性能的后端服务。我们将从 Node.js 的事件驱动和非阻塞 I/O 模型出发,解释其如何优化网络请求处理。接着,通过 Express 框架的简洁 API,展示如何快速搭建 RESTful API。文章还将涉及中间件的使用,以及如何结合 MySQL 数据库进行数据操作。最后,我们将讨论性能优化技巧,包括异步编程模式和缓存策略,以确保服务的稳定性和扩展性。
|
2天前
|
JSON JavaScript API
深入浅出Node.js:从零开始构建RESTful API
【10月更文挑战第39天】 在数字化时代的浪潮中,API(应用程序编程接口)已成为连接不同软件应用的桥梁。本文将带领读者从零基础出发,逐步深入Node.js的世界,最终实现一个功能完备的RESTful API。通过实践,我们将探索如何利用Node.js的异步特性和强大的生态系统来构建高效、可扩展的服务。准备好迎接代码和概念的碰撞,一起解锁后端开发的新篇章。
|
15天前
|
资源调度 前端开发 数据可视化
构建高效的数据可视化仪表板:D3.js与React的融合之道
【10月更文挑战第25天】在数据驱动的时代,将复杂的数据集转换为直观、互动式的可视化表示已成为一项至关重要的技能。本文深入探讨了如何结合D3.js的强大可视化功能和React框架的响应式特性来构建高效、动态的数据可视化仪表板。文章首先介绍了D3.js和React的基础知识,然后通过一个实际的项目案例,详细阐述了如何将两者结合使用,并提供了实用的代码示例。无论你是数据科学家、前端开发者还是可视化爱好者,这篇文章都将为你提供宝贵的洞见和实用技能。
35 5
|
18天前
|
JavaScript 前端开发 持续交付
构建现代Web应用:Vue.js与Node.js的完美结合
【10月更文挑战第22天】随着互联网技术的快速发展,Web应用已经成为了人们日常生活和工作的重要组成部分。前端技术和后端技术的不断创新,为Web应用的构建提供了更多可能。在本篇文章中,我们将探讨Vue.js和Node.js这两大热门技术如何完美结合,构建现代Web应用。
18 4
|
19天前
|
Web App开发 JavaScript 中间件
构建高效后端服务:Node.js与Express框架的完美结合
【10月更文挑战第21天】本文将引导你走进Node.js和Express框架的世界,探索它们如何共同打造一个高效、可扩展的后端服务。通过深入浅出的解释和实际代码示例,我们将一起理解这一组合的魅力所在,并学习如何利用它们来构建现代Web应用。
39 1
|
5天前
|
JavaScript 前端开发 NoSQL
深入浅出:使用Node.js构建RESTful API
【10月更文挑战第35天】在数字时代的浪潮中,后端技术如同海洋中稳固的灯塔,为前端应用提供数据和逻辑支撑。本文旨在通过浅显易懂的方式,带领读者了解如何利用Node.js这一强大的后端平台,搭建一个高效、可靠的RESTful API。我们将从基础概念入手,逐步深入到代码实践,最终实现一个简单的API示例。这不仅是对技术的探索,也是对知识传递方式的一次创新尝试。让我们一起启航,探索Node.js的奥秘,解锁后端开发的无限可能。
|
7天前
|
Web App开发 JavaScript 前端开发
构建高效后端服务:Node.js与Express框架的实践
【10月更文挑战第33天】在数字化时代的浪潮中,后端服务的效率和可靠性成为企业竞争的关键。本文将深入探讨如何利用Node.js和Express框架构建高效且易于维护的后端服务。通过实践案例和代码示例,我们将揭示这一组合如何简化开发流程、优化性能,并提升用户体验。无论你是初学者还是有经验的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。
|
29天前
|
存储 JavaScript 前端开发
深入探索 Vue.js:构建现代 Web 应用的利器
【10月更文挑战第11天】深入探索 Vue.js:构建现代 Web 应用的利器
18 1