refactor(style): update chat layout and scrolling

This commit is contained in:
2026-05-17 17:57:30 -04:00
parent 6307a64c9c
commit eddf5bf12d
9 changed files with 646 additions and 529 deletions

View File

@@ -14,6 +14,7 @@ import {
} from '../client';
import { Chat, Message, MessageChunk, Model, Settings } from '../types';
import { applyFilter } from '../utils';
import { createAutoScroll, AutoScroll } from '../utils/autoScroll';
const CHAT_ROUTE = '#/chats';
const MODEL_KEY = 'aethera-chat-model';
@@ -44,11 +45,14 @@ Alpine.data('chatManager', () => ({
error: '',
selectedChatID: null as string | null,
chatListOpen: typeof window !== 'undefined' && window.matchMedia('(min-width: 768px)').matches,
loading: false,
activeStreamChatID: null as string | null,
_autoScroll: null as AutoScroll | null,
async init() {
this._autoScroll = createAutoScroll();
// Acquire Data
this._models = await getModels();
this.settings = await getSettings();
@@ -58,6 +62,7 @@ Alpine.data('chatManager', () => ({
// Route Chat
const chatID = window.location.hash.split('/')[2];
if (chatID) await this.selectChat(chatID);
this._autoScroll.scrollToBottom();
},
async loadChats() {
@@ -82,7 +87,6 @@ Alpine.data('chatManager', () => ({
if (this.selectedChatID == chatId) {
const newIndex = Math.min(chatIndex, this.chats.length - 1);
this.selectChat(this.chats[newIndex]?.id);
if (!this.selectedChatID) this.chatListOpen = false;
}
} catch (err) {
console.error('Error deleting conversation:', err);
@@ -144,6 +148,7 @@ Alpine.data('chatManager', () => ({
created_at: new Date().toISOString(),
});
currentChat.message_count += 1;
this._autoScroll?.scrollToBottom('smooth');
try {
await sendMessage(
@@ -192,6 +197,8 @@ Alpine.data('chatManager', () => ({
if (chunk.user_message) this.upsertMessage(currentChat, chunk.user_message);
if (chunk.assistant_message)
this.upsertMessage(currentChat, chunk.assistant_message);
this._autoScroll?.maybeScrollToBottom();
},
upsertMessage(chat: Chat, message: Message) {
@@ -222,8 +229,10 @@ Alpine.data('chatManager', () => ({
// Load Messages
this.selectedChatID = chatID;
if (!this.selectedChatID) this.chatListOpen = false;
else this.loadChatMessages();
if (this.selectedChatID) {
await this.loadChatMessages();
this._autoScroll?.scrollToBottom();
}
},
async loadChatMessages() {
@@ -285,7 +294,11 @@ Alpine.data('chatManager', () => ({
const currentChat =
this.chats.find((c) => c.id === this.selectedChatID) ?? null;
if (!currentChat) return [];
return [...currentChat.messages].reverse();
return currentChat.messages;
},
get chatGroups(): { label: string; chats: Chat[] }[] {
return groupChatsByDay(this.chats);
},
renderMarkdown(content: string) {
@@ -293,6 +306,47 @@ Alpine.data('chatManager', () => ({
},
}));
function startOfDay(d: Date): number {
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
}
function groupChatsByDay(
chats: Chat[],
): { label: string; chats: Chat[] }[] {
const now = new Date();
const today = startOfDay(now);
const yesterday = today - 86_400_000;
const groups: { key: number; label: string; chats: Chat[] }[] = [];
let current: { key: number; label: string; chats: Chat[] } | null = null;
for (const chat of chats) {
const created = new Date(chat.created_at);
const day = startOfDay(created);
if (!current || current.key !== day) {
let label: string;
if (day === today) label = 'Today';
else if (day === yesterday) label = 'Yesterday';
else {
const opts: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
};
if (created.getFullYear() !== now.getFullYear())
opts.year = 'numeric';
label = created.toLocaleDateString(undefined, opts);
}
current = { key: day, label, chats: [] };
groups.push(current);
}
current.chats.push(chat);
}
return groups;
}
function parseError(err: unknown): string {
const msg = err instanceof Error ? err.message : String(err);