611 lines
18 KiB
Svelte
611 lines
18 KiB
Svelte
<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';
|
|
import {
|
|
connections,
|
|
connectionStatus,
|
|
activeProfileId,
|
|
activeProfile,
|
|
profiles,
|
|
addToOutputHistory,
|
|
updateGmcpData,
|
|
accessibilitySettings,
|
|
uiSettings,
|
|
outputHistory
|
|
} from '$lib/stores/mudStore';
|
|
import { get } from 'svelte/store';
|
|
|
|
const dispatch = createEventDispatcher();
|
|
|
|
// Props
|
|
export let profileId: string;
|
|
export let autoConnect = false;
|
|
|
|
// 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;
|
|
|
|
/**
|
|
* Handle keyboard events for speech control
|
|
*/
|
|
function handleKeyDown(event: KeyboardEvent): void {
|
|
// Control key stops speech - the key code for Control is 17
|
|
if (event.key === 'Control' || event.ctrlKey || event.keyCode === 17) {
|
|
// Stop propagation to ensure no other handlers interfere
|
|
event.stopPropagation();
|
|
|
|
if (accessibilityManager && accessibilityManager.isSpeaking()) {
|
|
console.log('Control key pressed - stopping speech');
|
|
accessibilityManager.stopSpeech();
|
|
}
|
|
}
|
|
|
|
// Enter key stops speech if the setting is enabled
|
|
if ((event.key === 'Enter' || event.keyCode === 13) && $accessibilitySettings.interruptSpeechOnEnter) {
|
|
if (accessibilityManager && accessibilityManager.isSpeaking()) {
|
|
console.log('Enter key pressed - interrupting speech');
|
|
accessibilityManager.stopSpeech();
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
// 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'
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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
|
|
|
|
// 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;
|
|
});
|
|
|
|
// Clean up subscriptions
|
|
unsubscribeFunctions.forEach(unsubFn => {
|
|
if (typeof unsubFn === 'function') {
|
|
try {
|
|
unsubFn();
|
|
} catch (error) {
|
|
console.error('Error during unsubscribe:', error);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Initialize connection components
|
|
*/
|
|
function initializeComponents() {
|
|
// Get singleton instance of ConnectionManager
|
|
connectionManager = ConnectionManager.getInstance();
|
|
|
|
// Create GMCP handler
|
|
gmcpHandler = new GmcpHandler();
|
|
|
|
// Create trigger system
|
|
triggerSystem = new TriggerSystem();
|
|
|
|
// Create accessibility manager
|
|
accessibilityManager = new AccessibilityManager();
|
|
|
|
// 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);
|
|
}
|
|
|
|
/**
|
|
* Set up event listeners for components
|
|
*/
|
|
function setupEventListeners() {
|
|
if (!gmcpHandler || !triggerSystem || !accessibilityManager) return;
|
|
|
|
// GMCP handler events
|
|
gmcpHandler.on('gmcp', (module, data) => {
|
|
updateGmcpData(module, data);
|
|
});
|
|
|
|
gmcpHandler.on('sendGmcp', (module, data) => {
|
|
if (connection) {
|
|
connection.sendGmcp(module, data);
|
|
}
|
|
});
|
|
|
|
gmcpHandler.on('playSound', (url, volume, loop) => {
|
|
dispatch('playSound', { url, volume, loop });
|
|
});
|
|
|
|
// Trigger system events
|
|
triggerSystem.on('sendText', (text) => {
|
|
if (connection) {
|
|
connection.send(text);
|
|
}
|
|
});
|
|
|
|
triggerSystem.on('highlight', (text, pattern, color, isRegex) => {
|
|
dispatch('highlight', { text, pattern, color, isRegex });
|
|
});
|
|
|
|
// Set up TTS subscription - use direct subscription to avoid circular updates
|
|
const unsubscribeTts = accessibilitySettings.subscribe(settings => {
|
|
try {
|
|
// Set speech enabled directly based on the store value
|
|
if (accessibilityManager) {
|
|
console.log('Setting TTS from store:', settings.textToSpeech);
|
|
accessibilityManager.setSpeechEnabled(settings.textToSpeech);
|
|
|
|
// Update speech options when settings change
|
|
accessibilityManager.updateSpeechOptions({
|
|
rate: settings.speechRate,
|
|
pitch: settings.speechPitch,
|
|
volume: settings.speechVolume
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating TTS setting:', error);
|
|
}
|
|
});
|
|
unsubscribeFunctions.push(unsubscribeTts);
|
|
|
|
// Set up only GMCP debugging subscription
|
|
let previousGmcpDebug = $uiSettings.debugGmcp;
|
|
const unsubscribeUiSettings = uiSettings.subscribe(settings => {
|
|
if (settings.debugGmcp !== previousGmcpDebug) {
|
|
previousGmcpDebug = settings.debugGmcp;
|
|
}
|
|
});
|
|
unsubscribeFunctions.push(unsubscribeUiSettings);
|
|
}
|
|
|
|
/**
|
|
* Connect to the MUD server
|
|
*/
|
|
export function connect() {
|
|
try {
|
|
// 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;
|
|
}
|
|
|
|
// 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
|
|
});
|
|
|
|
// Update local connection reference
|
|
setTimeout(() => {
|
|
updateConnectionFromStore();
|
|
}, 100);
|
|
|
|
} catch (error) {
|
|
console.error(`Failed to connect to ${profileId}:`, error);
|
|
|
|
connectionStatus.update(statuses => {
|
|
return {
|
|
...statuses,
|
|
[profileId]: '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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disconnect from the MUD server
|
|
*/
|
|
export function disconnect() {
|
|
if (!connectionManager) {
|
|
connectionManager = ConnectionManager.getInstance();
|
|
}
|
|
|
|
connectionManager.disconnect(profileId);
|
|
console.log(`Disconnected profile ${profileId}`);
|
|
}
|
|
|
|
/**
|
|
* Handle connection established
|
|
*/
|
|
function handleConnected() {
|
|
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) {
|
|
setTimeout(() => {
|
|
// Send username
|
|
if (profile.autoLogin?.username) {
|
|
if (connection) connection.send(profile.autoLogin.username);
|
|
}
|
|
|
|
// Send password after a delay
|
|
if (profile.autoLogin?.password) {
|
|
setTimeout(() => {
|
|
if (connection) connection.send(profile.autoLogin?.password || '');
|
|
|
|
// Send additional commands
|
|
if (profile.autoLogin?.commands && profile.autoLogin.commands.length > 0) {
|
|
let delay = 500;
|
|
profile.autoLogin.commands.forEach((cmd) => {
|
|
setTimeout(() => {
|
|
if (connection) connection.send(cmd);
|
|
}, delay);
|
|
delay += 500;
|
|
});
|
|
}
|
|
}, 1000);
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
dispatch('connected');
|
|
} catch (error) {
|
|
console.error(`Error handling connection for profile ${profileId}:`, error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle connection closed
|
|
*/
|
|
function handleDisconnected() {
|
|
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.');
|
|
}
|
|
|
|
dispatch('disconnected');
|
|
}
|
|
|
|
/**
|
|
* Handle connection error
|
|
*/
|
|
function handleError(error: any) {
|
|
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);
|
|
|
|
// 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 });
|
|
}
|
|
|
|
/**
|
|
* Handle received data
|
|
*/
|
|
function handleReceived(text: string) {
|
|
console.log(`Profile ${profileId} received data, active profile: ${get(activeProfileId)}`);
|
|
|
|
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;
|
|
}
|
|
|
|
// 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 {
|
|
triggerSystem.processTriggers(text);
|
|
} catch (error) {
|
|
console.error(`Error processing triggers for ${profileId}:`, error);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle sent data
|
|
*/
|
|
function handleSent(text: string) {
|
|
dispatch('sent', { text });
|
|
}
|
|
</script>
|
|
|
|
{#if $connectionStatus[profileId]}
|
|
<div class="mud-connection-status" role="status" aria-live="polite">
|
|
<div class="status-indicator status-{$connectionStatus[profileId]}" aria-hidden="true"></div>
|
|
<span>Server status: {$connectionStatus[profileId]}</span>
|
|
{#if $connectionStatus[profileId] === 'error'}
|
|
<span class="sr-only">There was an error connecting to the MUD server. Please check your settings and try again.</span>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.mud-connection-status {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 5px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.status-indicator {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.status-connected {
|
|
background-color: #50fa7b;
|
|
}
|
|
|
|
.status-connecting {
|
|
background-color: #f1fa8c;
|
|
}
|
|
|
|
.status-disconnected {
|
|
background-color: #999;
|
|
}
|
|
|
|
.status-error {
|
|
background-color: #ff5555;
|
|
}
|
|
|
|
.sr-only {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
padding: 0;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
clip: rect(0, 0, 0, 0);
|
|
white-space: nowrap;
|
|
border-width: 0;
|
|
}
|
|
</style>
|