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