Files
hub/frontend/src/components/Message.vue
T

249 lines
5.6 KiB
Vue

<script setup>
import { ref, watch } from 'vue';
import { marked } from "marked";
import * as backend from '../backend';
marked.use({
renderer: {
code (code, infostring, escaped) {
let langElem = infostring ?
`<div class="language">${infostring}</div>` : "";
return `<div class="code">
${langElem}
<div class="content">${code}</div>
</div>`;
}
}
});
const props = defineProps({
message: {
type: Object,
required: true
},
cancellable: {
type: Boolean,
required: false,
}
});
function chatEntryClass(msg) {
if (msg.type == 'note') {
return ['note'];
} else if (msg.type == 'message') {
return ['message', msg.sender == 'user' ? 'sent' : 'received'];
} else if (msg.type == 'resource') {
return ['resource', 'resource-' + (msg.subkind || 'generic')];
} else if (msg.type == 'log') {
return ['message', 'log'];
} else {
return ['info'];
}
}
function resourceLabel(msg) {
if (msg.subkind == 'remote') return 'Remote';
return msg.subkind || 'Resource';
}
const format = ref(null);
const content = ref("");
let currentStreamId = null;
watch(() => [props.message.format, props.message.content], ([newFormat, newContent]) => {
// TODO: if format.startsWith("stream:")
if (newFormat == "stream") {
format.value = null;
currentStreamId = parseInt(newContent);
let stream = backend.readStream(currentStreamId);
stream.on('data', (chunk) => {
if (stream.id == currentStreamId) {
content.value = stream.partialContent;
}
});
} else {
format.value = newFormat;
content.value = newContent;
currentStreamId = null;
}
}, {immediate: true})
</script>
<template>
<div :class="chatEntryClass(message)" @contextmenu.prevent="$emit('open-menu', message)">
<template v-if="message.type == 'message' || message.type == null">
<div v-if="message.sender != 'user'" class="sender">{{message.sender}}</div>
<div v-if="format == 'markdown'" v-html="marked.parse(message.content)" class="content"></div>
<div v-else class="content">{{content}}</div>
</template>
<template v-if="message.type == 'note'">
<div class="sender">{{message.sender}}</div>
<div class="content">{{content}}</div>
</template>
<template v-if="message.type == 'resource'">
<div class="resource-label">{{ resourceLabel(message) }}</div>
<div class="content">{{content}}</div>
</template>
<template v-if="message.type == 'log'">
<div class="log-sender">{{message.sender}}<span v-if="message.subkind"> [{{message.subkind}}]</span></div>
<div class="content">{{content}}</div>
</template>
<template v-if="message.type == 'thread-header'">
<div class="thread-header"
:class="{ 'thread-active': message.active }"
@click="!message.active && message.threadId != null && $emit('open-thread', message.threadId)"
>
<div class="thread-title">
{{ message.threadId == null ? 'New Thread' : `Thread ${message.threadId}` }}
<span v-if="message.messageCount" class="thread-count">({{ message.messageCount }} messages)</span>
</div>
<div v-if="message.preview" class="thread-preview">
<span class="thread-preview-sender">{{ message.preview.sender }}:</span>
{{ message.preview.content }}
</div>
</div>
</template>
<button v-if="cancellable" @click="$emit('cancel')">Cancel</button>
</div>
</template>
<style scoped>
.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 ul, .message ol {
white-space: normal;
}
.message .sender,
.note .sender {
font-weight: bold;
font-size: 100%;
margin-top: -5px; /* half message padding */
}
.message.log .content {
font-family: monospace;
font-size: 80%;
}
.message.log .log-sender {
font-family: monospace;
font-weight: bold;
font-size: 80%;
}
.resource {
align-self: center;
width: fit-content;
padding: 4px 12px;
border-radius: 12px;
border: 1px dashed var(--color-border);
background-color: var(--color-background-soft);
font-size: 85%;
display: flex;
gap: 0.5em;
align-items: baseline;
}
.resource .resource-label {
font-weight: bold;
opacity: 60%;
}
.resource .content {
font-family: monospace;
}
.thread-header {
align-self: center;
width: fit-content;
max-width: 40em;
padding: 5px 16px;
border-radius: 12px;
border: 1px dashed var(--color-border);
background-color: var(--color-background-soft);
font-size: 80%;
text-align: center;
cursor: pointer;
user-select: none;
}
.thread-header:hover {
border-style: solid;
}
.thread-header .thread-title {
font-weight: bold;
opacity: 70%;
}
.thread-header .thread-count {
font-weight: normal;
opacity: 80%;
}
.thread-header .thread-preview {
font-style: italic;
opacity: 60%;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.thread-header .thread-preview-sender {
font-style: normal;
font-weight: bold;
}
.thread-header.thread-active {
border-style: solid;
cursor: default;
}
.thread-header.thread-active .thread-title {
opacity: 100%;
}
.thread-header:hover {
border-style: solid;
}
</style>