Support creating chats. Add agents in sidebar. Improve chat state management
This commit is contained in:
1 parent
73fe8a95d4
commit
f8edd01c37
6 files changed
+184
-49
No files matched your search
@@ -16,7 +16,8 @@ let backendUrl = null;
|
||||
let nextNonce = 0;
|
||||
let reconnectTimeout = null; // For managing the reconnect timer
|
||||
let cache = {
|
||||
chat: null,
|
||||
chats: null,
|
||||
agents: null,
|
||||
};
|
||||
|
||||
// Auth
|
||||
@@ -256,11 +257,25 @@ export async function getChat(chatId) {
|
||||
return allChats.find(chat => chat.id === chatId) || null;
|
||||
}
|
||||
|
||||
export function invalidateCache({ chat = false } = {}) {
|
||||
if (chat) {
|
||||
cache.chats = null;
|
||||
export async function getAgents() {
|
||||
if (cache.agents === null) {
|
||||
const response = await sendAction("get-agents", {});
|
||||
if (response && response.agents) {
|
||||
cache.agents = response.agents.map(msg => ({
|
||||
id: msg.id,
|
||||
name: msg.name?.length > 0 ? msg.name : `<agent-${msg.id}>`,
|
||||
}));
|
||||
} else {
|
||||
throw new Error("Invalid response for get-agents: " + JSON.stringify(response));
|
||||
}
|
||||
}
|
||||
return cache.agents;
|
||||
}
|
||||
|
||||
export function invalidateCache({ chats=false, agents=false } = {}) {
|
||||
if (chats) cache.chats = null;
|
||||
if (agents) cache.agents = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -14,8 +14,11 @@
|
||||
const agents = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
chats.value = (await backend.getChats()).map(entry => {
|
||||
return {id: entry.id, name: entry.name};
|
||||
chats.value = (await backend.getChats()).map(it => {
|
||||
return {id: it.id, name: it.name};
|
||||
});
|
||||
agents.value = (await backend.getAgents()).map(it => {
|
||||
return {id: it.id, name: it.name};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -35,10 +38,13 @@
|
||||
</li>
|
||||
<li><i class="fa-solid fa-angles-left"></i></li>
|
||||
</ul>
|
||||
<nav>
|
||||
<RouterLink v-if="mode=='chats'" v-for="it in chats" :to="'/chat/' + it.id" class="nav-item">{{it.name}}</RouterLink>
|
||||
|
||||
<RouterLink v-if="mode=='agents'" v-for="it in agents" :to="'/agent/' + it.id" class="nav-item">{{it.name}}</RouterLink>
|
||||
<nav v-if="mode=='chats'">
|
||||
<RouterLink v-for="it in chats" :to="'/chat/' + it.id" class="nav-item">{{it.name}}</RouterLink>
|
||||
<RouterLink to="/chat/create" class="nav-item">Create Chat</RouterLink>
|
||||
</nav>
|
||||
<nav v-if="mode=='agents'">
|
||||
<RouterLink v-for="it in agents" :to="'/agent/' + it.id" class="nav-item">{{it.name}}</RouterLink>
|
||||
<RouterLink to="/agent/create" class="nav-item">Create Agent</RouterLink>
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -28,6 +28,11 @@ const router = createRouter({
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/AboutView.vue'),
|
||||
},
|
||||
{
|
||||
name: 'chat-create',
|
||||
path: `/chat/create`,
|
||||
component: () => import('../views/ChatCreate.vue')
|
||||
},
|
||||
objectView('chat', () => import('../views/Chat.vue')),
|
||||
],
|
||||
})
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Global utility functions related with UI functionality
|
||||
|
||||
import router from './router'
|
||||
|
||||
export function goto (path) {
|
||||
window.location.hash = "#" + path;
|
||||
router.push(path);
|
||||
}
|
||||
|
||||
export async function confirm (prompt) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import {
|
||||
ref, computed, reactive,
|
||||
ref, watch,
|
||||
onMounted, onUnmounted,
|
||||
} from "vue";
|
||||
|
||||
@@ -24,16 +24,73 @@
|
||||
const editingMessageId = ref(null);
|
||||
const noteMode = ref(false);
|
||||
|
||||
let _isMounted = false;
|
||||
let unsubscribeChat = null;
|
||||
|
||||
|
||||
// === State Management === //
|
||||
|
||||
async function loadChatState (chatId) {
|
||||
unsubscribeChat = backend.onChatMessages(chatId, it => {
|
||||
if (it.sender == 'user') return;
|
||||
|
||||
addMessage({
|
||||
type: it.kind,
|
||||
sender: it.sender,
|
||||
content: it.content,
|
||||
threadId: it.threadId,
|
||||
});
|
||||
});
|
||||
|
||||
let response = await backend.sendAction("get-chat-messages", {chatId});
|
||||
// Always good to duplicate when assigning to a ref
|
||||
messages.value = response['messages'].map(it => ({
|
||||
type: it.kind,
|
||||
sender: it.sender,
|
||||
content: it.content,
|
||||
threadId: it.threadId,
|
||||
}));
|
||||
|
||||
// This must happen on the same async-thread that sets `messages`
|
||||
requestRescroll({forceBottom: true});
|
||||
|
||||
// Reset UI State
|
||||
editingMessageId.value = null;
|
||||
noteMode.value = false;
|
||||
// no need to reset expandedThreads, it's nice that it will be remembered
|
||||
}
|
||||
|
||||
async function unloadChatState () {
|
||||
if (unsubscribeChat) unsubscribeChat();
|
||||
unsubscribeChat = null;
|
||||
}
|
||||
|
||||
// This won't fire when called, the mounted callback will take care
|
||||
watch(() => props.id, newChatId => {
|
||||
if (_isMounted) {
|
||||
unloadChatState();
|
||||
loadChatState(newChatId);
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
_isMounted = true;
|
||||
loadChatState(props.id);
|
||||
});
|
||||
|
||||
onUnmounted(async () => {
|
||||
_isMounted = false;
|
||||
unloadChatState();
|
||||
});
|
||||
|
||||
|
||||
// === Chat Functionality === //
|
||||
|
||||
const inputFieldElem = ref(null);
|
||||
const chatContainerElem = ref(null);
|
||||
|
||||
async function getChatMessages() {
|
||||
let response = await backend.sendAction("get-chat-messages", {'chatId': props.id});
|
||||
async function getChatMessages(chatId) {
|
||||
let response = await backend.sendAction("get-chat-messages", {chatId});
|
||||
let messageObjects = response['messages'];
|
||||
return messageObjects;
|
||||
}
|
||||
@@ -97,7 +154,7 @@
|
||||
|
||||
// === UI Helper Functions === //
|
||||
|
||||
function requestRescroll () {
|
||||
function requestRescroll ({forceBottom=false, bottomRelative=false}) {
|
||||
const elem = chatContainerElem.value;
|
||||
|
||||
const scroll = elem.scrollTop;
|
||||
@@ -107,7 +164,7 @@
|
||||
window.requestAnimationFrame(() => {
|
||||
window.requestAnimationFrame(() => {
|
||||
// Chat was fully scrolled down
|
||||
if (scroll == max) {
|
||||
if (forceBottom || scroll == max) {
|
||||
elem.scrollTop = elem.scrollTopMax;
|
||||
} else {
|
||||
// Do nothing, scroll remains in place
|
||||
@@ -133,39 +190,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// === Initialize === //
|
||||
|
||||
onMounted(async () => {
|
||||
requestRescroll();
|
||||
|
||||
messages.value = (await getChatMessages()).map(it => {
|
||||
return {
|
||||
type: it.kind,
|
||||
sender: it.sender,
|
||||
content: it.content,
|
||||
threadId: it.threadId,
|
||||
};
|
||||
});
|
||||
|
||||
if (unsubscribeChat) unsubscribeChat();
|
||||
unsubscribeChat = backend.onChatMessages(props.id, it => {
|
||||
if (it.sender == 'user') return;
|
||||
|
||||
addMessage({
|
||||
type: it.kind,
|
||||
sender: it.sender,
|
||||
content: it.content,
|
||||
threadId: it.threadId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (unsubscribeChat) {
|
||||
unsubscribeChat();
|
||||
unsubscribeChat = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<script setup>
|
||||
import {
|
||||
ref, computed, reactive,
|
||||
onMounted, onUnmounted,
|
||||
} from "vue";
|
||||
|
||||
import * as backend from '../backend';
|
||||
import * as ui from '../ui';
|
||||
|
||||
// List of agents, with their id, name, and whether they're currently selected
|
||||
const agents = ref([
|
||||
{id: 1, name: "John"},
|
||||
{id: 2, name: "Steve"},
|
||||
]);
|
||||
const title = ref("");
|
||||
|
||||
async function submitChat () {
|
||||
const selectedAgents = agents.value.filter(agent => agent.selected).map(agent => agent.id);
|
||||
|
||||
let response = await backend.sendAction("create-chat", {
|
||||
'name': title.value,
|
||||
'members': selectedAgents,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
ui.alert(`Could not create chat\n${response.error}`);
|
||||
} else {
|
||||
ui.alert(`Created Chat "${title.value}"`);
|
||||
backend.invalidateCache({chats: true});
|
||||
ui.goto(`/chat/${response.chatId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// === Initialize === //
|
||||
|
||||
onMounted(async () => {
|
||||
agents.value = (await backend.getAgents()).map(it => ({
|
||||
id: it.id,
|
||||
name: it.name,
|
||||
selected: false,
|
||||
}));
|
||||
// TODO: Get agent list
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<h1>Create New Chat</h1>
|
||||
|
||||
<div class="main-container">
|
||||
<input v-model="title" type="text" placeholder="title">
|
||||
<div class="multi-select-box">
|
||||
<div v-for="agent in agents" :class="{selected: Boolean(agent.selected)}" @click="agent.selected = !agent.selected">{{agent.name}}</div>
|
||||
</div>
|
||||
<button @click="submitChat()">Create Chat</button>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.multi-select-box {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.multi-select-box div {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.multi-select-box div:last {
|
||||
border-bottom: none;
|
||||
}
|
||||
.multi-select-box div:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
.multi-select-box div.selected {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user