initial commit
This commit is contained in:
243
frontend/src/components/chatManager.ts
Normal file
243
frontend/src/components/chatManager.ts
Normal 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';
|
||||
}
|
||||
Reference in New Issue
Block a user