ChatGPT中文版源码深度解析:前端技术、功能实现与未来展望

10

随着ChatGPT在全球范围内的火爆,越来越多的开发者和用户开始关注和探索其在各个领域的应用。今天,我们将深入研究一个ChatGPT中文版的源码,该源码基于nodejs编写,旨在为用户提供一个界面友好、功能强大的中文AI交互平台。我们将重点分析前端页面的代码实现,包括header、footer和问题框等核心组件,并探讨其设计思路和技术特点。

界面与功能概述

该ChatGPT中文版源码在界面设计上颇具科技感,力求为用户带来沉浸式的交互体验。其主要功能包括:

  1. 指令输入框:专门设计的指令输入框允许用户以舒适的方式输入和管理prompts,从而更精确地控制AI的回复内容。
  2. 主题切换:支持黑夜和白天两种模式,方便用户在不同光线环境下使用,保护视力。
  3. 回到顶部:一键回到顶部功能,方便用户快速浏览和查找历史信息。

AI快讯

Header组件分析

Header组件是页面顶部的重要组成部分,主要包含Logo和主题切换功能。以下是header文件的代码:

<header>
  <div class="flex justify-between">
    <Logo />
    <Themetoggle />
  </div>
  <div class="flex items-center mt-2">
    <span class="text-2xl text-slate font-extrabold mr-1">CTGPT.</span>
    <span class="text-2xl text-transparent font-extrabold bg-clip-text bg-gradient-to-r from-sky-400 to-emerald-600">CN</span>
  </div>
  <p mt-1 text-slate op-60>基于 OpenAI API (gpt-3.5-turbo)开发.</p>
</header>

这段代码使用了Flexbox布局,将Logo和Themetoggle组件放置在页面的左右两端。同时,在Logo下方显示了“CTGPT.CN”的字样,并注明了该平台基于OpenAI API (gpt-3.5-turbo)开发。

关键技术点:

  • Flexbox布局:用于灵活地控制子元素的排列方式,实现响应式布局。
  • Tailwind CSS:使用了Tailwind CSS的原子类,例如flexjustify-betweentext-2xl等,简化了样式编写。
  • 渐变色:使用了CSS的background-clip-textbg-gradient-to-r属性,实现了文字的渐变色效果,增强了视觉吸引力。

Footer组件分析

Footer组件位于页面底部,主要包含版权信息、下载链接和统计代码。以下是footer文件的代码:

<footer>
  <p mt-8 text-xs op-30>
    <br>
    <a
      border-b border-slate border-none hover:border-dashed
      href="https://wwji.lanzouf.com/iVoeB0o7wrxe" target="_blank"
    >
      安卓端下载
      <span px-1>|</span>
    </a>
    <a
      border-b border-slate border-none hover:border-dashed
      href="https://wwji.lanzouf.com/iYlRb0o7wryf" target="_blank"
    >
      苹果端下载(safari打开安装需要去设置描述文件安装)
    </a>
  </p>
  <script charset="UTF-8" id="LA_COLLECT" src="//sdk.51.la/js-sdk-pro.min.js"></script>
  <script>LA.init({id:"Jzntbyt4gmLpmsXz",ck:"Jzntbyt4gmLpmsXz"})</script>
  <script>
  var _hmt = _hmt || [];
  (function() {
    var hm = document.createElement("script");
    hm.src = "https://hm.baidu.com/hm.js?5aff25b20308b19618d1ea0a4797216b";
    var s = document.getElementsByTagName("script")[0];
    s.parentNode.insertBefore(hm, s);
  })();
  </script>
  </p>
</footer>

这段代码包含了安卓和苹果客户端的下载链接,以及用于统计网站访问量的代码。请注意,实际部署时需要替换为有效的下载链接和统计代码。

关键技术点:

  • 外部链接:使用了<a>标签创建外部链接,并通过target="_blank"属性在新标签页中打开链接。
  • 第三方统计:集成了第三方统计服务,例如51.la和百度统计,用于收集网站访问数据。

问题框组件分析

问题框组件是用户与ChatGPT进行交互的核心部分。以下是问题框组件的代码:

import type { ChatMessage } from '@/types'
import { createSignal, Index, Show } from 'solid-js'
import IconClear from './icons/Clear'
import MessageItem from './MessageItem'
import SystemRoleSettings from './SystemRoleSettings'
import _ from 'lodash'
import { generateSignature } from '@/utils/auth'

export default () => {
  let inputRef: HTMLTextAreaElement
  const [currentSystemRoleSettings, setCurrentSystemRoleSettings] = createSignal('')
  const [systemRoleEditing, setSystemRoleEditing] = createSignal(false)
  const [messageList, setMessageList] = createSignal<ChatMessage[]>([])
  const [currentAssistantMessage, setCurrentAssistantMessage] = createSignal('')
  const [loading, setLoading] = createSignal(false)
  const [controller, setController] = createSignal<AbortController>(null)

  const handleButtonClick = async () => {
    const inputValue = inputRef.value
    if (!inputValue) {
      return
    }
    // @ts-ignore
    if (window?.umami) umami.trackEvent('chat_generate')
    inputRef.value = ''
    setMessageList([
      ...messageList(),
      {
        role: 'user',
        content: inputValue,
      },
    ])
    requestWithLatestMessage()
  }
  const throttle =_.throttle(function(){
    window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'})
  }, 300, {
    leading: true,
    trailing: false
  })
  const requestWithLatestMessage = async () => {
    setLoading(true)
    setCurrentAssistantMessage('')
    const storagePassword = localStorage.getItem('pass')
    try {
      const controller = new AbortController()
      setController(controller)
      const requestMessageList = [...messageList()]
      if (currentSystemRoleSettings()) {
        requestMessageList.unshift({
          role: 'system',
          content: currentSystemRoleSettings(),
        })
      }
      const timestamp = Date.now()
      const response = await fetch('/api/generate', {
        method: 'POST',
        body: JSON.stringify({
          messages: requestMessageList,
          time: timestamp,
          pass: storagePassword,
          sign: await generateSignature({
            t: timestamp,
            m: requestMessageList?.[requestMessageList.length - 1]?.content || '',
          }),
        }),
        signal: controller.signal,
      })
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      const data = response.body
      if (!data) {
        throw new Error('No data')
      }
      const reader = data.getReader()
      const decoder = new TextDecoder('utf-8')
      let done = false

      while (!done) {
        const { value, done: readerDone } = await reader.read()
        if (value) {
          let char = decoder.decode(value)
          if (char === '\n' && currentAssistantMessage().endsWith('\n')) {
            continue
          }
          if (char) {
            setCurrentAssistantMessage(currentAssistantMessage() + char)
          }
          throttle()
        }
        done = readerDone
      }
    } catch (e) {
      console.error(e)
      setLoading(false)
      setController(null)
      return
    }
    archiveCurrentMessage()
  }

  const archiveCurrentMessage = () => {
    if (currentAssistantMessage()) {
      setMessageList([
        ...messageList(),
        {
          role: 'assistant',
          content: currentAssistantMessage(),
        },
      ])
      setCurrentAssistantMessage('')
      setLoading(false)
      setController(null)
      inputRef.focus()
    }
  }

  const clear = () => {
    inputRef.value = ''
    inputRef.style.height = 'auto';
    setMessageList([])
    setCurrentAssistantMessage('')
    setCurrentSystemRoleSettings('')
  }

  const stopStreamFetch = () => {
    if (controller()) {
      controller().abort()
      archiveCurrentMessage()
    }
  }

  const retryLastFetch = () => {
    if (messageList().length > 0) {
      const lastMessage = messageList()[messageList().length - 1]
      console.log(lastMessage)
      if (lastMessage.role === 'assistant') {
        setMessageList(messageList().slice(0, -1))
        requestWithLatestMessage()
      }
    }
  }

  const handleKeydown = (e: KeyboardEvent) => {
    if (e.isComposing || e.shiftKey) {
      return
    }
    if (e.key === 'Enter') {
      handleButtonClick()
    }
  }

  return (
    <div my-6>
      <SystemRoleSettings
        canEdit={() => messageList().length === 0}
        systemRoleEditing={systemRoleEditing}
        setSystemRoleEditing={setSystemRoleEditing}
        currentSystemRoleSettings={currentSystemRoleSettings}
        setCurrentSystemRoleSettings={setCurrentSystemRoleSettings}
      />
      <Index each={messageList()}>
        {(message, index) => (
          <MessageItem
            role={message().role}
            message={message().content}
            showRetry={() => (message().role === 'assistant' && index === messageList().length - 1)}
            onRetry={retryLastFetch}
          />
        )}
      </Index>
      {currentAssistantMessage() && (
        <MessageItem
          role="assistant"
          message={currentAssistantMessage}
        />
      )}
      <Show
        when={!loading()}
        fallback={() => (
          <div class="h-12 my-4 flex gap-4 items-center justify-center bg-slate bg-op-15 rounded-sm">
            <span>AI正在思考中...</span>
            <div class="px-2 py-0.5 border border-slate rounded-md text-sm op-70 cursor-pointer hover:bg-slate/10" onClick={stopStreamFetch}>停止</div>
          </div>
        )}
      >
        <div class="my-4 flex items-center gap-2 transition-opacity" class:op-50={systemRoleEditing()}>
          <textarea
            ref={inputRef!}
            disabled={systemRoleEditing()}
            onKeyDown={handleKeydown}
            placeholder="问些问题吧..."
            autocomplete="off"
            autofocus
            onInput={() => {
              inputRef.style.height = 'auto';
              inputRef.style.height = inputRef.scrollHeight + 'px';
            }}
            rows="1"
            w-full
            px-3 py-3
            min-h-12
            max-h-36
            rounded-sm
            bg-slate
            bg-op-15
            resize-none
            focus:bg-op-20
            focus:ring-0
            focus:outline-none
            placeholder:op-50
            dark="placeholder:op-30"
            scroll-pa-8px
          />
          <button onClick={handleButtonClick} disabled={systemRoleEditing()} h-12 px-4 py-2 bg-slate bg-op-15 hover:bg-op-20 rounded-sm>
            Send
          </button>
          <button title="Clear" onClick={clear} disabled={systemRoleEditing()} h-12 px-4 py-2 bg-slate bg-op-15 hover:bg-op-20 rounded-sm>
            <IconClear />
          </button>
        </div>
      </Show>
    </div>
  )
}

这段代码使用了Solid.js框架,实现了问题框的各种功能,例如发送消息、清空消息、停止流式请求和重试上次请求等。

关键技术点:

  • Solid.js:一个用于构建用户界面的声明式、高效且简单的JavaScript框架。
  • createSignal:Solid.js中的一个核心概念,用于创建响应式状态。
  • fetch API:用于向后端发送HTTP请求,获取AI的回复内容。
  • AbortController:用于中断正在进行的fetch请求。
  • lodash:一个流行的JavaScript实用工具库,提供了throttle函数用于限制函数的执行频率。

详细功能解析:

  1. 消息发送handleButtonClick函数用于发送用户输入的消息。它首先获取输入框的值,然后将其添加到消息列表中,并调用requestWithLatestMessage函数向后端发送请求。
  2. 流式响应处理requestWithLatestMessage函数使用fetch API向后端发送POST请求,并使用AbortController来支持中断请求。后端返回的数据以流式响应的形式返回,前端使用TextDecoder将数据解码并逐步更新currentAssistantMessage状态。
  3. 消息管理setMessageList函数用于更新消息列表,currentAssistantMessage函数用于显示AI的回复内容。
  4. 清空消息clear函数用于清空输入框和消息列表。
  5. 停止流式请求stopStreamFetch函数用于中断正在进行的fetch请求。
  6. 重试上次请求retryLastFetch函数用于重试上次失败的请求。
  7. 指令输入优化:通过SystemRoleSettings组件,允许用户自定义系统角色,从而更好地控制AI的行为。

安全性考虑

源码中包含了一个generateSignature函数,用于生成请求签名,以提高安全性。该函数可能使用了某种加密算法,例如HMAC,来生成基于时间戳和消息内容的签名。后端需要验证该签名,以防止恶意请求。

总结与展望

该ChatGPT中文版源码提供了一个功能完善、界面友好的AI交互平台。通过对header、footer和问题框等核心组件的分析,我们可以深入了解其设计思路和技术特点。然而,该源码仍然有改进的空间,例如:

  • 错误处理:可以添加更完善的错误处理机制,例如在fetch请求失败时显示错误信息。
  • 用户认证:可以添加用户认证功能,以保护用户数据和防止滥用。
  • 多语言支持:可以添加多语言支持,以满足不同用户的需求。

随着人工智能技术的不断发展,我们可以期待未来出现更多功能更强大、体验更出色的AI交互平台。