frontend,chat: chat redesign, all message styles are now beautiful

This commit is contained in:
Shivering Ocelot committed 2026-09-25 23:43:54 -05:00
1 parent 73b789b586
commit 98fbed03cc
3 files changed
+92 -65

No files matched your search

+6 -3
View File
@@ -788,6 +788,7 @@ export class ChatSession {
type: 'thread-header',
threadId: fork.id,
active: false,
isFork: true,
messageCount: fork.messageCount,
time: firstMessage?.time,
@@ -796,7 +797,7 @@ export class ChatSession {
});
}
}
const addThreadHeader = (thread) => {
const addThreadHeader = (thread, isRoot) => {
let isActive = this.activeThreadIds.includes(thread.id);
let referenceMessage;
@@ -837,11 +838,11 @@ export class ChatSession {
if (!referenceMessage) return;
headers.push({
// claim just before id
id: 't.' + thread.id,
type: 'thread-header',
threadId: thread.id,
active: isActive,
isFork: !isRoot,
messageCount: isActive ? 0 : thread.messageCount,
time: referenceMessage.time,
@@ -851,7 +852,9 @@ export class ChatSession {
});
}
this.threads.forEach(addThreadHeader);
for (let root of this.threads) {
addThreadHeader(root, true);
}
// Headers must be ordered by their attach id
headers.sort((a, b) => parseInt(a.attach.id) - parseInt(b.attach.id));
+78 -59
View File
@@ -16,6 +16,7 @@ marked.use({
}
});
const emit = defineEmits(['open-thread', 'open-menu', 'toggle-log', 'cancel'])
const props = defineProps({
message: {
type: Object,
@@ -39,7 +40,12 @@ function chatEntryClass(msg) {
} else if (msg.type == 'resource') {
return ['resource', 'resource-' + (msg.subkind || 'generic')];
} else if (msg.type == 'log') {
return ['message', 'log'];
return ['log'];
} else if (msg.type == 'thread-header') {
let cls = ['thread'];
if (!msg.active) cls.push('thread-open')
if (msg.isFork) cls.push('thread-fork');
return cls;
} else {
return ['info'];
}
@@ -50,6 +56,14 @@ function resourceLabel(msg) {
return msg.subkind || 'Resource';
}
function handleClick(msg) {
if (msg.type == 'thread-header' || msg.type == 'fork-header') {
if (!msg.active && msg.threadId != null) {
emit('open-thread', msg.threadId)
}
}
}
const format = ref(null);
const content = ref("");
@@ -94,7 +108,7 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
</script>
<template>
<div :class="chatEntryClass(message)" @contextmenu.prevent="$emit('open-menu', message, logMeta)">
<div :class="chatEntryClass(message)" @contextmenu.prevent="$emit('open-menu', message, logMeta)" @click="handleClick(message)">
<template v-if="message.type == 'message' || message.type == null">
<div v-if="message.sender != 'user'" class="sender">{{message.sender}}</div>
@@ -119,19 +133,14 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
</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>
<div v-if="message.preview" class="content">
<span class="sender">{{ message.preview.sender }}:</span>
{{ message.preview.content }}
</div>
</div>
</template>
<button v-if="cancellable" @click="$emit('cancel')">Cancel</button>
@@ -139,21 +148,36 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
</template>
<style scoped>
.info {
align-self: center;
}
.message {
width: fit-content;
max-width: 50em;
padding: 10px;
max-width: 100%;
padding: 1em;
border-radius: 8px;
background-color: var(--color-background-soft);
}
.note {
/* non-messages all share basics */
.note, .log, .info, .thread, .resource {
padding: 0.6em 1.4em;
align-self: center;
max-width: 100%;
}
/* solid entries have horizontal margin */
.note, .thread-fork.thread-open, .resource {
margin-left: 2em;
margin-right: 2em;
}
/* content by default fits in the entry */
.content {
white-space: preserve;
width: fit-content;
padding: 10px;
word-wrap: anywhere;
max-width: 50em;
}
.note {
background-color: var(--color-background-soft);
border: 1px solid var(--color-border);
}
@@ -168,7 +192,6 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
.message .content {
white-space: preserve;
width: fit-content;
}
.message ul, .message ol {
@@ -180,18 +203,21 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
font-weight: bold;
font-size: 100%;
margin-top: -5px; /* half message padding */
width: fit-content;
max-width: 100%;
}
.message.log .content {
.log .content {
font-family: monospace;
font-size: 80%;
max-width: 70em;
}
.message.log .log-sender {
.log .log-sender {
font-family: monospace;
font-weight: bold;
font-size: 80%;
}
.message.log .log-toggle {
.log .log-toggle {
font-family: monospace;
font-size: 80%;
cursor: pointer;
@@ -199,10 +225,7 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
}
.resource {
align-self: center;
width: fit-content;
padding: 4px 12px;
border-radius: 12px;
border-radius: var(--border-radius);
border: 1px dashed var(--color-border);
background-color: var(--color-background-soft);
font-size: 85%;
@@ -220,58 +243,54 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
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%;
.thread {
width: 100%;
text-align: center;
cursor: pointer;
user-select: none;
border-top: 1px solid var(--color-border);
}
.thread-header:hover {
border-style: solid;
.thread.thread-open {
cursor: pointer;
}
.thread-header .thread-title {
.thread-open:hover {
border-top: 1px solid var(--color-heading);
}
.thread.thread-fork {
border-top: none;
}
.thread.thread-fork.thread-open {
width: fit-content;
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
cursor: pointer;
}
.thread-title {
font-weight: bold;
opacity: 70%;
}
.thread-header .thread-count {
.thread-count {
font-weight: normal;
opacity: 80%;
}
.thread-header .thread-preview {
.thread .content {
justify-self: center;
font-style: italic;
opacity: 60%;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
opacity: 80%;
white-space: normal;
}
.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 {
.thread:hover .content {
opacity: 100%;
}
.thread-header:hover {
border-style: solid;
.thread .sender {
font-style: normal;
font-weight: bold;
}
</style>
+8 -3
View File
@@ -196,8 +196,8 @@
</TitleBar>
<div class="messages-container" ref="chatContainerElem">
<div class="message-container" v-for="msg in chatEntries">
<Message
v-for="msg in chatEntries"
:key="msg.id"
:message="msg"
@open-menu="chatEntryContextMenu"
@@ -210,6 +210,7 @@
@cancel="session.cancelMessageRun(msg.id)"
/>
</div>
</div>
<div class="input-container">
<div v-if="editInfo" style="font-size: 75%; margin-bottom: 0.7em;">
@@ -261,8 +262,12 @@
flex-direction: column;
}
.messages-container > div {
margin-bottom: 10px;
.message-container {
width: 100%;
max-width: 80em;
margin: 0 auto 0.6em;
display: flex;
flex-direction: column;
}
.input-container {