frontend: sidebar rework, add in chat
This commit is contained in:
1 parent
1232872360
commit
5b75d95a8c
7 files changed
+358
-311
No files matched your search
+168
-4
@@ -1,10 +1,12 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import Sidebar from './components/main/Sidebar.vue'
|
||||
import Sidebar from './components/Sidebar.vue'
|
||||
import Alerts from './components/main/Alerts.vue'
|
||||
import ContextMenu from './components/main/ContextMenu.vue'
|
||||
|
||||
import * as iconoir from '@iconoir/vue';
|
||||
|
||||
import * as ui from './ui';
|
||||
import * as backend from './backend';
|
||||
import * as runs from './runs';
|
||||
@@ -12,6 +14,26 @@ import * as runs from './runs';
|
||||
// possible states: loading, login, signup, logged
|
||||
const loginState = ref('loading');
|
||||
|
||||
// sidebar state
|
||||
const sidebarCollapsed = ref(false);
|
||||
const mode = ref('chats');
|
||||
const chats = ref([]);
|
||||
const agents = ref([]);
|
||||
|
||||
// View titles use this class for their sidebar interactions
|
||||
watch(sidebarCollapsed, newValue => {
|
||||
if (newValue) {
|
||||
document.body.classList.remove('with-sidebar-expanded');
|
||||
} else {
|
||||
document.body.classList.add('with-sidebar-expanded');
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// They communicate upwards too
|
||||
ui.listen('sidebar-expand', ev => {
|
||||
sidebarCollapsed.value = false;
|
||||
});
|
||||
|
||||
function handleAuthResult (success) {
|
||||
loginState.value = success ? 'logged' : 'login';
|
||||
}
|
||||
@@ -39,6 +61,18 @@ function signupSubmit (formElem) {
|
||||
backend.signup(username, email, password).then(handleAuthResult);
|
||||
}
|
||||
|
||||
// Populate the sidebar nav once the user is logged in
|
||||
watch(loginState, async state => {
|
||||
if (state != 'logged') return;
|
||||
|
||||
chats.value = (await backend.getChats()).map(it => {
|
||||
return Object.assign({}, it);
|
||||
});
|
||||
agents.value = (await backend.getAgents()).map(it => {
|
||||
return Object.assign({}, it);
|
||||
});
|
||||
});
|
||||
|
||||
const runsListener = runs.onCreated(run => {
|
||||
console.log("new run", run);
|
||||
});
|
||||
@@ -93,7 +127,63 @@ backend.authResult().then(handleAuthResult);
|
||||
</div>
|
||||
|
||||
<template v-if="loginState=='logged'">
|
||||
<Sidebar />
|
||||
<Sidebar align="left" minWidth=150 maxWidth=400 v-model:collapsed="sidebarCollapsed">
|
||||
<div class="tab-bar">
|
||||
<div class="tab-group">
|
||||
<div @click="mode='chats'" class="tab" :class="{active: mode=='chats'}">
|
||||
<iconoir.Message />
|
||||
</div>
|
||||
<div @click="mode='agents'" class="tab" :class="{active: mode=='agents'}">
|
||||
<iconoir.Group />
|
||||
</div>
|
||||
<div @click="mode='settings'" class="tab" :class="{active: mode=='settings'}">
|
||||
<iconoir.Settings />
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-group">
|
||||
<button class="hollow icon" style="margin: auto;" @click="sidebarCollapsed=true;">
|
||||
<<
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<nav v-if="mode=='runs'">
|
||||
</nav>
|
||||
<nav v-if="mode=='chats'">
|
||||
<RouterLink v-for="it in chats"
|
||||
:to="'/chat/' + it.id"
|
||||
activeClass="active"
|
||||
class="nav-item">
|
||||
<iconoir.Message class="icon" />{{it.name}}
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/chat/create"
|
||||
activeClass="active"
|
||||
class="nav-item">
|
||||
<iconoir.PlusCircle class="icon" />Create Chat
|
||||
</RouterLink>
|
||||
</nav>
|
||||
<nav v-if="mode=='agents'">
|
||||
<RouterLink v-for="it in agents"
|
||||
:to="'/agent/' + it.agentname"
|
||||
activeClass="active"
|
||||
class="nav-item">
|
||||
<iconoir.User class="icon" />{{it.name}}
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/agent/create"
|
||||
activeClass="active"
|
||||
class="nav-item">
|
||||
<iconoir.PlusCircle class="icon" />Create Agent
|
||||
</RouterLink>
|
||||
</nav>
|
||||
<nav v-if="mode=='settings'">
|
||||
<RouterLink to="/user" activeClass="active" class="nav-item no-icon">User</RouterLink>
|
||||
<RouterLink to="/models" activeClass="active" class="nav-item no-icon">Manage Models</RouterLink>
|
||||
<RouterLink to="/remotes" activeClass="active" class="nav-item no-icon">Remotes</RouterLink>
|
||||
<RouterLink to="/drives" activeClass="active" class="nav-item no-icon">Drives</RouterLink>
|
||||
<RouterLink to="/runs" activeClass="active" class="nav-item no-icon">Agent Runs</RouterLink>
|
||||
</nav>
|
||||
</Sidebar>
|
||||
<RouterView />
|
||||
</template>
|
||||
</template>
|
||||
@@ -118,4 +208,78 @@ form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
||||
/* Sidebar tabs (slot content, compiled in this component's scope) */
|
||||
.tab {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
list-style-type: none;
|
||||
height: var(--header-height);
|
||||
width: var(--header-height);
|
||||
|
||||
padding-top: 3px;
|
||||
color: var(--color-text-mute);
|
||||
/*flex-grow: 1;*/
|
||||
}
|
||||
|
||||
.tab-bar, .tab-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.tab-bar {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tab.active, .tab:hover {
|
||||
background: linear-gradient(to bottom, var(--color-surface-strong) 20%, var(--color-surface) 100%);
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--color-heading);
|
||||
padding-top: 0;
|
||||
border-top: 3px solid var(--color-heading);
|
||||
}
|
||||
.tab.active {
|
||||
color: var(--color-highlight);
|
||||
padding-top: 0;
|
||||
border-top: 3px solid var(--color-highlight);
|
||||
}
|
||||
|
||||
/* Sidebar nav items */
|
||||
.nav-item {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
font-size: inherit;
|
||||
|
||||
padding: 0 0.6em;
|
||||
width: 100%;
|
||||
height: 3em;
|
||||
|
||||
border-left: 3px solid rgba(1,1,1,0);
|
||||
}
|
||||
|
||||
.nav-item .icon {
|
||||
margin-right: 0.6em;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: var(--color-heading);
|
||||
background: linear-gradient(to left, var(--color-surface) 0%, var(--color-surface-strong) 80%);
|
||||
border-color: var(--color-heading);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--color-highlight);
|
||||
background: linear-gradient(to left, var(--color-surface) 0%, var(--color-surface-strong) 80%);
|
||||
border-color: var(--color-highlight);
|
||||
}
|
||||
</style>
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
--color-surface: var(--color-background-soft);
|
||||
--color-surface-strong: var(--color-background-mute);
|
||||
--color-surface-input: var(--color-background);
|
||||
--color-surface-input: #1e1e1e;
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
@@ -13,7 +13,10 @@ main {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
main section {
|
||||
.row { display: flex; flex-direction: row; flex: 1; }
|
||||
.column { display: flex; flex-direction: column; flex: 1; }
|
||||
|
||||
main section, .sidebar section {
|
||||
margin: 1em;
|
||||
padding: 1em;
|
||||
border: 1px solid var(--color-border);
|
||||
@@ -21,15 +24,6 @@ main section {
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
.main-sidebar {
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: var(--z-index-sidebar);
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
.main-header {
|
||||
height: var(--header-height);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
@@ -62,6 +56,12 @@ button.hollow:hover {
|
||||
border: none;
|
||||
}
|
||||
|
||||
button.icon {
|
||||
width: 3em;
|
||||
height: 3em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: var(--color-heading);
|
||||
border-color: var(--color-border-hover);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
||||
|
||||
// The container owns the collapsed state via v-model:collapsed
|
||||
const collapsed = defineModel('collapsed');
|
||||
|
||||
const props = defineProps({
|
||||
// Which side of the layout the sidebar sits on. Required.
|
||||
align: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: (value) => ['left', 'right'].includes(value)
|
||||
},
|
||||
minWidth: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
validator: (value) => isFinite(parseNumber(value)),
|
||||
},
|
||||
maxWidth: {
|
||||
type: Number,
|
||||
default: Infinity,
|
||||
validator: (value) => isFinite(parseNumber(value)),
|
||||
},
|
||||
});
|
||||
|
||||
const sidebarWidth = ref(200);
|
||||
let dragState = null;
|
||||
|
||||
onMounted(() => {
|
||||
document.body.addEventListener('mousemove', documentMouseMove);
|
||||
document.body.addEventListener('mouseup', documentMouseUp);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.body.removeEventListener('mousemove', documentMouseMove);
|
||||
document.body.removeEventListener('mouseup', documentMouseUp);
|
||||
});
|
||||
|
||||
watch([props.minWidth, sidebarWidth.value], ([min, width]) => {
|
||||
if (width < min) sidebarWidth.value = min;
|
||||
}, {immediate: true});
|
||||
|
||||
function dragBarMouseDown (event) {
|
||||
dragState = {x: event.clientX, y: event.clientY, width: sidebarWidth.value};
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
function documentMouseMove (event) {
|
||||
if (dragState) {
|
||||
let widthDiff = event.clientX - dragState.x;
|
||||
// A right-aligned sidebar grows when dragging towards the left.
|
||||
if (props.align === 'right') widthDiff = -widthDiff;
|
||||
let newWidth = dragState.width + widthDiff;
|
||||
newWidth = Math.min(props.maxWidth, Math.max(props.minWidth, newWidth));
|
||||
sidebarWidth.value = newWidth;
|
||||
}
|
||||
}
|
||||
|
||||
function documentMouseUp (event) {
|
||||
if (dragState) {
|
||||
dragState = null;
|
||||
}
|
||||
}
|
||||
|
||||
function sidebarStyle () {
|
||||
return {
|
||||
flex: `0 0 ${sidebarWidth.value}px`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="sidebar" :style="sidebarStyle()" v-if="!collapsed">
|
||||
<div :class="['drag-bar', align]" @mousedown="dragBarMouseDown($event)"></div>
|
||||
<slot></slot>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
background-color: var(--color-surface);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.drag-bar {
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--color-border);
|
||||
width: 0.5em;
|
||||
position: absolute;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.drag-bar.left { right: 0; }
|
||||
.drag-bar.right { left: 0; }
|
||||
|
||||
/* Adapt sidebar to small screens */
|
||||
@media (max-width: 480px) {
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.drag-bar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<template>
|
||||
<header class="main-header">
|
||||
<button class="header-button when-sidebar-collapsed" @click="sidebarExpand()">
|
||||
<button class="hollow icon when-sidebar-collapsed" @click="sidebarExpand()">
|
||||
>>
|
||||
<!--i class="fa-solid fa-angles-right"></i-->
|
||||
</button>
|
||||
@@ -30,11 +30,4 @@
|
||||
.main-header {
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
.header-button {
|
||||
height: var(--header-height);
|
||||
width: var(--header-height);
|
||||
background-color: none;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,228 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router'
|
||||
import {
|
||||
ref, computed, reactive, watch,
|
||||
onMounted, onUnmounted,
|
||||
} from "vue";
|
||||
|
||||
import * as backend from '../../backend';
|
||||
import * as ui from '../../ui';
|
||||
|
||||
import * as iconoir from '@iconoir/vue';
|
||||
|
||||
const chats = ref([]);
|
||||
const agents = ref([]);
|
||||
|
||||
const mode = ref('chats');
|
||||
const sidebarExpanded = ref(true);
|
||||
|
||||
const MIN_WIDTH = 150;
|
||||
const MAX_WIDTH = 400;
|
||||
const sidebarWidth = ref(200);
|
||||
let dragState = null;
|
||||
|
||||
watch(sidebarExpanded, newValue => {
|
||||
if (newValue) {
|
||||
document.body.classList.add('with-sidebar-expanded');
|
||||
} else {
|
||||
document.body.classList.remove('with-sidebar-expanded');
|
||||
}
|
||||
}, {immediate: true});
|
||||
|
||||
ui.listen('sidebar-expand', ev => {
|
||||
sidebarExpanded.value = true;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
document.body.addEventListener('mousemove', documentMouseMove);
|
||||
document.body.addEventListener('mouseup', documentMouseUp);
|
||||
|
||||
chats.value = (await backend.getChats()).map(it => {
|
||||
return Object.assign({}, it);
|
||||
});
|
||||
agents.value = (await backend.getAgents()).map(it => {
|
||||
return Object.assign({}, it);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.body.removeEventListener('mousemove', documentMouseMove);
|
||||
document.body.removeEventListener('mouseup', documentMouseUp);
|
||||
});
|
||||
|
||||
function dragBarMouseDown (event) {
|
||||
dragState = {x: event.clientX, y: event.clientY, width: sidebarWidth.value};
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
console.log("Set Drag State", dragState);
|
||||
}
|
||||
|
||||
function documentMouseMove (event) {
|
||||
if (dragState) {
|
||||
let widthDiff = event.clientX - dragState.x;
|
||||
let newWidth = dragState.width + widthDiff;
|
||||
newWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, newWidth));
|
||||
sidebarWidth.value = newWidth;
|
||||
}
|
||||
}
|
||||
|
||||
function documentMouseUp (event) {
|
||||
if (dragState) {
|
||||
dragState = null;
|
||||
}
|
||||
}
|
||||
|
||||
function sidebarStyle () {
|
||||
return {
|
||||
flex: `0 0 ${sidebarWidth.value}px`
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="main-sidebar" :style="sidebarStyle()" v-if="sidebarExpanded">
|
||||
<div class="drag-bar" @mousedown="dragBarMouseDown($event)"></div>
|
||||
<div class="tab-bar">
|
||||
<div class="tab-group">
|
||||
<div @click="mode='chats'" class="tab" :class="{active: mode=='chats'}">
|
||||
<iconoir.Message />
|
||||
</div>
|
||||
<div @click="mode='agents'" class="tab" :class="{active: mode=='agents'}">
|
||||
<iconoir.Group />
|
||||
</div>
|
||||
<div @click="mode='settings'" class="tab" :class="{active: mode=='settings'}">
|
||||
<iconoir.Settings />
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-group">
|
||||
<button class="header-button" @click="sidebarExpanded=false;">
|
||||
<!--i class="fa-solid fa-angles-left"></i-->
|
||||
<<
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<nav v-if="mode=='runs'">
|
||||
</nav>
|
||||
<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.agentname" class="nav-item">{{it.name}}</RouterLink>
|
||||
<RouterLink to="/agent/create" class="nav-item">Create Agent</RouterLink>
|
||||
</nav>
|
||||
<nav v-if="mode=='settings'">
|
||||
<RouterLink to="/user" class="nav-item">User</RouterLink>
|
||||
<RouterLink to="/models" class="nav-item">Manage Models</RouterLink>
|
||||
<RouterLink to="/remotes" class="nav-item">Remotes</RouterLink>
|
||||
<RouterLink to="/drives" class="nav-item">Drives</RouterLink>
|
||||
<RouterLink to="/runs" class="nav-item">Agent Runs</RouterLink>
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main-sidebar {
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
.tab-bar, .tab-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.tab-bar {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
list-style-type: none;
|
||||
height: var(--header-height);
|
||||
width: var(--header-height);
|
||||
|
||||
padding-top: 3px;
|
||||
color: var(--color-text-mute);
|
||||
/*flex-grow: 1;*/
|
||||
}
|
||||
|
||||
.tab.active, .tab:hover {
|
||||
background: linear-gradient(to bottom, var(--color-surface-strong) 20%, var(--color-surface) 100%);
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--color-heading);
|
||||
padding-top: 0;
|
||||
border-top: 3px solid var(--color-heading);
|
||||
}
|
||||
.tab.active {
|
||||
color: var(--color-highlight);
|
||||
padding-top: 0;
|
||||
border-top: 3px solid var(--color-highlight);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: block;
|
||||
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
border-radius: 0;
|
||||
padding: 0.2em 0.4em;
|
||||
width: 100%;
|
||||
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: var(--color-heading);
|
||||
background-color: var(--color-surface-strong);
|
||||
}
|
||||
|
||||
.nav-item i {
|
||||
font-size: 80%;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
.drag-bar {
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--color-border);
|
||||
width: 0.5em;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
/* Adapt sidebar to small screens */
|
||||
@media (max-width: 480px) {
|
||||
.main-sidebar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.drag-bar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.header-button {
|
||||
height: var(--header-height);
|
||||
width: var(--header-height);
|
||||
/*border-radius: 1.5em;*/
|
||||
background-color: none;
|
||||
border: none;
|
||||
}
|
||||
.header-button:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
</style>
|
||||
+70
-60
@@ -6,6 +6,7 @@
|
||||
|
||||
import TitleBar from '../components/TitleBar.vue';
|
||||
import Message from '../components/Message.vue';
|
||||
import Sidebar from '../components/Sidebar.vue'
|
||||
|
||||
import * as backend from '../backend';
|
||||
import * as ui from '../ui';
|
||||
@@ -35,6 +36,7 @@
|
||||
const expandedLogs = ui.useToggleSet();
|
||||
const inputFieldElem = ref(null);
|
||||
const chatContainerElem = ref(null);
|
||||
const sidebarCollapsed = ref(false);
|
||||
|
||||
let _isMounted = false;
|
||||
|
||||
@@ -194,67 +196,81 @@
|
||||
<main>
|
||||
<TitleBar>
|
||||
<h1>{{ chat.name || 'Chat #' + chat.id }}</h1>
|
||||
<div>
|
||||
<button @click="renameChat(chat)">Rename</button>
|
||||
<button @click="deleteChat(chat)">Delete</button>
|
||||
</div>
|
||||
<button class="hollow icon" style="margin-left: auto;" @click="sidebarCollapsed=!sidebarCollapsed">
|
||||
<iconoir.Settings />
|
||||
</button>
|
||||
</TitleBar>
|
||||
|
||||
<div class="messages-container" ref="chatContainerElem">
|
||||
<div class="message-container" v-for="msg in chatEntries">
|
||||
<Message
|
||||
:key="msg.id"
|
||||
:message="msg"
|
||||
@open-menu="chatEntryContextMenu"
|
||||
@open-thread="session.activateThread($event)"
|
||||
<div class="row" style="overflow: auto;">
|
||||
<div class="column">
|
||||
<div class="messages-container" ref="chatContainerElem">
|
||||
<div class="message-container" v-for="msg in chatEntries">
|
||||
<Message
|
||||
:key="msg.id"
|
||||
:message="msg"
|
||||
@open-menu="chatEntryContextMenu"
|
||||
@open-thread="session.activateThread($event)"
|
||||
|
||||
:log-expanded="expandedLogs.has(msg.id)"
|
||||
@toggle-log="expandedLogs.toggle(msg.id)"
|
||||
:log-expanded="expandedLogs.has(msg.id)"
|
||||
@toggle-log="expandedLogs.toggle(msg.id)"
|
||||
|
||||
:cancellable="messageRunMap[msg.id]"
|
||||
@cancel="session.cancelMessageRun(msg.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-container">
|
||||
<div v-if="statusLine" class="status-line">
|
||||
{{statusLine}}
|
||||
<button class="hollow status-close" v-if="editingMessageId != null" @click="editMessage(null)">
|
||||
<iconoir.Xmark />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="input-field" ref="inputFieldElem" @keydown.ctrl.enter.prevent="sendMessage" contenteditable></div>
|
||||
|
||||
<div class="input-row">
|
||||
<div>
|
||||
<button class="hollow" :class="{primary: creatingThread}" @click="newThreadClick()">
|
||||
<iconoir.ChatPlusInSolid v-if='creatingThread' />
|
||||
<iconoir.ChatPlusIn v-else />
|
||||
</button>
|
||||
<button class="hollow" :class="{primary: noteMode}" @click="noteMode=!noteMode">
|
||||
<iconoir.PostSolid v-if='noteMode' />
|
||||
<iconoir.Post v-else />
|
||||
</button>
|
||||
<button class="hollow" @click="sendPicture()">
|
||||
<iconoir.Camera />
|
||||
</button>
|
||||
<button class="hollow" @click="uploadFile()">
|
||||
<iconoir.Attachment />
|
||||
</button>
|
||||
:cancellable="messageRunMap[msg.id]"
|
||||
@cancel="session.cancelMessageRun(msg.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="hollow" @click="addRemoteResource()">
|
||||
Add Remote
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button class="hollow primary" id="send-button" @click="sendMessage()">
|
||||
<iconoir.SendDiagonalSolid width="3em" height="3em" />
|
||||
</button>
|
||||
|
||||
<div class="input-container">
|
||||
<div v-if="statusLine" class="status-line">
|
||||
{{statusLine}}
|
||||
<button class="hollow status-close" v-if="editingMessageId != null" @click="editMessage(null)">
|
||||
<iconoir.Xmark />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="input-field" ref="inputFieldElem" @keydown.ctrl.enter.prevent="sendMessage" contenteditable></div>
|
||||
|
||||
<div class="input-row">
|
||||
<div>
|
||||
<button class="hollow icon" :class="{primary: creatingThread}" @click="newThreadClick()">
|
||||
<iconoir.ChatPlusInSolid v-if='creatingThread' />
|
||||
<iconoir.ChatPlusIn v-else />
|
||||
</button>
|
||||
<button class="hollow icon" :class="{primary: noteMode}" @click="noteMode=!noteMode">
|
||||
<iconoir.PostSolid v-if='noteMode' />
|
||||
<iconoir.Post v-else />
|
||||
</button>
|
||||
<button class="hollow icon" @click="sendPicture()">
|
||||
<iconoir.Camera />
|
||||
</button>
|
||||
<button class="hollow icon" @click="uploadFile()">
|
||||
<iconoir.Attachment />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button class="hollow icon primary" id="send-button" @click="sendMessage()">
|
||||
<iconoir.SendDiagonalSolid width="3em" height="3em" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Sidebar align="right" minWidth=300 maxWidth=800 v-model:collapsed="sidebarCollapsed">
|
||||
<section>
|
||||
<h3>Settings</h3>
|
||||
<div>
|
||||
<button @click="renameChat(chat)">Rename</button>
|
||||
<button @click="deleteChat(chat)">Delete</button>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Remotes</h3>
|
||||
<button @click="addRemoteResource()">
|
||||
Add Remote to Current Thread
|
||||
</button>
|
||||
</section>
|
||||
</Sidebar>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
@@ -320,12 +336,6 @@
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.input-row button {
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#send-button {
|
||||
width: 5em;
|
||||
height: 4em;
|
||||
|
||||
Reference in new issue
Block a user