mirror of
https://github.com/ParisNeo/lollms-webui.git
synced 2024-12-21 05:13:10 +00:00
commit
089a35e926
38
docs/dev/new_ui_dev.md
Normal file
38
docs/dev/new_ui_dev.md
Normal file
@ -0,0 +1,38 @@
|
||||
# UI development log, todo's and more
|
||||
|
||||
The new UI is build using Node.js VUE3 + Vite. It uses tailwindcss, feathericons, openfonts and flowbite-vue components.
|
||||
|
||||
## Todo's
|
||||
|
||||
- Add ability to select multiple discussions to export or delete [WIP]
|
||||
- Add toast messages for errors and successes
|
||||
- Populate settings with settings controls [WIP]
|
||||
- Connect Settings to backend, ability to save changes
|
||||
- Add DB switcher (im thinking in the settings view)
|
||||
- Make the UI work good on mobile
|
||||
- Scroll to bottom
|
||||
- Scroll to top
|
||||
- Need to fix colors for `<input />` fields
|
||||
- Create status bar for backend to display if something is generating on the backend
|
||||
- Create stop generating button
|
||||
- Fix the generated message formatting - add line breaks, also for user input messages.
|
||||
- Add ability for users to style the whole UI, either changing Hue or changing every color manually.
|
||||
- Maybe try to set the chatbox to float to the bottom (always on the bottom of the screen)
|
||||
- Create a panel in the Settings tab to create new personalities
|
||||
- Need to fix when user inputs message it shows up in the discussion array and then add new message for bot that is typing.
|
||||
- Need to investigate performance of websocket when message is being streamed back to the UI
|
||||
- On first launch of the UI force users to create "User" personality, to be used as "User" for any or all input messages.
|
||||
- Connect delete / export discussions to backend functions.
|
||||
- Need to fix when deleting multiple discussions to not loose loading animation for each discussion when list gets updated
|
||||
- Need to add loading feedback for when a new discussion is being created
|
||||
|
||||
## Done
|
||||
|
||||
- Fix discussion list width so that it stays static and dont resize depending on message contents [DONE]
|
||||
- Add chat input field [DONE]
|
||||
- Make search filter work [DONE]
|
||||
- Add clear filter button to search input field [DONE]
|
||||
- Add modal to ask user if you sure about to delete [DONE but in different way]
|
||||
- Fix up the discussion array to filter out the messages by type not by count. (conditionner and )[DONE]
|
||||
- Add title of current discussion to page [DONE]
|
||||
|
@ -1,21 +1,5 @@
|
||||
# GPT4ALL-UI Web interface VUE3
|
||||
|
||||
## Todo's
|
||||
|
||||
- Fix discussion list width so that it stays static and dont resize depending on message contents [DONE]
|
||||
- Add chat input field [DONE]
|
||||
- Add ability to select multiple discussions to export or delete
|
||||
- Add modal to ask user if you sure about to delete
|
||||
- Add toast messages for errors and successes
|
||||
- Populate settings with settings controls [WIP]
|
||||
- Make search filter work [DONE]
|
||||
- Add clear filter button to search input field [DONE]
|
||||
- Add DB switcher (im thinking in the settings view)
|
||||
- Make the UI work good on mobile
|
||||
- Scroll to bottom
|
||||
- Scroll to top
|
||||
- Need to fix colors for `<input />` fields
|
||||
|
||||
## Dependencies for development
|
||||
|
||||
You mus have [Node.js](https://nodejs.org/en) installed on your computer.
|
||||
|
1
web/dist/assets/index-2edf1005.css
vendored
1
web/dist/assets/index-2edf1005.css
vendored
File diff suppressed because one or more lines are too long
11
web/dist/assets/index-2f3efff7.js
vendored
Normal file
11
web/dist/assets/index-2f3efff7.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/index-76ef06fb.css
vendored
Normal file
1
web/dist/assets/index-76ef06fb.css
vendored
Normal file
File diff suppressed because one or more lines are too long
11
web/dist/assets/index-ffda5761.js
vendored
11
web/dist/assets/index-ffda5761.js
vendored
File diff suppressed because one or more lines are too long
4
web/dist/index.html
vendored
4
web/dist/index.html
vendored
@ -6,8 +6,8 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GPT4All - WEBUI</title>
|
||||
<script type="module" crossorigin src="/assets/index-ffda5761.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-2edf1005.css">
|
||||
<script type="module" crossorigin src="/assets/index-2f3efff7.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-76ef06fb.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
@ -5,16 +5,37 @@
|
||||
<div
|
||||
class="flex items-center gap-2 px-3 py-3 rounded-lg bg-bg-light-tone-panel dark:bg-bg-dark-tone-panel shadow-lg ">
|
||||
|
||||
<textarea id="chat" rows="1"
|
||||
<textarea id="chat" rows="1" v-model="message"
|
||||
class="block min-h-11 no-scrollbar p-2.5 w-full text-sm text-gray-900 bg-bg-light rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-bg-dark dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="Send message..." @keydown.enter.exact="submitOnEnter($event)"></textarea>
|
||||
<button type="submit" on on-click=""
|
||||
class="inline-flex justify-center p-2 rounded-full cursor-pointer hover:text-primary duration-75 active:scale-90">
|
||||
|
||||
<i data-feather="send" class=" w-6 h-6 m-1"></i>
|
||||
<!-- BUTTONS -->
|
||||
<div class="inline-flex justify-center rounded-full cursor-pointer">
|
||||
|
||||
<span class="sr-only">Send message</span>
|
||||
</button>
|
||||
<button v-if="!loading" type="button" @click="submit"
|
||||
class=" w-6 hover:text-secondary duration-75 active:scale-90">
|
||||
|
||||
<i data-feather="send"></i>
|
||||
|
||||
<span class="sr-only">Send message</span>
|
||||
</button>
|
||||
<div v-if=" loading" title="Waiting for reply">
|
||||
<!-- SPINNER -->
|
||||
<div role="status">
|
||||
<svg aria-hidden="true"
|
||||
class="w-6 h-6 animate-spin fill-secondary"
|
||||
viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor" />
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill" />
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -28,9 +49,19 @@ import feather from 'feather-icons'
|
||||
export default {
|
||||
name: 'ChatBox',
|
||||
emits: ["messageSentEvent"],
|
||||
props: {
|
||||
|
||||
loading: false
|
||||
|
||||
},
|
||||
setup() {
|
||||
return {}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
message: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sendMessageEvent(msg) {
|
||||
|
||||
@ -40,15 +71,22 @@ export default {
|
||||
submitOnEnter(event) {
|
||||
if (event.which === 13) {
|
||||
event.preventDefault(); // Prevents the addition of a new line in the text field
|
||||
console.log("enter detected");
|
||||
|
||||
if (!event.repeat) {
|
||||
|
||||
this.sendMessageEvent(event.target.value)
|
||||
event.target.value="" // Clear input field
|
||||
this.sendMessageEvent(this.message)
|
||||
this.message = "" // Clear input field
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
submit() {
|
||||
if (this.message) {
|
||||
this.sendMessageEvent(this.message)
|
||||
this.message = ""
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
nextTick(() => {
|
||||
|
@ -1,17 +1,32 @@
|
||||
<template>
|
||||
<div :class="selected ? 'bg-bg-light-discussion dark:bg-bg-dark-discussion shadow-md' : ''"
|
||||
class="container flex flex-col sm:flex-row item-center shadow-sm gap-2 py-2 my-2 hover:shadow-md hover:bg-primary-light dark:hover:bg-primary rounded-md p-2 duration-75 group cursor-pointer"
|
||||
class="container flex sm:flex-row item-center shadow-sm gap-2 py-2 my-2 hover:shadow-md hover:bg-primary-light dark:hover:bg-primary rounded-md p-2 duration-75 group cursor-pointer"
|
||||
:id="'dis-' + id" @click.stop="selectEvent()">
|
||||
<!-- INDICATOR FOR SELECTED ITEM -->
|
||||
<div v-if="selected" class="items-center inline-block min-h-full w-2 rounded-xl self-stretch "
|
||||
:class="loading ? 'animate-bounce bg-accent ' : ' bg-secondary '"></div>
|
||||
<div v-if="!selected" class="items-center inline-block min-h-full w-2 rounded-xl self-stretch"></div>
|
||||
|
||||
<!-- PRE TITLE SECTION -->
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<!-- CHECKBOX -->
|
||||
<div v-if="isCheckbox">
|
||||
<input type="checkbox"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
|
||||
@click.stop v-model="checkBoxValue_local" @input="checkedChangeEvent($event, id)">
|
||||
|
||||
</div>
|
||||
<!-- INDICATOR FOR SELECTED ITEM -->
|
||||
<div v-if="selected" class="min-h-full w-2 rounded-xl self-stretch "
|
||||
:class="loading ? 'animate-bounce bg-accent ' : ' bg-secondary '"></div>
|
||||
<div v-if="!selected" class="w-2"
|
||||
:class="loading ? 'min-h-full w-2 rounded-xl self-stretch animate-bounce bg-accent ' : ' '"></div>
|
||||
|
||||
</div>
|
||||
<!-- TITLE -->
|
||||
<p v-if="!editTitle" :title="title" class="truncate w-full">{{ title ? title === "untitled" ? "New discussion" : title : "New discussion" }}</p>
|
||||
<p v-if="!editTitle" :title="title" class="truncate w-full">{{ title ? title === "untitled" ? "New discussion" :
|
||||
title : "New discussion" }}</p>
|
||||
|
||||
<input v-if="editTitle" type="text" id="title-box" class="bg-bg-light dark:bg-bg-dark rounded-md border-0 w-full -m-1 p-1"
|
||||
:value="title" required @input="chnageTitle($event.target.value)" @click.stop>
|
||||
<input v-if="editTitle" type="text" id="title-box"
|
||||
class="bg-bg-light dark:bg-bg-dark rounded-md border-0 w-full -m-1 p-1" :value="title" required
|
||||
@keydown.enter.exact="editTitleEvent()" @keydown.esc.exact="editTitleMode = false"
|
||||
@input="chnageTitle($event.target.value)" @click.stop>
|
||||
|
||||
<!-- CONTROL BUTTONS -->
|
||||
<div class="flex items-center flex-1 max-h-6">
|
||||
@ -29,7 +44,7 @@
|
||||
<!-- EDIT TITLE CONFIRM -->
|
||||
<div v-if="showConfirmation && editTitleMode" class="flex gap-3 flex-1 items-center justify-end duration-75">
|
||||
<button class="text-2xl hover:text-red-600 duration-75 active:scale-90 " title="Discard title changes"
|
||||
type="button" @click.stop="editTitleMode = false ">
|
||||
type="button" @click.stop="editTitleMode = false">
|
||||
<i data-feather="x"></i>
|
||||
</button>
|
||||
<button class="text-2xl hover:text-secondary duration-75 active:scale-90" title="Confirm title changes"
|
||||
@ -60,12 +75,14 @@ import feather from 'feather-icons'
|
||||
|
||||
export default {
|
||||
name: 'Discussion',
|
||||
emits: ['delete', 'select', 'editTitle'],
|
||||
emits: ['delete', 'select', 'editTitle', 'checked'],
|
||||
props: {
|
||||
id: Number,
|
||||
title: String,
|
||||
selected: Boolean,
|
||||
loading: Boolean
|
||||
loading: Boolean,
|
||||
isCheckbox: Boolean,
|
||||
checkBoxValue: Boolean
|
||||
},
|
||||
setup() {
|
||||
|
||||
@ -76,6 +93,7 @@ export default {
|
||||
editTitleMode: false,
|
||||
editTitle: false,
|
||||
newTitle: String,
|
||||
checkBoxValue_local: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -87,21 +105,24 @@ export default {
|
||||
this.$emit("select")
|
||||
},
|
||||
editTitleEvent() {
|
||||
this.editTitle= false
|
||||
this.editTitleMode= false
|
||||
this.editTitle = false
|
||||
this.editTitleMode = false
|
||||
this.showConfirmation = false
|
||||
this.$emit("editTitle",
|
||||
{
|
||||
title: this.newTitle,
|
||||
id: this.id
|
||||
})
|
||||
{
|
||||
title: this.newTitle,
|
||||
id: this.id
|
||||
})
|
||||
},
|
||||
chnageTitle(text){
|
||||
this.newTitle=text
|
||||
chnageTitle(text) {
|
||||
this.newTitle = text
|
||||
},
|
||||
checkedChangeEvent(event, id) {
|
||||
this.$emit("checked", event, id)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.newTitle= this.title
|
||||
this.newTitle = this.title
|
||||
nextTick(() => {
|
||||
feather.replace()
|
||||
|
||||
@ -116,8 +137,18 @@ export default {
|
||||
},
|
||||
editTitleMode(newval) {
|
||||
|
||||
this.showConfirmation=newval
|
||||
this.showConfirmation = newval
|
||||
this.editTitle = newval
|
||||
},
|
||||
checkBoxValue(newval, oldval) {
|
||||
this.checkBoxValue_local = newval
|
||||
|
||||
},
|
||||
selected(newval, oldval) {
|
||||
if (newval) {
|
||||
const realTitle= this.title ? this.title === "untitled" ? "New discussion" : this.title : "New discussion"
|
||||
document.title = 'GPT4ALL - WEBUI - '+ realTitle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,6 @@ import SettingsView from '../views/SettingsView.vue'
|
||||
import TrainingView from '../views/TrainingView.vue'
|
||||
import DiscussionsView from '../views/DiscussionsView.vue'
|
||||
|
||||
// const scrollBehavior = (to, from, savedPosition) => {
|
||||
// return savedPosition || { top: 0, left: 0 }
|
||||
// }
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@ -40,6 +37,8 @@ const router = createRouter({
|
||||
|
||||
],
|
||||
// scrollBehavior
|
||||
|
||||
})
|
||||
|
||||
|
||||
export default router
|
||||
|
@ -1,73 +1,98 @@
|
||||
<template>
|
||||
<div
|
||||
class="overflow-y-scroll flex flex-col no-scrollbar shadow-lg min-w-[24rem] max-w-[24rem] bg-bg-light-tone dark:bg-bg-dark-tone ">
|
||||
class="overflow-y-scroll flex flex-col no-scrollbar shadow-lg min-w-[24rem] max-w-[24rem] bg-bg-light-tone dark:bg-bg-dark-tone">
|
||||
<!-- LEFT SIDE PANEL -->
|
||||
<div
|
||||
class="z-10 sticky top-0 flex-row p-2 flex items-center gap-3 flex-0 bg-bg-light-tone dark:bg-bg-dark-tone mt-0 px-4 shadow-md">
|
||||
<!-- CONTROL PANEL -->
|
||||
<button class=" text-2xl hover:text-secondary duration-75 active:scale-90 " title="Create new discussion"
|
||||
type="button" @click="createNewDiscussion()">
|
||||
<i data-feather="plus"></i>
|
||||
</button>
|
||||
<button class=" text-2xl hover:text-secondary duration-75 active:scale-90 "
|
||||
title="Reset database, remove all discussions">
|
||||
<i data-feather="refresh-ccw"></i>
|
||||
</button>
|
||||
<button class=" text-2xl hover:text-secondary duration-75 active:scale-90 " title="Export database"
|
||||
type="button">
|
||||
<i data-feather="database"></i>
|
||||
</button>
|
||||
<button class=" text-2xl hover:text-secondary duration-75 active:scale-90 rotate-90"
|
||||
title="Export discussion to a file" type="button">
|
||||
<i data-feather="log-out"></i>
|
||||
</button>
|
||||
|
||||
<div class="z-10 sticky top-0 flex-col bg-bg-light-tone dark:bg-bg-dark-tone shadow-md">
|
||||
<!-- SEARCH BAR -->
|
||||
<form>
|
||||
<form class="flex-row p-4 items-center gap-3 flex-0 w-full">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<div class="scale-75 ">
|
||||
<div class="scale-75">
|
||||
<i data-feather="search"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3 ">
|
||||
<div class=" hover:text-secondary duration-75 active:scale-90 "
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<div class="hover:text-secondary duration-75 active:scale-90"
|
||||
:class="filterTitle ? 'visible' : 'invisible'" title="Clear" @click="filterTitle = ''">
|
||||
<i data-feather="x"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="search" id="default-search"
|
||||
class="block w-full p-2 pl-10 pr-10 text-sm border border-gray-300 rounded-lg bg-bg-light focus:ring-secondary focus:border-secondary dark:bg-bg-dark dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-secondary dark:focus:border-secondary "
|
||||
class="block w-full p-2 pl-10 pr-10 text-sm border border-gray-300 rounded-lg bg-bg-light focus:ring-secondary focus:border-secondary dark:bg-bg-dark dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-secondary dark:focus:border-secondary"
|
||||
placeholder="Search..." title="Filter discussions by title" v-model="filterTitle"
|
||||
@input="filterDiscussions()">
|
||||
@input="filterDiscussions()" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- CONTROL PANEL -->
|
||||
<div class="flex-row p-4 pt-0 flex items-center gap-3 flex-0">
|
||||
|
||||
<!-- MAIN BUTTONS -->
|
||||
<button class="text-2xl hover:text-secondary duration-75 active:scale-90" title="Create new discussion"
|
||||
type="button" @click="createNewDiscussion()">
|
||||
<i data-feather="plus"></i>
|
||||
</button>
|
||||
<button class="text-2xl hover:text-secondary duration-75 active:scale-90" title="Edit discussion list"
|
||||
type="button" @click="isCheckbox = !isCheckbox" :class="isCheckbox ? 'text-secondary' : ''">
|
||||
<i data-feather="check-square"></i>
|
||||
</button>
|
||||
<button class="text-2xl hover:text-secondary duration-75 active:scale-90"
|
||||
title="Reset database, remove all discussions">
|
||||
<i data-feather="refresh-ccw"></i>
|
||||
</button>
|
||||
<button class="text-2xl hover:text-secondary duration-75 active:scale-90" title="Export database"
|
||||
type="button">
|
||||
<i data-feather="database"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<hr v-if="isCheckbox" class="h-px bg-bg-light p-0 mb-4 px-4 mx-4 border-0 dark:bg-bg-dark">
|
||||
<div v-if="isCheckbox" class="flex flex-row flex-grow p-4 pt-0 items-center">
|
||||
|
||||
<!-- CHECK BOX OPERATIONS -->
|
||||
<div class="flex flex-row flex-grow gap-3">
|
||||
Selected: {{ list.filter((item) => item.checkBoxValue == true).length }}
|
||||
</div>
|
||||
<div class="flex flex-row gap-3">
|
||||
|
||||
|
||||
<button class="text-2xl hover:text-secondary duration-75 active:scale-90 " title="Select All" type="button"
|
||||
@click.stop="selectAllDiscussions">
|
||||
<i data-feather="list"></i>
|
||||
</button>
|
||||
<button class="text-2xl hover:text-secondary duration-75 active:scale-90 rotate-90"
|
||||
title="Export selected to a file" type="button">
|
||||
<i data-feather="log-out"></i>
|
||||
</button>
|
||||
<button class="text-2xl hover:text-red-600 duration-75 active:scale-90 " title="Remove selected"
|
||||
type="button">
|
||||
<i data-feather="trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative overflow-y-scroll no-scrollbar">
|
||||
<!-- DISCUSSION LIST -->
|
||||
<div class="mx-4 flex-grow" :class="filterInProgress ? 'opacity-20 pointer-events-none' : ''">
|
||||
|
||||
<Discussion v-for="(item, index) in list" :key="index" :id="item.id" :title="item.title" ref="discussionList"
|
||||
:selected="currentDiscussion.id == item.id" :loading="currentDiscussion.id == item.id && loading"
|
||||
@select="selectDiscussion(item)" @delete="deleteDiscussion(item.id)" @editTitle="editTitle" />
|
||||
<Discussion v-for="(item, index) in list" :key="index" :id="item.id" :title="item.title"
|
||||
:selected="currentDiscussion.id == item.id" :loading="item.loading"
|
||||
:isCheckbox="isCheckbox" :checkBoxValue="item.checkBoxValue" @select="selectDiscussion(item)"
|
||||
@delete="deleteDiscussion(item.id)" @editTitle="editTitle" @checked="checkUncheckDiscussion" />
|
||||
|
||||
<div v-if="list.length < 1"
|
||||
class=" gap-2 py-2 my-2 hover:shadow-md hover:bg-primary-light dark:hover:bg-primary rounded-md p-2 duration-75 group cursor-pointer">
|
||||
class="gap-2 py-2 my-2 hover:shadow-md hover:bg-primary-light dark:hover:bg-primary rounded-md p-2 duration-75 group cursor-pointer">
|
||||
<p class="px-3">No discussions are found</p>
|
||||
</div>
|
||||
<div
|
||||
class="sticky bottom-0 bg-gradient-to-t pointer-events-none from-bg-light-tone dark:from-bg-dark-tone flex height-64 ">
|
||||
class="sticky bottom-0 bg-gradient-to-t pointer-events-none from-bg-light-tone dark:from-bg-dark-tone flex height-64">
|
||||
<!-- FADING DISCUSSION LIST END ELEMENT -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="overflow-y-scroll flex flex-col no-scrollbar flex-grow "
|
||||
:class="loading ? 'opacity-20 pointer-events-none' : ''">
|
||||
<div class="overflow-y-scroll flex flex-col no-scrollbar flex-grow">
|
||||
<!-- :class="loading ? 'opacity-20 pointer-events-none' : ''"> -->
|
||||
<!-- CHAT AREA -->
|
||||
<div>
|
||||
<Message v-for="(msg, index) in discussionArr" :key="index" :message="msg"
|
||||
@ -75,11 +100,8 @@
|
||||
|
||||
<WelcomeComponent v-if="discussionArr.length < 1" />
|
||||
|
||||
<ChatBox v-if="discussionArr.length > 0" @messageSentEvent="sendMsg" />
|
||||
|
||||
<ChatBox v-if="discussionArr.length > 0" @messageSentEvent="sendMsg" :loading="isGenerating" />
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
@ -89,72 +111,73 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
|
||||
setup() {
|
||||
|
||||
|
||||
},
|
||||
setup() { },
|
||||
data() {
|
||||
|
||||
return {
|
||||
list: [],
|
||||
tempList: [],
|
||||
currentDiscussion: Number,
|
||||
list: [], // Discussion list
|
||||
tempList: [], // Copy of Discussion list (used for keeping the original list during filtering discussions/searching action)
|
||||
currentDiscussion: {}, // Current/selected discussion id
|
||||
discussionArr: [],
|
||||
loading: false,
|
||||
filterTitle: "",
|
||||
filterTitle: '',
|
||||
filterInProgress: false,
|
||||
showCreateDiscussionModal: false
|
||||
isCreated: false,
|
||||
isGenerating: false,
|
||||
isCheckbox: false,
|
||||
isSelectAll: false,
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async list_discussions() {
|
||||
try {
|
||||
const res = await axios.get("/list_discussions");
|
||||
const res = await axios.get('/list_discussions')
|
||||
|
||||
if (res) {
|
||||
this.list = res.data
|
||||
this.tempList = this.list
|
||||
return res.data
|
||||
|
||||
this.createDiscussionList(res.data)
|
||||
return res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
async load_discussion(id) {
|
||||
try {
|
||||
if (id) {
|
||||
this.loading = true
|
||||
const res = await axios.post("/load_discussion", {
|
||||
this.setDiscussionLoading(id,this.loading)
|
||||
const res = await axios.post('/load_discussion', {
|
||||
id: id
|
||||
});
|
||||
})
|
||||
this.loading = false
|
||||
this.setDiscussionLoading(id,this.loading)
|
||||
if (res) {
|
||||
|
||||
this.discussionArr = res.data.filter((item) => item.sender != "conditionner") // Filter out the conditionner entries
|
||||
|
||||
// Filter out the user and bot entries
|
||||
this.discussionArr = res.data.filter((item) => item.type == 0)
|
||||
const lastMessage = this.discussionArr[this.discussionArr.length - 1]
|
||||
if (lastMessage) {
|
||||
nextTick(() => {
|
||||
const selectedElement = document.getElementById('msg-' + lastMessage.id)
|
||||
this.scrollToElement(selectedElement)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
this.loading = false
|
||||
this.setDiscussionLoading(id,this.loading)
|
||||
}
|
||||
|
||||
},
|
||||
async new_discussion(title) {
|
||||
try {
|
||||
const res = await axios.get("/new_discussion", { params: { title: title } });
|
||||
const res = await axios.get('/new_discussion', { params: { title: title } })
|
||||
|
||||
if (res) {
|
||||
return res.data
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
@ -165,97 +188,112 @@ export default {
|
||||
try {
|
||||
if (id) {
|
||||
this.loading = true
|
||||
const res = await axios.post("/delete_discussion", {
|
||||
this.setDiscussionLoading(id,this.loading)
|
||||
const res = await axios.post('/delete_discussion', {
|
||||
id: id
|
||||
});
|
||||
})
|
||||
this.loading = false
|
||||
this.setDiscussionLoading(id,this.loading)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
this.loading = false
|
||||
this.setDiscussionLoading(id,this.loading)
|
||||
}
|
||||
|
||||
},
|
||||
async edit_title(discussion_id, new_title) {
|
||||
async edit_title(id, new_title) {
|
||||
try {
|
||||
if (discussion_id) {
|
||||
if (id) {
|
||||
this.loading = true
|
||||
const res = await axios.post("/edit_title", {
|
||||
id: discussion_id,
|
||||
this.setDiscussionLoading(id,this.loading)
|
||||
const res = await axios.post('/edit_title', {
|
||||
id: id,
|
||||
title: new_title
|
||||
});
|
||||
})
|
||||
this.loading = false
|
||||
this.setDiscussionLoading(id,this.loading)
|
||||
if (res.status == 200) {
|
||||
const index = this.list.findIndex(x => x.id == discussion_id);
|
||||
const index = this.list.findIndex((x) => x.id == id)
|
||||
const discussionItem = this.list[index]
|
||||
discussionItem.title = new_title
|
||||
this.tempList = this.list
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
this.loading = false
|
||||
this.setDiscussionLoading(id,this.loading)
|
||||
}
|
||||
|
||||
},
|
||||
filterDiscussions() {
|
||||
// Search bar in for filtering discussions by title (serch)
|
||||
|
||||
if (!this.filterInProgress) {
|
||||
this.filterInProgress = true
|
||||
setTimeout(() => {
|
||||
|
||||
this.list = this.tempList.filter((item) => item.title.includes(this.filterTitle))
|
||||
this.list = this.tempList.filter((item) => item.title && item.title.includes(this.filterTitle))
|
||||
this.filterInProgress = false
|
||||
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
async selectDiscussion(item) {
|
||||
// When discussion is selected it loads the discussion array
|
||||
|
||||
this.currentDiscussion = item
|
||||
|
||||
localStorage.setItem('selected_discussion', this.currentDiscussion.id)
|
||||
|
||||
await this.load_discussion(item.id)
|
||||
|
||||
if (this.discussionArr.length > 1) {
|
||||
|
||||
if (this.currentDiscussion.title === "" || this.currentDiscussion.title === null ) {
|
||||
if (this.currentDiscussion.title === '' || this.currentDiscussion.title === null) {
|
||||
this.changeTitleUsingUserMSG(this.currentDiscussion.id, this.discussionArr[1].content)
|
||||
}
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
const selectedDisElement = document.getElementById('dis-' + item.id)
|
||||
this.scrollToElement(selectedDisElement)
|
||||
})
|
||||
},
|
||||
scrollToElement(el) {
|
||||
|
||||
//console.log("scroll", el.id)
|
||||
if (el) {
|
||||
|
||||
el.scrollIntoView({ behavior: 'smooth', block: "center", inline: "nearest" });
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
|
||||
}
|
||||
},
|
||||
createMsg(msgObj) {
|
||||
// From websocket.on("infos")
|
||||
// {
|
||||
// "type": "input_message_infos",
|
||||
// "bot": "gpt4all",
|
||||
// "user": "user",
|
||||
// "message": "giv emoney",
|
||||
// "id": 112,
|
||||
// "response_id": 113
|
||||
// }
|
||||
|
||||
// Create user input message
|
||||
let usrMessage = {
|
||||
content: msgObj.message,
|
||||
id: msgObj.message,
|
||||
id: msgObj.id,
|
||||
//parent: 10,
|
||||
rank: 0,
|
||||
sender: msgObj.user,
|
||||
sender: msgObj.user
|
||||
//type: 0
|
||||
}
|
||||
this.discussionArr.push(usrMessage)
|
||||
nextTick(() => {
|
||||
const userMsgElement = document.getElementById('msg-' + msgObj.message)
|
||||
this.scrollToElement(userMsgElement)
|
||||
|
||||
})
|
||||
|
||||
// Create response message
|
||||
let responseMessage = {
|
||||
content: "..typing",
|
||||
content: '..typing',
|
||||
id: msgObj.response_id,
|
||||
//parent: 10,
|
||||
rank: 0,
|
||||
sender: msgObj.bot,
|
||||
sender: msgObj.bot
|
||||
//type: 0
|
||||
}
|
||||
this.discussionArr.push(responseMessage)
|
||||
@ -264,120 +302,204 @@ export default {
|
||||
this.scrollToElement(responseMessageElement)
|
||||
})
|
||||
|
||||
if (this.currentDiscussion.title === "" || this.currentDiscussion.title === null) {
|
||||
if (this.currentDiscussion.title === '' || this.currentDiscussion.title === null) {
|
||||
this.changeTitleUsingUserMSG(this.currentDiscussion.id, usrMessage.content)
|
||||
}
|
||||
|
||||
this.isGenerating = false
|
||||
this.setDiscussionLoading(this.currentDiscussion.id,this.isGenerating)
|
||||
},
|
||||
sendMsg(msg) {
|
||||
|
||||
websocket.emit('generate_msg', { prompt: msg });
|
||||
|
||||
// Sends message to backend
|
||||
this.isGenerating = true
|
||||
this.setDiscussionLoading(this.currentDiscussion.id,this.isGenerating)
|
||||
websocket.emit('generate_msg', { prompt: msg })
|
||||
},
|
||||
steamMessageContent(content) {
|
||||
// Streams response message content from backend
|
||||
|
||||
const lastMsg = this.discussionArr[this.discussionArr.length - 1]
|
||||
lastMsg.content = content.data
|
||||
},
|
||||
async changeTitleUsingUserMSG(id, msg) {
|
||||
const index = this.list.findIndex(x => x.id == id);
|
||||
// If discussion is untitled or title is null then it sets the title to first user message.
|
||||
|
||||
const index = this.list.findIndex((x) => x.id == id)
|
||||
const discussionItem = this.list[index]
|
||||
if (msg) {
|
||||
discussionItem.title = msg
|
||||
this.tempList = this.list
|
||||
}
|
||||
await this.edit_title(id,msg)
|
||||
|
||||
await this.edit_title(id, msg)
|
||||
},
|
||||
async createNewDiscussion() {
|
||||
// Creates new discussion on backend,
|
||||
// gets new discussion list, selects
|
||||
// newly created discussion,
|
||||
// scrolls to the discussion
|
||||
|
||||
const res = await this.new_discussion()
|
||||
await this.list_discussions()
|
||||
const index = this.list.findIndex(x => x.id == res.id);
|
||||
const index = this.list.findIndex((x) => x.id == res.id)
|
||||
const discussionItem = this.list[index]
|
||||
this.selectDiscussion(discussionItem)
|
||||
nextTick(() => {
|
||||
const selectedDisElement = document.getElementById('dis-' + res.id)
|
||||
this.scrollToElement(selectedDisElement)
|
||||
|
||||
})
|
||||
|
||||
},
|
||||
async deleteDiscussion(id) {
|
||||
const index = this.list.findIndex(x => x.id == id);
|
||||
loadLastUsedDiscussion() {
|
||||
// Checks local storage for last selected discussion
|
||||
const id = localStorage.getItem('selected_discussion')
|
||||
if (id) {
|
||||
const index = this.list.findIndex((x) => x.id == id)
|
||||
const discussionItem = this.list[index]
|
||||
if(discussionItem){
|
||||
this.selectDiscussion(discussionItem)
|
||||
}
|
||||
}
|
||||
},
|
||||
async deleteDiscussion(id) {
|
||||
// Deletes discussion from backend and frontend
|
||||
|
||||
const index = this.list.findIndex((x) => x.id == id)
|
||||
const discussionItem = this.list[index]
|
||||
discussionItem.loading=true
|
||||
this.delete_discussion(id)
|
||||
discussionItem.loading = true
|
||||
await this.delete_discussion(id)
|
||||
if (this.currentDiscussion.id == id) {
|
||||
this.currentDiscussion = {}
|
||||
this.discussionArr=[]
|
||||
}
|
||||
await this.list_discussions()
|
||||
this.list.splice(this.list.findIndex(item => item.id==id),1)
|
||||
|
||||
this.createDiscussionList(this.list)
|
||||
//await this.list_discussions()
|
||||
},
|
||||
async editTitle(newTitleObj) {
|
||||
//const index = this.$refs.discussionList.findIndex(x => x.id == newTitleObj.id);
|
||||
//const discussionItem = this.$refs.discussionList[index]
|
||||
//console.log(JSON.stringify(discussionItem))
|
||||
//discussionItem.loading.value=true
|
||||
//console.log(discussionItem.title)
|
||||
|
||||
const index = this.list.findIndex((x) => x.id == newTitleObj.id)
|
||||
const discussionItem = this.list[index]
|
||||
discussionItem.title = newTitleObj.title
|
||||
discussionItem.loading = true
|
||||
await this.edit_title(newTitleObj.id, newTitleObj.title)
|
||||
|
||||
discussionItem.loading = false
|
||||
},
|
||||
checkUncheckDiscussion(event, id) {
|
||||
// If checked = true and item is not in array then add item to list
|
||||
const index = this.list.findIndex((x) => x.id == id)
|
||||
const discussionItem = this.list[index]
|
||||
discussionItem.checkBoxValue = event.target.checked
|
||||
this.tempList = this.list
|
||||
},
|
||||
selectAllDiscussions() {
|
||||
|
||||
// Check if there is one discussion not selected
|
||||
this.isSelectAll = !this.tempList.filter((item) => item.checkBoxValue == false).length > 0
|
||||
// Selects or deselects all discussions
|
||||
for (let i = 0; i < this.tempList.length; i++) {
|
||||
this.tempList[i].checkBoxValue = !this.isSelectAll
|
||||
}
|
||||
|
||||
this.tempList = this.list
|
||||
this.isSelectAll = !this.isSelectAll
|
||||
},
|
||||
createDiscussionList(disList) {
|
||||
// This creates a discussion list for UI with additional properties
|
||||
if (disList) {
|
||||
const newDisList = disList.map((item) => {
|
||||
|
||||
const newItem = {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
selected: false,
|
||||
loading: false,
|
||||
checkBoxValue: false
|
||||
}
|
||||
return newItem
|
||||
|
||||
})
|
||||
this.list = newDisList
|
||||
this.tempList = newDisList
|
||||
|
||||
}
|
||||
},
|
||||
setDiscussionLoading(id,loading){
|
||||
const index = this.list.findIndex((x) => x.id == id)
|
||||
const discussionItem = this.list[index]
|
||||
discussionItem.loading = loading
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
|
||||
// Constructor
|
||||
|
||||
await this.list_discussions()
|
||||
|
||||
this.loadLastUsedDiscussion()
|
||||
this.isCreated = true
|
||||
|
||||
nextTick(() => {
|
||||
feather.replace()
|
||||
})
|
||||
|
||||
// WebSocket responses
|
||||
websocket.on("infos", this.createMsg)
|
||||
websocket.on("message", this.steamMessageContent)
|
||||
websocket.on('infos', this.createMsg)
|
||||
websocket.on('message', this.steamMessageContent)
|
||||
},
|
||||
activated() {
|
||||
// This lifecycle hook runs every time you switch from other page back to this page (vue-router)
|
||||
// To fix scrolling back to last message, this hook is needed.
|
||||
// If anyone knows hor to fix scroll issue when changing pages, please do fix it :D
|
||||
|
||||
}, components: {
|
||||
if (this.isCreated) {
|
||||
this.loadLastUsedDiscussion()
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Discussion,
|
||||
Message,
|
||||
ChatBox,
|
||||
WelcomeComponent,
|
||||
|
||||
}, watch: {
|
||||
WelcomeComponent
|
||||
},
|
||||
watch: {
|
||||
filterTitle(newVal, oldVal) {
|
||||
if (newVal == "") {
|
||||
if (newVal == '') {
|
||||
this.filterInProgress = true
|
||||
this.list = this.tempList
|
||||
this.filterInProgress = false
|
||||
}
|
||||
},
|
||||
isCheckbox(newval, oldval) {
|
||||
nextTick(() => {
|
||||
feather.replace()
|
||||
})
|
||||
if (!newval) {
|
||||
this.isSelectAll = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup >
|
||||
import Discussion from '../components/Discussion.vue';
|
||||
import Message from '../components/Message.vue';
|
||||
<script setup>
|
||||
import Discussion from '../components/Discussion.vue'
|
||||
import Message from '../components/Message.vue'
|
||||
import ChatBox from '../components/ChatBox.vue'
|
||||
import WelcomeComponent from '../components/WelcomeComponent.vue'
|
||||
|
||||
import feather from 'feather-icons'
|
||||
|
||||
import axios from "axios";
|
||||
import { nextTick } from 'vue';
|
||||
import axios from 'axios'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import websocket from '@/services/websocket.js';
|
||||
import websocket from '@/services/websocket.js'
|
||||
|
||||
import { onMounted } from 'vue'
|
||||
import { initFlowbite } from 'flowbite'
|
||||
|
||||
// initialize components based on data attribute selectors
|
||||
onMounted(() => {
|
||||
initFlowbite();
|
||||
initFlowbite()
|
||||
})
|
||||
|
||||
axios.defaults.baseURL = import.meta.env.VITE_GPT4ALL_API_BASEURL;
|
||||
|
||||
axios.defaults.baseURL = import.meta.env.VITE_GPT4ALL_API_BASEURL
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user