基于html5 开发的仿ChatGPT的前端UI项目,采用了VUE3 + TypeScript等技术选型开发,实现了基本的消息会话、PDF会话、新增会话、删除会话、会话历史、Token统计等新增功能。

- 消息会话、新建会话、会话历史、删除会话、Token统计、PDF文件上传等功能代码如下:
- <script setup lang=''ts''>
- import { ref } from ''vue''
- import { useRoute } from ''vue-router''
- import { router } from ''@/router''
- import { useScroll } from ''./hooks/useScroll''
- import VuePdfApp from "vue3-pdf-app"
- import "vue3-pdf-app/dist/icons/main.css"
- import { encode } from ''gpt-tokenizer''
-
-
- const { scrollRef, scrollToBottom } = useScroll()
-
- // Conversation and PDF preview panel toggle control
- let showTab = ref<string>("nav-tab-chat")
- let tabWidth = ref<string>("")
-
- // vue3-pdf-app UI configuration
- let pdfFile = ref<string>("")
- const config = ref<{}>({
- sidebar: true,
- toolbar: {
- toolbarViewerLeft: {
- findbar: true,
- previous: true,
- next: true,
- pageNumber: false,
- },
- toolbarViewerRight: {
- presentationMode: true,
- openFile: false,
- print: false,
- download: false,
- viewBookmark: false,
- },
- toolbarViewerMiddle: {
- zoomOut: true,
- zoomIn: true,
- scaleSelectContainer: true,
- }
- },
- })
-
- // Message input box
- const prompt = ref<string>('''')
-
- // Loading state and button state
- const buttonDisabled = ref<boolean>(false)
-
- // Get uuid from URL params
- const route = useRoute()
- let { uuid } = route.params as { uuid: string }
-
- interface Conversation {
- title: string;
- uuid: string;
- isEdit: boolean;
- createDate: string;
- lastChatContent: string;
- active: boolean;
- }
-
- interface Message {
- send: {
- model: string;
- messages: {
- role: string;
- content: string;
- fileName: any;
- fileSize: number;
- }[];
- temperature: number;
- };
- loading: boolean;
- receive?: {
- model: string;
- choices: {
- message?: {
- content: string;
- };
- delta: {
- content: string;
- };
- }[];
- };
- }
-
- // Conversation list and message list
- var conversationList = ref<Conversation[]>([])
- var messageList = ref<Message[]>([]);
-
- let conversations = window.localStorage.getItem("chatStore")
- if(conversations){
- conversationList.value = JSON.parse(conversations)
- }
-
- // Check if new conversation
- if (!uuid || uuid === ''0'') {
- uuid = Date.now().toString()
-
- // Initialize empty conversation
- if(!conversations){
- conversationList.value.push({
- title: ''New Chat'',
- uuid: uuid,
- isEdit: false,
- createDate: new Date().toLocaleString(),
- lastChatContent: ''Hello I am ChatGPT3.5...'',
- active: true
- })
- }else{
- // If has history, get last conversation
- let lastConversation = conversationList.value[conversationList.value.length-1]
- uuid = lastConversation.uuid
-
- let messages = window.localStorage.getItem(uuid)
- if(messages) {
- messageList.value = JSON.parse(messages)
- }
-
- router.push({ name: ''Chat'', params: { uuid } })
- }
- }else{
- // Load current conversation messages
- let messages = window.localStorage.getItem(uuid)
- if(messages) {
- messageList.value = JSON.parse(messages)
- }
-
- conversationList.value.forEach((item, index) => {
- if(item.uuid == uuid){
- item.active = true
- }else{
- item.active = false
- }
- })
-
- scrollToBottom()
- }
-
- // Set active conversation
- function handleAdd() {
- // Reset the message record of the new conversation
- messageList.value = []
-
- // Reset the active status of the conversation list
- conversationList.value.forEach((item, index) => {
- item.active = false
- })
-
- // Initialize an empty conversation
- uuid = Date.now().toString()
-
- conversationList.value.unshift({
- title: "New Chat",
- uuid: uuid,
- isEdit: false,
- createDate: new Date().toLocaleString(),
- lastChatContent: ''Hello I am ChatGPT3.5...'',
- active: true
- })
-
- // Save the conversation to local storage
- window.localStorage.setItem("chatStore", JSON.stringify(conversationList.value))
- }
-
- // Menu toggle
- function handleMenu(){
- let rootbody = document.getElementById("rootbody")
- if (rootbody) {
- if(rootbody.classList.value==""){
- rootbody.classList.value="open-sidebar-menu"
- }else{
- rootbody.classList.value=""
- }
- }
- }
-
- // Switch conversation
- function handleSwitch(selectedUuid: string) {
- uuid = selectedUuid
-
- // Reset message record of the new conversation
- let messages = window.localStorage.getItem(selectedUuid)
- if(messages){
- messageList.value = JSON.parse(messages)
- }else{
- messageList.value = []
- }
-
- // Reset active status of the conversation list
- conversationList.value.forEach((item, index) => {
- if(item.uuid == selectedUuid){
- item.active = true
- }else{
- item.active = false
- }
- })
-
- router.push({ name: ''Chat'', params: { uuid } })
- }
-
- // File upload related
- var fileName = ref()
- var fileSize = ref<number>(0)
- var formattedFileSize = ref<string>(''0B'')
- var fileUploadCard = ref<boolean>(false)
-
- var fileContent = ref()
-
-
- // Handle file upload
- function handleUpload(e: Event) {
- const target = e.target as HTMLInputElement;
- if(target.files && target.files[0].size >= 5 * 1024 * 1024){
- alert(''Maximum file size limit is 5MB'')
- return
- }else if (!target.files || target.files.length === 0) {
- alert(''Please select a file'')
- return
- }
-
- // Set file upload style
- fileName.value = target.files[0].name
- fileSize.value = target.files[0].size
- formatFileSize()
-
- // Preview PDF
- showTab.value = ''nav-tab-doc''
- tabWidth.value = ''width: 60%''
-
- pdfFile.value = URL.createObjectURL(target.files[0])
-
- // Upload file and extract content
- const formData = new FormData()
- formData.append(''doc'', target.files[0])
-
- fetch(import.meta.env.VITE_API_UPLOAD, {
- method: ''POST'',
- body: formData,
- })
- .then(response => response.text())
- .catch(error => console.error(''Error:'', error))
- .then(function (docContent) {
- if (typeof docContent !== ''string'') {
- alert("Failed to extract file content")
- return
- }
-
- const tokens = encode(docContent)
-
- if(tokens.length > 4096){
- alert("Exceeded maximum token limit of 4096")
- fileName.value = ''''
- fileSize.value = 0
- formattedFileSize.value = ''0B''
- }else{
- // Set the extracted content
- fileContent.value = docContent
-
- // Show file upload card
- fileUploadCard.value = true
- }
- })
- }
-
- function handleBackChat(){
- showTab.value = ''nav-tab-chat''
- tabWidth.value = ''''
- }
-
- function handleBackDoc(){
- showTab.value = ''nav-tab-doc''
- tabWidth.value = ''width: 40%''
- }
-
- // Format file size in Bytes, KB, MB, GB
- function formatFileSize() {
- if (fileSize.value < 1024) {
- formattedFileSize.value = fileSize.value + ''B'';
- } else if (fileSize.value < (1024*1024)) {
- var temp = fileSize.value / 1024
- formattedFileSize.value = temp.toFixed(2) + ''KB''
- } else if (fileSize.value < (1024*1024*1024)) {
- var temp = fileSize.value / (1024*1024)
- formattedFileSize.value = temp.toFixed(2) + ''MB''
- } else {
- var temp = fileSize.value / (1024*1024*1024);
- formattedFileSize.value = temp.toFixed(2) + ''GB''
- }
- }
-
- // Submit message
- function handleSubmit() {
- onConversation()
- }
-
- // Stream request to ChatGPT3.5
- async function onConversation() {
- let message = prompt.value
- if (!message || message.trim() === '''')
- return
-
- // Clear input box and disable button
- prompt.value = ''''
- buttonDisabled.value = true
- fileUploadCard.value = false
-
- // Send message (for local display, not directly sent to GPT)
- messageList.value.push({
- send: {
- model: "gpt-3.5-turbo-1106",
- messages: [
- {
- role: "user",
- content: message,
- fileName: fileName.value,
- fileSize: fileSize.value,
- },
- ],
- temperature: 0.7,
- },
- loading: true,
- });
-
- scrollToBottom()
-
- // Stream request to ChatGPT3.5
- try {
- if(fileContent.value){
- message += '', Uploaded file content: '' + fileContent.value
- }
-
- let data = {
- "model": "gpt-3.5-turbo-1106",
- "messages": [{"role": "user", "content": message }],
- "temperature": 0.7,
- "stream": true
- }
-
- let headers = {
- ''Content-Type'': ''application/json'',
- ''Authorization'': ''Bearer '' + import.meta.env.VITE_API_KEY,
- }
-
- // Send request
- let response = await fetch(import.meta.env.VITE_APP_URL, {
- method: ''POST'',
- headers: headers,
- body: JSON.stringify(data)
- })
-
- // Reset file upload related states immediately after sending to ChatGPT
- fileName.value = ''''
- fileSize.value = 0
- formattedFileSize.value = ''0B''
-
- if (!response.ok) {
- throw new Error(''Network response was not ok'')
- }
-
- // Read the data returned from the stream
- const reader = response.body?.getReader();
-
- const textDecoder = new TextDecoder()
- let result = true
- while (reader && result) {
- // Get a chunk
- const { done, value } = await reader.read()
-
- if (done) {
- console.log(''Stream ended'')
- result = false
-
- // Restore button state
- buttonDisabled.value = false
- fileContent.value = ''''
-
- // Save current messages
- window.localStorage.setItem(uuid, JSON.stringify(messageList.value))
- window.localStorage.setItem("chatStore", JSON.stringify(conversationList.value))
- break
- }
-
- // Convert chunk string to array
- let chunkText = textDecoder.decode(value)
- chunkText = chunkText.replace(/data:/g, '''')
- let results = chunkText.split(''\n\n'').filter(Boolean)
-
- // Iterate through the array and process multiple chunks
- for (let i = 0; i < results.length; i++) {
- var chunk = results[i]
- if (chunk.indexOf(''DONE'') == -1) {
- var chunkData = JSON.parse(chunk)
- if (chunkData.choices[0].delta.content) {
- if (!messageList.value[messageList.value.length - 1].receive) {
- // If it is the first result, set the state directly
- messageList.value[messageList.value.length - 1].receive = chunkData
- messageList.value[messageList.value.length - 1].loading = false
- } else {
- const lastMessage = messageList.value[messageList.value.length - 1]?.receive;
- if (lastMessage && lastMessage.choices[0].delta.content) {
- lastMessage.choices[0].delta.content += chunkData.choices[0].delta.content;
- }
- }
- scrollToBottom()
- }
- }
- }
- }
- } catch (e) {
- console.log(e)
- }
- }
-
- function handleDele(selectedUuid: string){
- // Reset the active state of the conversation list
- conversationList.value.forEach((item, index) => {
- if(item.uuid == selectedUuid){
- conversationList.value.splice(index,1)
-
- // Save the conversation to local storage
- window.localStorage.setItem("chatStore", JSON.stringify(conversationList.value))
- return false
- }
- })
-
- // Reset the message records of the new conversation
- if(uuid == selectedUuid){
- let messages = window.localStorage.getItem(selectedUuid)
- if(messages){
- window.localStorage.removeItem(selectedUuid)
- messageList.value = []
- }
- }
- }
- </script>
-
- <template>
- <div id="layout" >
- <!-- Sidebar -->
- <div >
- <a href="#" title="ChatGPT-UI" >
- <svg viewBox="0 0 128 128" width="24" height="24" data-v-c0161dce=""><path fill="#42b883" d="M78.8,10L64,35.4L49.2,10H0l64,110l64-110C128,10,78.8,10,78.8,10z" data-v-c0161dce=""></path><path fill="#35495e" d="M78.8,10L64,35.4L49.2,10H25.6L64,76l38.4-66H78.8z" data-v-c0161dce=""></path></svg>
- </a>
- <div role="tablist" aria-orientation="vertical">
- <a data-toggle="pill" href="#" role="tab">
- <i ></i> <!-- Chat -->
- </a>
- <a data-toggle="pill" href="#" role="tab">
- <i ></i> <!-- Layers -->
- </a>
-
- <a href="#">
- <i ></i> <!-- Light/Dark Mode -->
- <input type="checkbox">
- </a>
- <a href="#" role="tab">
- <i ></i> <!-- Settings -->
- </a>
- </div>
- <button type="submit" @click="handleMenu">
- <i ></i> <!-- Menu -->
- </button>
- </div>
- <!-- Sidebar -->
- <div :>
- <div >
- <!-- Chat Records -->
- <div id="nav-tab-chat" role="tabpanel" v-if="showTab === ''nav-tab-chat''">
- <div >
- <h3 >ChatGPT-UI</h3>
- <div>
- <button type="button" @click="handleAdd">New Chat</button></div>
- </div>
- <ul >
- <li >
- <span>RECENT CHATS</span>
- </li>
- <li v-for="(item, index) in conversationList" : @click="handleSwitch(item.uuid)">
- <div >
- <button type="button" ><i ></i></button>
- <button type="button" @click="handleDele(item.uuid)"><i ></i></button>
- </div>
- <a href="#" >
- <div >
- <div >
- <div >
- <span ></span>
- <img : src="../assets/chatgpt.jpg" alt="avatar"></div>
- <div >
- <div >
- <h6 >{{ item.title }}</h6>
- <p >{{ item.createDate }}</p></div>
- <div >{{ item.lastChatContent }}</div></div>
- </div>
- </div>
- </a>
- </li>
- </ul>
- </div>
- <!-- end Chat Records -->
- <!-- PDF Preview -->
- <div id="nav-tab-doc" role="tabpanel" v-if="showTab === ''nav-tab-doc''">
- <div >
- <h3 >ChatGPT-PDF</h3>
- <div>
- <button type="button" @click="handleBackChat">Back Chat</button></div>
- </div>
- <ul >
- <li >
- <span>PREVIEW</span>
- </li>
- <li>
- <vue-pdf-app :config="config" :pdf="pdfFile"></vue-pdf-app>
- </li>
- </ul>
- </div>
- <!-- end PDF Preview -->
- </div>
- </div>
-
- <div >
- <div >
- <!-- Chat Box Header -->
- <div >
- <div >
- <div >
- <div >
- <div >
- <div >
- <span ></span>
- <img src="../assets/chatgpt.jpg" alt="avatar"></div>
- <div >
- <div >
- <h6 >ChatGPT 3.5</h6></div>
- <div >Powered By OpenAI</div></div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- end Chat Box Header -->
-
- <div id="scrollRef" ref="scrollRef">
- <div >
- <ul v-for="(item, index) of messageList">
- <!-- Right Message -->
- <li >
- <div >
- <span ></span>
- <div >
- <div >
- {{ item.send.messages[0].content }}
- <div v-show="item.send.messages[0].fileName" @click="handleBackDoc">
- <div >
- <div >
- <div >
- <i ></i>
- </div>
- </div>
- <div >
- <h6 >{{ item.send.messages[0].fileName }}</h6>
- <span >{{ item.send.messages[0].fileSize }}</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </li>
- <!-- end Right Message -->
- <!-- Left Message -->
- <li v-if="item.receive">
- <div >
- <img src="../assets/chatgpt.jpg" alt="avatar"></div>
- <div >
- <span >{{ item.receive.model }}</span>
- <div >
- <div >
- <v-md-preview :text="item.receive.choices[0].message?item.receive.choices[0].message.content:item.receive.choices[0].delta.content"></v-md-preview>
- </div>
- </div>
- </div>
- </li>
- <!-- end Left Message -->
- <!-- Loading Message -->
- <li v-if="item.loading">
- <div >
- <img src="../assets/chatgpt.jpg" alt="avatar"></div>
- <div >
- <div >
- <div >
- <div >
- <span ></span>
- <span ></span>
- <span ></span>
- </div>
- </div>
- </div>
- </div>
- </li>
- <!-- end Loading Message -->
- </ul>
- </div>
- </div>
- <!-- Message Input Box -->
- <div >
- <div >
- <div >
- <div >
- <form @submit.prevent="handleSubmit">
- <div >
- <input type="text" v-model="prompt" placeholder="Type your message...">
- <div v-show="fileUploadCard" @click="handleBackDoc">
- <div >
- <div >
- <div >
- <i ></i>
- </div>
- </div>
- <div >
- <h6 >{{ fileName }}</h6>
- <span >{{ fileSize }}</span>
- </div>
- </div>
- </div>
- <div >
- <span >
- <input type="file" accept="application/pdf" id="fileInput" ref="file" @change="handleUpload" >
- <button data-toggle="tooltip" @click="($refs.file as HTMLInputElement).click()" title="" type="button" data-original-title="Attachment">
- <i ></i>
- </button>
- </span>
- </div>
- <div >
- <span >
- <button type="submit" :disabled="buttonDisabled" @click="handleSubmit">
- <i ></i>
- </button>
- </span>
- </div>
- </div>
- </form>
- </div>
- </div>
- </div>
- </div>
- <!-- end Message Input Box -->
- </div>
- </div>
- <!-- Empty Page -->
- <div >
- <div >
- <div >
- <div >
- <div >
- <img src="../assets/user.png" alt="">
- <span ></span>
- <span ></span>
- </div>
- <h5 >Hey, Robert!</h5>
- <p>Please select a chat to start messaging.</p>
- </div>
- </div>
- </div>
- </div>
- <!-- end Empty Page -->
- </div>
- </template>
- PDF会话、PDF预览、文件上传和附件信息等功能,功能代码如下:

- // vue3-pdf-app 组件配置,预览PDF界面配置
- let pdfFile = ref<string>("")
- const config = ref<{}>({
- sidebar: true,
- toolbar: { //工具条配置
- toolbarViewerLeft: {
- findbar: true, //查找功能
- previous: true,//上一页
- next: true,//下一页
- pageNumber: false, //页号
- },
- toolbarViewerRight: { // 工具条右侧
- presentationMode: true,
- openFile: false,
- print: false,
- download: false,
- viewBookmark: false,
- },
- toolbarViewerMiddle: { //工具条中间位置
- zoomOut: true,
- zoomIn: true,
- scaleSelectContainer: true,
- }
- },
- })
- <ul class="chat-list">
- <li class="header d-flex justify-content-between ps-3 pe-3 mb-1">
- <span>PREVIEW</span>
- </li>
- <li>
- // PDF预览组件
- <vue-pdf-app style="height: 100vh;" :config="config" :pdf="pdfFile"></vue-pdf-app>
- </li>
- </ul>
环境要求
开发版本: Node 18.15.0 + Vue 3
项目配置
ChatGPT UI的默认配置存储在“.env”文件中。您将需要覆盖一些值以使ChatGPT UI在本地运行。
- VITE_APP_URL = 填写OpenAI的API地址或第三方封装的API,格式示例:https://api.openai.com/v1/chat/completions
-
- VITE_API_KEY= 填写OpenAI的ApiKey, 格式示例: sk-FihjnhGKO14eYLmPpV1234BlbkFJUq1lS0RNenkDsjgGLopx
-
- VITE_API_UPLOAD = 填写解析pdf文件的API地址,格式示例: http://domain.com/upload/pdf
项目初始化
npm install
运行开发环境
npm run dev
访问项目
http://localhost:1003
构建生产环境
npm run build