knowledgebase share button
This commit is contained in:
		
							parent
							
								
									9ce2098439
								
							
						
					
					
						commit
						9a3122745d
					
				@ -771,35 +771,28 @@ const sortedTags = Object.entries(tagFrequency)
 | 
			
		||||
      btn.addEventListener('click', () => {
 | 
			
		||||
        const view = btn.getAttribute('data-view');
 | 
			
		||||
        
 | 
			
		||||
        // Update active states
 | 
			
		||||
        elements.viewToggles.forEach(b => {
 | 
			
		||||
          b.classList.toggle('active', b.getAttribute('data-view') === view);
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        // Call the global switchToView function
 | 
			
		||||
        if (window.switchToView) {
 | 
			
		||||
          window.switchToView(view);
 | 
			
		||||
        } else {
 | 
			
		||||
          console.error('switchToView function not available');
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Dispatch view changed event  
 | 
			
		||||
        window.dispatchEvent(new CustomEvent('viewChanged', { 
 | 
			
		||||
          detail: view,
 | 
			
		||||
          triggeredByButton: true 
 | 
			
		||||
        }));
 | 
			
		||||
        
 | 
			
		||||
        // Handle filtering after view switch
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          if (view === 'matrix') {
 | 
			
		||||
            // Ensure matrix gets populated by triggering filter
 | 
			
		||||
            filterTools();
 | 
			
		||||
          } else if (view === 'grid') {
 | 
			
		||||
            // Standard filtering for grid view
 | 
			
		||||
            filterTools();
 | 
			
		||||
          }
 | 
			
		||||
          // AI view doesn't need filtering from here
 | 
			
		||||
        }, 100); // Slightly longer delay to ensure view switch completes
 | 
			
		||||
        }, 100); 
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
@ -789,7 +789,7 @@ domains.forEach((domain: any) => {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  window.addEventListener('toolsFiltered', (event) => {
 | 
			
		||||
    const { tools: filtered, semanticSearch } = event.detail; // ✅ Correct destructuring
 | 
			
		||||
    const { tools: filtered, semanticSearch } = event.detail;
 | 
			
		||||
    const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
 | 
			
		||||
    
 | 
			
		||||
    if (currentView === 'matrix') {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								src/env.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								src/env.d.ts
									
									
									
									
										vendored
									
									
								
							@ -39,6 +39,9 @@ declare global {
 | 
			
		||||
    toggleAllScenarios?: () => void;
 | 
			
		||||
    showShareDialog?: (shareButton: Element) => void;
 | 
			
		||||
    modalHideInProgress?: boolean;
 | 
			
		||||
    
 | 
			
		||||
    shareArticle: (button: HTMLElement, url: string, title: string) => Promise<void>;
 | 
			
		||||
    shareCurrentArticle: (button: HTMLElement) => Promise<void>;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -22,18 +22,15 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
 | 
			
		||||
  <link rel="icon" type="image/x-icon" href="/favicon.ico">
 | 
			
		||||
  
 | 
			
		||||
<script>
 | 
			
		||||
  // Import utility functions from shared client module instead of duplicating
 | 
			
		||||
  async function loadUtilityFunctions() {
 | 
			
		||||
    try {
 | 
			
		||||
      const { createToolSlug, findToolByIdentifier, isToolHosted } = await import('../utils/clientUtils.js');
 | 
			
		||||
      
 | 
			
		||||
      // Make functions available globally for backward compatibility
 | 
			
		||||
      (window as any).createToolSlug = createToolSlug;
 | 
			
		||||
      (window as any).findToolByIdentifier = findToolByIdentifier;
 | 
			
		||||
      (window as any).isToolHosted = isToolHosted;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Failed to load utility functions:', error);
 | 
			
		||||
      // Minimal fallback for critical functionality only
 | 
			
		||||
      (window as any).createToolSlug = (toolName: string) => {
 | 
			
		||||
        if (!toolName || typeof toolName !== 'string') return '';
 | 
			
		||||
        return toolName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
 | 
			
		||||
@ -92,14 +89,12 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Set non-duplicated functions on window
 | 
			
		||||
  (window as any).scrollToElement = scrollToElement;
 | 
			
		||||
  (window as any).scrollToElementById = scrollToElementById;
 | 
			
		||||
  (window as any).scrollToElementBySelector = scrollToElementBySelector;
 | 
			
		||||
  (window as any).prioritizeSearchResults = prioritizeSearchResults;
 | 
			
		||||
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    // Load utility functions first
 | 
			
		||||
    loadUtilityFunctions();
 | 
			
		||||
    
 | 
			
		||||
    const THEME_KEY = 'dfir-theme';
 | 
			
		||||
@ -236,6 +231,51 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
 | 
			
		||||
    (window as any).showIfAuthenticated = showIfAuthenticated;
 | 
			
		||||
    (window as any).setupAuthButtons = setupAuthButtons;
 | 
			
		||||
 | 
			
		||||
    async function copyUrlToClipboard(url: string, button: HTMLElement) {
 | 
			
		||||
      try {
 | 
			
		||||
        await navigator.clipboard.writeText(url);
 | 
			
		||||
        
 | 
			
		||||
        const originalHTML = button.innerHTML;
 | 
			
		||||
        button.innerHTML = `
 | 
			
		||||
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
 | 
			
		||||
            <polyline points="20,6 9,17 4,12"/>
 | 
			
		||||
          </svg>
 | 
			
		||||
          Kopiert!
 | 
			
		||||
        `;
 | 
			
		||||
        button.style.color = 'var(--color-accent)';
 | 
			
		||||
        
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          button.innerHTML = originalHTML;
 | 
			
		||||
          button.style.color = '';
 | 
			
		||||
        }, 2000);
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        const textArea = document.createElement('textarea');
 | 
			
		||||
        textArea.value = url;
 | 
			
		||||
        document.body.appendChild(textArea);
 | 
			
		||||
        textArea.select();
 | 
			
		||||
        document.execCommand('copy');
 | 
			
		||||
        document.body.removeChild(textArea);
 | 
			
		||||
        
 | 
			
		||||
        const originalHTML = button.innerHTML;
 | 
			
		||||
        button.innerHTML = 'Kopiert!';
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          button.innerHTML = originalHTML;
 | 
			
		||||
        }, 2000);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function shareArticle(button: HTMLElement, url: string, title: string) {
 | 
			
		||||
      const fullUrl = window.location.origin + url;
 | 
			
		||||
      await copyUrlToClipboard(fullUrl, button);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function shareCurrentArticle(button: HTMLElement) {
 | 
			
		||||
      await copyUrlToClipboard(window.location.href, button);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    (window as any).shareArticle = shareArticle;
 | 
			
		||||
    (window as any).shareCurrentArticle = shareCurrentArticle;
 | 
			
		||||
 | 
			
		||||
    initTheme();
 | 
			
		||||
    setupAuthButtons('[data-contribute-button]');
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
// src/pages/api/ai/query.ts - FIXED: Rate limiting for micro-task pipeline
 | 
			
		||||
// src/pages/api/ai/query.ts
 | 
			
		||||
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { withAPIAuth } from '../../../utils/auth.js';
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ configDotenv();
 | 
			
		||||
const DEFAULT_MAX_RESULTS = (() => {
 | 
			
		||||
  const raw = process.env.AI_EMBEDDING_CANDIDATES;
 | 
			
		||||
  const n   = Number.parseInt(raw ?? '', 10);
 | 
			
		||||
  return Number.isFinite(n) && n > 0 ? n : 50;          // fallback
 | 
			
		||||
  return Number.isFinite(n) && n > 0 ? n : 50;
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
const DEFAULT_THRESHOLD = (() => {
 | 
			
		||||
@ -22,7 +22,6 @@ export const prerender = false;
 | 
			
		||||
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    /* ---------- get body & apply defaults from env ---------------- */
 | 
			
		||||
    const {
 | 
			
		||||
      query,
 | 
			
		||||
      maxResults = DEFAULT_MAX_RESULTS,
 | 
			
		||||
 | 
			
		||||
@ -367,7 +367,6 @@ if (aiAuthRequired) {
 | 
			
		||||
      const methodologySection = document.getElementById('methodology-section');
 | 
			
		||||
      const targetedSection = document.getElementById('targeted-section');
 | 
			
		||||
      
 | 
			
		||||
      // Hide all views
 | 
			
		||||
      if (toolsGrid) toolsGrid.style.display = 'none';
 | 
			
		||||
      if (matrixContainer) {
 | 
			
		||||
        matrixContainer.style.display = 'none';
 | 
			
		||||
@ -424,7 +423,6 @@ if (aiAuthRequired) {
 | 
			
		||||
          console.warn('[VIEW] Unknown view:', view);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Restore all filter sections for non-AI views
 | 
			
		||||
      if (view !== 'ai' && filtersSection) {
 | 
			
		||||
        const filterSections = filtersSection.querySelectorAll('.filter-section');
 | 
			
		||||
        filterSections.forEach(section => {
 | 
			
		||||
 | 
			
		||||
@ -108,13 +108,15 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
            const isConcept = hasAssociatedTool && entry.associatedTool.type === 'concept';
 | 
			
		||||
            const isStandalone = !hasAssociatedTool;
 | 
			
		||||
            
 | 
			
		||||
            const articleUrl = `/knowledgebase/${entry.slug}`;
 | 
			
		||||
            
 | 
			
		||||
            return (
 | 
			
		||||
              <article 
 | 
			
		||||
                class="kb-entry card cursor-pointer" 
 | 
			
		||||
                id={`kb-${entry.slug}`} 
 | 
			
		||||
                data-tool-name={entry.title.toLowerCase()}
 | 
			
		||||
                data-article-type={isStandalone ? 'standalone' : 'tool-associated'}
 | 
			
		||||
                onclick={`window.location.href='/knowledgebase/${entry.slug}'`}
 | 
			
		||||
                onclick={`window.location.href='${articleUrl}'`}
 | 
			
		||||
              >
 | 
			
		||||
                <!-- Card Header -->
 | 
			
		||||
                <div class="flex-between mb-3">
 | 
			
		||||
@ -137,7 +139,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
                  </div>
 | 
			
		||||
                  
 | 
			
		||||
                  <div class="flex gap-2 flex-shrink-0" onclick="event.stopPropagation();">
 | 
			
		||||
                    <a href={`/knowledgebase/${entry.slug}`} class="btn btn-primary btn-sm">
 | 
			
		||||
                    <a href={articleUrl} class="btn btn-primary btn-sm">
 | 
			
		||||
                      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
 | 
			
		||||
                        <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
 | 
			
		||||
                        <polyline points="15 3 21 3 21 9"/>
 | 
			
		||||
@ -145,7 +147,21 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
                      </svg>
 | 
			
		||||
                      Öffnen
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <ContributionButton type="edit" toolName={entry.tool_name || entry.title} variant="secondary" text="Edit" style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;" />
 | 
			
		||||
                    <button 
 | 
			
		||||
                      class="btn btn-secondary btn-sm"
 | 
			
		||||
                      onclick="window.shareArticle(this, '${articleUrl}', '${entry.title}')"
 | 
			
		||||
                      title="Artikel teilen"
 | 
			
		||||
                      style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;"
 | 
			
		||||
                    >
 | 
			
		||||
                      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
 | 
			
		||||
                        <circle cx="18" cy="5" r="3"/>
 | 
			
		||||
                        <circle cx="6" cy="12" r="3"/>
 | 
			
		||||
                        <circle cx="18" cy="19" r="3"/>
 | 
			
		||||
                        <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
 | 
			
		||||
                        <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
 | 
			
		||||
                      </svg>
 | 
			
		||||
                      Teilen
 | 
			
		||||
                    </button>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
@ -283,5 +299,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
        lastScrollY = window.scrollY;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
@ -47,6 +47,8 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
 | 
			
		||||
                          displayTool.projectUrl !== null && 
 | 
			
		||||
                          displayTool.projectUrl !== "" && 
 | 
			
		||||
                          displayTool.projectUrl.trim() !== "";
 | 
			
		||||
 | 
			
		||||
const currentUrl = Astro.url.href;
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title={entry.data.title} description={entry.data.description}>
 | 
			
		||||
@ -77,6 +79,22 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
 | 
			
		||||
            )}
 | 
			
		||||
            <span class="badge badge-error">📖</span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <button 
 | 
			
		||||
            id="share-article-btn"
 | 
			
		||||
            class="btn btn-secondary btn-sm"
 | 
			
		||||
            onclick="window.shareCurrentArticle(this)"
 | 
			
		||||
            title="Artikel teilen"
 | 
			
		||||
            style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;"
 | 
			
		||||
          >
 | 
			
		||||
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
 | 
			
		||||
              <circle cx="18" cy="5" r="3"/>
 | 
			
		||||
              <circle cx="6" cy="12" r="3"/>
 | 
			
		||||
              <circle cx="18" cy="19" r="3"/>
 | 
			
		||||
              <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
 | 
			
		||||
              <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
 | 
			
		||||
            </svg>
 | 
			
		||||
            Artikel teilen
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user