rename frontend directory
This commit is contained in:
1 parent
f870869d47
commit
c8b86927e2
36 files changed
+4
-10
No files matched your search
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<main>
|
||||
<h1>This is an about page</h1>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@media (min-width: 1024px) {
|
||||
.about {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,196 @@
|
||||
<script setup>
|
||||
import {
|
||||
ref, watch,
|
||||
onMounted, onUnmounted,
|
||||
} from "vue";
|
||||
|
||||
import * as backend from '../backend';
|
||||
import * as ui from '../ui';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const agent = ref({});
|
||||
const kindInfo = ref({});
|
||||
const configFields = ref([]);
|
||||
|
||||
|
||||
// === State Management === //
|
||||
|
||||
async function loadAgentData (agentId) {
|
||||
const _agent = await backend.getAgent(agentId);
|
||||
if (!_agent) {
|
||||
ui.alert("Agent not found");
|
||||
return;
|
||||
}
|
||||
|
||||
agent.value = {
|
||||
id: _agent.id,
|
||||
name: _agent.name,
|
||||
agentname: _agent.agentname,
|
||||
description: _agent.description,
|
||||
picture: _agent.picture,
|
||||
};
|
||||
|
||||
backend.sendAction("get-agent-kind-info", {kind: _agent.kind}).then(res => {
|
||||
kindInfo.value.description = res.info.description;
|
||||
});
|
||||
|
||||
backend.sendAction('get-agent-config-fields', {agentId}).then(res => {
|
||||
configFields.value = res.fields.map(field => ({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
value: field.value,
|
||||
default: null,
|
||||
enabled: field.value != null,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// This won't fire when immediately, the mounted callback will take care
|
||||
watch(() => props.id, newId => {
|
||||
loadAgentData(newId);
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
loadAgentData(props.id);
|
||||
});
|
||||
|
||||
function updateIdentifier() {
|
||||
const value = prompt(`Enter a new identifier for ${agent.value.name}`);
|
||||
if (value) {
|
||||
backend.sendAction("update-agent", {
|
||||
agentId: agent.value.id,
|
||||
agentname: value,
|
||||
}).then(res => {
|
||||
ui.alert(res.error ?
|
||||
'Error updating agent identifier' :
|
||||
'Agent identifier updated'
|
||||
);
|
||||
if (!res.error) {
|
||||
agent.value.agentname = value;
|
||||
backend.invalidateCache({agents: true});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.alert("Cancelled identifier update");
|
||||
}
|
||||
}
|
||||
|
||||
function updateName() {
|
||||
const value = prompt(`Enter a new name for ${agent.value.name}`);
|
||||
if (value) {
|
||||
backend.sendAction("update-agent", {
|
||||
agentId: agent.value.id,
|
||||
agentname: value,
|
||||
}).then(res => {
|
||||
ui.alert(res.error ?
|
||||
'Error updating agent name' :
|
||||
'Agent name updated'
|
||||
);
|
||||
if (!res.error) {
|
||||
agent.value.name = value;
|
||||
backend.invalidateCache({agents: true});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.alert("Cancelled name update");
|
||||
}
|
||||
}
|
||||
async function saveChanges() {
|
||||
const dataObject = {};
|
||||
for (let field of configFields.value) {
|
||||
if (field.enabled) {
|
||||
dataObject[field.name] = field.value;
|
||||
}
|
||||
}
|
||||
|
||||
let res = await backend.sendAction("update-agent", {
|
||||
agentId: agent.value.id,
|
||||
data: dataObject,
|
||||
});
|
||||
|
||||
ui.alert(res.error ?
|
||||
'Error updating agent' :
|
||||
'Agent updated'
|
||||
);
|
||||
|
||||
if (!res.error) {
|
||||
backend.invalidateCache({agents: true});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<h1>{{ agent.name || `Agent-${id}` }}</h1>
|
||||
<section class="top-section">
|
||||
<div>
|
||||
<span class="label">Agent ID</span>
|
||||
<span class="value">{{id}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">Identifier</span>
|
||||
<span class="value">{{agent.agentname}}</span>
|
||||
<button @click="updateIdentifier">Edit</button>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="updateName">Change Name</button>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<p style="padding-bottom: 1em; border-bottom: 1px solid var(--color-border)">{{ kindInfo.description }}</p>
|
||||
|
||||
<div v-for="(configField, index) in configFields" :key="index">
|
||||
<span class="label">{{ configField.name }}</span>
|
||||
<input type="checkbox" v-model="configField.enabled" />
|
||||
<br>
|
||||
<textarea v-model="configField.value" class="config-input" :disabled="!configField.enabled"/>
|
||||
</div>
|
||||
|
||||
<button type="submit" @click="saveChanges()">Save</button>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
section {
|
||||
margin: 1em;
|
||||
padding: 1em;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.top-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.top-section > div {
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 80%;
|
||||
font-weight: bold;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.value {
|
||||
padding: 0 0.5em;
|
||||
padding-top: 0.35em;
|
||||
|
||||
border: 1px solid var(--color-border);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
textarea.config-input {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
height: 5em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup>
|
||||
import {
|
||||
ref, computed, reactive,
|
||||
onMounted, onUnmounted,
|
||||
} from "vue";
|
||||
|
||||
import * as backend from '../backend';
|
||||
import * as ui from '../ui';
|
||||
|
||||
// List of agent class names
|
||||
const kinds = ref([]);
|
||||
const agent = ref({
|
||||
name: "",
|
||||
identifier: "",
|
||||
kind: "",
|
||||
});
|
||||
|
||||
async function submitAgent () {
|
||||
const payload = {
|
||||
'name': agent.value.name,
|
||||
'kind': agent.value.kind,
|
||||
};
|
||||
|
||||
if (!payload.kind) {
|
||||
ui.alert("You must select a kind");
|
||||
return;
|
||||
}
|
||||
|
||||
if (agent.value.identifier) {
|
||||
payload.identifier = agent.value.identifier;
|
||||
}
|
||||
|
||||
let response = await backend.sendAction("create-agent", payload);
|
||||
|
||||
if (response.error) {
|
||||
ui.alert(`Could not create agent\n${response.error}`);
|
||||
} else {
|
||||
ui.alert(`Created Agent "${payload.name}"`);
|
||||
backend.invalidateCache({agents: true});
|
||||
ui.goto(`/agent/${response.agentId}`);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
kinds.value = (await backend.sendAction("get-agent-kinds")).kinds;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<h1>Create New Chat</h1>
|
||||
|
||||
<div class="main-container">
|
||||
<input v-model="agent.name" type="text" placeholder="name">
|
||||
<input v-model="agent.identifier" type="text" placeholder="identifier">
|
||||
<select v-model="agent.kind">
|
||||
<option v-for="k in kinds" :value="k">{{k}}</option>
|
||||
</select>
|
||||
|
||||
<button @click="submitAgent()">Create Agent</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>
|
||||
@@ -0,0 +1,533 @@
|
||||
<script setup>
|
||||
import {
|
||||
ref, watch, computed,
|
||||
onMounted, onUnmounted,
|
||||
} from "vue";
|
||||
|
||||
import * as backend from '../backend';
|
||||
import * as ui from '../ui';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
let chatId = null;
|
||||
let agentNames = {};
|
||||
|
||||
// Chat State
|
||||
const chat = ref({});
|
||||
const messages = ref([]);
|
||||
const unsentMessages = ref([]);
|
||||
const currentThreadId = ref(null);
|
||||
const chatEntries = computed(() => {
|
||||
return computeChatEntries(messages.value.concat(unsentMessages.value));
|
||||
});
|
||||
|
||||
// UI State
|
||||
const expandedThreads = ref({}); // Object<int: bool>, to simulate a Set<int>
|
||||
const editingMessageId = ref(null);
|
||||
const noteMode = ref(false);
|
||||
|
||||
const editInfo = computed(() => {
|
||||
if (editingMessageId.value != null) {
|
||||
if (noteMode.value) {
|
||||
return `Editing Note ${editingMessageId.value}`;
|
||||
} else {
|
||||
return `Editing ${editingMessageId.value}`;
|
||||
}
|
||||
} else if (noteMode.value) {
|
||||
return 'Editing Note';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
let _isMounted = false;
|
||||
let unsubscribeChat = null;
|
||||
|
||||
// TODO: user sent message is added without id.
|
||||
// User should receive his own message notification, and find its message to assign it an id.
|
||||
// Alternatively, return the message id in send-chat-message action. Think thats better.
|
||||
let _messageNonce = 1;
|
||||
|
||||
|
||||
// === State Management === //
|
||||
|
||||
async function loadChatState (newId) {
|
||||
chatId = parseInt(newId);
|
||||
if (chatId == null || isNaN(chatId)) {
|
||||
console.error("Invalid chat id", chatId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an agent name dictionary
|
||||
const _agents = await backend.getAgents();
|
||||
for (let agent of _agents) {
|
||||
agentNames[agent.id] = agent.name;
|
||||
}
|
||||
|
||||
backend.getChat(chatId).then(info => {
|
||||
chat.value = Object.assign({}, info);
|
||||
});
|
||||
|
||||
unsubscribeChat = backend.onChatMessages(chatId, it => {
|
||||
if (it.sender == 'user') {
|
||||
let unsent = unsentMessages.value.shift();
|
||||
if (unsent) {
|
||||
if (unsent.content == it.content) {
|
||||
unsent.id = it.id;
|
||||
addMessage(unsent);
|
||||
return;
|
||||
} else {
|
||||
// Remove from unsent list and add message as received
|
||||
ui.alert("Unsent messages received differently");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addMessage({
|
||||
id: it.id,
|
||||
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 => ({
|
||||
//id: it.id,
|
||||
id: _messageNonce++,
|
||||
type: it.kind,
|
||||
sender: it.sender,
|
||||
content: it.content,
|
||||
threadId: it.threadId,
|
||||
}));
|
||||
|
||||
// Always send messages in a thread, determine which one to start with
|
||||
if (response.messages.length > 0) {
|
||||
const lastMessage = response.messages[response.messages.length - 1];
|
||||
const lastThread = lastMessage.threadId;
|
||||
if (lastThread == null) {
|
||||
startNewThread();
|
||||
} else {
|
||||
currentThreadId.value = lastMessage.threadId;
|
||||
expandThread(lastMessage.threadId, true);
|
||||
}
|
||||
} else {
|
||||
startNewThread();
|
||||
}
|
||||
|
||||
// This must happen on the same async-thread that sets `messages`
|
||||
requestRescroll({forceBottom: true});
|
||||
|
||||
// Reset UI State
|
||||
editingMessageId.value = null;
|
||||
noteMode.value = false;
|
||||
// don't reset expandedThreads, it's nice to keep for the session
|
||||
}
|
||||
|
||||
async function unloadChatState () {
|
||||
if (unsubscribeChat) unsubscribeChat();
|
||||
unsubscribeChat = null;
|
||||
}
|
||||
|
||||
// This won't fire when called, the mounted callback will take care
|
||||
watch(() => props.id, newId => {
|
||||
if (_isMounted) {
|
||||
unloadChatState();
|
||||
loadChatState(newId);
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
_isMounted = true;
|
||||
loadChatState(props.id);
|
||||
});
|
||||
|
||||
onUnmounted(async () => {
|
||||
_isMounted = false;
|
||||
unloadChatState();
|
||||
});
|
||||
|
||||
|
||||
// === Chat Functionality === //
|
||||
|
||||
const inputFieldElem = ref(null);
|
||||
const chatContainerElem = ref(null);
|
||||
|
||||
function addMessage (message, options={}) {
|
||||
requestRescroll();
|
||||
if (options.unsent) {
|
||||
unsentMessages.value.push(message);
|
||||
} else {
|
||||
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, content, 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, {unsent: true});
|
||||
} else {
|
||||
const messageRef = messages.value.find((it) => it.id == messageId);
|
||||
|
||||
backend.sendAction("update-chat-message", {
|
||||
messageId, 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;
|
||||
}
|
||||
|
||||
function computeChatEntries (messages) {
|
||||
let entries = [];
|
||||
let threadGroups = {}; // Map<int, message[]>
|
||||
|
||||
function senderName (sender) {
|
||||
let isInt = !isNaN(parseInt(sender));
|
||||
if (isInt) {
|
||||
return agentNames[sender] || `<agent-${sender}>`;
|
||||
} else {
|
||||
return sender;
|
||||
}
|
||||
}
|
||||
|
||||
function addThreadHeader (threadId, group) {
|
||||
entries.push({
|
||||
type: 'thread-header',
|
||||
threadId,
|
||||
messageCount: group.length,
|
||||
expanded: Boolean(expandedThreads.value[threadId]),
|
||||
});
|
||||
}
|
||||
|
||||
// First pass: Group messages by thread
|
||||
for (let message of messages) {
|
||||
if (message.threadId != null) {
|
||||
let group = threadGroups[message.threadId] ||
|
||||
(threadGroups[message.threadId] = []);
|
||||
group.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: Build the display list with correct ordering
|
||||
for (let _msg of messages) {
|
||||
// New message object with updated name
|
||||
const message = Object.assign({}, _msg, {sender: senderName(_msg.sender)});
|
||||
|
||||
// Non-threaded messages and notes go directly to the entry list
|
||||
if (message.threadId == null || message.type == 'note') {
|
||||
entries.push(message);
|
||||
|
||||
} else {
|
||||
let threadId = message.threadId;
|
||||
let group = threadGroups[threadId];
|
||||
|
||||
// If the thread is expanded, add messages as they appear
|
||||
if (expandedThreads.value[threadId]) {
|
||||
// Add the thread header before the first message
|
||||
if (message.id == group[0].id) {
|
||||
addThreadHeader(threadId, group);
|
||||
}
|
||||
entries.push(message);
|
||||
} else {
|
||||
// Add collapsed thread header in place of the last message
|
||||
if (message.id == group[group.length-1].id) {
|
||||
addThreadHeader(threadId, group);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add current thread mannually if it has no messages
|
||||
const current = currentThreadId.value;
|
||||
if (current != null && threadGroups[current] == null) {
|
||||
addThreadHeader(current, []);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function expandThread (threadId, value) {
|
||||
expandedThreads.value[threadId] = value;
|
||||
requestRescroll();
|
||||
}
|
||||
|
||||
async function startNewThread () {
|
||||
const res = await backend.sendAction("create-chat-thread", {chatId});
|
||||
console.log("Starting a new thread", res);
|
||||
if (res.threadId != null) {
|
||||
currentThreadId.value = res.threadId;
|
||||
expandThread(res.threadId, true);
|
||||
}
|
||||
}
|
||||
|
||||
function editMessage (message) {
|
||||
let elem = inputFieldElem.value;
|
||||
if (message) {
|
||||
editingMessageId.value = message.id;
|
||||
elem.innerText = message.content;
|
||||
} else {
|
||||
editingMessageId.value = null;
|
||||
elem.innerText = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === UI Helper Functions === //
|
||||
|
||||
async function chatEntryContextMenu (message) {
|
||||
let selected = await ui.contextMenu([
|
||||
{value: 'pin', title: "Pin message"},
|
||||
{value: 'edit', title: "Edit message"},
|
||||
{value: 'start-thread', title: "Start thread from here"},
|
||||
{value: 'delete', icon: 'fa-trash', title: "Delete message"},
|
||||
]);
|
||||
|
||||
switch (selected) {
|
||||
case 'pin': {
|
||||
alert("Pin Message (TODO)");
|
||||
break;
|
||||
}
|
||||
case 'edit': {
|
||||
editMessage(message);
|
||||
break;
|
||||
}
|
||||
case 'start-thread': {
|
||||
alert("Start Thread (TODO)");
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
if (confirm("Are you sure you want to delete this message?")) {
|
||||
alert("Delete Message (TODO)");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function requestRescroll ({forceBottom=false, bottomRelative=false}={}) {
|
||||
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 (forceBottom || scroll == max) {
|
||||
elem.scrollTop = elem.scrollTopMax;
|
||||
} else if (bottomRelative) {
|
||||
// TODO: Keep scroll at the same distance from the bottom.
|
||||
// This will be used when the user scrolls to the top and previous messages are loaded
|
||||
} else {
|
||||
// Do nothing, scroll remains in place
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function chatEntryClass (msg) {
|
||||
if (msg.type == 'note') {
|
||||
return ['note'];
|
||||
} else if (msg.type == 'message') {
|
||||
return ['message', msg.sender == 'user' ? 'sent' : 'received'];
|
||||
} else {
|
||||
return ['info'];
|
||||
}
|
||||
}
|
||||
|
||||
function inputKeyDown (event) {
|
||||
if (event.key == "Enter" && event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<h1>{{ chat.name || 'Chat #' + chat.id }}</h1>
|
||||
|
||||
<div class="messages-container" ref="chatContainerElem">
|
||||
<div v-for="msg in chatEntries" :class="chatEntryClass(msg)" @contextmenu.prevent="chatEntryContextMenu(msg)">
|
||||
<template v-if="msg.type == 'message' || msg.type == null">
|
||||
<div v-if="msg.sender != 'user'" class="sender">{{msg.sender}}</div>
|
||||
<div class="content">{{msg.content}}</div>
|
||||
</template>
|
||||
|
||||
<template v-if="msg.type == 'note'">
|
||||
<div class="sender">{{msg.sender}}</div>
|
||||
<div class="content">{{msg.content}}</div>
|
||||
</template>
|
||||
|
||||
<template v-if="msg.type == 'thread-header'">
|
||||
<div class="content">
|
||||
Thread {{msg.threadId}} ({{msg.messageCount}} messages)
|
||||
<a hred="#" @click.prevent="expandThread(msg.threadId, !msg.expanded)">{{msg.expanded ? "Hide" : "Show"}}</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!--message v-if="!msg.collapse" :value="msg.content" :format="msg.format"></message-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-container">
|
||||
<div v-if="editInfo" style="font-size: 75%; margin-bottom: 0.7em;">
|
||||
{{editInfo}}
|
||||
</div>
|
||||
|
||||
<div class="input-field" ref="inputFieldElem" @keydown="inputKeyDown($event)" contenteditable></div>
|
||||
|
||||
<div class="input-row">
|
||||
<button @click="startNewThread()">
|
||||
New Thread
|
||||
</button>
|
||||
<button @click="noteMode=!noteMode">
|
||||
{{noteMode ? 'Cancel Note' : 'Make Note'}}
|
||||
</button>
|
||||
<button v-if="editingMessageId != null" @click="editMessage(null)">
|
||||
Cancel Edit
|
||||
</button>
|
||||
<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;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.messages-container > div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.info {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
width: fit-content;
|
||||
max-width: 50em;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
|
||||
.note {
|
||||
width: fit-content;
|
||||
padding: 10px;
|
||||
background-color: var(--color-background-soft);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.message.sent {
|
||||
align-self: end;
|
||||
margin-left: 2em;
|
||||
}
|
||||
.message.received {
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
.message .content {
|
||||
white-space: preserve;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.message .sender,
|
||||
.note .sender {
|
||||
font-weight: bold;
|
||||
font-size: 100%;
|
||||
margin-top: -5px; /* half message padding */
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -0,0 +1,77 @@
|
||||
<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([]);
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
agents.value = (await backend.getAgents()).map(it => ({
|
||||
id: it.id,
|
||||
name: it.name,
|
||||
selected: false,
|
||||
}));
|
||||
});
|
||||
</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>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
import TheWelcome from '../components/TheWelcome.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<TheWelcome />
|
||||
</main>
|
||||
</template>
|
||||
Reference in new issue
Block a user