使用 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…


相关文章
|
2月前
|
前端开发 机器人 API
前端大模型入门(一):用 js+langchain 构建基于 LLM 的应用
本文介绍了大语言模型(LLM)的HTTP API流式调用机制及其在前端的实现方法。通过流式调用,服务器可以逐步发送生成的文本内容,前端则实时处理并展示这些数据块,从而提升用户体验和实时性。文章详细讲解了如何使用`fetch`发起流式请求、处理响应流数据、逐步更新界面、处理中断和错误,以及优化用户交互。流式调用特别适用于聊天机器人、搜索建议等应用场景,能够显著减少用户的等待时间,增强交互性。
556 2
|
15天前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
29天前
|
Web App开发 JavaScript 前端开发
深入浅出Node.js:从零开始构建后端服务
【10月更文挑战第42天】在数字时代的浪潮中,掌握一门后端技术对于开发者来说至关重要。Node.js,作为一种基于Chrome V8引擎的JavaScript运行环境,允许开发者使用JavaScript编写服务器端代码,极大地拓宽了前端开发者的技能边界。本文将从Node.js的基础概念讲起,逐步引导读者理解其事件驱动、非阻塞I/O模型的核心原理,并指导如何在实战中应用这些知识构建高效、可扩展的后端服务。通过深入浅出的方式,我们将一起探索Node.js的魅力和潜力,解锁更多可能。
|
2月前
|
存储 JavaScript 前端开发
使用JavaScript构建动态交互式网页:从基础到实践
【10月更文挑战第12天】使用JavaScript构建动态交互式网页:从基础到实践
137 1
|
2天前
|
人工智能 数据可视化 JavaScript
NodeTool:AI 工作流可视化构建器,通过拖放节点设计复杂的工作流,集成 OpenAI 等多个平台
NodeTool 是一个开源的 AI 工作流可视化构建器,通过拖放节点的方式设计复杂的工作流,无需编码即可快速原型设计和测试。它支持本地 GPU 运行 AI 模型,并与 Hugging Face、OpenAI 等平台集成,提供模型访问能力。
34 14
NodeTool:AI 工作流可视化构建器,通过拖放节点设计复杂的工作流,集成 OpenAI 等多个平台
|
24天前
|
JSON 缓存 JavaScript
深入浅出:使用Node.js构建RESTful API
在这个数字时代,API已成为软件开发的基石之一。本文旨在引导初学者通过Node.js和Express框架快速搭建一个功能完备的RESTful API。我们将从零开始,逐步深入,不仅涉及代码编写,还包括设计原则、最佳实践及调试技巧。无论你是初探后端开发,还是希望扩展你的技术栈,这篇文章都将是你的理想指南。
|
17天前
|
JSON JavaScript 前端开发
深入浅出Node.js:从零开始构建RESTful API
在数字化时代的浪潮中,后端开发作为连接用户与数据的桥梁,扮演着至关重要的角色。本文将引导您步入Node.js的奇妙世界,通过实践操作,掌握如何使用这一强大的JavaScript运行时环境构建高效、可扩展的RESTful API。我们将一同探索Express框架的使用,学习如何设计API端点,处理数据请求,并实现身份验证机制,最终部署我们的成果到云服务器上。无论您是初学者还是有一定基础的开发者,这篇文章都将为您打开一扇通往后端开发深层知识的大门。
33 12
|
24天前
|
JavaScript NoSQL API
深入浅出Node.js:从零开始构建RESTful API
在数字化时代的浪潮中,后端开发如同一座灯塔,指引着数据的海洋。本文将带你航行在Node.js的海域,探索如何从一张白纸到完成一个功能完备的RESTful API。我们将一起学习如何搭建开发环境、设计API结构、处理数据请求与响应,以及实现数据库交互。准备好了吗?启航吧!
|
27天前
|
缓存 JavaScript 前端开发
JavaScript 与 DOM 交互的基础及进阶技巧,涵盖 DOM 获取、修改、创建、删除元素的方法,事件处理,性能优化及与其他前端技术的结合,助你构建动态交互的网页应用
本文深入讲解了 JavaScript 与 DOM 交互的基础及进阶技巧,涵盖 DOM 获取、修改、创建、删除元素的方法,事件处理,性能优化及与其他前端技术的结合,助你构建动态交互的网页应用。
40 5
|
26天前
|
缓存 负载均衡 JavaScript
构建高效后端服务:Node.js与Express框架实践
在数字化时代的浪潮中,后端服务的重要性不言而喻。本文将通过深入浅出的方式介绍如何利用Node.js及其强大的Express框架来搭建一个高效的后端服务。我们将从零开始,逐步深入,不仅涉及基础的代码编写,更会探讨如何优化性能和处理高并发场景。无论你是后端新手还是希望提高现有技能的开发者,这篇文章都将为你提供宝贵的知识和启示。