Chat View and Sidebar chat list

This commit is contained in:
Shivering Ocelot committed 2025-07-09 22:16:38 -05:00
1 parent 5b59d3df75
commit 73fe8a95d4
7 files changed
+350 -39

No files matched your search

+269
View File
@@ -0,0 +1,269 @@
<script setup>
import {
ref, computed, reactive,
onMounted, onUnmounted,
} from "vue";
import * as backend from '../backend';
import * as ui from '../ui';
const props = defineProps({
id: {
type: String,
required: true,
},
});
// Chat State
const chat = ref({});
const messages = ref([]);
const currentThreadId = ref(null);
// UI State
const expandedThreads = ref({}); // Object<int: bool>, to simulate a Set<int>
const editingMessageId = ref(null);
const noteMode = ref(false);
let unsubscribeChat = null;
// === Chat Functionality === //
const inputFieldElem = ref(null);
const chatContainerElem = ref(null);
async function getChatMessages() {
let response = await backend.sendAction("get-chat-messages", {'chatId': props.id});
let messageObjects = response['messages'];
return messageObjects;
}
function addMessage (message) {
requestRescroll();
messages.value.push(message);
}
async function sendMessage () {
let elem = inputFieldElem.value;
let content = elem.innerText;
elem.innerText = "";
if (!content.trim()) return; // Don't send empty messages
let threadId = currentThreadId.value;
let messageId = editingMessageId.value;
let isNote = noteMode.value;
if (messageId == null) {
backend.sendAction("send-chat-message", {
'chatId': props.id,
'content': content,
'threadId': threadId,
'kind': isNote ? 'note' : null,
}).then((response) => {
if (response['error']) {
print("Error sending chat message: ${response['error']}");
}
});
const newMessage = {
type: isNote ? 'note' : 'message',
sender: 'user',
content,
threadId,
};
addMessage(newMessage);
} else {
const messageRef = messages.firstWhere((element) => element.id == messageId);
backend.sendAction("update-chat-message", {
'messageId': messageId,
'content': content,
'kind': isNote ? 'note' : null,
}).then((response) => {
if (response['error']) {
print("Error updating message: ${response['error']}");
}
});
messageRef.type = isNote ? 'note' : 'message';
messageRef.content = content;
}
if (isNote) noteMode.value = false;
}
// === UI Helper Functions === //
function requestRescroll () {
const elem = chatContainerElem.value;
const scroll = elem.scrollTop;
const max = elem.scrollTopMax;
// Rescroll in 2 frames
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
// Chat was fully scrolled down
if (scroll == max) {
elem.scrollTop = elem.scrollTopMax;
} else {
// Do nothing, scroll remains in place
}
});
});
// Something different has to be done when previous messages are added
}
function msgClass (msg) {
if (msg.sender == 'user') {
return ['user']
} else {
return [];
}
}
function inputKeyDown (event) {
if (event.key == "Enter" && event.ctrlKey) {
event.preventDefault();
sendMessage();
}
}
// === 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>
<main>
<h1>{{ chat.name || 'Chat #' + chat.id }}</h1>
<div class="messages-container" ref="chatContainerElem">
<div v-for="msg in messages" class="message" :class="msgClass(msg)">
<div v-if="msg.type == 'message'" class="sender">{{msg.sender}}</div>
<!--message v-if="!msg.collapse" :value="msg.content" :format="msg.format"></message-->
<div v-if="!msg.collapse" class="content">{{msg.content}}</div>
<div v-if="msg.collapse">
<a href="#" @click.prevent="msg.collapse = false">Show</a>
</div>
<div v-if="msg.collapse === false">
<a href="#" @click.prevent="msg.collapse = true">Hide</a>
</div>
</div>
</div>
<div class="input-container">
<div class="input-field" ref="inputFieldElem" @keydown="inputKeyDown($event)" contenteditable></div>
<div class="input-row">
<button id="send-button" @click="sendMessage()">
Send
<span style="font-size: 66%;">(Ctrl+Enter)</span>
</button>
</div>
</div>
</main>
</template>
<style scoped>
main {
display: flex;
flex-direction: column;
}
.messages-container {
flex: 1;
overflow-y: scroll;
padding: 20px;
}
.message {
margin-bottom: 10px;
padding: 10px;
border-radius: 8px;
background-color: var(--color-background-soft);
}
.message .content {
white-space: preserve
}
.message .sender {
font-weight: bold;
font-size: 100%;
}
.message.user .content {
right: 100%;
width: auto;
}
.input-container {
padding: 10px;
background-color: var(--color-background-soft);
border-top: 1px solid var(--color-border);
}
.input-field {
white-space: preserve;
outline: none;
}
.input-field:empty::before {
opacity: 50%;
content: "Type your message...";
}
.input-row {
width: 100%;
}
#send-button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: #4caf50;
color: #fff;
cursor: pointer;
height: min-content;
/* align-self: end; /* for flex rows */
float: right; /* for flow rows */
}
</style>