Improve triggers
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if localTrigger.soundFile}
|
||||
<div class="form-group">
|
||||
<label for="soundVolume">Sound Volume</label>
|
||||
<div class="range-control">
|
||||
<input
|
||||
type="range"
|
||||
id="soundVolume"
|
||||
bind:value={localTrigger.soundVolume}
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
/>
|
||||
<span class="range-value">{Math.round(localTrigger.soundVolume * 100)}%</span>
|
||||
</div>
|
||||
<small>Trigger sound volume is multiplied by global volume setting</small>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="textAction">Text Action</label>
|
||||
<select
|
||||
id="textAction"
|
||||
on:change={(e) => {
|
||||
const val = e.target.value;
|
||||
if (val === 'gag') {
|
||||
localTrigger.gag = true;
|
||||
localTrigger.replaceText = '';
|
||||
} else if (val === 'replace') {
|
||||
localTrigger.gag = false;
|
||||
localTrigger.replaceText = localTrigger.replaceText || '';
|
||||
} else {
|
||||
localTrigger.gag = false;
|
||||
localTrigger.replaceText = '';
|
||||
}
|
||||
}}
|
||||
value={localTrigger.gag ? 'gag' : (localTrigger.replaceText ? 'replace' : 'none')}
|
||||
>
|
||||
<option value="none">No change (show original text)</option>
|
||||
<option value="replace">Replace text</option>
|
||||
<option value="gag">Hide text (gag)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{#if !localTrigger.gag && localTrigger.replaceText !== undefined && localTrigger.replaceText !== null}
|
||||
<div class="form-group">
|
||||
<label for="replaceText">Replacement Text</label>
|
||||
<textarea
|
||||
id="replaceText"
|
||||
bind:value={localTrigger.replaceText}
|
||||
rows="3"
|
||||
placeholder="Text to replace the matched text with"
|
||||
></textarea>
|
||||
{#if localTrigger.isRegex}
|
||||
<small>Use $1, $2, etc. to reference captured groups</small>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sendText">Send Text</label>
|
||||
<input type="text" id="sendText" bind:value={localTrigger.sendText} />
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
// Emit highlight event if color specified
|
||||
// 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 });
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
private loadSound(triggerId: string, soundFile: string): Promise<Howl> {
|
||||
return new Promise((resolve, reject) => {
|
||||
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}`;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Create minimal sound object
|
||||
const sound = new Howl({ src: [soundPath] });
|
||||
// Build path based on whether it's a URL or local file
|
||||
const soundPath = soundFile.startsWith('http') || soundFile.startsWith('/')
|
||||
? soundFile
|
||||
: `/sounds/${soundFile}`;
|
||||
|
||||
// Set only necessary handlers
|
||||
sound.once('load', () => {
|
||||
this.sounds.set(triggerId, sound);
|
||||
});
|
||||
// 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
|
||||
});
|
||||
|
||||
sound.once('loaderror', () => {
|
||||
console.error(`Failed to load sound ${soundFile}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading sound:', error);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user