diff --git a/src/lib/components/MudConnection.svelte b/src/lib/components/MudConnection.svelte
index c343964..b3e14bc 100644
--- a/src/lib/components/MudConnection.svelte
+++ b/src/lib/components/MudConnection.svelte
@@ -348,43 +348,62 @@
try {
const isActiveProfile = get(activeProfileId) === profileId;
- // Always add to output history for this profile
- addToOutputHistory(text);
-
// Process triggers if available for this profile
+ let processedText = text;
+ let isGagged = false;
+ let triggerMatched = false;
+
if (triggerSystem) {
try {
- triggerSystem.processTriggers(text);
+ const result = triggerSystem.processTriggers(text);
+ processedText = result.processed;
+ isGagged = result.gagged;
+ triggerMatched = result.matched;
+
+ console.log(`Trigger processing result - gagged: ${isGagged}, matched: ${triggerMatched}, modified: ${processedText !== text}`);
} catch (error) {
console.error(`Error processing triggers for ${profileId}:`, error);
}
}
-
- // Handle text-to-speech
- console.log(`TTS check for ${profileId}: isTTS=${$accessibilitySettings.textToSpeech}, isActive=${isActiveProfile}, speakAll=${$accessibilitySettings.speakAllProfiles}`);
- if (accessibilityManager && $accessibilitySettings.textToSpeech) {
- // Speak if this is active profile OR speakAllProfiles is enabled
- if (isActiveProfile || $accessibilitySettings.speakAllProfiles) {
- console.log(`TTS condition satisfied, will attempt to speak for ${profileId}`);
- // Use a small timeout to avoid UI blocking
- setTimeout(() => {
- try {
- // If not active profile, add profile name prefix for context
- const speechText = isActiveProfile ? text : `From ${getProfileName(profileId)}: ${text}`;
- console.log(`Speaking text for ${profileId}:`, speechText.substring(0, 50) + (speechText.length > 50 ? '...' : ''));
- accessibilityManager.speak(speechText);
- } catch (error) {
- console.error('Error using text-to-speech:', error);
- }
- }, 10);
+
+ // Add to output history if not gagged
+ if (!isGagged) {
+ addToOutputHistory(processedText);
+
+ // Handle text-to-speech for processed text
+ console.log(`TTS check for ${profileId}: isTTS=${$accessibilitySettings.textToSpeech}, isActive=${isActiveProfile}, speakAll=${$accessibilitySettings.speakAllProfiles}`);
+ if (accessibilityManager && $accessibilitySettings.textToSpeech) {
+ // Speak if this is active profile OR speakAllProfiles is enabled
+ if (isActiveProfile || $accessibilitySettings.speakAllProfiles) {
+ console.log(`TTS condition satisfied, will attempt to speak for ${profileId}`);
+ // Use a small timeout to avoid UI blocking
+ setTimeout(() => {
+ try {
+ // If not active profile, add profile name prefix for context
+ const speechText = isActiveProfile ? processedText : `From ${getProfileName(profileId)}: ${processedText}`;
+ console.log(`Speaking text for ${profileId}:`, speechText.substring(0, 50) + (speechText.length > 50 ? '...' : ''));
+ accessibilityManager.speak(speechText);
+ } catch (error) {
+ console.error('Error using text-to-speech:', error);
+ }
+ }, 10);
+ } else {
+ console.log(`TTS skipped: profile ${profileId} is not active and speakAll is disabled`);
+ }
} else {
- console.log(`TTS skipped: profile ${profileId} is not active and speakAll is disabled`);
+ console.log(`TTS not enabled for profile ${profileId} or accessibilityManager is null`);
}
} else {
- console.log(`TTS not enabled for profile ${profileId} or accessibilityManager is null`);
+ console.log(`Text was gagged by trigger - not adding to output history: ${text.substring(0, 50)}...`);
}
- dispatch('received', { text });
+ // Dispatch event with information about trigger processing
+ dispatch('received', {
+ originalText: text,
+ processedText: processedText,
+ gagged: isGagged,
+ triggerMatched: triggerMatched
+ });
} catch (error) {
console.error(`Error handling received data for profile ${profileId}:`, error);
}
diff --git a/src/lib/components/TriggerEditor.svelte b/src/lib/components/TriggerEditor.svelte
index 3c302af..9d2a8f2 100644
--- a/src/lib/components/TriggerEditor.svelte
+++ b/src/lib/components/TriggerEditor.svelte
@@ -16,9 +16,12 @@
isRegex: false,
isEnabled: true,
soundFile: '',
+ soundVolume: 0.7, // Default sound volume
sendText: '',
highlightColor: '',
- priority: 0
+ priority: 0,
+ replaceText: '', // New text replacement option
+ gag: false // Option to hide matched text
};
// Available sounds
@@ -135,6 +138,64 @@
{/if}
+ {#if localTrigger.soundFile}
+
+ {/if}
+
+
+
+
+
+
+ {#if !localTrigger.gag && localTrigger.replaceText !== undefined && localTrigger.replaceText !== null}
+
+
+
+ {#if localTrigger.isRegex}
+ Use $1, $2, etc. to reference captured groups
+ {/if}
+
+ {/if}
+
@@ -306,6 +367,21 @@
font-weight: bold;
}
+ .range-control {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+
+ .range-control input {
+ flex: 1;
+ }
+
+ .range-value {
+ min-width: 40px;
+ text-align: right;
+ }
+
.match-list {
margin-top: 10px;
padding: 10px;
diff --git a/src/lib/connection/MudConnection.ts b/src/lib/connection/MudConnection.ts
index 7d9ec5c..df51272 100644
--- a/src/lib/connection/MudConnection.ts
+++ b/src/lib/connection/MudConnection.ts
@@ -133,7 +133,8 @@ export class MudConnection extends EventEmitter {
// Binary data
this.handleIncomingData(new Uint8Array(event.data));
} else if (typeof event.data === 'string') {
- // Text data
+ // Text data - let listeners process it directly
+ // TriggerSystem will handle gagging and replacing in the component
this.emit('received', event.data);
} else if (event.data instanceof Blob) {
// Blob data (sometimes WebSockets send this instead of ArrayBuffer)
diff --git a/src/lib/triggers/TriggerSystem.ts b/src/lib/triggers/TriggerSystem.ts
index f5a61b5..b00413e 100644
--- a/src/lib/triggers/TriggerSystem.ts
+++ b/src/lib/triggers/TriggerSystem.ts
@@ -10,10 +10,13 @@ export interface Trigger {
isRegex: boolean;
isEnabled: boolean;
soundFile?: string;
+ soundVolume?: number; // Sound volume (0-1)
action?: string;
sendText?: string;
highlightColor?: string;
priority: number;
+ replaceText?: string; // New text to replace the matched text with
+ gag?: boolean; // If true, don't display the matched text at all
}
export class TriggerSystem extends EventEmitter {
@@ -91,9 +94,11 @@ export class TriggerSystem extends EventEmitter {
// Sort triggers by priority
this.triggers.sort((a, b) => b.priority - a.priority);
- // Preload sound if specified
+ // Preload sound if specified (now uses async method but doesn't wait)
if (trigger.soundFile) {
- this.loadSound(trigger.id, trigger.soundFile);
+ this.loadSound(trigger.id, trigger.soundFile).catch(error => {
+ console.error(`Failed to preload sound for trigger ${trigger.id}:`, error);
+ });
}
// Save triggers to storage
@@ -126,9 +131,18 @@ export class TriggerSystem extends EventEmitter {
/**
* Process text for triggers
+ * @returns Object with information about gagging and replacement
*/
- public processTriggers(text: string): void {
- // Process only enabled triggers
+ public processTriggers(text: string): {
+ processed: string; // Text after all replacements
+ gagged: boolean; // Whether the text should be completely hidden
+ matched: boolean; // Whether any triggers matched
+ } {
+ let processedText = text;
+ let isGagged = false;
+ let anyTriggerMatched = false;
+
+ // Process only enabled triggers in priority order
for (const trigger of this.triggers.filter(t => t.isEnabled)) {
let matched = false;
let matches: RegExpMatchArray | null = null;
@@ -136,81 +150,209 @@ export class TriggerSystem extends EventEmitter {
if (trigger.isRegex) {
try {
const regex = new RegExp(trigger.pattern, 'g');
- matches = text.match(regex);
- matched = matches !== null;
+ matches = processedText.match(regex);
+ matched = matches !== null && matches.length > 0;
} catch (error) {
console.error(`Invalid regex pattern in trigger ${trigger.name}:`, error);
}
} else {
// Simple text matching
- matched = text.includes(trigger.pattern);
+ matched = processedText.includes(trigger.pattern);
}
if (matched) {
- this.executeTrigger(trigger, text, matches);
+ anyTriggerMatched = true;
+
+ // Execute the trigger actions
+ this.executeTrigger(trigger, processedText, matches);
+
+ // Handle gagging - if any trigger gags, the whole line is gagged
+ if (trigger.gag) {
+ isGagged = true;
+ }
+
+ // Handle text replacement if not gagged
+ if (!isGagged && trigger.replaceText) {
+ if (trigger.isRegex) {
+ try {
+ let replacedText = trigger.replaceText;
+
+ // If we have regex matches, replace $1, $2, etc. with capture groups
+ if (matches && matches.length > 0) {
+ // For actual text replacement, we need to use the regex directly
+ const regex = new RegExp(trigger.pattern, 'g');
+
+ // Use replace function to handle capture groups dynamically
+ processedText = processedText.replace(regex, (match, ...groups) => {
+ // Create a copy of the replacement template
+ let result = trigger.replaceText || '';
+
+ // Replace $1, $2, etc. with the actual captured values
+ for (let i = 0; i < groups.length - 2; i++) { // -2 to skip lastIndex and input
+ const captureValue = groups[i] || '';
+ result = result.replace(new RegExp(`\\$${i + 1}`, 'g'), captureValue);
+ }
+
+ return result;
+ });
+ }
+ } catch (error) {
+ console.error(`Error replacing text with regex in trigger ${trigger.name}:`, error);
+ }
+ } else {
+ // Simple string replacement for non-regex triggers
+ processedText = processedText.replace(
+ new RegExp(this.escapeRegExp(trigger.pattern), 'g'),
+ trigger.replaceText
+ );
+ }
+ }
}
}
+
+ return {
+ processed: processedText,
+ gagged: isGagged,
+ matched: anyTriggerMatched
+ };
}
/**
- * Execute a triggered action - simplified for stability
+ * Helper to escape special regex characters in strings
+ */
+ private escapeRegExp(string: string): string {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ }
+
+ /**
+ * Execute a triggered action with support for advanced features
*/
private executeTrigger(trigger: Trigger, text: string, matches: RegExpMatchArray | null): void {
- // Play sound if specified - with minimal options
- if (trigger.soundFile && this.sounds.has(trigger.id)) {
- try {
- const sound = this.sounds.get(trigger.id);
+ // Get settings
+ const uiSettingsValue = get(uiSettings);
+ const globalVolume = uiSettingsValue.globalVolume || 0.7;
+
+ // Handle sound playback, loading on demand if needed
+ if (trigger.soundFile) {
+ // Try to get the existing sound, or load it if not already loaded
+ const soundPromise = this.sounds.has(trigger.id)
+ ? Promise.resolve(this.sounds.get(trigger.id)!)
+ : this.loadSound(trigger.id, trigger.soundFile);
+
+ soundPromise.then(sound => {
if (sound) {
- sound.volume(0.7); // Fixed volume for stability
+ // Calculate volume based on trigger-specific volume and global volume
+ const triggerVolume = trigger.soundVolume !== undefined ? trigger.soundVolume : 0.7;
+ const finalVolume = Math.min(triggerVolume * globalVolume, 1.0); // Cap at 1.0
+
+ console.log(`Playing sound with final volume: ${finalVolume} (trigger: ${triggerVolume} * global: ${globalVolume})`);
+ sound.volume(finalVolume);
sound.play();
}
- } catch (error) {
- console.error('Error playing sound:', error);
- }
+ }).catch(error => {
+ console.error(`Error playing sound for trigger ${trigger.id}:`, error);
+ });
}
- // Send text if specified
+ // Send text if specified - support for regex capture group substitution
if (trigger.sendText) {
- this.emit('sendText', trigger.sendText);
+ let textToSend = trigger.sendText;
+
+ // If we have regex matches, replace $1, $2, etc. with capture groups
+ if (trigger.isRegex && matches && matches.length > 1) {
+ // matches[0] is the full match, captures start at index 1
+ for (let i = 1; i < matches.length; i++) {
+ const captureGroup = matches[i] || '';
+ textToSend = textToSend.replace(new RegExp(`\\$${i}`, 'g'), captureGroup);
+ }
+ }
+
+ this.emit('sendText', textToSend);
}
- // Skip custom actions for stability
+ // Handle text replacement or gagging
+ if (trigger.gag) {
+ // If gagged, emit a special event to prevent displaying the text
+ this.emit('gagText', text);
+ } else if (trigger.replaceText) {
+ let replacedText = trigger.replaceText;
+
+ // If we have regex matches, replace $1, $2, etc. with capture groups
+ if (trigger.isRegex && matches && matches.length > 1) {
+ for (let i = 1; i < matches.length; i++) {
+ const captureGroup = matches[i] || '';
+ replacedText = replacedText.replace(new RegExp(`\\$${i}`, 'g'), captureGroup);
+ }
+ }
+
+ // Emit the replaced text instead of the original
+ this.emit('replaceText', { original: text, replacement: replacedText });
+ }
- // Emit highlight event if color specified
+ // Handle highlight if specified
if (trigger.highlightColor) {
this.emit('highlight', text, trigger.pattern, trigger.highlightColor, trigger.isRegex);
}
// Emit basic trigger fired event
this.emit('triggerFired', trigger.id);
+
+ // Execute custom action if specified and in browser environment
+ if (trigger.action && typeof window !== 'undefined') {
+ try {
+ // Create a sandboxed function with limited scope
+ const actionFn = new Function('text', 'matches', trigger.action);
+ actionFn(text, matches);
+ } catch (error) {
+ console.error(`Error executing custom action for trigger ${trigger.id}:`, error);
+ }
+ }
}
/**
- * Load a sound file for a trigger - simplified version
+ * Load a sound file for a trigger with better error handling and loading status
+ * @returns Promise that resolves with the sound object when loaded
*/
- private loadSound(triggerId: string, soundFile: string): void {
- try {
- console.log(`Loading sound for trigger ${triggerId}: ${soundFile}`);
-
- // Build path based on whether it's a URL or local file
- const soundPath = soundFile.startsWith('http') || soundFile.startsWith('/')
- ? soundFile
- : `/sounds/${soundFile}`;
-
- // Create minimal sound object
- const sound = new Howl({ src: [soundPath] });
-
- // Set only necessary handlers
- sound.once('load', () => {
- this.sounds.set(triggerId, sound);
- });
-
- sound.once('loaderror', () => {
- console.error(`Failed to load sound ${soundFile}`);
- });
- } catch (error) {
- console.error('Error loading sound:', error);
- }
+ private loadSound(triggerId: string, soundFile: string): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ console.log(`Loading sound for trigger ${triggerId}: ${soundFile}`);
+
+ // If we already have this sound loaded, return it immediately
+ if (this.sounds.has(triggerId)) {
+ console.log(`Sound for trigger ${triggerId} already loaded, reusing`);
+ resolve(this.sounds.get(triggerId)!);
+ return;
+ }
+
+ // Build path based on whether it's a URL or local file
+ const soundPath = soundFile.startsWith('http') || soundFile.startsWith('/')
+ ? soundFile
+ : `/sounds/${soundFile}`;
+
+ // Create sound object with HTML5 audio to better support streaming
+ const sound = new Howl({
+ src: [soundPath],
+ html5: true // Better for streaming and prevents global audio lock
+ });
+
+ // Set up handlers
+ sound.once('load', () => {
+ console.log(`Sound for trigger ${triggerId} loaded successfully`);
+ this.sounds.set(triggerId, sound);
+ resolve(sound);
+ });
+
+ sound.once('loaderror', (id, error) => {
+ console.error(`Failed to load sound ${soundFile}:`, error);
+ reject(new Error(`Failed to load sound: ${error || 'Unknown error'}`));
+ });
+
+ } catch (error) {
+ console.error('Error setting up sound:', error);
+ reject(error);
+ }
+ });
}
/**
@@ -275,4 +417,4 @@ export class TriggerSystem extends EventEmitter {
typeof obj.priority === 'number'
);
}
-}
+}
\ No newline at end of file