This commit is contained in:
2025-04-22 17:21:44 +02:00
parent f3b508c9f7
commit 9309509858
7 changed files with 814 additions and 198 deletions

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
import { MudConnection } from '$lib/connection/MudConnection';
import { ConnectionManager, activeConnections } from '$lib/connection/ConnectionManager';
import { GmcpHandler } from '$lib/gmcp/GmcpHandler';
import { TriggerSystem } from '$lib/triggers/TriggerSystem';
import { AccessibilityManager } from '$lib/accessibility/AccessibilityManager';
@@ -8,11 +9,13 @@
connections,
connectionStatus,
activeProfileId,
activeProfile,
activeProfile,
profiles,
addToOutputHistory,
updateGmcpData,
accessibilitySettings,
uiSettings
uiSettings,
outputHistory
} from '$lib/stores/mudStore';
import { get } from 'svelte/store';
@@ -24,6 +27,7 @@
// Local state
let connection: MudConnection | null = null;
let connectionManager: ConnectionManager | null = null;
let gmcpHandler: GmcpHandler | null = null;
let triggerSystem: TriggerSystem | null = null;
let accessibilityManager: AccessibilityManager | null = null;
@@ -52,47 +56,95 @@
}
}
let unsubscribeFunctions = [];
onMount(() => {
console.log(`MudConnection component mounted for profile: ${profileId}`);
// Initialize components
initializeComponents();
// Set up keyboard listener for speech control - use capture phase and make it top-level
document.addEventListener('keydown', handleKeyDown, true); // Using document instead of window
// Auto-connect if enabled
if (autoConnect) {
console.log('Auto-connecting profile:', profileId);
connect();
// Get the connection status from the store
const currentStatus = get(connectionStatus)[profileId];
// Check if we already have a connection in the ConnectionManager
if (currentStatus === 'connected' || currentStatus === 'connecting') {
console.log(`Connection exists for ${profileId}, getting from manager`);
const connectionManager = ConnectionManager.getInstance();
const existingConnection = connectionManager.getExistingConnection(profileId);
if (existingConnection) {
console.log(`Found existing connection for ${profileId}, updating local ref`);
// Update the connections store
connections.update(conns => ({
...conns,
[profileId]: existingConnection
}));
// Update local reference
connection = existingConnection;
// Re-attach listeners
setupConnectionListeners(existingConnection);
}
} else {
// Register this connection in the connections store with component instance
connections.update(conns => {
console.log(`Registering connection component for profile: ${profileId}`);
return {
...conns,
[profileId]: this
};
});
// Update connection status - ensure UI shows status as 'disconnected' at start
if (!currentStatus) {
connectionStatus.update(statuses => {
console.log(`Setting connection status for ${profileId} to 'disconnected'`);
return {
...statuses,
[profileId]: 'disconnected'
};
});
}
}
// Update connection status - ensure UI shows status as 'disconnected' at start
connectionStatus.update(statuses => ({
...statuses,
[profileId]: 'disconnected'
}));
// Dump the current connection status for debugging
setTimeout(() => {
console.log('Current connection status at initialization:', get(connectionStatus));
}, 100);
// Auto-connect if enabled and not already connected - with a slight delay
if (autoConnect && currentStatus !== 'connected') {
console.log('Auto-connecting profile:', profileId);
setTimeout(() => connect(), 100);
}
});
onDestroy(() => {
console.log(`MudConnection component being destroyed for profile: ${profileId}`);
// Remove keyboard listener
document.removeEventListener('keydown', handleKeyDown, true); // Match document and capture phase
// Clean up connection
if (connection) {
connection.disconnect();
}
// Remove from connections store
// Don't disconnect the connection - it should persist even when the component is unmounted
// Only remove this component instance from the local connections store (not the ConnectionManager)
connections.update(conns => {
const newConns = { ...conns };
delete newConns[profileId];
return newConns;
});
// Update connection status
connectionStatus.update(statuses => {
const newStatuses = { ...statuses };
delete newStatuses[profileId];
return newStatuses;
// Clean up subscriptions
unsubscribeFunctions.forEach(unsubFn => {
if (typeof unsubFn === 'function') {
try {
unsubFn();
} catch (error) {
console.error('Error during unsubscribe:', error);
}
}
});
});
@@ -100,6 +152,9 @@
* Initialize connection components
*/
function initializeComponents() {
// Get singleton instance of ConnectionManager
connectionManager = ConnectionManager.getInstance();
// Create GMCP handler
gmcpHandler = new GmcpHandler();
@@ -111,6 +166,47 @@
// Set up event listeners
setupEventListeners();
// Get or create connection from ConnectionManager
updateConnectionFromStore();
}
/**
* Update local connection reference from store
*/
function updateConnectionFromStore() {
// Subscribe to activeConnections to get updates
const unsubscribe = activeConnections.subscribe(connections => {
connection = connections[profileId] || null;
if (connection) {
// Re-attach event listeners when connection changes
setupConnectionListeners(connection);
}
});
// Add to unsubscribe functions
unsubscribeFunctions.push(unsubscribe);
}
/**
* Set up listeners for a specific connection
*/
function setupConnectionListeners(conn: MudConnection) {
// First remove ALL event listeners to prevent duplicates
console.log('Setting up connection listeners and removing old ones');
conn.off('received');
conn.off('sent');
conn.off('connected');
conn.off('disconnected');
conn.off('error');
// Add new listeners
conn.on('received', handleReceived);
conn.on('sent', handleSent);
conn.on('connected', handleConnected);
conn.on('disconnected', handleDisconnected);
conn.on('error', handleError);
}
/**
@@ -164,6 +260,7 @@
console.error('Error updating TTS setting:', error);
}
});
unsubscribeFunctions.push(unsubscribeTts);
// Set up only GMCP debugging subscription
let previousGmcpDebug = $uiSettings.debugGmcp;
@@ -172,70 +269,82 @@
previousGmcpDebug = settings.debugGmcp;
}
});
// Clean up subscriptions on component destruction
onDestroy(() => {
unsubscribeUiSettings();
unsubscribeTts();
});
unsubscribeFunctions.push(unsubscribeUiSettings);
}
/**
* Connect to the MUD server
*/
export function connect() {
const profile = get(activeProfile);
console.log('Connecting to profile:', profile);
if (!profile) {
addToOutputHistory('Error: No active profile selected.');
console.error('No active profile selected');
return;
}
// Update connection status
connectionStatus.update(statuses => ({
...statuses,
[profileId]: 'connecting'
}));
try {
addToOutputHistory(`Connecting to ${profile.host}:${profile.port}...`);
// First check if we are already connected
const currentStatus = get(connectionStatus)[profileId];
if (currentStatus === 'connected' || currentStatus === 'connecting') {
console.log(`Already ${currentStatus} for profile ${profileId}, not connecting again`);
return;
}
// Create connection
connection = new MudConnection({
// Find the profile by ID - don't rely on activeProfile
const allProfiles = get(profiles);
const profile = allProfiles.find(p => p.id === profileId);
console.log(`Connecting to profile ${profileId}:`, profile);
if (!profile) {
addToOutputHistory(`Error: Profile ${profileId} not found.`);
console.error(`Profile ${profileId} not found`);
return;
}
// Update connection status
connectionStatus.update(statuses => {
console.log(`Setting connection status for ${profileId} to 'connecting'`);
return {
...statuses,
[profileId]: 'connecting'
};
});
// Only add to output history if this is the active profile
if (get(activeProfileId) === profileId) {
addToOutputHistory(`Connecting to ${profile.host}:${profile.port}...`);
}
// Use the connection manager to get or create a connection
if (!connectionManager) {
connectionManager = ConnectionManager.getInstance();
}
// Connect using the connection manager
connectionManager.connect(profileId, {
host: profile.host,
port: profile.port,
useSSL: profile.useSSL,
gmcpHandler: gmcpHandler || undefined
});
console.log('Connection object created:', connection);
// Update local connection reference
setTimeout(() => {
updateConnectionFromStore();
}, 100);
// Set up connection event listeners
connection.on('connected', handleConnected);
connection.on('disconnected', handleDisconnected);
connection.on('error', handleError);
connection.on('received', handleReceived);
connection.on('sent', handleSent);
// Add to connections store
connections.update(conns => ({
...conns,
[profileId]: connection as MudConnection
}));
// Connect
connection.connect();
} catch (error) {
console.error('Failed to connect:', error);
console.error(`Failed to connect to ${profileId}:`, error);
connectionStatus.update(statuses => ({
...statuses,
[profileId]: 'error'
}));
connectionStatus.update(statuses => {
return {
...statuses,
[profileId]: 'error'
};
});
addToOutputHistory(`Error connecting to ${profile.host}:${profile.port} - ${error}`);
const errorMsg = `Error connecting to profile ${profileId}: ${error}`;
console.error(errorMsg);
// Only add to output history if this is the active profile
if (get(activeProfileId) === profileId) {
addToOutputHistory(errorMsg);
}
}
}
@@ -243,23 +352,46 @@
* Disconnect from the MUD server
*/
export function disconnect() {
if (connection) {
connection.disconnect();
if (!connectionManager) {
connectionManager = ConnectionManager.getInstance();
}
connectionManager.disconnect(profileId);
console.log(`Disconnected profile ${profileId}`);
}
/**
* Handle connection established
*/
function handleConnected() {
const profile = get(activeProfile);
connectionStatus.update(statuses => ({
...statuses,
[profileId]: 'connected'
}));
addToOutputHistory(`Connected to ${profile?.host}:${profile?.port}`);
try {
// Find the profile by ID
const allProfiles = get(profiles);
const profile = allProfiles.find(p => p.id === profileId);
console.log(`Profile ${profileId} connected:`, profile);
// Update connection status GLOBALLY, not just for this profile
connectionStatus.update(statuses => {
console.log(`Setting connection status for ${profileId} to 'connected'`);
console.log('Previous connection statuses:', statuses);
const newStatuses = {
...statuses,
[profileId]: 'connected'
};
console.log('New connection statuses:', newStatuses);
return newStatuses;
});
// Dump the current connection status for debugging
setTimeout(() => {
console.log('Current connection status after connect:', get(connectionStatus));
}, 100);
// Only add to output history if this is the active profile
if (get(activeProfileId) === profileId) {
addToOutputHistory(`Connected to ${profile?.host}:${profile?.port}`);
}
// Handle auto-login if enabled
if (profile?.autoLogin?.enabled) {
@@ -290,18 +422,30 @@
}
dispatch('connected');
} catch (error) {
console.error(`Error handling connection for profile ${profileId}:`, error);
}
}
/**
* Handle connection closed
*/
function handleDisconnected() {
connectionStatus.update(statuses => ({
...statuses,
[profileId]: 'disconnected'
}));
console.log(`Profile ${profileId} disconnected`);
connectionStatus.update(statuses => {
console.log(`Setting connection status for ${profileId} to 'disconnected'`);
return {
...statuses,
[profileId]: 'disconnected'
};
});
// Only add to output history if this is the active profile
if (get(activeProfileId) === profileId) {
addToOutputHistory('Disconnected from server.');
}
addToOutputHistory('Disconnected from server.');
dispatch('disconnected');
}
@@ -309,19 +453,27 @@
* Handle connection error
*/
function handleError(error: any) {
connectionStatus.update(statuses => ({
...statuses,
[profileId]: 'error'
}));
console.log(`Profile ${profileId} connection error:`, error);
connectionStatus.update(statuses => {
console.log(`Setting connection status for ${profileId} to 'error'`);
return {
...statuses,
[profileId]: 'error'
};
});
// Format the error message for display
const errorMessage = typeof error === 'object' ?
(error.message || JSON.stringify(error)) :
String(error);
addToOutputHistory(`Connection error: ${errorMessage}`, false, [
{ pattern: 'Connection error', color: '#ff5555', isRegex: false }
]);
// Only add to output history if this is the active profile
if (get(activeProfileId) === profileId) {
addToOutputHistory(`Connection error: ${errorMessage}`, false, [
{ pattern: 'Connection error', color: '#ff5555', isRegex: false }
]);
}
dispatch('error', { error });
}
@@ -330,31 +482,69 @@
* Handle received data
*/
function handleReceived(text: string) {
// Add to output history first
addToOutputHistory(text);
console.log(`Profile ${profileId} received data, active profile: ${get(activeProfileId)}`);
// Process triggers with safe error handling
if (triggerSystem) {
try {
triggerSystem.processTriggers(text);
} catch (error) {
console.error('Error processing triggers:', error);
try {
// First check if this is the active profile - only handle messages for the active profile
if (get(activeProfileId) !== profileId) {
console.log(`Ignoring received data for inactive profile ${profileId}`);
return;
}
}
// Try to use text-to-speech if enabled
if (accessibilityManager && $accessibilitySettings.textToSpeech) {
// Use a small timeout to avoid UI blocking
setTimeout(() => {
// First, ensure this profile has an output history entry
const history = get(outputHistory);
if (!history[profileId] || !Array.isArray(history[profileId])) {
console.log(`Creating fresh output history for profile ${profileId}`);
outputHistory.update(h => ({
...h,
[profileId]: []
}));
}
// Create the output item
const outputItem = {
id: `output-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
text,
timestamp: Date.now(),
isInput: false,
highlights: []
};
// Add directly to this profile's history
outputHistory.update(history => {
console.log(`Adding text to history for profile ${profileId}`);
const profileHistory = history[profileId] || [];
return {
...history,
[profileId]: [...profileHistory, outputItem]
};
});
// Process triggers with safe error handling
if (triggerSystem) {
try {
accessibilityManager.speak(text);
triggerSystem.processTriggers(text);
} catch (error) {
console.error('Error using text-to-speech:', error);
console.error(`Error processing triggers for ${profileId}:`, error);
}
}, 10);
}
}
dispatch('received', { text });
// Try to use text-to-speech if enabled
if (accessibilityManager && $accessibilitySettings.textToSpeech && get(activeProfileId) === profileId) {
// Use a small timeout to avoid UI blocking
setTimeout(() => {
try {
accessibilityManager.speak(text);
} catch (error) {
console.error('Error using text-to-speech:', error);
}
}, 10);
}
dispatch('received', { text });
} catch (error) {
console.error(`Error handling received data for profile ${profileId}:`, error);
}
}
/**