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 nextNonce = 0;
|
||||||
let reconnectTimeout = null; // For managing the reconnect timer
|
let reconnectTimeout = null; // For managing the reconnect timer
|
||||||
let cache = {
|
let cache = {
|
||||||
chat: null,
|
chats: null,
|
||||||
|
agents: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
@@ -256,10 +257,24 @@ export async function getChat(chatId) {
|
|||||||
return allChats.find(chat => chat.id === chatId) || null;
|
return allChats.find(chat => chat.id === chatId) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invalidateCache({ chat = false } = {}) {
|
export async function getAgents() {
|
||||||
if (chat) {
|
if (cache.agents === null) {
|
||||||
cache.chats = 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([]);
|
const agents = ref([]);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
chats.value = (await backend.getChats()).map(entry => {
|
chats.value = (await backend.getChats()).map(it => {
|
||||||
return {id: entry.id, name: entry.name};
|
return {id: it.id, name: it.name};
|
||||||
|
});
|
||||||
|
agents.value = (await backend.getAgents()).map(it => {
|
||||||
|
return {id: it.id, name: it.name};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -35,10 +38,13 @@
|
|||||||
</li>
|
</li>
|
||||||
<li><i class="fa-solid fa-angles-left"></i></li>
|
<li><i class="fa-solid fa-angles-left"></i></li>
|
||||||
</ul>
|
</ul>
|
||||||
<nav>
|
<nav v-if="mode=='chats'">
|
||||||
<RouterLink v-if="mode=='chats'" v-for="it in chats" :to="'/chat/' + it.id" class="nav-item">{{it.name}}</RouterLink>
|
<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>
|
||||||
<RouterLink v-if="mode=='agents'" v-for="it in agents" :to="'/agent/' + it.id" class="nav-item">{{it.name}}</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>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ const router = createRouter({
|
|||||||
// which is lazy-loaded when the route is visited.
|
// which is lazy-loaded when the route is visited.
|
||||||
component: () => import('../views/AboutView.vue'),
|
component: () => import('../views/AboutView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'chat-create',
|
||||||
|
path: `/chat/create`,
|
||||||
|
component: () => import('../views/ChatCreate.vue')
|
||||||
|
},
|
||||||
objectView('chat', () => import('../views/Chat.vue')),
|
objectView('chat', () => import('../views/Chat.vue')),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
// Global utility functions related with UI functionality
|
// Global utility functions related with UI functionality
|
||||||
|
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
export function goto (path) {
|
export function goto (path) {
|
||||||
window.location.hash = "#" + path;
|
router.push(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function confirm (prompt) {
|
export async function confirm (prompt) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
ref, computed, reactive,
|
ref, watch,
|
||||||
onMounted, onUnmounted,
|
onMounted, onUnmounted,
|
||||||
} from "vue";
|
} from "vue";
|
||||||
|
|
||||||
@@ -24,16 +24,73 @@
|
|||||||
const editingMessageId = ref(null);
|
const editingMessageId = ref(null);
|
||||||
const noteMode = ref(false);
|
const noteMode = ref(false);
|
||||||
|
|
||||||
|
let _isMounted = false;
|
||||||
let unsubscribeChat = null;
|
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 === //
|
// === Chat Functionality === //
|
||||||
|
|
||||||
const inputFieldElem = ref(null);
|
const inputFieldElem = ref(null);
|
||||||
const chatContainerElem = ref(null);
|
const chatContainerElem = ref(null);
|
||||||
|
|
||||||
async function getChatMessages() {
|
async function getChatMessages(chatId) {
|
||||||
let response = await backend.sendAction("get-chat-messages", {'chatId': props.id});
|
let response = await backend.sendAction("get-chat-messages", {chatId});
|
||||||
let messageObjects = response['messages'];
|
let messageObjects = response['messages'];
|
||||||
return messageObjects;
|
return messageObjects;
|
||||||
}
|
}
|
||||||
@@ -97,7 +154,7 @@
|
|||||||
|
|
||||||
// === UI Helper Functions === //
|
// === UI Helper Functions === //
|
||||||
|
|
||||||
function requestRescroll () {
|
function requestRescroll ({forceBottom=false, bottomRelative=false}) {
|
||||||
const elem = chatContainerElem.value;
|
const elem = chatContainerElem.value;
|
||||||
|
|
||||||
const scroll = elem.scrollTop;
|
const scroll = elem.scrollTop;
|
||||||
@@ -107,7 +164,7 @@
|
|||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
// Chat was fully scrolled down
|
// Chat was fully scrolled down
|
||||||
if (scroll == max) {
|
if (forceBottom || scroll == max) {
|
||||||
elem.scrollTop = elem.scrollTopMax;
|
elem.scrollTop = elem.scrollTopMax;
|
||||||
} else {
|
} else {
|
||||||
// Do nothing, scroll remains in place
|
// 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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