UI overhaul

This commit is contained in:
overcuriousity 2025-07-27 17:31:15 +02:00
parent 0d22210040
commit 0adabad94d
7 changed files with 192 additions and 104 deletions

View File

@ -444,39 +444,46 @@ document.addEventListener('DOMContentLoaded', () => {
}
};
// Smart Prompting Input Handling
// Smart Prompting Input Handling - Fixed Race Conditions
aiInput.addEventListener('input', () => {
console.log('[DEBUG] Input event triggered, length:', aiInput.value.trim().length);
const inputLength = aiInput.value.trim().length;
// Clear existing timeout
// Clear ALL existing timeouts and abort controllers
clearTimeout(enhancementTimeout);
// Cancel any pending enhancement call
if (enhancementAbortController) {
enhancementAbortController.abort();
enhancementAbortController = null;
}
// Hide suggestions if input is too short
// Hide suggestions immediately if input is too short
if (inputLength < 40) {
showPromptingStatus('hidden');
return;
}
// Show analyzing state after 1 second
setTimeout(() => {
if (aiInput.value.trim().length >= 50) {
showPromptingStatus('analyzing');
}
}, 1000);
// Trigger AI enhancement after 1.5 seconds
// Single consolidated timeout for all smart prompting logic
enhancementTimeout = setTimeout(() => {
console.log('[DEBUG] Enhancement timeout fired, calling triggerSmartPrompting');
if (aiInput.value.trim().length >= 40) {
triggerSmartPrompting();
const currentLength = aiInput.value.trim().length;
// Double-check length hasn't changed during timeout
if (currentLength < 40) {
showPromptingStatus('hidden');
return;
}
}, 1500);
// Show analyzing state first
if (currentLength >= 50) {
showPromptingStatus('analyzing');
// Trigger enhancement after showing analyzing state
setTimeout(() => {
if (aiInput.value.trim().length >= 50) {
triggerSmartPrompting();
}
}, 500);
}
}, 1000); // Single timeout instead of multiple
});
aiInput.addEventListener('input', updateCharacterCount);

View File

@ -68,40 +68,59 @@ const displayedScenarios = scenarios.slice(0, maxDisplayed);
<script define:vars={{ allScenarios: scenarios, maxDisplay: maxDisplayed }}>
let showingAllScenarios = false;
// Apply scenario search using existing search functionality
window.applyScenarioSearch = function(scenarioId) {
console.log(`Applying scenario search: ${scenarioId}`);
// Find the main search input (existing)
const clickedChip = document.querySelector(`[data-scenario-id="${scenarioId}"]`);
const mainSearchInput = document.getElementById('search-input');
if (mainSearchInput) {
// Use scenario ID as search term (it should match tool tags)
mainSearchInput.value = scenarioId;
// Trigger existing search functionality
if (!mainSearchInput) return;
// Check if this scenario is already active (allow deselection)
if (clickedChip && clickedChip.classList.contains('active')) {
// Deselect: clear search and remove active state
mainSearchInput.value = '';
document.querySelectorAll('.suggestion-chip').forEach(chip => {
chip.classList.remove('active');
});
// Clear the targeted search input too
const targetedInput = document.getElementById('targeted-search-input');
if (targetedInput) {
targetedInput.value = '';
}
// Trigger search to show all results
const inputEvent = new Event('input', { bubbles: true });
mainSearchInput.dispatchEvent(inputEvent);
// Switch to grid view
const gridToggle = document.querySelector('.view-toggle[data-view="grid"]');
if (gridToggle && !gridToggle.classList.contains('active')) {
gridToggle.click();
}
return;
}
// Scroll to results
setTimeout(() => {
const toolsGrid = document.getElementById('tools-grid');
if (toolsGrid) {
toolsGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 200);
// Apply new search
mainSearchInput.value = scenarioId;
// Trigger existing search functionality
const inputEvent = new Event('input', { bubbles: true });
mainSearchInput.dispatchEvent(inputEvent);
// Switch to grid view
const gridToggle = document.querySelector('.view-toggle[data-view="grid"]');
if (gridToggle && !gridToggle.classList.contains('active')) {
gridToggle.click();
}
// Visual feedback
document.querySelectorAll('.suggestion-chip').forEach(chip => {
chip.classList.remove('active');
});
document.querySelector(`[data-scenario-id="${scenarioId}"]`)?.classList.add('active');
if (clickedChip) {
clickedChip.classList.add('active');
}
// Scroll to results with better positioning
window.scrollToElementById('tools-grid');
};
// Toggle showing all scenarios
@ -149,18 +168,14 @@ const displayedScenarios = scenarios.slice(0, maxDisplayed);
targetedInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
// Switch to grid view and scroll to results
const gridToggle = document.querySelector('.view-toggle[data-view="grid"]');
if (gridToggle) {
e.preventDefault();
// Switch to grid view and scroll to results
const gridToggle = document.querySelector('.view-toggle[data-view="grid"]');
if (gridToggle) {
gridToggle.click();
setTimeout(() => {
const toolsGrid = document.getElementById('tools-grid');
if (toolsGrid) {
toolsGrid.scrollIntoView({ behavior: 'smooth' });
}
}, 100);
}
// Use consolidated scroll utility
window.scrollToElementById('tools-grid');
}
}
});
}

View File

@ -117,15 +117,24 @@ const sortedTags = Object.entries(tagFrequency)
window.toolsData = toolsData;
document.addEventListener('DOMContentLoaded', () => {
const searchInput = document.getElementById('search-input');
const domainSelect = document.getElementById('domain-select');
const phaseButtons = document.querySelectorAll('.phase-button');
const proprietaryCheckbox = document.getElementById('include-proprietary');
const tagCloudItems = document.querySelectorAll('.tag-cloud-item');
const tagCloud = document.getElementById('tag-cloud');
const tagCloudToggle = document.getElementById('tag-cloud-toggle');
const viewToggles = document.querySelectorAll('.view-toggle');
const aiViewToggle = document.getElementById('ai-view-toggle');
// Cache DOM elements once
const elements = {
searchInput: document.getElementById('search-input'),
domainSelect: document.getElementById('domain-select'),
phaseButtons: document.querySelectorAll('.phase-button'),
proprietaryCheckbox: document.getElementById('include-proprietary'),
tagCloudItems: document.querySelectorAll('.tag-cloud-item'),
tagCloud: document.getElementById('tag-cloud'),
tagCloudToggle: document.getElementById('tag-cloud-toggle'),
viewToggles: document.querySelectorAll('.view-toggle'),
aiViewToggle: document.getElementById('ai-view-toggle')
};
// Verify critical elements exist
if (!elements.searchInput || !elements.domainSelect || !elements.proprietaryCheckbox) {
console.error('Critical filter elements not found');
return;
}
let selectedTags = new Set();
let selectedPhase = '';
@ -133,7 +142,7 @@ const sortedTags = Object.entries(tagFrequency)
function initTagCloud() {
const visibleCount = 22;
tagCloudItems.forEach((item, index) => {
elements.tagCloudItems.forEach((item, index) => {
if (index >= visibleCount) {
item.style.display = 'none';
}
@ -145,22 +154,22 @@ const sortedTags = Object.entries(tagFrequency)
const visibleCount = 22;
if (isTagCloudExpanded) {
tagCloud.classList.add('expanded');
tagCloudToggle.textContent = 'Weniger zeigen';
tagCloudToggle.setAttribute('data-expanded', 'true');
elements.tagCloud.classList.add('expanded');
elements.tagCloudToggle.textContent = 'Weniger zeigen';
elements.tagCloudToggle.setAttribute('data-expanded', 'true');
tagCloudItems.forEach(item => {
elements.tagCloudItems.forEach(item => {
if (!item.classList.contains('hidden')) {
item.style.display = 'inline-flex';
}
});
} else {
tagCloud.classList.remove('expanded');
tagCloudToggle.textContent = 'Mehr zeigen';
tagCloudToggle.setAttribute('data-expanded', 'false');
elements.tagCloud.classList.remove('expanded');
elements.tagCloudToggle.textContent = 'Mehr zeigen';
elements.tagCloudToggle.setAttribute('data-expanded', 'false');
let visibleIndex = 0;
tagCloudItems.forEach(item => {
elements.tagCloudItems.forEach(item => {
if (!item.classList.contains('hidden')) {
if (visibleIndex < visibleCount) {
item.style.display = 'inline-flex';
@ -174,11 +183,11 @@ const sortedTags = Object.entries(tagFrequency)
}
function filterTagCloud() {
const searchTerm = searchInput.value.toLowerCase();
const searchTerm = elements.searchInput.value.toLowerCase();
let visibleCount = 0;
const maxVisibleWhenCollapsed = 22;
tagCloudItems.forEach(item => {
elements.tagCloudItems.forEach(item => {
const tagName = item.getAttribute('data-tag').toLowerCase();
const shouldShow = tagName.includes(searchTerm);
@ -196,10 +205,10 @@ const sortedTags = Object.entries(tagFrequency)
}
});
const hasHiddenTags = Array.from(tagCloudItems).some(item =>
const hasHiddenTags = Array.from(elements.tagCloudItems).some(item =>
!item.classList.contains('hidden') && item.style.display === 'none'
);
tagCloudToggle.style.display = hasHiddenTags ? 'block' : 'none';
elements.tagCloudToggle.style.display = hasHiddenTags ? 'block' : 'none';
}
function isToolHosted(tool) {
@ -224,7 +233,7 @@ const sortedTags = Object.entries(tagFrequency)
el.classList.remove('highlight-row', 'highlight-column');
});
const selectedDomain = domainSelect.value;
const selectedDomain = elements.domainSelect.value;
if (selectedDomain) {
const domainRow = matrixTable.querySelector(`tr[data-domain="${selectedDomain}"]`);
@ -252,9 +261,9 @@ const sortedTags = Object.entries(tagFrequency)
}
function filterTools() {
const searchTerm = searchInput.value.toLowerCase();
const selectedDomain = domainSelect.value;
const includeProprietary = proprietaryCheckbox.checked;
const searchTerm = elements.searchInput.value.toLowerCase();
const selectedDomain = elements.domainSelect.value;
const includeProprietary = elements.proprietaryCheckbox.checked;
const filtered = window.toolsData.filter(tool => {
const domains = tool.domains || [];
@ -314,7 +323,7 @@ const sortedTags = Object.entries(tagFrequency)
selectedPhase = '';
button.classList.remove('active');
} else {
phaseButtons.forEach(btn => btn.classList.remove('active'));
elements.phaseButtons.forEach(btn => btn.classList.remove('active'));
selectedPhase = phase;
button.classList.add('active');
}
@ -323,7 +332,7 @@ const sortedTags = Object.entries(tagFrequency)
}
function handleViewToggle(view) {
viewToggles.forEach(btn => {
elements.viewToggles.forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-view') === view);
});
@ -339,37 +348,38 @@ const sortedTags = Object.entries(tagFrequency)
function clearTagFilters() {
selectedTags.clear();
tagCloudItems.forEach(item => item.classList.remove('active'));
elements.tagCloudItems.forEach(item => item.classList.remove('active'));
filterTools();
}
function clearAllFilters() {
searchInput.value = '';
domainSelect.value = '';
elements.searchInput.value = '';
elements.domainSelect.value = '';
selectedPhase = '';
phaseButtons.forEach(btn => btn.classList.remove('active'));
elements.phaseButtons.forEach(btn => btn.classList.remove('active'));
clearTagFilters();
filterTagCloud();
}
searchInput.addEventListener('input', () => {
// Event listeners using cached elements
elements.searchInput.addEventListener('input', () => {
filterTagCloud();
filterTools();
});
domainSelect.addEventListener('change', filterTools);
proprietaryCheckbox.addEventListener('change', filterTools);
tagCloudToggle.addEventListener('click', toggleTagCloud);
elements.domainSelect.addEventListener('change', filterTools);
elements.proprietaryCheckbox.addEventListener('change', filterTools);
elements.tagCloudToggle.addEventListener('click', toggleTagCloud);
tagCloudItems.forEach(item => {
elements.tagCloudItems.forEach(item => {
item.addEventListener('click', () => handleTagClick(item));
});
phaseButtons.forEach(btn => {
elements.phaseButtons.forEach(btn => {
btn.addEventListener('click', () => handlePhaseClick(btn));
});
viewToggles.forEach(btn => {
elements.viewToggles.forEach(btn => {
btn.addEventListener('click', () => handleViewToggle(btn.getAttribute('data-view')));
});

View File

@ -672,6 +672,13 @@ domains.forEach((domain: any) => {
const primaryModal = document.getElementById('tool-details-primary');
const secondaryModal = document.getElementById('tool-details-secondary');
// Debounce rapid calls
if (window.modalHideInProgress) return;
window.modalHideInProgress = true;
setTimeout(() => {
window.modalHideInProgress = false;
}, 100);
if (modalType === 'both' || modalType === 'all') {
if (primaryModal) {
@ -702,13 +709,19 @@ domains.forEach((domain: any) => {
if (contributeButtonSecondary) contributeButtonSecondary.style.display = 'none';
}
// Consolidated state checking with safety checks
const primaryActive = primaryModal && primaryModal.classList.contains('active');
const secondaryActive = secondaryModal && secondaryModal.classList.contains('active');
// Update overlay and body classes atomically
if (!primaryActive && !secondaryActive) {
if (overlay) overlay.classList.remove('active');
document.body.classList.remove('modals-side-by-side');
} else if (primaryActive !== secondaryActive) {
} else if (primaryActive && secondaryActive) {
// Both active - ensure side-by-side class
document.body.classList.add('modals-side-by-side');
} else {
// Only one active - remove side-by-side class
document.body.classList.remove('modals-side-by-side');
}
};

21
src/env.d.ts vendored
View File

@ -22,10 +22,25 @@ declare global {
findToolByIdentifier: (tools: any[], identifier: string) => any | undefined;
isToolHosted: (tool: any) => boolean;
checkClientAuth: () => Promise<{authenticated: boolean; authRequired: boolean; expires?: string}>;
requireClientAuth: (callback?: () => void, returnUrl?: string) => Promise<boolean>;
showIfAuthenticated: (selector: string) => Promise<void>;
checkClientAuth: (context?: string) => Promise<{authenticated: boolean; authRequired: boolean; expires?: string}>;
requireClientAuth: (callback?: () => void, returnUrl?: string, context?: string) => Promise<boolean>;
showIfAuthenticated: (selector: string, context?: string) => Promise<void>;
setupAuthButtons: (selector?: string) => void;
// Consolidated scroll utilities
scrollToElement: (element: Element | null, options?: ScrollIntoViewOptions) => void;
scrollToElementById: (elementId: string, options?: ScrollIntoViewOptions) => void;
scrollToElementBySelector: (selector: string, options?: ScrollIntoViewOptions) => void;
// Additional global functions that might be called
applyScenarioSearch?: (scenarioId: string) => void;
selectPhase?: (phase: string) => void;
selectApproach?: (approach: string) => void;
navigateToGrid?: (toolName: string) => void;
navigateToMatrix?: (toolName: string) => void;
toggleAllScenarios?: () => void;
showShareDialog?: (shareButton: Element) => void;
modalHideInProgress?: boolean;
}
}

View File

@ -73,6 +73,35 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
getStoredTheme
};
// Consolidated scrolling utility
(window as any).scrollToElement = function(element, options = {}) {
if (!element) return;
// Calculate target position manually to avoid double-scroll
setTimeout(() => {
const headerHeight = document.querySelector('nav')?.offsetHeight || 80;
const elementRect = element.getBoundingClientRect();
const absoluteElementTop = elementRect.top + window.pageYOffset;
const targetPosition = absoluteElementTop - headerHeight - 20; // Adjust this 20 as needed
window.scrollTo({
top: targetPosition,
behavior: 'smooth'
});
}, 100);
};
// Convenience functions for common scroll targets
(window as any).scrollToElementById = function(elementId, options = {}) {
const element = document.getElementById(elementId);
(window as any).scrollToElement(element, options);
};
(window as any).scrollToElementBySelector = function(selector, options = {}) {
const element = document.querySelector(selector);
(window as any).scrollToElement(element, options);
};
function createToolSlug(toolName) {
if (!toolName || typeof toolName !== 'string') {
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);

View File

@ -192,14 +192,14 @@ const phases = data.phases;
const methodologySection = document.getElementById('methodology-section');
if (methodologySection) {
methodologySection.classList.add('active');
methodologySection.scrollIntoView({ behavior: 'smooth', block: 'start' });
window.scrollToElementById('methodology-section');
}
} else if (approach === 'targeted') {
// Show targeted scenarios section
const targetedSection = document.getElementById('targeted-section');
if (targetedSection) {
targetedSection.classList.add('active');
targetedSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
window.scrollToElementById('targeted-section');
}
}
};
@ -213,13 +213,13 @@ const phases = data.phases;
card.classList.remove('active');
});
// Add active class to selected phase card (use actual phase ID)
// Add active class to selected phase card
const selectedCard = document.querySelector(`.phase-card.phase-${phase}`);
if (selectedCard) {
selectedCard.classList.add('active');
}
// Use existing phase filter functionality with correct phase ID
// Use existing phase filter functionality
const existingPhaseButton = document.querySelector(`[data-phase="${phase}"]`);
if (existingPhaseButton && !existingPhaseButton.classList.contains('active')) {
existingPhaseButton.click();
@ -231,13 +231,8 @@ const phases = data.phases;
gridToggle.click();
}
// Scroll to results after a short delay
setTimeout(() => {
const toolsGrid = document.getElementById('tools-grid');
if (toolsGrid) {
toolsGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 300);
// Scroll to results using consolidated utility
window.scrollToElementById('tools-grid');
};
document.addEventListener('DOMContentLoaded', () => {
@ -367,7 +362,7 @@ const phases = data.phases;
if (targetCard) {
console.log('Found target card, scrolling...');
targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
window.scrollToElement(targetCard, { block: 'center' });
targetCard.style.animation = 'highlight-flash 2s ease-out';
setTimeout(() => {
@ -377,6 +372,8 @@ const phases = data.phases;
}, 2000);
} else {
console.warn('Tool card not found in grid:', toolName);
// Fallback to tools grid
window.scrollToElementById('tools-grid');
}
}, 300);
}, 200);
@ -410,9 +407,11 @@ const phases = data.phases;
if (firstMatch) {
console.log(`Found ${matchCount} occurrences of tool, highlighting all and scrolling to first`);
firstMatch.scrollIntoView({ behavior: 'smooth', block: 'center' });
window.scrollToElement(firstMatch, { block: 'center' });
} else {
console.warn('Tool chip not found in matrix:', toolName);
// Fallback to matrix container
window.scrollToElementById('matrix-container');
}
}, 500);
};