Compare commits
No commits in common. "4fd257cbd641b04253f5be00e34ea261a2d3d62b" and "d6760d0f840b81ded3d5c694538e35925c26683c" have entirely different histories.
4fd257cbd6
...
d6760d0f84
File diff suppressed because one or more lines are too long
30
.env.example
30
.env.example
@ -68,36 +68,6 @@ AI_EMBEDDINGS_MODEL=mistral-embed
|
||||
# User rate limiting (queries per minute)
|
||||
AI_RATE_LIMIT_MAX_REQUESTS=4
|
||||
|
||||
# ============================================================================
|
||||
# 🎥 VIDEO EMBEDDING - PRODUCTION CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
# Enable local caching of Nextcloud videos (highly recommended)
|
||||
VIDEO_CACHE_ENABLED=true
|
||||
|
||||
# Directory for cached videos (ensure it's writable and has sufficient space)
|
||||
# This directory will grow over time as videos are cached permanently
|
||||
VIDEO_CACHE_DIR=./cache/videos
|
||||
|
||||
# Emergency cleanup threshold in MB - videos are cached indefinitely
|
||||
# Only triggers cleanup when approaching this limit to prevent disk full
|
||||
# Recommended: 2000MB (2GB) for small deployments, 5000MB+ for larger ones
|
||||
VIDEO_CACHE_MAX_SIZE=2000
|
||||
|
||||
# Maximum individual video file size for caching in MB
|
||||
# Videos larger than this will stream directly without caching
|
||||
VIDEO_MAX_SIZE=200
|
||||
|
||||
# ============================================================================
|
||||
# CACHING BEHAVIOR
|
||||
# ============================================================================
|
||||
# - Videos downloaded once, cached permanently
|
||||
# - No time-based expiration
|
||||
# - Dramatically improves loading times after first download
|
||||
# - Emergency cleanup only when approaching disk space limit
|
||||
# - Perfect for manually curated forensics training content
|
||||
# ============================================================================
|
||||
|
||||
# ============================================================================
|
||||
# 🎛️ PERFORMANCE TUNING - SENSIBLE DEFAULTS PROVIDED
|
||||
# ============================================================================
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import node from '@astrojs/node';
|
||||
import { remarkVideoPlugin } from './src/utils/remarkVideoPlugin.ts';
|
||||
|
||||
export default defineConfig({
|
||||
output: 'server',
|
||||
@ -8,13 +7,6 @@ export default defineConfig({
|
||||
mode: 'standalone'
|
||||
}),
|
||||
|
||||
markdown: {
|
||||
remarkPlugins: [
|
||||
remarkVideoPlugin
|
||||
],
|
||||
extendDefaultPlugins: true
|
||||
},
|
||||
|
||||
build: {
|
||||
assets: '_astro'
|
||||
},
|
||||
@ -24,4 +16,4 @@ export default defineConfig({
|
||||
host: true
|
||||
},
|
||||
allowImportingTsExtensions: true
|
||||
});
|
||||
});
|
||||
|
@ -11,8 +11,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^9.3.0",
|
||||
"@aws-sdk/client-s3": "^3.864.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.864.0",
|
||||
"astro": "^5.12.3",
|
||||
"cookie": "^1.0.2",
|
||||
"dotenv": "^16.4.5",
|
||||
|
@ -193,6 +193,7 @@ domains.forEach((domain: any) => {
|
||||
</div>
|
||||
|
||||
<script define:vars={{ toolsData: tools, domainAgnosticSoftware, domainAgnosticTools }}>
|
||||
// Ensure isToolHosted is available
|
||||
if (!window.isToolHosted) {
|
||||
window.isToolHosted = function(tool) {
|
||||
return tool.projectUrl !== undefined &&
|
||||
@ -764,12 +765,14 @@ domains.forEach((domain: any) => {
|
||||
hideToolDetails('both');
|
||||
}
|
||||
|
||||
// Register all functions globally
|
||||
window.showToolDetails = showToolDetails;
|
||||
window.hideToolDetails = hideToolDetails;
|
||||
window.hideAllToolDetails = hideAllToolDetails;
|
||||
window.toggleDomainAgnosticSection = toggleDomainAgnosticSection;
|
||||
window.showShareDialog = showShareDialog;
|
||||
|
||||
// Register matrix-prefixed versions for delegation
|
||||
window.matrixShowToolDetails = showToolDetails;
|
||||
window.matrixHideToolDetails = hideToolDetails;
|
||||
|
||||
|
@ -1,46 +0,0 @@
|
||||
---
|
||||
// src/components/Video.astro - SIMPLE responsive video component
|
||||
export interface Props {
|
||||
src: string;
|
||||
title?: string;
|
||||
controls?: boolean;
|
||||
autoplay?: boolean;
|
||||
muted?: boolean;
|
||||
loop?: boolean;
|
||||
aspectRatio?: '16:9' | '4:3' | '1:1';
|
||||
preload?: 'none' | 'metadata' | 'auto';
|
||||
}
|
||||
|
||||
const {
|
||||
src,
|
||||
title = 'Video',
|
||||
controls = true,
|
||||
autoplay = false,
|
||||
muted = false,
|
||||
loop = false,
|
||||
aspectRatio = '16:9',
|
||||
preload = 'metadata'
|
||||
} = Astro.props;
|
||||
|
||||
const aspectClass = `aspect-${aspectRatio.replace(':', '-')}`;
|
||||
---
|
||||
|
||||
<div class={`video-container ${aspectClass}`}>
|
||||
<video
|
||||
src={src}
|
||||
controls={controls}
|
||||
autoplay={autoplay}
|
||||
muted={muted}
|
||||
loop={loop}
|
||||
preload={preload}
|
||||
style="width: 100%; height: 100%;"
|
||||
data-video-title={title}
|
||||
>
|
||||
<p>Your browser does not support the video element.</p>
|
||||
</video>
|
||||
{title !== 'Video' && (
|
||||
<div class="video-metadata">
|
||||
<div class="video-title">{title}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
@ -18,7 +18,6 @@ sections:
|
||||
review_status: "published"
|
||||
---
|
||||
|
||||
<video src="https://cloud.cc24.dev/s/ZmPK86M86fWyGQk" controls title="Training Video"></video>
|
||||
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
|
||||
|
||||
|
||||
|
@ -18,8 +18,6 @@ sections:
|
||||
advanced_topics: true
|
||||
review_status: "published"
|
||||
---
|
||||
<video src="https://cloud.cc24.dev/s/ZmPK86M86fWyGQk" controls title="Training Video"></video>
|
||||
|
||||
|
||||
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
|
||||
|
||||
|
@ -37,6 +37,7 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
||||
} catch (error) {
|
||||
console.error('Failed to load utility functions:', error);
|
||||
|
||||
// Provide fallback implementations
|
||||
(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, '');
|
||||
@ -118,6 +119,7 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
||||
(window as any).prioritizeSearchResults = prioritizeSearchResults;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// CRITICAL: Load utility functions FIRST before any URL handling
|
||||
await loadUtilityFunctions();
|
||||
|
||||
const THEME_KEY = 'dfir-theme';
|
||||
@ -171,31 +173,32 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
||||
getStoredTheme
|
||||
};
|
||||
|
||||
(window as any).showToolDetails = function(toolName: string, modalType: string = 'primary') {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50;
|
||||
(window as any).showToolDetails = function(toolName: string, modalType: string = 'primary') {
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50;
|
||||
|
||||
const tryDelegate = () => {
|
||||
const matrixShowToolDetails = (window as any).matrixShowToolDetails;
|
||||
|
||||
const tryDelegate = () => {
|
||||
const matrixShowToolDetails = (window as any).matrixShowToolDetails;
|
||||
|
||||
if (matrixShowToolDetails && typeof matrixShowToolDetails === 'function') {
|
||||
return matrixShowToolDetails(toolName, modalType);
|
||||
}
|
||||
|
||||
const directShowToolDetails = (window as any).directShowToolDetails;
|
||||
if (directShowToolDetails && typeof directShowToolDetails === 'function') {
|
||||
return directShowToolDetails(toolName, modalType);
|
||||
}
|
||||
|
||||
attempts++;
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(tryDelegate, 100);
|
||||
} else {
|
||||
}
|
||||
};
|
||||
if (matrixShowToolDetails && typeof matrixShowToolDetails === 'function') {
|
||||
return matrixShowToolDetails(toolName, modalType);
|
||||
}
|
||||
|
||||
tryDelegate();
|
||||
const directShowToolDetails = (window as any).directShowToolDetails;
|
||||
if (directShowToolDetails && typeof directShowToolDetails === 'function') {
|
||||
return directShowToolDetails(toolName, modalType);
|
||||
}
|
||||
|
||||
attempts++;
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(tryDelegate, 100);
|
||||
} else {
|
||||
}
|
||||
};
|
||||
|
||||
tryDelegate();
|
||||
};
|
||||
|
||||
(window as any).hideToolDetails = function(modalType: string = 'both') {
|
||||
const matrixHideToolDetails = (window as any).matrixHideToolDetails;
|
||||
@ -226,7 +229,7 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
||||
authRequired: data.aiAuthRequired,
|
||||
expires: data.expires
|
||||
};
|
||||
case 'gatedcontent':
|
||||
case 'gatedcontent': // ADD THIS CASE
|
||||
return {
|
||||
authenticated: data.gatedContentAuthenticated,
|
||||
authRequired: data.gatedContentAuthRequired,
|
||||
@ -350,36 +353,6 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
|
||||
};
|
||||
initAIButton();
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox') ||
|
||||
navigator.userAgent.toLowerCase().includes('librewolf');
|
||||
|
||||
if (isFirefox) {
|
||||
console.log('[VIDEO] Firefox detected - setting up error recovery');
|
||||
|
||||
document.querySelectorAll('video').forEach(video => {
|
||||
let errorCount = 0;
|
||||
|
||||
video.addEventListener('error', () => {
|
||||
errorCount++;
|
||||
console.log(`[VIDEO] Error ${errorCount} in Firefox for: ${video.getAttribute('data-video-title')}`);
|
||||
|
||||
if (errorCount === 1 && video.src.includes('/download')) {
|
||||
console.log('[VIDEO] Trying /preview URL for Firefox compatibility');
|
||||
video.src = video.src.replace('/download', '/preview');
|
||||
video.load();
|
||||
} else if (errorCount === 1) {
|
||||
console.log('[VIDEO] Video failed to load in Firefox');
|
||||
}
|
||||
});
|
||||
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
const title = video.getAttribute('data-video-title') || 'Video';
|
||||
console.log(`[VIDEO] Successfully loaded: ${title}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -1,7 +1,5 @@
|
||||
// src/pages/api/auth/login.ts
|
||||
import type { APIRoute } from 'astro';
|
||||
import { generateAuthUrl, generateState, logAuthEvent } from '../../../utils/auth.js';
|
||||
import { serialize } from 'cookie';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@ -10,27 +8,14 @@ export const GET: APIRoute = async ({ url, redirect }) => {
|
||||
const state = generateState();
|
||||
const authUrl = generateAuthUrl(state);
|
||||
|
||||
console.log('[AUTH] Generated auth URL:', authUrl);
|
||||
console.log('Generated auth URL:', authUrl);
|
||||
|
||||
const returnTo = url.searchParams.get('returnTo') || '/';
|
||||
|
||||
logAuthEvent('Login initiated', { returnTo, authUrl });
|
||||
|
||||
const stateData = JSON.stringify({ state, returnTo });
|
||||
|
||||
const publicBaseUrl = process.env.PUBLIC_BASE_URL || '';
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const isSecure = publicBaseUrl.startsWith('https://') || isProduction;
|
||||
|
||||
const stateCookie = serialize('auth_state', stateData, {
|
||||
httpOnly: true,
|
||||
secure: isSecure,
|
||||
sameSite: 'lax',
|
||||
maxAge: 600, // 10 minutes
|
||||
path: '/'
|
||||
});
|
||||
|
||||
console.log('[AUTH] Setting auth state cookie:', stateCookie.substring(0, 50) + '...');
|
||||
const stateCookie = `auth_state=${encodeURIComponent(stateData)}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600`;
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
|
@ -1,4 +1,4 @@
|
||||
// src/pages/api/auth/process.ts
|
||||
// src/pages/api/auth/process.ts (FIXED - Proper cookie handling)
|
||||
import type { APIRoute } from 'astro';
|
||||
import {
|
||||
verifyAuthState,
|
||||
@ -7,7 +7,7 @@ import {
|
||||
createSessionWithCookie,
|
||||
logAuthEvent
|
||||
} from '../../../utils/auth.js';
|
||||
import { apiError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
|
||||
import { apiError, apiSpecial, apiWithHeaders, handleAPIRequest } from '../../../utils/api.js';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@ -30,15 +30,9 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
|
||||
const stateVerification = verifyAuthState(request, state);
|
||||
if (!stateVerification.isValid || !stateVerification.stateData) {
|
||||
logAuthEvent('State verification failed', {
|
||||
error: stateVerification.error,
|
||||
hasStateData: !!stateVerification.stateData
|
||||
});
|
||||
return apiError.badRequest(stateVerification.error || 'Invalid state parameter');
|
||||
}
|
||||
|
||||
console.log('[AUTH] State verification successful, exchanging code for tokens');
|
||||
|
||||
const tokens = await exchangeCodeForTokens(code);
|
||||
const userInfo = await getUserInfo(tokens.access_token);
|
||||
|
||||
@ -49,12 +43,6 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
email: sessionResult.userEmail
|
||||
});
|
||||
|
||||
const returnUrl = new URL(stateVerification.stateData.returnTo, request.url);
|
||||
returnUrl.searchParams.set('auth', 'success');
|
||||
const redirectUrl = returnUrl.toString();
|
||||
|
||||
console.log('[AUTH] Redirecting to:', redirectUrl);
|
||||
|
||||
const responseHeaders = new Headers();
|
||||
responseHeaders.set('Content-Type', 'application/json');
|
||||
|
||||
@ -63,7 +51,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
redirectTo: redirectUrl
|
||||
redirectTo: stateVerification.stateData.returnTo
|
||||
}), {
|
||||
status: 200,
|
||||
headers: responseHeaders
|
||||
|
@ -9,16 +9,16 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
return await handleAPIRequest(async () => {
|
||||
const contributionAuth = await withAPIAuth(request, 'contributions');
|
||||
const aiAuth = await withAPIAuth(request, 'ai');
|
||||
const gatedContentAuth = await withAPIAuth(request, 'gatedcontent');
|
||||
const gatedContentAuth = await withAPIAuth(request, 'gatedcontent'); // ADDED
|
||||
|
||||
return apiResponse.success({
|
||||
authenticated: contributionAuth.authenticated || aiAuth.authenticated || gatedContentAuth.authenticated,
|
||||
contributionAuthRequired: contributionAuth.authRequired,
|
||||
aiAuthRequired: aiAuth.authRequired,
|
||||
gatedContentAuthRequired: gatedContentAuth.authRequired,
|
||||
gatedContentAuthRequired: gatedContentAuth.authRequired, // ADDED
|
||||
contributionAuthenticated: contributionAuth.authenticated,
|
||||
aiAuthenticated: aiAuth.authenticated,
|
||||
gatedContentAuthenticated: gatedContentAuth.authenticated,
|
||||
gatedContentAuthenticated: gatedContentAuth.authenticated, // ADDED
|
||||
expires: contributionAuth.session?.exp ? new Date(contributionAuth.session.exp * 1000).toISOString() : null
|
||||
});
|
||||
}, 'Status check failed');
|
||||
|
@ -1,4 +1,4 @@
|
||||
// src/pages/api/contribute/knowledgebase.ts
|
||||
// src/pages/api/contribute/knowledgebase.ts - SIMPLIFIED: Issues only, minimal validation
|
||||
import type { APIRoute } from 'astro';
|
||||
import { withAPIAuth } from '../../../utils/auth.js';
|
||||
import { apiResponse, apiError, apiServerError, handleAPIRequest } from '../../../utils/api.js';
|
||||
|
@ -1,4 +1,4 @@
|
||||
// src/pages/api/contribute/tool.ts
|
||||
// src/pages/api/contribute/tool.ts (UPDATED - Using consolidated API responses + related_software)
|
||||
import type { APIRoute } from 'astro';
|
||||
import { withAPIAuth } from '../../../utils/auth.js';
|
||||
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
|
||||
@ -82,27 +82,31 @@ function sanitizeInput(obj: any): any {
|
||||
}
|
||||
|
||||
function preprocessFormData(body: any): any {
|
||||
// Handle comma-separated strings from autocomplete inputs
|
||||
if (body.tool) {
|
||||
// Handle tags
|
||||
if (typeof body.tool.tags === 'string') {
|
||||
body.tool.tags = body.tool.tags.split(',').map((t: string) => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// Handle related concepts
|
||||
if (body.tool.relatedConcepts) {
|
||||
if (typeof body.tool.relatedConcepts === 'string') {
|
||||
body.tool.related_concepts = body.tool.relatedConcepts.split(',').map((t: string) => t.trim()).filter(Boolean);
|
||||
} else {
|
||||
body.tool.related_concepts = body.tool.relatedConcepts;
|
||||
}
|
||||
delete body.tool.relatedConcepts;
|
||||
delete body.tool.relatedConcepts; // Remove the original key
|
||||
}
|
||||
|
||||
// Handle related software
|
||||
if (body.tool.relatedSoftware) {
|
||||
if (typeof body.tool.relatedSoftware === 'string') {
|
||||
body.tool.related_software = body.tool.relatedSoftware.split(',').map((t: string) => t.trim()).filter(Boolean);
|
||||
} else {
|
||||
body.tool.related_software = body.tool.relatedSoftware;
|
||||
}
|
||||
delete body.tool.relatedSoftware;
|
||||
delete body.tool.relatedSoftware; // Remove the original key
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,11 +142,14 @@ async function validateToolData(tool: any, action: string): Promise<{ valid: boo
|
||||
}
|
||||
}
|
||||
|
||||
// Validate related items exist (optional validation - could be enhanced)
|
||||
if (tool.related_concepts && tool.related_concepts.length > 0) {
|
||||
// Could validate that referenced concepts actually exist
|
||||
console.log('[VALIDATION] Related concepts provided:', tool.related_concepts);
|
||||
}
|
||||
|
||||
if (tool.related_software && tool.related_software.length > 0) {
|
||||
// Could validate that referenced software actually exists
|
||||
console.log('[VALIDATION] Related software provided:', tool.related_software);
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
);
|
||||
}
|
||||
|
||||
/* --- (rest of the handler unchanged) -------------------------- */
|
||||
const { embeddingsService } = await import('../../../utils/embeddings.js');
|
||||
|
||||
if (!embeddingsService.isEnabled()) {
|
||||
|
@ -23,6 +23,7 @@ const editToolName = Astro.url.searchParams.get('edit');
|
||||
const editTool = editToolName ? existingTools.find(tool => tool.name === editToolName) : null;
|
||||
const isEdit = !!editTool;
|
||||
|
||||
// Extract data for autocomplete
|
||||
const allTags = [...new Set(existingTools.flatMap(tool => tool.tags || []))].sort();
|
||||
const allSoftwareAndMethods = existingTools
|
||||
.filter(tool => tool.type === 'software' || tool.type === 'method')
|
||||
@ -299,6 +300,7 @@ const allConcepts = existingTools
|
||||
</BaseLayout>
|
||||
|
||||
<script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware, allTags, allSoftwareAndMethods, allConcepts }}>
|
||||
// Consolidated Autocomplete Functionality - inlined to avoid module loading issues
|
||||
class AutocompleteManager {
|
||||
constructor(inputElement, dataSource, options = {}) {
|
||||
this.input = inputElement;
|
||||
@ -335,6 +337,7 @@ class AutocompleteManager {
|
||||
this.dropdown = document.createElement('div');
|
||||
this.dropdown.className = 'autocomplete-dropdown';
|
||||
|
||||
// Insert dropdown after input
|
||||
this.input.parentNode.style.position = 'relative';
|
||||
this.input.parentNode.insertBefore(this.dropdown, this.input.nextSibling);
|
||||
}
|
||||
@ -355,6 +358,7 @@ class AutocompleteManager {
|
||||
});
|
||||
|
||||
this.input.addEventListener('blur', (e) => {
|
||||
// Delay to allow click events on dropdown items
|
||||
setTimeout(() => {
|
||||
if (!this.dropdown.contains(document.activeElement)) {
|
||||
this.hideDropdown();
|
||||
@ -446,6 +450,7 @@ class AutocompleteManager {
|
||||
})
|
||||
.join('');
|
||||
|
||||
// Bind click events
|
||||
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
|
||||
option.addEventListener('click', () => {
|
||||
this.selectItem(this.filteredData[index]);
|
||||
@ -479,6 +484,7 @@ class AutocompleteManager {
|
||||
this.hideDropdown();
|
||||
}
|
||||
|
||||
// Trigger change event
|
||||
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
|
||||
detail: { item, text, selectedItems: Array.from(this.selectedItems) }
|
||||
}));
|
||||
@ -504,6 +510,7 @@ class AutocompleteManager {
|
||||
`)
|
||||
.join('');
|
||||
|
||||
// Bind remove events
|
||||
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
@ -629,6 +636,7 @@ class ContributionForm {
|
||||
}
|
||||
|
||||
setupAutocomplete() {
|
||||
// Tags autocomplete
|
||||
if (this.elements.tagsInput && this.elements.tagsHidden) {
|
||||
const tagsManager = new AutocompleteManager(this.elements.tagsInput, allTags, {
|
||||
allowMultiple: true,
|
||||
@ -636,6 +644,7 @@ class ContributionForm {
|
||||
placeholder: 'Beginne zu tippen, um Tags hinzuzufügen...'
|
||||
});
|
||||
|
||||
// Set initial values if editing
|
||||
if (this.editTool?.tags) {
|
||||
tagsManager.setSelectedItems(this.editTool.tags);
|
||||
}
|
||||
@ -643,6 +652,7 @@ class ContributionForm {
|
||||
this.autocompleteManagers.set('tags', tagsManager);
|
||||
}
|
||||
|
||||
// Related concepts autocomplete
|
||||
if (this.elements.relatedConceptsInput && this.elements.relatedConceptsHidden) {
|
||||
const conceptsManager = new AutocompleteManager(this.elements.relatedConceptsInput, allConcepts, {
|
||||
allowMultiple: true,
|
||||
@ -650,6 +660,7 @@ class ContributionForm {
|
||||
placeholder: 'Beginne zu tippen, um Konzepte zu finden...'
|
||||
});
|
||||
|
||||
// Set initial values if editing
|
||||
if (this.editTool?.related_concepts) {
|
||||
conceptsManager.setSelectedItems(this.editTool.related_concepts);
|
||||
}
|
||||
@ -657,6 +668,7 @@ class ContributionForm {
|
||||
this.autocompleteManagers.set('relatedConcepts', conceptsManager);
|
||||
}
|
||||
|
||||
// Related software autocomplete
|
||||
if (this.elements.relatedSoftwareInput && this.elements.relatedSoftwareHidden) {
|
||||
const softwareManager = new AutocompleteManager(this.elements.relatedSoftwareInput, allSoftwareAndMethods, {
|
||||
allowMultiple: true,
|
||||
@ -664,6 +676,7 @@ class ContributionForm {
|
||||
placeholder: 'Beginne zu tippen, um Software/Methoden zu finden...'
|
||||
});
|
||||
|
||||
// Set initial values if editing
|
||||
if (this.editTool?.related_software) {
|
||||
softwareManager.setSelectedItems(this.editTool.related_software);
|
||||
}
|
||||
@ -671,6 +684,7 @@ class ContributionForm {
|
||||
this.autocompleteManagers.set('relatedSoftware', softwareManager);
|
||||
}
|
||||
|
||||
// Listen for autocomplete changes to update YAML preview
|
||||
Object.values(this.autocompleteManagers).forEach(manager => {
|
||||
if (manager.input) {
|
||||
manager.input.addEventListener('autocomplete:select', () => {
|
||||
@ -712,10 +726,14 @@ class ContributionForm {
|
||||
updateFieldVisibility() {
|
||||
const type = this.elements.typeSelect.value;
|
||||
|
||||
// Only hide/show software-specific fields (platforms, license)
|
||||
// Relations should always be visible since all tool types can have relationships
|
||||
this.elements.softwareFields.style.display = type === 'software' ? 'block' : 'none';
|
||||
|
||||
// Always show relations - all tool types can have relationships
|
||||
this.elements.relationsFields.style.display = 'block';
|
||||
|
||||
// Only mark platform/license as required for software
|
||||
if (this.elements.platformsRequired) {
|
||||
this.elements.platformsRequired.style.display = type === 'software' ? 'inline' : 'none';
|
||||
}
|
||||
@ -723,6 +741,7 @@ class ContributionForm {
|
||||
this.elements.licenseRequired.style.display = type === 'software' ? 'inline' : 'none';
|
||||
}
|
||||
|
||||
// Always show both relation sections - let users decide what's relevant
|
||||
const conceptsSection = document.getElementById('related-concepts-section');
|
||||
const softwareSection = document.getElementById('related-software-section');
|
||||
if (conceptsSection) conceptsSection.style.display = 'block';
|
||||
@ -787,16 +806,19 @@ class ContributionForm {
|
||||
tool.knowledgebase = true;
|
||||
}
|
||||
|
||||
// Handle tags from autocomplete
|
||||
const tagsValue = this.elements.tagsHidden?.value || '';
|
||||
if (tagsValue) {
|
||||
tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// Handle related concepts from autocomplete
|
||||
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
|
||||
if (relatedConceptsValue) {
|
||||
tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// Handle related software from autocomplete
|
||||
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
|
||||
if (relatedSoftwareValue) {
|
||||
tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||
@ -961,16 +983,19 @@ class ContributionForm {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle tags from autocomplete
|
||||
const tagsValue = this.elements.tagsHidden?.value || '';
|
||||
if (tagsValue) {
|
||||
submission.tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// Handle related concepts from autocomplete
|
||||
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
|
||||
if (relatedConceptsValue) {
|
||||
submission.tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// Handle related software from autocomplete
|
||||
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
|
||||
if (relatedSoftwareValue) {
|
||||
submission.tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||
@ -1047,6 +1072,7 @@ class ContributionForm {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Clean up autocomplete managers
|
||||
this.autocompleteManagers.forEach(manager => {
|
||||
manager.destroy();
|
||||
});
|
||||
|
@ -686,6 +686,8 @@ if (aiAuthRequired) {
|
||||
window.switchToAIView = () => switchToView('ai');
|
||||
window.switchToView = switchToView;
|
||||
|
||||
// CRITICAL: Handle shared URLs AFTER everything is set up
|
||||
// Increased timeout to ensure all components and utility functions are loaded
|
||||
setTimeout(() => {
|
||||
handleSharedURL();
|
||||
}, 1000);
|
||||
|
@ -10,6 +10,7 @@ const allKnowledgebaseEntries = await getCollection('knowledgebase', (entry) =>
|
||||
return entry.data.published !== false;
|
||||
});
|
||||
|
||||
// Check if gated content authentication is enabled globally
|
||||
const gatedContentAuthEnabled = isGatedContentAuthRequired();
|
||||
|
||||
const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
|
||||
@ -26,7 +27,8 @@ const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
|
||||
difficulty: entry.data.difficulty,
|
||||
categories: entry.data.categories || [],
|
||||
tags: entry.data.tags || [],
|
||||
gated_content: entry.data.gated_content || false,
|
||||
gated_content: entry.data.gated_content || false, // NEW: Include gated content flag
|
||||
|
||||
tool_name: entry.data.tool_name,
|
||||
related_tools: entry.data.related_tools || [],
|
||||
associatedTool,
|
||||
@ -43,6 +45,7 @@ const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
|
||||
|
||||
knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
|
||||
|
||||
// Count gated vs public articles for statistics
|
||||
const gatedCount = knowledgebaseEntries.filter(entry => entry.gated_content).length;
|
||||
const publicCount = knowledgebaseEntries.length - gatedCount;
|
||||
---
|
||||
|
@ -21,6 +21,7 @@ export async function getStaticPaths() {
|
||||
|
||||
const { entry }: { entry: any } = Astro.props;
|
||||
|
||||
// Check if this article is gated and if gated content auth is required globally
|
||||
const isGatedContent = entry.data.gated_content === true;
|
||||
const gatedContentAuthRequired = isGatedContentAuthRequired();
|
||||
const requiresAuth = isGatedContent && gatedContentAuthRequired;
|
||||
@ -61,28 +62,24 @@ const currentUrl = Astro.url.href;
|
||||
<BaseLayout title={entry.data.title} description={entry.data.description}>
|
||||
{requiresAuth && (
|
||||
<script define:vars={{ requiresAuth, articleTitle: entry.data.title }}>
|
||||
// Client-side authentication check for gated content
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (!requiresAuth) return;
|
||||
|
||||
console.log('[GATED CONTENT] Checking client-side auth for: ' + articleTitle);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const authSuccess = urlParams.get('auth') === 'success';
|
||||
|
||||
// Hide content immediately while checking auth
|
||||
const contentArea = document.querySelector('.article-content');
|
||||
const sidebar = document.querySelector('.article-sidebar');
|
||||
|
||||
|
||||
if (contentArea) {
|
||||
contentArea.style.display = 'none';
|
||||
}
|
||||
|
||||
if (authSuccess) {
|
||||
console.log('[GATED CONTENT] Auth success detected, waiting for session...');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
|
||||
window.history.replaceState({}, document.title, cleanUrl);
|
||||
}
|
||||
// DON'T hide the sidebar container - just prevent TOC generation
|
||||
//if (sidebar) {
|
||||
//sidebar.innerHTML = ''; // Clear any content instead of hiding
|
||||
//}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/status');
|
||||
@ -96,6 +93,7 @@ const currentUrl = Astro.url.href;
|
||||
if (authRequired && !isAuthenticated) {
|
||||
console.log('[GATED CONTENT] Access denied - showing auth required message: ' + articleTitle);
|
||||
|
||||
// Show authentication required message (no auto-redirect)
|
||||
if (contentArea) {
|
||||
const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
|
||||
contentArea.innerHTML = [
|
||||
@ -123,9 +121,11 @@ const currentUrl = Astro.url.href;
|
||||
}
|
||||
} else {
|
||||
console.log('[GATED CONTENT] Access granted for: ' + articleTitle);
|
||||
// Show content for authenticated users
|
||||
if (contentArea) {
|
||||
contentArea.style.display = 'block';
|
||||
}
|
||||
// Let TOC generate normally for authenticated users
|
||||
setTimeout(() => {
|
||||
if (typeof generateTOCContent === 'function') {
|
||||
generateTOCContent();
|
||||
@ -134,6 +134,7 @@ const currentUrl = Astro.url.href;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[GATED CONTENT] Auth check failed:', error);
|
||||
// On error, show auth required message
|
||||
if (requiresAuth && contentArea) {
|
||||
const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
|
||||
contentArea.innerHTML = [
|
||||
@ -401,10 +402,29 @@ const currentUrl = Astro.url.href;
|
||||
}
|
||||
|
||||
function generateSidebarTOC() {
|
||||
// NEW: Don't generate TOC for gated content that requires auth
|
||||
if (requiresAuth) {
|
||||
fetch('/api/auth/status')
|
||||
.then(response => response.json())
|
||||
.then(authStatus => {
|
||||
const isAuthenticated = authStatus.gatedContentAuthenticated || false;
|
||||
const authRequired = authStatus.gatedContentAuthRequired || false;
|
||||
|
||||
// Only generate TOC if user is authenticated for gated content
|
||||
if (authRequired && !isAuthenticated) {
|
||||
return; // Don't generate TOC
|
||||
} else {
|
||||
generateTOCContent(); // Generate TOC for authenticated users
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// On error, don't generate TOC for gated content
|
||||
return;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// For non-gated content, generate TOC normally
|
||||
generateTOCContent();
|
||||
}
|
||||
|
||||
@ -510,14 +530,17 @@ const currentUrl = Astro.url.href;
|
||||
pre.dataset.copyEnhanced = 'true';
|
||||
pre.style.position ||= 'relative';
|
||||
|
||||
// Try to find an existing copy button we can reuse
|
||||
let btn =
|
||||
pre.querySelector('.copy-btn') ||
|
||||
pre.querySelector('.copy-btn') || // our class
|
||||
pre.querySelector('.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]');
|
||||
|
||||
// If there is an "old" button that is NOT ours, prefer to reuse it by giving it our class.
|
||||
if (btn && !btn.classList.contains('copy-btn')) {
|
||||
btn.classList.add('copy-btn');
|
||||
}
|
||||
|
||||
// If no button at all, create one
|
||||
if (!btn) {
|
||||
btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
@ -532,6 +555,7 @@ const currentUrl = Astro.url.href;
|
||||
pre.appendChild(btn);
|
||||
}
|
||||
|
||||
// If there is a SECOND old button lingering (top-left in your case), hide it
|
||||
const possibleOldButtons = pre.querySelectorAll(
|
||||
'.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]'
|
||||
);
|
||||
@ -539,6 +563,7 @@ const currentUrl = Astro.url.href;
|
||||
if (b !== btn) b.style.display = 'none';
|
||||
});
|
||||
|
||||
// Success pill
|
||||
if (!pre.querySelector('.copied-pill')) {
|
||||
const pill = document.createElement('div');
|
||||
pill.className = 'copied-pill';
|
||||
@ -546,6 +571,7 @@ const currentUrl = Astro.url.href;
|
||||
pre.appendChild(pill);
|
||||
}
|
||||
|
||||
// Screen reader live region
|
||||
if (!pre.querySelector('.sr-live')) {
|
||||
const live = document.createElement('div');
|
||||
live.className = 'sr-live';
|
||||
@ -588,13 +614,12 @@ const currentUrl = Astro.url.href;
|
||||
});
|
||||
}
|
||||
|
||||
// Make generateTOCContent available globally for the auth check script
|
||||
window.generateTOCContent = generateTOCContent;
|
||||
|
||||
// Initialize everything on page load
|
||||
// keep your existing DOMContentLoaded; just ensure this is called
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// existing:
|
||||
calculateReadingTime();
|
||||
generateSidebarTOC();
|
||||
// new/updated:
|
||||
enhanceCodeCopy();
|
||||
});
|
||||
</script>
|
||||
|
@ -688,245 +688,3 @@
|
||||
/* Expand content */
|
||||
.article-main { max-width: 100% !important; }
|
||||
}
|
||||
|
||||
|
||||
/* ==========================================================================
|
||||
VIDEO EMBEDDING - Add to knowledgebase.css
|
||||
========================================================================== */
|
||||
|
||||
/* Video Container and Responsive Wrapper */
|
||||
:where(.markdown-content) .video-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 2rem 0;
|
||||
border-radius: var(--radius-lg, 0.75rem);
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg-tertiary, #000);
|
||||
box-shadow: var(--shadow-lg, 0 12px 30px rgba(0,0,0,0.16));
|
||||
}
|
||||
|
||||
/* Responsive 16:9 aspect ratio by default */
|
||||
:where(.markdown-content) .video-container.aspect-16-9 {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
:where(.markdown-content) .video-container.aspect-4-3 {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
|
||||
:where(.markdown-content) .video-container.aspect-1-1 {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
/* Video Element Styling */
|
||||
:where(.markdown-content) .video-container video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background-color: #000;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Custom Video Controls Enhancement */
|
||||
:where(.markdown-content) video::-webkit-media-controls-panel {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
:where(.markdown-content) video::-webkit-media-controls-current-time-display,
|
||||
:where(.markdown-content) video::-webkit-media-controls-time-remaining-display {
|
||||
color: white;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
/* Video Loading State */
|
||||
:where(.markdown-content) .video-container .video-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--color-text-secondary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
:where(.markdown-content) .video-container .video-loading .spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 3px solid var(--color-border);
|
||||
border-top: 3px solid var(--color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Video Error State */
|
||||
:where(.markdown-content) .video-container .video-error {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
color: var(--color-error, #dc3545);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
:where(.markdown-content) .video-container .video-error .error-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Video Metadata Overlay */
|
||||
:where(.markdown-content) .video-metadata {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-top: none;
|
||||
padding: 1rem 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
border-radius: 0 0 var(--radius-lg, 0.75rem) var(--radius-lg, 0.75rem);
|
||||
}
|
||||
|
||||
:where(.markdown-content) .video-metadata .video-title {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
:where(.markdown-content) .video-metadata .video-info {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:where(.markdown-content) .video-metadata .video-duration,
|
||||
:where(.markdown-content) .video-metadata .video-size,
|
||||
:where(.markdown-content) .video-metadata .video-format {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* Fullscreen Support */
|
||||
:where(.markdown-content) .video-container video:fullscreen {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
:where(.markdown-content) .video-container video:-webkit-full-screen {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
:where(.markdown-content) .video-container video:-moz-full-screen {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
/* Video Thumbnail/Poster Styling */
|
||||
:where(.markdown-content) .video-container video[poster] {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Protected Video Overlay */
|
||||
:where(.markdown-content) .video-container .video-protected {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
:where(.markdown-content) .video-container .video-protected .lock-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
:where(.markdown-content) .video-container {
|
||||
margin: 1.5rem -0.5rem; /* Extend to edges on mobile */
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:where(.markdown-content) .video-metadata {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.8125rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:where(.markdown-content) .video-metadata .video-info {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Theme Adjustments */
|
||||
[data-theme="dark"] :where(.markdown-content) .video-container {
|
||||
box-shadow: 0 12px 30px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
[data-theme="dark"] :where(.markdown-content) .video-metadata {
|
||||
background-color: var(--color-bg-tertiary);
|
||||
border-color: color-mix(in srgb, var(--color-border) 60%, transparent);
|
||||
}
|
||||
|
||||
/* Video Caption/Description Support */
|
||||
:where(.markdown-content) .video-caption {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.9375rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Video Gallery Support (multiple videos) */
|
||||
:where(.markdown-content) .video-gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
:where(.markdown-content) .video-gallery .video-container {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Accessibility Improvements */
|
||||
:where(.markdown-content) .video-container video:focus {
|
||||
outline: 3px solid var(--color-primary);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* Print Media - Hide Videos */
|
||||
@media print {
|
||||
:where(.markdown-content) .video-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:where(.markdown-content) .video-container::after {
|
||||
content: "[Video: " attr(data-video-title, "Embedded Video") "]";
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
}
|
||||
}
|
@ -1083,6 +1083,7 @@ class ImprovedMicroTaskAIPipeline {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 1: AI selection of tools for completion
|
||||
const selectionPrompt = AI_PROMPTS.generatePhaseCompletionPrompt(originalQuery, phase, phaseTools, phaseConcepts);
|
||||
const selectionResult = await this.callMicroTaskAI(selectionPrompt, context, 800);
|
||||
|
||||
@ -1107,6 +1108,7 @@ class ImprovedMicroTaskAIPipeline {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Generate detailed reasoning for each selected tool
|
||||
for (const tool of validTools) {
|
||||
console.log('[AI-PIPELINE] Generating reasoning for phase completion tool:', tool.name);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
// src/utils/auth.js
|
||||
// src/utils/auth.js (ENHANCED - Added gated content support)
|
||||
import type { AstroGlobal } from 'astro';
|
||||
import crypto from 'crypto';
|
||||
import { config } from 'dotenv';
|
||||
@ -390,10 +390,12 @@ export function getAuthRequirementForContext(context: AuthContextType): boolean
|
||||
return getAuthRequirement(context);
|
||||
}
|
||||
|
||||
// NEW: Helper function to check if gated content requires authentication
|
||||
export function isGatedContentAuthRequired(): boolean {
|
||||
return getAuthRequirement('gatedcontent');
|
||||
}
|
||||
|
||||
// NEW: Check if specific content should be gated
|
||||
export function shouldGateContent(isGatedContent: boolean): boolean {
|
||||
return isGatedContent && isGatedContentAuthRequired();
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// src/utils/clientUtils.ts
|
||||
|
||||
// Client-side utilities that mirror server-side toolHelpers.ts
|
||||
|
||||
export function createToolSlug(toolName: string): string {
|
||||
if (!toolName || typeof toolName !== 'string') {
|
||||
@ -8,10 +8,10 @@ export function createToolSlug(toolName: string): string {
|
||||
}
|
||||
|
||||
return toolName.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
|
||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
.replace(/-+/g, '-') // Remove duplicate hyphens
|
||||
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
||||
}
|
||||
|
||||
export function findToolByIdentifier(tools: any[], identifier: string): any | undefined {
|
||||
@ -30,6 +30,7 @@ export function isToolHosted(tool: any): boolean {
|
||||
tool.projectUrl.trim() !== "";
|
||||
}
|
||||
|
||||
// Consolidated Autocomplete Functionality
|
||||
interface AutocompleteOptions {
|
||||
minLength?: number;
|
||||
maxResults?: number;
|
||||
@ -96,6 +97,7 @@ export class AutocompleteManager {
|
||||
display: none;
|
||||
`;
|
||||
|
||||
// Insert dropdown after input
|
||||
const parentElement = this.input.parentNode as HTMLElement;
|
||||
parentElement.style.position = 'relative';
|
||||
parentElement.insertBefore(this.dropdown, this.input.nextSibling);
|
||||
@ -117,6 +119,7 @@ export class AutocompleteManager {
|
||||
});
|
||||
|
||||
this.input.addEventListener('blur', () => {
|
||||
// Delay to allow click events on dropdown items
|
||||
setTimeout(() => {
|
||||
const activeElement = document.activeElement;
|
||||
if (!activeElement || !this.dropdown.contains(activeElement)) {
|
||||
@ -223,6 +226,7 @@ export class AutocompleteManager {
|
||||
})
|
||||
.join('');
|
||||
|
||||
// Bind click events
|
||||
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
|
||||
option.addEventListener('click', () => {
|
||||
this.selectItem(this.filteredData[index]);
|
||||
@ -256,6 +260,7 @@ export class AutocompleteManager {
|
||||
this.hideDropdown();
|
||||
}
|
||||
|
||||
// Trigger change event
|
||||
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
|
||||
detail: { item, text, selectedItems: Array.from(this.selectedItems) }
|
||||
}));
|
||||
@ -302,6 +307,7 @@ export class AutocompleteManager {
|
||||
`)
|
||||
.join('');
|
||||
|
||||
// Bind remove events
|
||||
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
@ -1,85 +0,0 @@
|
||||
// src/utils/remarkVideoPlugin.ts
|
||||
import { visit } from 'unist-util-visit';
|
||||
import type { Plugin } from 'unified';
|
||||
import type { Root } from 'hast';
|
||||
|
||||
|
||||
export const remarkVideoPlugin: Plugin<[], Root> = () => {
|
||||
return (tree: Root) => {
|
||||
visit(tree, 'html', (node: any, index: number | undefined, parent: any) => {
|
||||
if (node.value && node.value.includes('<video') && typeof index === 'number') {
|
||||
|
||||
const srcMatch = node.value.match(/src=["']([^"']+)["']/);
|
||||
const titleMatch = node.value.match(/title=["']([^"']+)["']/);
|
||||
|
||||
if (srcMatch) {
|
||||
const originalSrc = srcMatch[1];
|
||||
const title = titleMatch?.[1] || 'Video';
|
||||
|
||||
const finalSrc = processNextcloudUrl(originalSrc);
|
||||
|
||||
const hasControls = node.value.includes('controls');
|
||||
const hasAutoplay = node.value.includes('autoplay');
|
||||
const hasMuted = node.value.includes('muted');
|
||||
const hasLoop = node.value.includes('loop');
|
||||
const hasPreload = node.value.match(/preload=["']([^"']+)["']/);
|
||||
|
||||
const enhancedHTML = `
|
||||
<div class="video-container aspect-16-9">
|
||||
<video
|
||||
src="${escapeHtml(finalSrc)}"
|
||||
${hasControls ? 'controls' : ''}
|
||||
${hasAutoplay ? 'autoplay' : ''}
|
||||
${hasMuted ? 'muted' : ''}
|
||||
${hasLoop ? 'loop' : ''}
|
||||
${hasPreload ? `preload="${hasPreload[1]}"` : 'preload="metadata"'}
|
||||
style="width: 100%; height: 100%;"
|
||||
data-video-title="${escapeHtml(title)}"
|
||||
data-original-src="${escapeHtml(originalSrc)}"
|
||||
>
|
||||
<p>Your browser does not support the video element.</p>
|
||||
</video>
|
||||
${title !== 'Video' ? `
|
||||
<div class="video-metadata">
|
||||
<div class="video-title">${escapeHtml(title)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`.trim();
|
||||
|
||||
parent.children[index] = { type: 'html', value: enhancedHTML };
|
||||
|
||||
console.log(`[VIDEO] Processed: ${title}`);
|
||||
console.log(`[VIDEO] Final URL: ${finalSrc}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
function processNextcloudUrl(originalUrl: string): string {
|
||||
if (isNextcloudShareUrl(originalUrl) && !originalUrl.includes('/download')) {
|
||||
const downloadUrl = `${originalUrl}/download`;
|
||||
console.log(`[VIDEO] Auto-added /download: ${originalUrl} → ${downloadUrl}`);
|
||||
return downloadUrl;
|
||||
}
|
||||
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
function isNextcloudShareUrl(url: string): boolean {
|
||||
const pattern = /\/s\/[a-zA-Z0-9]+/;
|
||||
return pattern.test(url) && (url.includes('nextcloud') || url.includes('cloud.'));
|
||||
}
|
||||
|
||||
function escapeHtml(unsafe: string): string {
|
||||
if (typeof unsafe !== 'string') return '';
|
||||
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
// src/utils/toolHelpers.ts
|
||||
|
||||
export interface Tool {
|
||||
name: string;
|
||||
type?: 'software' | 'method' | 'concept';
|
||||
@ -15,8 +13,31 @@ export interface Tool {
|
||||
related_concepts?: string[];
|
||||
}
|
||||
|
||||
export {
|
||||
createToolSlug,
|
||||
findToolByIdentifier,
|
||||
isToolHosted
|
||||
} from './clientUtils.js';
|
||||
export function createToolSlug(toolName: string): string {
|
||||
if (!toolName || typeof toolName !== 'string') {
|
||||
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
|
||||
return '';
|
||||
}
|
||||
|
||||
return toolName.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
|
||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
.replace(/-+/g, '-') // Remove duplicate hyphens
|
||||
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
||||
}
|
||||
|
||||
export function findToolByIdentifier(tools: Tool[], identifier: string): Tool | undefined {
|
||||
if (!identifier || !Array.isArray(tools)) return undefined;
|
||||
|
||||
return tools.find(tool =>
|
||||
tool.name === identifier ||
|
||||
createToolSlug(tool.name) === identifier.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
export function isToolHosted(tool: Tool): boolean {
|
||||
return tool.projectUrl !== undefined &&
|
||||
tool.projectUrl !== null &&
|
||||
tool.projectUrl !== "" &&
|
||||
tool.projectUrl.trim() !== "";
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
// src/utils/videoUtils.ts - SIMPLIFIED - Basic utilities only
|
||||
import 'dotenv/config';
|
||||
|
||||
|
||||
export interface SimpleVideoMetadata {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function getVideoMimeType(url: string): string {
|
||||
let extension: string | undefined;
|
||||
try {
|
||||
const pathname = new URL(url).pathname;
|
||||
extension = pathname.split('.').pop()?.toLowerCase();
|
||||
} catch {
|
||||
extension = url.split('?')[0].split('.').pop()?.toLowerCase();
|
||||
}
|
||||
|
||||
const mimeTypes: Record<string, string> = {
|
||||
mp4: 'video/mp4',
|
||||
webm: 'video/webm',
|
||||
ogg: 'video/ogg',
|
||||
mov: 'video/quicktime',
|
||||
avi: 'video/x-msvideo',
|
||||
m4v: 'video/m4v',
|
||||
mkv: 'video/x-matroska',
|
||||
flv: 'video/x-flv'
|
||||
};
|
||||
|
||||
return (extension && mimeTypes[extension]) || 'video/mp4';
|
||||
}
|
||||
|
||||
export function formatDuration(seconds: number): string {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||
}
|
||||
|
||||
export function escapeHtml(unsafe: string): string {
|
||||
if (typeof unsafe !== 'string') return '';
|
||||
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
export function generateVideoHTML(
|
||||
src: string,
|
||||
options: {
|
||||
title?: string;
|
||||
controls?: boolean;
|
||||
autoplay?: boolean;
|
||||
muted?: boolean;
|
||||
loop?: boolean;
|
||||
preload?: 'none' | 'metadata' | 'auto';
|
||||
aspectRatio?: '16:9' | '4:3' | '1:1';
|
||||
showMetadata?: boolean;
|
||||
} = {}
|
||||
): string {
|
||||
const {
|
||||
title = 'Video',
|
||||
controls = true,
|
||||
autoplay = false,
|
||||
muted = false,
|
||||
loop = false,
|
||||
preload = 'metadata',
|
||||
aspectRatio = '16:9',
|
||||
showMetadata = true
|
||||
} = options;
|
||||
|
||||
const aspectClass = `aspect-${aspectRatio.replace(':', '-')}`;
|
||||
const videoAttributes = [
|
||||
controls ? 'controls' : '',
|
||||
autoplay ? 'autoplay' : '',
|
||||
muted ? 'muted' : '',
|
||||
loop ? 'loop' : '',
|
||||
`preload="${preload}"`
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const metadataHTML = showMetadata && title !== 'Video' ? `
|
||||
<div class="video-metadata">
|
||||
<div class="video-title">${escapeHtml(title)}</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
return `
|
||||
<div class="video-container ${aspectClass}">
|
||||
<video
|
||||
src="${escapeHtml(src)}"
|
||||
${videoAttributes}
|
||||
style="width: 100%; height: 100%;"
|
||||
data-video-title="${escapeHtml(title)}"
|
||||
>
|
||||
<p>Your browser does not support the video element.</p>
|
||||
</video>
|
||||
${metadataHTML}
|
||||
</div>
|
||||
`.trim();
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user