欢迎回到本系列,我们将使用 AI 工具构建 Web 应用程序。

  1. 简介和设置
  2. 您的第一个 AI 提示
  3. 流式响应
  4. 人工智能如何运作
  5. 快速工程
  6. 人工智能生成的图像
  7. 安全性和可靠性
  8. 正在部署

在上一篇文章中,我们将人工智能生成的笑话添加到了 Qwik 应用程序中来自 OpenAI API。它有效,但用户体验受到影响,因为我们必须等到 API 完成整个响应才能更新客户端。

如果您使用过任何人工智能聊天工具,您就会知道,更好的体验是在生成每段文本后立即做出响应。它变成了一种电传打字机效果。

这就是我们今天要使用 HTTP 流

<图>

< iframe allowedfullscreen =“”frameborder =“0”height =“360”loading =“lazy”src =“https://www.youtube.com/embed/GkyHBwUA0EQ?&wmode = opaque”width =“640”>先决条件

在我们进入流之前,我们需要探索一些与 HTTP 请求相关的 Qwik 怪癖。

如果我们检查表单发送的当前 POST 请求,我们可以看到返回的有效负载不仅仅是我们从操作处理程序返回的纯文本。相反,它是这种序列化数据 .

<图>
Chrome devtools Network 选项卡的屏幕截图,其中包含来自 Qwik 后端的序列化响应Qwik 优化器 的结果延迟加载资源,并且对于正确处理返回的数据是必要的。不幸的是,这会阻止标准的流响应。

因此,虽然 routeAction$Form 组件非常方便,我们必须做点别的事情。

值得赞扬的是,Qwik 团队确实提供了一种记录良好的流式响应方法。但是,它涉及到它们的 server$ 函数和 异步生成器函数。如果我们严格讨论 Qwik,这可能是正确的方法,但本系列适合所有人。我将避免这种实现,因为它对于 Qwik 来说过于具体,而是专注于广泛适用的概念。

重构服务器逻辑

我们不能使用路线操作,这很糟糕,因为它们很棒。那么我们可以使用什么呢?

Qwik City 提供多种选择。我发现最好的是中间件。它们提供了对原始工具的足够访问权限,使我们能够完成所需的任务,并且这些概念将适用于 Qwik 之外的其他环境。

中间件本质上是一组函数,我们可以在路由处理程序的请求生命周期内的各个点注入它们。我们可以通过导出我们想要定位的钩子的命名常量来定义它们(onRequestonGetonPostonPutonDelete)。

因此,我们可以使用一个中间件,通过导出 onPost 中间件来挂钩任何 POST 请求,而不是依赖路由操作。为了支持流式传输,我们需要返回一个标准的 响应对象。我们可以通过创建一个 Response 对象并将其传递给 requestEvent.send() 方法。

这是一个基本(非流式传输)示例:

/** @type {import('@builder.io/qwik-city').RequestHandler} */
导出 const onPost = (requestEvent) => {
  requestEvent.send(new Response('你好松鼠!'))
}

在我们处理流媒体之前,让我们从使用中间件实现的旧路由操作中获得相同的功能。我们可以将大部分代码复制到 onPost 中间件中,但我们无法访问 formData

幸运的是,我们可以从 requestEvent 重新创建该数据.parseBody() 方法。我们还希望使用 requestEvent.send() 来响应 OpenAI 数据,而不是 return 语句。

/** @type {import('@builder.io/qwik-city').RequestHandler} */
导出 const onPost = async (requestEvent) => {
  const OPENAI_API_KEY = requestEvent.env.get('OPENAI_API_KEY')
  const formData = 等待 requestEvent.parseBody()

  常量提示 = formData.prompt
  常量主体 = {
    型号:'gpt-3.5-turbo',
    消息:[{角色:'用户',内容:提示}]
  }

  const 响应 = 等待 fetch('https://api.openai.com/v1/chat/completions', {
    // ... 获取选项
  })
  const data = 等待response.json()

  const responseBody = data.choices[0].message.content

  requestEvent.send(新响应(responseBody))
}

重构客户端逻辑

替换路由操作会产生不幸的副作用,这意味着我们也无法再使用

组件。我们必须使用常规 HTML

元素并重新创建我们之前拥有的所有好处,包括使用 JavaScript 发送 HTTP 请求、跟踪加载状态和访问结果。让我们重构我们的客户端以再次支持这些功能。

我们可以将这些要求分解为需要两件事:用于提交表单的 JavaScript 解决方案和用于管理加载状态和结果的反应状态。

我过去曾多次深入介绍过如何使用 JavaScript 提交 HTML 表单:

所以今天我将分享该代码片段,我将其放入项目根目录的 utils.js 文件中。此 jsFormSubmit 函数接受 HTMLFormElement 然后构造一个 基于表单属性获取请求并返回结果承诺

<前><代码>/**
* @param {HTMLFormElement} 表单
*/
导出函数 jsFormSubmit(form) {
const url = 新 URL(form.action)
const formData = new FormData(表单)
const searchParameters = new URLSearchParams(formData)

/** @type {参数[1]} */
常量 fetchOptions = {
方法:表单.方法
}

if (form.method.toLowerCase() === 'post') {
fetchOptions.body = form.enctype === 'multipart/form-data' ?表单数据:搜索参数
} 别的 {
url.search = 搜索参数
}

返回 fetch(url, fetchOptions)
}

这个通用函数可用于提交任何 HTML 表单,因此在 提交事件处理程序。甜甜的!

对于反应式数据,Qwik 提供了两个选项,useStore反应式 – 意味着对对象属性的更改将自动反映在 UI 中引用的任何位置。

我们可以使用 useStore 在组件中创建一个“状态”对象来跟踪 HTTP 请求的加载状态以及文本响应。

从“@builder.io/qwik”导入{ $, component$, useStore };
// 其他设置逻辑

导出默认组件$(() => {
  常量状态 = useStore({
    正在加载:假,
    文本: '',
  })

  // 其他组件逻辑
})

接下来,我们可以更新模板。由于我们无法再使用之前的 action 对象,因此我们可以将 action.isRunningaction.value 的引用替换为 分别是 state.isLoadingstate.text(不要问我为什么更改属性名称)。我还将向名为 handleSbumit 的表单添加一个 “submit” 事件处理程序,我们很快就会看到它。

<前><代码><主>
<表格 方法=“帖子” 防止默认:提交 onSubmit$={handleSubmit} >