随着ChatGPT在全球范围内的火爆,越来越多的开发者和用户开始关注和探索其在各个领域的应用。今天,我们将深入研究一个ChatGPT中文版的源码,该源码基于nodejs编写,旨在为用户提供一个界面友好、功能强大的中文AI交互平台。我们将重点分析前端页面的代码实现,包括header、footer和问题框等核心组件,并探讨其设计思路和技术特点。
界面与功能概述
该ChatGPT中文版源码在界面设计上颇具科技感,力求为用户带来沉浸式的交互体验。其主要功能包括:
- 指令输入框:专门设计的指令输入框允许用户以舒适的方式输入和管理prompts,从而更精确地控制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的原子类,例如
flex
、justify-between
、text-2xl
等,简化了样式编写。 - 渐变色:使用了CSS的
background-clip-text
和bg-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函数用于限制函数的执行频率。
详细功能解析:
- 消息发送:
handleButtonClick
函数用于发送用户输入的消息。它首先获取输入框的值,然后将其添加到消息列表中,并调用requestWithLatestMessage
函数向后端发送请求。 - 流式响应处理:
requestWithLatestMessage
函数使用fetch API向后端发送POST请求,并使用AbortController
来支持中断请求。后端返回的数据以流式响应的形式返回,前端使用TextDecoder
将数据解码并逐步更新currentAssistantMessage
状态。 - 消息管理:
setMessageList
函数用于更新消息列表,currentAssistantMessage
函数用于显示AI的回复内容。 - 清空消息:
clear
函数用于清空输入框和消息列表。 - 停止流式请求:
stopStreamFetch
函数用于中断正在进行的fetch请求。 - 重试上次请求:
retryLastFetch
函数用于重试上次失败的请求。 - 指令输入优化:通过SystemRoleSettings组件,允许用户自定义系统角色,从而更好地控制AI的行为。
安全性考虑
源码中包含了一个generateSignature
函数,用于生成请求签名,以提高安全性。该函数可能使用了某种加密算法,例如HMAC,来生成基于时间戳和消息内容的签名。后端需要验证该签名,以防止恶意请求。
总结与展望
该ChatGPT中文版源码提供了一个功能完善、界面友好的AI交互平台。通过对header、footer和问题框等核心组件的分析,我们可以深入了解其设计思路和技术特点。然而,该源码仍然有改进的空间,例如:
- 错误处理:可以添加更完善的错误处理机制,例如在fetch请求失败时显示错误信息。
- 用户认证:可以添加用户认证功能,以保护用户数据和防止滥用。
- 多语言支持:可以添加多语言支持,以满足不同用户的需求。
随着人工智能技术的不断发展,我们可以期待未来出现更多功能更强大、体验更出色的AI交互平台。