Compare commits

...

3 Commits

Author SHA1 Message Date
Rafi
91ca495f20 Using @nuxtjs/color-mode module to control theme 2023-02-12 18:22:08 +08:00
Rafi
42c9864487 Update the layout to accommodate mobile 2023-02-12 17:37:46 +08:00
Rafi
60f8454578 Update the layout to accommodate mobile 2023-02-12 17:32:25 +08:00
5 changed files with 211 additions and 169 deletions

208
app.vue
View File

@@ -1,123 +1,23 @@
<script setup>
import { fetchEventSource } from '@microsoft/fetch-event-source'
const runtimeConfig = useRuntimeConfig()
const currentModel = useCurrentModel()
const openaiApiKey = useApiKey()
const fetchingResponse = ref(false)
const fetchReply = async (message, parentMessageId) => {
const ctrl = new AbortController()
try {
await fetchEventSource('/api/conversation', {
signal: ctrl.signal,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: currentModel.value,
openaiApiKey: openaiApiKey.value,
message: message,
parentMessageId: parentMessageId,
conversationId: currentConversation.value.id
}),
onopen(response) {
if (response.status === 200) {
return;
}
throw new Error(`Failed to send message. HTTP ${response.status} - ${response.statusText}`);
},
onclose() {
throw new Error(`Failed to send message. Server closed the connection unexpectedly.`);
},
onerror(err) {
throw err;
},
onmessage(message) {
if (message.event === 'error') {
throw new Error(JSON.parse(message.data).error);
}
const { type, data } = JSON.parse(message.data);
if (type === 'done') {
if (currentConversation.value.id === null) {
currentConversation.value.id = data.conversationId
}
currentConversation.value.messages[currentConversation.value.messages.length - 1].id = data.messageId
ctrl.abort();
fetchingResponse.value = false
return;
}
if (currentConversation.value.messages[currentConversation.value.messages.length - 1].from === 'ai') {
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += data
} else {
currentConversation.value.messages.push({id: null, from: 'ai', message: data})
}
scrollChatWindow()
},
})
} catch (err) {
ctrl.abort()
showSnackbar(err.message)
fetchingResponse.value = false
}
const colorMode = useColorMode()
const drawer = ref(null)
const themes = ref([
{ title: 'Light', value: 'light' },
{ title: 'Dark', value: 'dark' },
{ title: 'System', value: 'system'}
])
const setTheme = (theme) => {
colorMode.preference = theme
}
const theme = ref('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
const defaultConversation = ref({
id: null,
messages: []
})
const currentConversation = ref({})
const grab = ref(null)
const scrollChatWindow = () => {
grab.value.scrollIntoView({behavior: 'smooth'})
}
const createNewConversation = () => {
currentConversation.value = Object.assign(defaultConversation.value, {
})
}
const send = (message) => {
fetchingResponse.value = true
let parentMessageId = null
if (currentConversation.value.messages.length > 0) {
const lastMessage = currentConversation.value.messages[currentConversation.value.messages.length - 1]
if (lastMessage.from === 'ai' && lastMessage.id !== null) {
parentMessageId = lastMessage.id
}
}
currentConversation.value.messages.push({from: 'me', parentMessageId: parentMessageId, message: message})
fetchReply(message, parentMessageId)
scrollChatWindow()
}
const stop = () => {
ctrl.abort();
fetchingResponse.value = false
}
const snackbar = ref(false)
const snackbarText = ref('')
const showSnackbar = (text) => {
snackbarText.value = text
snackbar.value = true
}
onNuxtReady(() => {
createNewConversation()
})
</script>
<template>
<v-app
:theme="theme"
:theme="$colorMode.value"
>
<v-navigation-drawer
permanent
v-model="drawer"
>
<v-list>
<ModelDialog/>
@@ -128,68 +28,40 @@ onNuxtReady(() => {
<v-list>
<ApiKeyDialog/>
<v-list-item
rounded="xl"
:prepend-icon="theme === 'light' ? 'dark_mode' : 'light_mode'"
:title="(theme === 'light' ? 'Dark' : 'Light') + ' mode'"
@click="toggleTheme"
></v-list-item>
<v-menu
>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
rounded="xl"
:prepend-icon="$colorMode.value === 'light' ? 'light_mode' : 'dark_mode'"
title="Theme mode"
></v-list-item>
</template>
<v-list>
<v-list-item
v-for="(theme, idx) in themes"
:key="idx"
@click="setTheme(theme.value)"
>
<v-list-item-title>{{ theme.title }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-list>
</template>
</v-navigation-drawer>
<v-main>
<div ref="chatWindow">
<v-card
rounded="0"
elevation="0"
v-for="(conversation, index) in currentConversation.messages"
:key="index"
:variant="conversation.from === 'ai' ? 'tonal' : ''"
>
<v-container>
<v-card-text class="text-caption text-disabled">{{ conversation.from }}</v-card-text>
<v-card-text>
<MsgContent :content="conversation.message" />
</v-card-text>
</v-container>
<v-divider></v-divider>
</v-card>
<div ref="grab" class="w-100" style="height: 150px;"></div>
</div>
<v-container>
</v-container>
</v-main>
<v-footer app class="d-flex flex-column">
<div class="px-16 w-100 d-flex align-center">
<v-btn
v-show="fetchingResponse"
icon="close"
title="stop"
class="mr-3"
@click="stop"
></v-btn>
<MsgEditor :send-message="send" :disabled="fetchingResponse" :loading="fetchingResponse" />
</div>
<div class="px-4 py-2 text-disabled text-caption font-weight-light text-center w-100">
{{ new Date().getFullYear() }} {{ runtimeConfig.public.appName }}
</div>
</v-footer>
<v-snackbar
v-model="snackbar"
multi-line
<v-app-bar
class="d-lg-none"
>
{{ snackbarText }}
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
<template v-slot:actions>
<v-btn
color="red"
variant="text"
@click="snackbar = false"
>
Close
</v-btn>
</template>
</v-snackbar>
<v-toolbar-title>{{ runtimeConfig.public.appName }}</v-toolbar-title>
</v-app-bar>
<v-main>
<NuxtPage/>
</v-main>
</v-app>
</template>

View File

@@ -21,5 +21,6 @@ export default defineNuxtConfig({
'vuetify/styles',
'material-design-icons-iconfont/dist/material-design-icons.css',
'highlight.js/styles/panda-syntax-dark.css',
]
],
modules: ['@nuxtjs/color-mode']
})

View File

@@ -8,6 +8,7 @@
"postinstall": "nuxt prepare"
},
"devDependencies": {
"@nuxtjs/color-mode": "^3.2.0",
"material-design-icons-iconfont": "^6.7.0",
"nuxt": "^3.1.2"
},

159
pages/index.vue Normal file
View File

@@ -0,0 +1,159 @@
<script setup>
import { fetchEventSource } from '@microsoft/fetch-event-source'
const runtimeConfig = useRuntimeConfig()
const currentModel = useCurrentModel()
const openaiApiKey = useApiKey()
const fetchingResponse = ref(false)
const fetchReply = async (message, parentMessageId) => {
const ctrl = new AbortController()
try {
await fetchEventSource('/api/conversation', {
signal: ctrl.signal,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: currentModel.value,
openaiApiKey: openaiApiKey.value,
message: message,
parentMessageId: parentMessageId,
conversationId: currentConversation.value.id
}),
onopen(response) {
if (response.status === 200) {
return;
}
throw new Error(`Failed to send message. HTTP ${response.status} - ${response.statusText}`);
},
onclose() {
throw new Error(`Failed to send message. Server closed the connection unexpectedly.`);
},
onerror(err) {
throw err;
},
onmessage(message) {
if (message.event === 'error') {
throw new Error(JSON.parse(message.data).error);
}
const { type, data } = JSON.parse(message.data);
if (type === 'done') {
if (currentConversation.value.id === null) {
currentConversation.value.id = data.conversationId
}
currentConversation.value.messages[currentConversation.value.messages.length - 1].id = data.messageId
ctrl.abort();
fetchingResponse.value = false
return;
}
if (currentConversation.value.messages[currentConversation.value.messages.length - 1].from === 'ai') {
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += data
} else {
currentConversation.value.messages.push({id: null, from: 'ai', message: data})
}
scrollChatWindow()
},
})
} catch (err) {
ctrl.abort()
showSnackbar(err.message)
fetchingResponse.value = false
}
}
const defaultConversation = ref({
id: null,
messages: []
})
const currentConversation = ref({})
const grab = ref(null)
const scrollChatWindow = () => {
grab.value.scrollIntoView({behavior: 'smooth'})
}
const createNewConversation = () => {
currentConversation.value = Object.assign(defaultConversation.value, {
})
}
const send = (message) => {
fetchingResponse.value = true
let parentMessageId = null
if (currentConversation.value.messages.length > 0) {
const lastMessage = currentConversation.value.messages[currentConversation.value.messages.length - 1]
if (lastMessage.from === 'ai' && lastMessage.id !== null) {
parentMessageId = lastMessage.id
}
}
currentConversation.value.messages.push({from: 'me', parentMessageId: parentMessageId, message: message})
fetchReply(message, parentMessageId)
scrollChatWindow()
}
const stop = () => {
ctrl.abort();
fetchingResponse.value = false
}
const snackbar = ref(false)
const snackbarText = ref('')
const showSnackbar = (text) => {
snackbarText.value = text
snackbar.value = true
}
createNewConversation()
</script>
<template>
<div ref="chatWindow">
<v-card
rounded="0"
elevation="0"
v-for="(conversation, index) in currentConversation.messages"
:key="index"
:variant="conversation.from === 'ai' ? 'tonal' : ''"
>
<v-container>
<v-card-text class="text-caption text-disabled">{{ conversation.from }}</v-card-text>
<v-card-text>
<MsgContent :content="conversation.message" />
</v-card-text>
</v-container>
<v-divider></v-divider>
</v-card>
<div ref="grab" class="w-100" style="height: 150px;"></div>
</div>
<v-footer app class="d-flex flex-column">
<div class="px-md-16 w-100 d-flex align-center">
<v-btn
v-show="fetchingResponse"
icon="close"
title="stop"
class="mr-3"
@click="stop"
></v-btn>
<MsgEditor :send-message="send" :disabled="fetchingResponse" :loading="fetchingResponse" />
</div>
<div class="px-4 py-2 text-disabled text-caption font-weight-light text-center w-100">
{{ new Date().getFullYear() }} {{ runtimeConfig.public.appName }}
</div>
</v-footer>
<v-snackbar
v-model="snackbar"
multi-line
>
{{ snackbarText }}
<template v-slot:actions>
<v-btn
color="red"
variant="text"
@click="snackbar = false"
>
Close
</v-btn>
</template>
</v-snackbar>
</template>

View File

@@ -786,6 +786,15 @@
vite-plugin-checker "^0.5.5"
vue-bundle-renderer "^1.0.0"
"@nuxtjs/color-mode@^3.2.0":
version "3.2.0"
resolved "https://registry.npmmirror.com/@nuxtjs/color-mode/-/color-mode-3.2.0.tgz#b5b6a3931a6ddd9646c3aad121d357c635792eb7"
integrity sha512-isDR01yfadopiHQ/VEVUpyNSPrk5PCjUHS4t1qYRZwuRGefU4s9Iaxf6H9nmr1QFzoMgTm+3T0r/54jLwtpZbA==
dependencies:
"@nuxt/kit" "^3.0.0"
lodash.template "^4.5.0"
pathe "^1.0.0"
"@planetscale/database@^1.5.0":
version "1.5.0"
resolved "https://registry.npmmirror.com/@planetscale/database/-/database-1.5.0.tgz#073d9ca9841ad62896a6e31f610e89112e6264ef"