commit
361494d444
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["@commitlint/config-conventional"]
|
||||||
|
}
|
@ -1,5 +1,2 @@
|
|||||||
# OpenAI API Key
|
|
||||||
OPENAI_API_KEY='xxxx'
|
|
||||||
|
|
||||||
# Glob API URL
|
# Glob API URL
|
||||||
VITE_GLOB_API_URL='http://localhost:3002'
|
VITE_GLOB_API_URL='http://localhost:3002'
|
||||||
|
@ -1 +0,0 @@
|
|||||||
components.d.ts
|
|
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
pnpm exec commitlint --config .commitlintrc.json --edit "${1}"
|
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
pnpm exec lint-staged
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
# ChatGPT Web Bot
|
|
||||||
|
|
||||||
[中文](./README.md) | English
|
|
||||||
|
|
||||||
ChartGPT demo page built with express and vue3
|
|
||||||
|
|
||||||
![cover](./docs/cover.png)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
> Make sure `node >= 18`
|
|
||||||
|
|
||||||
If pnpm is not installed
|
|
||||||
```shell
|
|
||||||
npm install pnpm -g
|
|
||||||
```
|
|
||||||
|
|
||||||
install node deps
|
|
||||||
```shell
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
Sign up for an [OpenAI API key](https://platform.openai.com/overview) and store it in your environment.
|
|
||||||
|
|
||||||
```
|
|
||||||
# .env
|
|
||||||
OPENAI_API_KEY="Your Key"
|
|
||||||
```
|
|
||||||
|
|
||||||
Run service
|
|
||||||
```shell
|
|
||||||
pnpm run service
|
|
||||||
```
|
|
||||||
|
|
||||||
Run web
|
|
||||||
```shell
|
|
||||||
pnpm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
Binary file not shown.
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 200 KiB |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,2 @@
|
|||||||
|
# OpenAI API Key
|
||||||
|
OPENAI_API_KEY=
|
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"extends": ["@antfu"]
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["dbaeumer.vscode-eslint"]
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"prettier.enable": false,
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
},
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"typescript",
|
||||||
|
"json",
|
||||||
|
"jsonc",
|
||||||
|
"json5",
|
||||||
|
"yaml"
|
||||||
|
],
|
||||||
|
"cSpell.words": [
|
||||||
|
"antfu",
|
||||||
|
"chatgpt",
|
||||||
|
"esno",
|
||||||
|
"GPTAPI",
|
||||||
|
"OPENAI"
|
||||||
|
]
|
||||||
|
}
|
@ -1,54 +0,0 @@
|
|||||||
import dotenv from 'dotenv'
|
|
||||||
import { ChatGPTAPI } from 'chatgpt'
|
|
||||||
|
|
||||||
interface ChatContext {
|
|
||||||
conversationId?: string
|
|
||||||
parentMessageId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
dotenv.config()
|
|
||||||
|
|
||||||
const apiKey = process.env.OPENAI_API_KEY
|
|
||||||
|
|
||||||
if (apiKey === undefined)
|
|
||||||
throw new Error('OPENAI_API_KEY is not defined')
|
|
||||||
|
|
||||||
const chatContext = new Set<ChatContext>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* More Info: https://github.com/transitive-bullshit/chatgpt-api
|
|
||||||
*/
|
|
||||||
const api = new ChatGPTAPI({ apiKey })
|
|
||||||
|
|
||||||
async function chatReply(message: string) {
|
|
||||||
if (!message)
|
|
||||||
return
|
|
||||||
|
|
||||||
// Get the last context from the chat context
|
|
||||||
// If there is a last context, add it to the options
|
|
||||||
let options = {}
|
|
||||||
const lastContext = Array.from(chatContext).pop()
|
|
||||||
if (lastContext) {
|
|
||||||
const { conversationId, parentMessageId } = lastContext
|
|
||||||
options = { conversationId, parentMessageId }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the message to the API
|
|
||||||
const response = await api.sendMessage(message, { ...options })
|
|
||||||
|
|
||||||
const { conversationId, id } = response
|
|
||||||
|
|
||||||
// Add the new context to the chat context
|
|
||||||
if (conversationId && id)
|
|
||||||
chatContext.add({ conversationId, parentMessageId: id })
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clearChatContext() {
|
|
||||||
// Clear the chat context
|
|
||||||
chatContext.clear()
|
|
||||||
return Promise.resolve({ message: 'Chat context cleared' })
|
|
||||||
}
|
|
||||||
|
|
||||||
export { chatReply, clearChatContext }
|
|
@ -1,26 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
import { chatReply, clearChatContext } from './chatgpt'
|
|
||||||
|
|
||||||
const app = express()
|
|
||||||
|
|
||||||
app.use(express.json())
|
|
||||||
|
|
||||||
app.all('*', (req, res, next) => {
|
|
||||||
res.header('Access-Control-Allow-Origin', '*')
|
|
||||||
res.header('Access-Control-Allow-Headers', 'Content-Type')
|
|
||||||
res.header('Access-Control-Allow-Methods', '*')
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
|
|
||||||
app.listen(3002, () => globalThis.console.log('Server is running on port 3002'))
|
|
||||||
|
|
||||||
app.post('/chat', async (req, res) => {
|
|
||||||
const { message } = req.body
|
|
||||||
const response = await chatReply(message)
|
|
||||||
res.send(response)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post('/clear', async (req, res) => {
|
|
||||||
const response = await clearChatContext()
|
|
||||||
res.send(response)
|
|
||||||
})
|
|
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "chatgpt-web-service",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": false,
|
||||||
|
"description": "ChatGPT Web Bot node service",
|
||||||
|
"keywords": [
|
||||||
|
"chatgpt",
|
||||||
|
"chatbot",
|
||||||
|
"web",
|
||||||
|
"vue"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "esno ./src/index.ts",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chatgpt": "^4.2.0",
|
||||||
|
"express": "^4.18.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^0.35.2",
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/node": "^18.13.0",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"eslint": "^8.34.0",
|
||||||
|
"esno": "^0.16.3",
|
||||||
|
"typescript": "^4.9.5"
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,81 @@
|
|||||||
|
import * as dotenv from 'dotenv'
|
||||||
|
import type { SendMessageOptions } from 'chatgpt'
|
||||||
|
import { ChatGPTAPI } from 'chatgpt'
|
||||||
|
import { sendResponse } from './utils'
|
||||||
|
|
||||||
|
export interface ChatContext {
|
||||||
|
conversationId?: string
|
||||||
|
parentMessageId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
const apiKey = process.env.OPENAI_API_KEY
|
||||||
|
|
||||||
|
if (apiKey === undefined)
|
||||||
|
throw new Error('OPENAI_API_KEY is not defined')
|
||||||
|
|
||||||
|
const chatContext = new Set<ChatContext>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* More Info: https://github.com/transitive-bullshit/chatgpt-api
|
||||||
|
*/
|
||||||
|
const api = new ChatGPTAPI({ apiKey })
|
||||||
|
|
||||||
|
async function chatReply(message: string) {
|
||||||
|
if (!message)
|
||||||
|
return sendResponse({ type: 'Fail', message: 'Message is empty' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the last context from the chat context
|
||||||
|
let options: SendMessageOptions = {}
|
||||||
|
|
||||||
|
const lastContext = Array.from(chatContext).pop()
|
||||||
|
|
||||||
|
if (lastContext)
|
||||||
|
options = { ...lastContext }
|
||||||
|
|
||||||
|
const response = await api.sendMessage(message, { ...options })
|
||||||
|
|
||||||
|
const { conversationId, id } = response
|
||||||
|
|
||||||
|
// Add the new context to the chat context
|
||||||
|
if (conversationId && id)
|
||||||
|
chatContext.add({ conversationId, parentMessageId: id })
|
||||||
|
|
||||||
|
return sendResponse({ type: 'Success', data: response })
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
global.console.log(error)
|
||||||
|
return sendResponse({ type: 'Fail', message: error.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chatReplayOne(message: string, options?: ChatContext) {
|
||||||
|
if (!message)
|
||||||
|
return sendResponse({ type: 'Fail', message: 'Message is empty' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
let messageOptions: SendMessageOptions = {}
|
||||||
|
|
||||||
|
if (options) {
|
||||||
|
const { conversationId, parentMessageId } = options
|
||||||
|
messageOptions = { conversationId, parentMessageId }
|
||||||
|
|
||||||
|
const response = await api.sendMessage(message, { ...messageOptions })
|
||||||
|
|
||||||
|
return sendResponse({ type: 'Success', data: response })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
return sendResponse({ type: 'Fail', message: error.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearChatContext() {
|
||||||
|
// Clear the chat context
|
||||||
|
chatContext.clear()
|
||||||
|
return sendResponse({ type: 'Success', message: 'Chat context cleared' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export { chatReply, chatReplayOne, clearChatContext }
|
@ -0,0 +1,43 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import type { ChatContext } from './chatgpt'
|
||||||
|
import { chatReplayOne, chatReply, clearChatContext } from './chatgpt'
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
app.use(express.json())
|
||||||
|
|
||||||
|
app.all('*', (req, res, next) => {
|
||||||
|
res.header('Access-Control-Allow-Origin', '*')
|
||||||
|
res.header('Access-Control-Allow-Headers', 'Content-Type')
|
||||||
|
res.header('Access-Control-Allow-Methods', '*')
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(3002, () => globalThis.console.log('Server is running on port 3002'))
|
||||||
|
|
||||||
|
app.post('/chat', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { prompt } = req.body as { prompt: string }
|
||||||
|
const response = await chatReply(prompt)
|
||||||
|
res.send(response)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
res.send(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('./chatOne', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { prompt, options = {} } = req.body as { prompt: string; options?: ChatContext }
|
||||||
|
const response = await chatReplayOne(prompt, options)
|
||||||
|
res.send(response)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
res.send(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/clear', async (req, res) => {
|
||||||
|
const response = await clearChatContext()
|
||||||
|
res.send(response)
|
||||||
|
})
|
@ -0,0 +1,22 @@
|
|||||||
|
interface SendResponseOptions {
|
||||||
|
type: 'Success' | 'Fail'
|
||||||
|
message?: string
|
||||||
|
data?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendResponse(options: SendResponseOptions) {
|
||||||
|
if (options.type === 'Success') {
|
||||||
|
return Promise.resolve({
|
||||||
|
message: options.message ?? null,
|
||||||
|
data: options.data ?? null,
|
||||||
|
status: options.type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
return Promise.reject({
|
||||||
|
message: options.message ?? 'Failed',
|
||||||
|
data: options.data ?? null,
|
||||||
|
status: options.type,
|
||||||
|
})
|
||||||
|
}
|
@ -1,13 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { GithubSite, NaiveProvider } from '@/components'
|
import { NConfigProvider } from 'naive-ui'
|
||||||
import Chat from '@/views/Chat/index.vue'
|
import { NaiveProvider } from '@/components/common'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NaiveProvider>
|
<NConfigProvider class="h-full">
|
||||||
<div class="h-full overflow-hidden pb-[50px] p-4">
|
<NaiveProvider>
|
||||||
<Chat />
|
<RouterView />
|
||||||
<GithubSite />
|
</NaiveProvider>
|
||||||
</div>
|
</NConfigProvider>
|
||||||
</NaiveProvider>
|
|
||||||
</template>
|
</template>
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import { post } from '@/utils/request'
|
||||||
|
|
||||||
|
export function fetchChatAPI<T = any>(prompt: string) {
|
||||||
|
return post<T>({
|
||||||
|
url: '/chat',
|
||||||
|
data: { prompt },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearConversations<T = any>() {
|
||||||
|
return post<T>({
|
||||||
|
url: '/clear',
|
||||||
|
})
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { defineComponent, h } from 'vue'
|
|
||||||
import { NConfigProvider, NMessageProvider, useMessage } from 'naive-ui'
|
|
||||||
|
|
||||||
function registerNaiveTools() {
|
|
||||||
window.$message = useMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
const NaiveProviderContent = defineComponent({
|
|
||||||
name: 'NaiveProviderContent',
|
|
||||||
setup() {
|
|
||||||
registerNaiveTools()
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
return h('div')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<NConfigProvider class="h-full">
|
|
||||||
<NMessageProvider>
|
|
||||||
<slot />
|
|
||||||
<NaiveProviderContent />
|
|
||||||
</NMessageProvider>
|
|
||||||
</NConfigProvider>
|
|
||||||
</template>
|
|
@ -0,0 +1,52 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import { SvgIcon } from '@/components/common'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emit {
|
||||||
|
(e: 'click',): void
|
||||||
|
(e: 'edit',): void
|
||||||
|
(e: 'delete',): void
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<Emit>()
|
||||||
|
|
||||||
|
function handleClick(event: Event) {
|
||||||
|
emit('click')
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit() {
|
||||||
|
emit('edit')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
emit('delete')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a
|
||||||
|
class="relative flex items-center gap-3 px-3 py-3 break-all rounded-md cursor-pointer bg-neutral-50 pr-14 hover:bg-neutral-100 group"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<SvgIcon icon="ri:message-3-line" />
|
||||||
|
</span>
|
||||||
|
<div class="relative flex-1 overflow-hidden break-all text-ellipsis whitespace-nowrap max-h-5">
|
||||||
|
<span>{{ text }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="absolute z-10 flex visible right-1">
|
||||||
|
<button class="p-1" @click="handleEdit">
|
||||||
|
<SvgIcon icon="ri:edit-line" />
|
||||||
|
</button>
|
||||||
|
<button class="p-1" @click="handleDelete">
|
||||||
|
<SvgIcon icon="ri:delete-bin-line" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</template>
|
@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
interface Props {
|
||||||
|
reversal?: boolean
|
||||||
|
error?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-2 mt-2 rounded-md" :class="[reversal ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]']">
|
||||||
|
<span class="leading-relaxed whitespace-pre-wrap" :class="[{ 'text-red-400': error }]">
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,4 @@
|
|||||||
|
import Message from './Message/index.vue'
|
||||||
|
import ListItem from './ListItem/index.vue'
|
||||||
|
|
||||||
|
export { Message, ListItem }
|
@ -0,0 +1,116 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import { nextTick, onMounted, ref } from 'vue'
|
||||||
|
import { NButton, NInput, useMessage } from 'naive-ui'
|
||||||
|
import { Message } from './components'
|
||||||
|
import { Layout } from './layout'
|
||||||
|
import { clearConversations, fetchChatAPI } from '@/api'
|
||||||
|
import { HoverButton, SvgIcon } from '@/components/common'
|
||||||
|
|
||||||
|
interface ListProps {
|
||||||
|
dateTime: string
|
||||||
|
message: string
|
||||||
|
reversal?: boolean
|
||||||
|
error?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollRef = ref<HTMLDivElement>()
|
||||||
|
|
||||||
|
const ms = useMessage()
|
||||||
|
|
||||||
|
const prompt = ref('')
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const list = ref<ListProps[]>([])
|
||||||
|
|
||||||
|
onMounted(initChat)
|
||||||
|
|
||||||
|
function initChat() {
|
||||||
|
addMessage('Hi, I am ChatGPT, a chatbot based on GPT-3.', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClear() {
|
||||||
|
try {
|
||||||
|
const { message } = await clearConversations()
|
||||||
|
ms.success(message ?? 'Success')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
ms.error('Clear failed, please try again later.')
|
||||||
|
list.value = []
|
||||||
|
setTimeout(initChat, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEnter(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Enter')
|
||||||
|
handleSubmit()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (loading.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
const message = prompt.value.trim()
|
||||||
|
|
||||||
|
if (!message || !message.length) {
|
||||||
|
ms.warning('Please enter a message')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage(message, true)
|
||||||
|
prompt.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const { data } = await fetchChatAPI(message)
|
||||||
|
addMessage(data?.text ?? '', false)
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
addMessage(`Error: ${error.message ?? 'Request failed, please try again later.'}`, false, true)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMessage(message: string, reversal = false, error = false) {
|
||||||
|
list.value.push({ dateTime: new Date().toLocaleString(), message, reversal, error })
|
||||||
|
nextTick(() => scrollRef.value && (scrollRef.value.scrollTop = scrollRef.value.scrollHeight))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Layout>
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<main class="flex-1 overflow-hidden">
|
||||||
|
<div ref="scrollRef" class="h-full p-4 overflow-hidden overflow-y-auto">
|
||||||
|
<div>
|
||||||
|
<Message
|
||||||
|
v-for="(item, index) of list"
|
||||||
|
:key="index"
|
||||||
|
:date-time="item.dateTime"
|
||||||
|
:message="item.message"
|
||||||
|
:reversal="item.reversal"
|
||||||
|
:error="item.error"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="p-4">
|
||||||
|
<div class="flex items-center justify-between space-x-2">
|
||||||
|
<HoverButton tooltip="Clear conversations" @click="handleClear">
|
||||||
|
<span class="text-xl text-[#4f555e]">
|
||||||
|
<SvgIcon icon="ri:delete-bin-line" />
|
||||||
|
</span>
|
||||||
|
</HoverButton>
|
||||||
|
<NInput v-model:value="prompt" placeholder="Type a message..." @keypress="handleEnter" />
|
||||||
|
<NButton type="primary" :loading="loading" @click="handleSubmit">
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon icon="ri:send-plane-fill" />
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</template>
|
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import { NLayout, NLayoutContent } from 'naive-ui'
|
||||||
|
import Sider from './sider/index.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full overflow-hidden border rounded-md shadow-md min-w-[640px]">
|
||||||
|
<NLayout class="h-full" has-sider>
|
||||||
|
<Sider />
|
||||||
|
<NLayoutContent class="h-full">
|
||||||
|
<slot />
|
||||||
|
</NLayoutContent>
|
||||||
|
</NLayout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
@ -0,0 +1,3 @@
|
|||||||
|
import Layout from './Layout.vue'
|
||||||
|
|
||||||
|
export { Layout }
|
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import { HoverButton, SvgIcon, UserAvatar } from '@/components/common'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<footer class="flex items-center justify-between p-4 overflow-hidden border-t">
|
||||||
|
<UserAvatar />
|
||||||
|
|
||||||
|
<HoverButton tooltip="Setting">
|
||||||
|
<span class="text-xl text-[#4f555e]">
|
||||||
|
<SvgIcon icon="ri:settings-4-line" />
|
||||||
|
</span>
|
||||||
|
</HoverButton>
|
||||||
|
</footer>
|
||||||
|
</template>
|
@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import { NScrollbar } from 'naive-ui'
|
||||||
|
import ListItem from './ListItem.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NScrollbar class="px-4">
|
||||||
|
<div class="flex flex-col gap-2 text-sm">
|
||||||
|
<ListItem text="Learning correlation" />
|
||||||
|
<ListItem text="Write Code" />
|
||||||
|
<ListItem text="docs..." />
|
||||||
|
</div>
|
||||||
|
</NScrollbar>
|
||||||
|
</template>
|
@ -0,0 +1,52 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import { SvgIcon } from '@/components/common'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emit {
|
||||||
|
(e: 'click',): void
|
||||||
|
(e: 'edit',): void
|
||||||
|
(e: 'delete',): void
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits<Emit>()
|
||||||
|
|
||||||
|
function handleClick(event: Event) {
|
||||||
|
emit('click')
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit() {
|
||||||
|
emit('edit')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
emit('delete')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a
|
||||||
|
class="relative flex items-center gap-3 px-3 py-3 break-all rounded-md cursor-pointer bg-neutral-50 pr-14 hover:bg-neutral-100 group"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<SvgIcon icon="ri:message-3-line" />
|
||||||
|
</span>
|
||||||
|
<div class="relative flex-1 overflow-hidden break-all text-ellipsis whitespace-nowrap max-h-5">
|
||||||
|
<span>{{ text }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="absolute z-10 flex visible right-1">
|
||||||
|
<button class="p-1" @click="handleEdit">
|
||||||
|
<SvgIcon icon="ri:edit-line" />
|
||||||
|
</button>
|
||||||
|
<button class="p-1" @click="handleDelete">
|
||||||
|
<SvgIcon icon="ri:delete-bin-line" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</template>
|
@ -0,0 +1,65 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { NButton, NLayoutSider, useMessage } from 'naive-ui'
|
||||||
|
import List from './List.vue'
|
||||||
|
import Footer from './Footer.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
collapsed?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emit {
|
||||||
|
(e: 'update:collapsed', value: boolean): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
collapsed: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emit>()
|
||||||
|
|
||||||
|
const ms = useMessage()
|
||||||
|
|
||||||
|
const collapsed = ref(props.collapsed)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.collapsed,
|
||||||
|
(value: boolean) => {
|
||||||
|
collapsed.value = value
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
function handleAdd() {
|
||||||
|
ms.info('Coming soon...')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCollapsed() {
|
||||||
|
collapsed.value = !collapsed.value
|
||||||
|
emit('update:collapsed', collapsed.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NLayoutSider
|
||||||
|
:collapsed="collapsed"
|
||||||
|
:collapsed-width="0"
|
||||||
|
:width="260"
|
||||||
|
collapse-mode="width"
|
||||||
|
show-trigger="arrow-circle"
|
||||||
|
bordered
|
||||||
|
@update:collapsed="handleCollapsed"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<main class="flex-1 min-h-0 overflow-hidden">
|
||||||
|
<div class="p-4">
|
||||||
|
<NButton dashed block @click="handleAdd">
|
||||||
|
New chat
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<List />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</NLayoutSider>
|
||||||
|
</template>
|
@ -0,0 +1,3 @@
|
|||||||
|
import Chat from './Chat/index.vue'
|
||||||
|
|
||||||
|
export { Chat }
|
@ -0,0 +1,20 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
interface Emit {
|
||||||
|
(e: 'click'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emit>()
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
emit('click')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-center w-10 h-10 transition rounded-full hover:bg-neutral-100"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
@ -0,0 +1,46 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { PopoverPlacement } from 'naive-ui'
|
||||||
|
import { NTooltip } from 'naive-ui'
|
||||||
|
import Button from './Button.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tooltip?: string
|
||||||
|
placement?: PopoverPlacement
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emit {
|
||||||
|
(e: 'click'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
tooltip: '',
|
||||||
|
placement: 'bottom',
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emit>()
|
||||||
|
|
||||||
|
const showTooltip = computed(() => Boolean(props.tooltip))
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
emit('click')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="showTooltip">
|
||||||
|
<NTooltip :placement="placement" trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<Button @click="handleClick">
|
||||||
|
<slot />
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
{{ tooltip }}
|
||||||
|
</NTooltip>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<Button @click="handleClick">
|
||||||
|
<slot />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, h } from 'vue'
|
||||||
|
import {
|
||||||
|
NDialogProvider,
|
||||||
|
NLoadingBarProvider,
|
||||||
|
NMessageProvider,
|
||||||
|
NNotificationProvider,
|
||||||
|
useDialog,
|
||||||
|
useLoadingBar,
|
||||||
|
useMessage,
|
||||||
|
useNotification,
|
||||||
|
} from 'naive-ui'
|
||||||
|
|
||||||
|
function registerNaiveTools() {
|
||||||
|
window.$loadingBar = useLoadingBar()
|
||||||
|
window.$dialog = useDialog()
|
||||||
|
window.$message = useMessage()
|
||||||
|
window.$notification = useNotification()
|
||||||
|
}
|
||||||
|
|
||||||
|
const NaiveProviderContent = defineComponent({
|
||||||
|
name: 'NaiveProviderContent',
|
||||||
|
setup() {
|
||||||
|
registerNaiveTools()
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h('div')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NLoadingBarProvider>
|
||||||
|
<NDialogProvider>
|
||||||
|
<NNotificationProvider>
|
||||||
|
<NMessageProvider>
|
||||||
|
<slot />
|
||||||
|
<NaiveProviderContent />
|
||||||
|
</NMessageProvider>
|
||||||
|
</NNotificationProvider>
|
||||||
|
</NDialogProvider>
|
||||||
|
</NLoadingBarProvider>
|
||||||
|
</template>
|
@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import { GithubSite } from '@/components/custom'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-10 h-10 overflow-hidden rounded-full">
|
||||||
|
<img class="object-cover" src="@/assets/avatar.jpg" alt="avatar">
|
||||||
|
</div>
|
||||||
|
<div class="ml-2">
|
||||||
|
<h2 class="font-bold text-md">
|
||||||
|
ChenZhaoYu
|
||||||
|
</h2>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
<GithubSite />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
@ -0,0 +1,6 @@
|
|||||||
|
import HoverButton from './HoverButton/index.vue'
|
||||||
|
import NaiveProvider from './NaiveProvider/index.vue'
|
||||||
|
import SvgIcon from './SvgIcon/index.vue'
|
||||||
|
import UserAvatar from './UserAvatar/index.vue'
|
||||||
|
|
||||||
|
export { HoverButton, NaiveProvider, SvgIcon, UserAvatar }
|
@ -0,0 +1,3 @@
|
|||||||
|
import GithubSite from './GithubSite.vue'
|
||||||
|
|
||||||
|
export { GithubSite }
|
@ -1,5 +0,0 @@
|
|||||||
import NaiveProvider from './NaiveProvider.vue'
|
|
||||||
import Icon from './Icon.vue'
|
|
||||||
import GithubSite from './GithubSite.vue'
|
|
||||||
|
|
||||||
export { NaiveProvider, Icon, GithubSite }
|
|
@ -0,0 +1,22 @@
|
|||||||
|
import type { App } from 'vue'
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: () => import('@/views/home/index.vue'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes,
|
||||||
|
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function setupRouter(app: App) {
|
||||||
|
app.use(router)
|
||||||
|
await router.isReady()
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
interface Window {
|
interface Window {
|
||||||
|
$loadingBar?: import('naive-ui').LoadingBarProviderInst;
|
||||||
|
$dialog?: import('naive-ui').DialogProviderInst;
|
||||||
$message?: import('naive-ui').MessageProviderInst;
|
$message?: import('naive-ui').MessageProviderInst;
|
||||||
|
$notification?: import('naive-ui').NotificationProviderInst;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import axios, { type AxiosResponse } from 'axios'
|
||||||
|
|
||||||
|
const service = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_GLOB_API_URL,
|
||||||
|
timeout: 10 * 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
service.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error.response)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
service.interceptors.response.use(
|
||||||
|
(response: AxiosResponse): AxiosResponse => {
|
||||||
|
if (response.status === 200)
|
||||||
|
return response
|
||||||
|
|
||||||
|
throw new Error(response.status.toString())
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export default service
|
@ -0,0 +1,72 @@
|
|||||||
|
import type { AxiosResponse } from 'axios'
|
||||||
|
import request from './axios'
|
||||||
|
|
||||||
|
export interface HttpOption {
|
||||||
|
url: string
|
||||||
|
data?: any
|
||||||
|
method?: string
|
||||||
|
headers?: any
|
||||||
|
beforeRequest?: () => void
|
||||||
|
afterRequest?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtraOption {
|
||||||
|
notification?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Response<T = any> {
|
||||||
|
data: T
|
||||||
|
message: string | null
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function http<T = any>({ url, data, method, headers, beforeRequest, afterRequest }: HttpOption) {
|
||||||
|
const successHandler = (res: AxiosResponse<Response<T>>) => {
|
||||||
|
if (res.data.status === 'Success')
|
||||||
|
return res.data
|
||||||
|
|
||||||
|
return Promise.reject(res.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const failHandler = (error: Response<Error>) => {
|
||||||
|
afterRequest?.()
|
||||||
|
throw new Error(error?.message || 'Error')
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeRequest?.()
|
||||||
|
|
||||||
|
method = method || 'GET'
|
||||||
|
|
||||||
|
const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {})
|
||||||
|
|
||||||
|
return method === 'GET'
|
||||||
|
? request.get(url, { params }).then(successHandler, failHandler)
|
||||||
|
: request.post(url, params, { headers }).then(successHandler, failHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get<T = any>(
|
||||||
|
{ url, data, method = 'GET', beforeRequest, afterRequest }: HttpOption,
|
||||||
|
): Promise<Response<T>> {
|
||||||
|
return http<T>({
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
data,
|
||||||
|
beforeRequest,
|
||||||
|
afterRequest,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function post<T = any>(
|
||||||
|
{ url, data, method = 'POST', headers, beforeRequest, afterRequest }: HttpOption,
|
||||||
|
): Promise<Response<T>> {
|
||||||
|
return http<T>({
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
data,
|
||||||
|
headers,
|
||||||
|
beforeRequest,
|
||||||
|
afterRequest,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default post
|
@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span class="leading-relaxed whitespace-pre-wrap">
|
|
||||||
<slot />
|
|
||||||
</span>
|
|
||||||
</template>
|
|
@ -1,3 +0,0 @@
|
|||||||
import Message from './Message/index.vue'
|
|
||||||
|
|
||||||
export { Message }
|
|
@ -1,118 +0,0 @@
|
|||||||
<script setup lang='ts'>
|
|
||||||
import { nextTick, onMounted, ref } from 'vue'
|
|
||||||
import { NButton, NInput, NPopover, useMessage } from 'naive-ui'
|
|
||||||
import { Message } from './components'
|
|
||||||
import { clearChatContext, fetchChatAPI } from './request'
|
|
||||||
import { Icon } from '@/components'
|
|
||||||
|
|
||||||
interface ListProps {
|
|
||||||
dateTime: string
|
|
||||||
message: string
|
|
||||||
reversal?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollRef = ref<HTMLDivElement>()
|
|
||||||
|
|
||||||
const ms = useMessage()
|
|
||||||
|
|
||||||
const prompt = ref('')
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const list = ref<ListProps[]>([])
|
|
||||||
|
|
||||||
onMounted(initChat)
|
|
||||||
|
|
||||||
function initChat() {
|
|
||||||
addMessage('Hi, I am ChatGPT, a chatbot based on GPT-3.', false)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleClear() {
|
|
||||||
try {
|
|
||||||
const { message } = await clearChatContext()
|
|
||||||
ms.success(message)
|
|
||||||
list.value = []
|
|
||||||
setTimeout(initChat, 100)
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
ms.error('Clear failed, please try again later.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEnter(event: KeyboardEvent) {
|
|
||||||
if (event.key === 'Enter')
|
|
||||||
handleSubmit()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
|
||||||
const message = prompt.value.trim()
|
|
||||||
|
|
||||||
if (!message || !message.length) {
|
|
||||||
ms.warning('Please enter a message')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
addMessage(message, true)
|
|
||||||
prompt.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
const { text } = await fetchChatAPI(message)
|
|
||||||
addMessage(text, false)
|
|
||||||
}
|
|
||||||
catch (error: any) {
|
|
||||||
addMessage(error.message ?? 'Request failed, please try again later.', false)
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addMessage(message: string, reversal = false) {
|
|
||||||
list.value.push({ dateTime: new Date().toLocaleString(), message, reversal })
|
|
||||||
nextTick(() => scrollRef.value && (scrollRef.value.scrollTop = scrollRef.value.scrollHeight))
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-col h-full overflow-hidden border rounded-md shadow-md">
|
|
||||||
<header class="flex items-center justify-between p-4">
|
|
||||||
<h1 class="text-xl font-bold">
|
|
||||||
ChatGPT Web
|
|
||||||
</h1>
|
|
||||||
<div>
|
|
||||||
<NPopover>
|
|
||||||
<template #trigger>
|
|
||||||
<button
|
|
||||||
class="w-[40px] h-[40px] rounded-full hover:bg-neutral-100 transition flex justify-center items-center"
|
|
||||||
@click="handleClear"
|
|
||||||
>
|
|
||||||
<Icon icon="ri:delete-bin-6-line" />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<span>Clear Context</span>
|
|
||||||
</NPopover>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="flex-1 overflow-hidden border-y">
|
|
||||||
<div ref="scrollRef" class="h-full p-4 overflow-hidden overflow-y-auto">
|
|
||||||
<div>
|
|
||||||
<Message
|
|
||||||
v-for="(item, index) of list" :key="index" :date-time="item.dateTime" :message="item.message"
|
|
||||||
:reversal="item.reversal"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer class="p-4">
|
|
||||||
<div class="flex items-center justify-between space-x-2">
|
|
||||||
<NInput v-model:value="prompt" placeholder="Type a message..." @keypress="handleEnter" />
|
|
||||||
<NButton type="primary" :loading="loading" @click="handleSubmit">
|
|
||||||
<template #icon>
|
|
||||||
<Icon icon="ri:send-plane-fill" />
|
|
||||||
</template>
|
|
||||||
</NButton>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@ -1,41 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
const BASE_URL = import.meta.env.VITE_GLOB_API_URL
|
|
||||||
|
|
||||||
async function fetchChatAPI(message: string) {
|
|
||||||
if (!message || message.trim() === '')
|
|
||||||
return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { status, data } = await axios.post(`${BASE_URL}/chat`, { message })
|
|
||||||
|
|
||||||
if (status === 200) {
|
|
||||||
if (data.text)
|
|
||||||
return Promise.resolve(data)
|
|
||||||
|
|
||||||
if (data.statusText)
|
|
||||||
return Promise.reject(new Error(data.statusText))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(new Error('Request failed'))
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clearChatContext() {
|
|
||||||
try {
|
|
||||||
const { status, data } = await axios.post(`${BASE_URL}/clear`)
|
|
||||||
|
|
||||||
if (status === 200)
|
|
||||||
return Promise.resolve(data)
|
|
||||||
|
|
||||||
return Promise.reject(new Error('Request failed'))
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { fetchChatAPI, clearChatContext }
|
|
@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang='ts'>
|
||||||
|
import { Chat } from '@/components/business'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full p-4 overflow-hidden">
|
||||||
|
<Chat />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"lib": ["DOM", "ESNext"],
|
"lib": ["DOM", "ESNext"],
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
},
|
},
|
||||||
"types": ["vite/client", "node", "naive-ui/volar"]
|
"types": ["vite/client", "node", "naive-ui/volar"]
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue