Add MessageService and AIThoughtMessage component

This commit is contained in:
Andrew Beal 2025-09-28 14:57:58 +01:00
parent ce41512c42
commit dbe9068b0b
7 changed files with 104 additions and 18 deletions

View file

@ -2,11 +2,13 @@
import { Resolve } from "Services/DependencyService";
import { Services } from "Services/Services";
import type { StreamingMarkdownService } from "Services/StreamingMarkdownService";
import ChatAreaThought from "./ChatAreaThought.svelte";
export let messages: Array<{id: string, content: string, isUser: boolean, isStreaming: boolean}> = [];
let chatContainer: HTMLDivElement;
let streamingMarkdownService: StreamingMarkdownService = Resolve(Services.StreamingMarkdownService);
let chatContainer: HTMLDivElement;
let messageElements = new Map<string, HTMLElement>();
let lastProcessedContent = new Map<string, string>();
@ -30,10 +32,8 @@
if (!element) return;
if (message.isStreaming) {
// Use streaming update
streamingMarkdownService.streamChunk(message.id, message.content);
} else {
// Finalize the message
streamingMarkdownService.finalizeStream(message.id, message.content);
}
}
@ -49,7 +49,6 @@
return {
destroy() {
// Cleanup when element is removed
messageElements.delete(messageId);
}
};
@ -74,12 +73,6 @@
return ''; // Streaming messages will be handled by the streaming service
}
// Clean up when component is destroyed
function cleanup() {
messageElements.clear();
lastProcessedContent.clear();
}
// Make sure to clean up when messages are removed
$: {
const currentMessageIds = new Set(messages.map(m => m.id));
@ -102,16 +95,14 @@
<p>{message.content}</p>
{:else}
<div class="markdown-content" class:streaming={message.isStreaming}>
<!-- Streaming message: use action for initialization -->
{#if message.isStreaming}
<!-- Streaming message: use action for initialization -->
<div
use:streamingAction={message.id}
class="streaming-content"
></div>
<span class="streaming-indicator">● ● ●</span>
<div use:streamingAction={message.id} class="streaming-content"></div>
<span class="streaming-indicator">● ● ●</span>
<ChatAreaThought/>
{:else}
<!-- Static message: use traditional rendering -->
{@html getStaticHTML(message)}
<!-- Static message: use traditional rendering -->
{@html getStaticHTML(message)}
{/if}
</div>
{/if}

View file

@ -0,0 +1,53 @@
<script lang="ts">
import { AIThoughtMessage } from "Messages/AIThoughtMessage";
import { Resolve } from "Services/DependencyService";
import type { MessageService } from "Services/MessageService";
import { Services } from "Services/Services";
import { onDestroy } from "svelte";
import { fade } from "svelte/transition";
let messageService: MessageService = Resolve(Services.MessageService);
let currentThought: string | null = null;
let isVisible: boolean = false;
// Handler for AI thought messages
function handleAIThought(message: AIThoughtMessage): void {
currentThought = message.thought;
isVisible = currentThought !== null && currentThought.trim().length > 0;
}
messageService.register(AIThoughtMessage, handleAIThought);
onDestroy(() => {
messageService.unregister(AIThoughtMessage, handleAIThought);
});
</script>
{#if isVisible && currentThought}
<div class="ai-thought-container" in:fade={{ duration: 200 }} out:fade={{ duration: 200 }}>
<div class="ai-thought-bubble">
<span>{currentThought}</span>
</div>
</div>
{/if}
<style>
.ai-thought-container {
margin-top: var(--size-2-1);
margin-bottom: var(--size-2-1);
}
.ai-thought-bubble {
display: inline-flex;
align-items: center;
background: var(--metadata-background);
border: var(--border-width) solid var(--metadata-border-color);
border-radius: var(--metadata-border-radius);
padding: var(--metadata-padding);
font-size: var(--metadata-label-font-size);
color: var(--text-muted);
font-style: italic;
max-width: 100%;
word-wrap: break-word;
}
</style>

View file

@ -0,0 +1,7 @@
import type { IMessage } from "./IMessage";
export class AIThoughtMessage implements IMessage {
constructor(public thought: string | null) {}
invoke(): void {
}
}

3
Messages/IMessage.ts Normal file
View file

@ -0,0 +1,3 @@
export interface IMessage {
invoke(): void;
}

View file

@ -0,0 +1,29 @@
import type { IMessage } from "Messages/IMessage";
type MessageConstructor<T extends IMessage> = new (...args: any[]) => T;
export class MessageService {
private handlers = new Map<MessageConstructor<any>, Set<(message: any) => void>>();
register<T extends IMessage>(messageType: MessageConstructor<T>, handler: (message: T) => void): void {
if (!this.handlers.has(messageType)) {
this.handlers.set(messageType, new Set());
}
let messageHandler: Set<(message: any) => void> | undefined = this.handlers.get(messageType);
if (messageHandler != null) {
messageHandler.add(handler);
}
}
unregister<T extends IMessage>(messageType: MessageConstructor<T>, handler: (message: T) => void): void {
this.handlers.get(messageType)?.delete(handler);
}
send<T extends IMessage>(message: T): void {
const messageType = message.constructor as MessageConstructor<T>;
const handlers = this.handlers.get(messageType);
handlers?.forEach(handler => handler(message));
}
}

View file

@ -12,8 +12,10 @@ import { GeminiActionDefinitions } from "Actioner/Gemini/GeminiActionDefinitions
import type { IActionDefinitions } from "Actioner/IActionDefinitions";
import { Gemini } from "AIClasses/Gemini/Gemini";
import { StreamingMarkdownService } from "./StreamingMarkdownService";
import { MessageService } from "./MessageService";
export function RegisterDependencies(plugin: DmsAssistantPlugin) {
RegisterSingleton(Services.MessageService, new MessageService());
RegisterSingleton(Services.DmsAssistantPlugin, plugin);
RegisterSingleton(Services.OdbCache, new OdbCache());
RegisterSingleton(Services.ModalService, new ModalService())

View file

@ -1,4 +1,5 @@
export class Services {
static MessageService = Symbol("MessageService");
static DmsAssistantPlugin = Symbol("DmsAssistantPlugin");
static OdbCache = Symbol("OdbCache");
static ModalService = Symbol("ModalService");