initial commit

This commit is contained in:
2025-12-31 15:33:16 -05:00
commit 89f2114b06
51 changed files with 4779 additions and 0 deletions

View File

@@ -0,0 +1,243 @@
import Alpine from 'alpinejs';
import { Marked } from 'marked';
import { markedHighlight } from 'marked-highlight';
import hljs from 'highlight.js/lib/common';
import {
getSettings,
getModels,
sendMessage,
getChatMessages,
listChats,
deleteChat,
} from '../client';
import { Chat, Message, MessageChunk, Model, Settings } from '../types';
import { applyFilter } from '../utils';
const CHAT_ROUTE = '#/chats';
const MODEL_KEY = 'aethera-chat-model';
const IN_PROGRESS_UUID = '00000000-0000-0000-0000-000000000000';
// Markdown Renderer
const marked = new Marked(
markedHighlight({
emptyLangClass: 'hljs',
langPrefix: 'hljs language-',
highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
},
}),
);
Alpine.data('chatManager', () => ({
chats: [] as Chat[],
settings: {} as Settings,
_models: [] as Model[],
selectedModel: '',
inputMessage: '',
error: '',
selectedChatID: null as string | null,
chatListOpen: false,
loading: false,
async init() {
// Acquire Data
this._models = await getModels();
this.settings = await getSettings();
this.selectedModel = localStorage.getItem(MODEL_KEY) || '';
await this.loadChats();
// Route Chat
const chatID = window.location.hash.split('/')[2];
if (chatID) await this.selectChat(chatID);
},
async loadChats() {
try {
const response = await listChats();
this.chats = response.chats || [];
} catch (err) {
console.error('Error loading conversations:', err);
}
},
async deleteChat(event: Event, chatId: string) {
event.stopPropagation();
try {
await deleteChat(chatId);
// Delete Chat
const chatIndex = this.chats.findIndex((c) => c.id == chatId);
this.chats.splice(chatIndex, 1);
// Update Index
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);
this.error = 'Failed to delete conversation';
}
},
async sendMessage() {
const message = this.inputMessage.trim();
if (!message || this.loading) return;
// Update State
this.inputMessage = '';
this.loading = true;
this.error = '';
// Save Model
if (this.selectedModel) localStorage.setItem(MODEL_KEY, this.selectedModel);
// New Chat
if (!this.selectedChatID) {
this.chats.unshift({
id: IN_PROGRESS_UUID,
created_at: new Date().toISOString(),
title: '',
initial_message: message,
message_count: 0,
messages: [],
});
this.selectedChatID = IN_PROGRESS_UUID;
}
// New User Message
let userMessage: Message = {
id: IN_PROGRESS_UUID,
chat_id: this.selectedChatID,
role: 'user',
thinking: '',
content: message,
created_at: new Date().toISOString(),
};
// Get Chat
let currentChat: Chat = this.chats.find(
(c) => c.id === this.selectedChatID,
)!;
// Add User Message
currentChat.messages.push(userMessage);
currentChat.message_count += 1;
// Assistant Message Placeholder
let assistantMessage: Message | undefined;
try {
await sendMessage(
this.selectedChatID === IN_PROGRESS_UUID ? '' : this.selectedChatID,
{ model: this.selectedModel, prompt: message },
(chunk: MessageChunk) => {
// Handle Chat
if (chunk.chat) {
Object.assign(currentChat, {
...chunk.chat,
messages: currentChat.messages,
});
this.selectedChatID = chunk.chat.id;
this.updateHash(chunk.chat.id);
}
// Handle User Message
if (chunk.user_message) {
Object.assign(userMessage, chunk.user_message);
}
// Handle Assistant Message
if (chunk.assistant_message) {
if (!assistantMessage) {
assistantMessage = chunk.assistant_message;
currentChat.messages.push(assistantMessage);
} else {
const index = currentChat.messages.findIndex(
(m) => m.id === assistantMessage!.id,
);
if (index !== -1) {
currentChat.messages[index] = {
...assistantMessage,
...chunk.assistant_message,
};
currentChat.messages = [...currentChat.messages];
}
}
}
},
);
} catch (err) {
console.error('Error sending message:', err);
this.error = parseError(err);
} finally {
this.loading = false;
}
},
updateHash(chatID: string | null) {
const newRoute = CHAT_ROUTE + (chatID ? '/' + chatID : '');
window.history.pushState(null, '', newRoute);
},
async selectChat(chatID: string | null) {
this.updateHash(chatID);
// Load Messages
this.selectedChatID = chatID;
if (!this.selectedChatID) this.chatListOpen = false;
else this.loadChatMessages();
},
async loadChatMessages() {
if (!this.selectedChatID) return;
try {
const response = await getChatMessages(this.selectedChatID);
const chatIndex = this.chats.findIndex(
(c) => c.id == this.selectedChatID,
);
this.chats[chatIndex].messages = response.messages || [];
} catch (err) {
console.error('Error loading chat messages:', err);
this.error = 'Failed to load messages';
}
},
get models(): Model[] {
if (!this.settings.text_generation_selector) return this._models;
return applyFilter(this._models, this.settings.text_generation_selector);
},
get currentChatMessages(): Message[] {
if (!this.selectedChatID) return [];
const currentChat =
this.chats.find((c) => c.id === this.selectedChatID) ?? null;
if (!currentChat) return [];
return [...currentChat.messages].reverse();
},
renderMarkdown(content: string) {
return marked.parse(content);
},
}));
function parseError(err: unknown): string {
const msg = err instanceof Error ? err.message : String(err);
if (msg.includes('401'))
return 'Authentication failed. Please check your API settings.';
if (msg.includes('404'))
return 'API endpoint not found. Please check your configuration.';
if (msg.includes('500'))
return 'Server error. The text generation service is unavailable.';
if (msg.includes('network') || msg.includes('failed to fetch'))
return 'Network error. Please check your internet connection.';
return msg || 'Failed to send message';
}