diff --git a/docs/blog/release-v0.7.4.md b/docs/blog/release-v0.7.4.md
index d9a57845..72cfe3ae 100644
--- a/docs/blog/release-v0.7.4.md
+++ b/docs/blog/release-v0.7.4.md
@@ -10,7 +10,6 @@ Today I'm releasing Crawl4AI v0.7.4βthe Intelligent Table Extraction & Perform
- **π LLMTableExtraction**: Revolutionary table extraction with intelligent chunking for massive tables
- **β‘ Enhanced Concurrency**: True concurrency improvements for fast-completing tasks in batch operations
-- **π§Ή Memory Management Refactor**: Streamlined memory utilities and better resource management
- **π§ Browser Manager Fixes**: Resolved race conditions in concurrent page creation
- **β¨οΈ Cross-Platform Browser Profiler**: Improved keyboard handling and quit mechanisms
- **π Advanced URL Processing**: Better handling of raw URLs and base tag link resolution
@@ -158,40 +157,6 @@ async with AsyncWebCrawler() as crawler:
- **Monitoring Systems**: Faster health checks and status page monitoring
- **Data Aggregation**: Improved performance for real-time data collection
-## π§Ή Memory Management Refactor: Cleaner Architecture
-
-**The Problem:** Memory utilities were scattered and difficult to maintain, with potential import conflicts and unclear organization.
-
-**My Solution:** I consolidated all memory-related utilities into the main `utils.py` module, creating a cleaner, more maintainable architecture.
-
-### Improved Memory Handling
-
-```python
-# All memory utilities now consolidated
-from crawl4ai.utils import get_true_memory_usage_percent, MemoryMonitor
-
-# Enhanced memory monitoring
-monitor = MemoryMonitor()
-monitor.start_monitoring()
-
-async with AsyncWebCrawler() as crawler:
- # Memory-efficient batch processing
- results = await crawler.arun_many(large_url_list)
-
- # Get accurate memory metrics
- memory_usage = get_true_memory_usage_percent()
- memory_report = monitor.get_report()
-
- print(f"Memory efficiency: {memory_report['efficiency']:.1f}%")
- print(f"Peak usage: {memory_report['peak_mb']:.1f} MB")
-```
-
-**Expected Real-World Impact:**
-- **Production Stability**: More reliable memory tracking and management
-- **Code Maintainability**: Cleaner architecture for easier debugging
-- **Import Clarity**: Resolved potential conflicts and import issues
-- **Developer Experience**: Simpler API for memory monitoring
-
## π§ Critical Stability Fixes
### Browser Manager Race Condition Resolution
diff --git a/docs/md_v2/assets/page_actions.css b/docs/md_v2/assets/page_actions.css
index 13fbffad..0d406445 100644
--- a/docs/md_v2/assets/page_actions.css
+++ b/docs/md_v2/assets/page_actions.css
@@ -201,18 +201,6 @@ ul>li.page-action-item::after{
}
/* Badge */
-.page-action-badge {
- display: inline-block;
- background: #f59e0b;
- color: #070708;
- padding: 0.125rem 0.5rem;
- border-radius: 12px;
- font-size: 0.65rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.05em;
-}
-
/* External link indicator */
.page-action-external::after {
content: 'β';
diff --git a/docs/md_v2/assets/page_actions.js b/docs/md_v2/assets/page_actions.js
index 92893730..bb143840 100644
--- a/docs/md_v2/assets/page_actions.js
+++ b/docs/md_v2/assets/page_actions.js
@@ -7,9 +7,12 @@ document.addEventListener('DOMContentLoaded', () => {
githubRepo: 'unclecode/crawl4ai',
githubBranch: 'main',
docsPath: 'docs/md_v2',
- excludePaths: ['/apps/c4a-script/', '/apps/llmtxt/', '/apps/crawl4ai-assistant/'], // Don't show on app pages
+ excludePaths: ['/apps/c4a-script/', '/apps/llmtxt/', '/apps/crawl4ai-assistant/', '/core/ask-ai/'], // Don't show on app pages
};
+ let cachedMarkdown = null;
+ let cachedMarkdownPath = null;
+
// Check if we should show the button on this page
function shouldShowButton() {
const currentPath = window.location.pathname;
@@ -19,6 +22,17 @@ document.addEventListener('DOMContentLoaded', () => {
return false;
}
+ // Don't show on 404 pages
+ if (document.title && document.title.toLowerCase().includes('404')) {
+ return false;
+ }
+
+ // Require mkdocs main content container
+ const mainContent = document.getElementById('terminal-mkdocs-main-content');
+ if (!mainContent) {
+ return false;
+ }
+
// Don't show on excluded paths (apps)
for (const excludePath of config.excludePaths) {
if (currentPath.includes(excludePath)) {
@@ -53,6 +67,56 @@ document.addEventListener('DOMContentLoaded', () => {
return `${path}.md`;
}
+ async function loadMarkdownContent() {
+ const mdPath = getCurrentMarkdownPath();
+
+ if (!mdPath) {
+ throw new Error('Invalid markdown path');
+ }
+
+ const rawUrl = getGithubRawUrl();
+ const response = await fetch(rawUrl);
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch markdown: ${response.status}`);
+ }
+
+ const markdown = await response.text();
+ cachedMarkdown = markdown;
+ cachedMarkdownPath = mdPath;
+ return markdown;
+ }
+
+ async function ensureMarkdownCached() {
+ const mdPath = getCurrentMarkdownPath();
+
+ if (!mdPath) {
+ return false;
+ }
+
+ if (cachedMarkdown && cachedMarkdownPath === mdPath) {
+ return true;
+ }
+
+ try {
+ await loadMarkdownContent();
+ return true;
+ } catch (error) {
+ console.warn('Page Actions: Markdown not available for this page.', error);
+ cachedMarkdown = null;
+ cachedMarkdownPath = null;
+ return false;
+ }
+ }
+
+ async function getMarkdownContent() {
+ const available = await ensureMarkdownCached();
+ if (!available) {
+ throw new Error('Markdown not available for this page.');
+ }
+ return cachedMarkdown;
+ }
+
// Get GitHub raw URL for current page
function getGithubRawUrl() {
const mdPath = getCurrentMarkdownPath();
@@ -112,13 +176,11 @@ document.addEventListener('DOMContentLoaded', () => {
-
+
- Ask AI about page
-
- Coming Soon
-
+ Open in ChatGPT
+ Ask questions about this page
@@ -180,19 +242,11 @@ document.addEventListener('DOMContentLoaded', () => {
// Copy markdown to clipboard
async function copyMarkdownToClipboard(link) {
- const rawUrl = getGithubRawUrl();
-
// Add loading state
link.classList.add('loading');
try {
- const response = await fetch(rawUrl);
-
- if (!response.ok) {
- throw new Error(`Failed to fetch markdown: ${response.status}`);
- }
-
- const markdown = await response.text();
+ const markdown = await getMarkdownContent();
// Copy to clipboard
await navigator.clipboard.writeText(markdown);
@@ -221,126 +275,153 @@ document.addEventListener('DOMContentLoaded', () => {
window.open(githubUrl, '_blank', 'noopener,noreferrer');
}
- // Initialize
- const { button, dropdown, overlay } = createPageActionsUI();
+ function getCurrentPageUrl() {
+ const { href } = window.location;
+ return href.split('#')[0];
+ }
- // Event listeners
- button.addEventListener('click', (e) => {
- e.stopPropagation();
- toggleDropdown(button, dropdown, overlay);
- });
+ function openChatGPT() {
+ const pageUrl = getCurrentPageUrl();
+ const prompt = encodeURIComponent(`Read ${pageUrl} so I can ask questions about it.`);
+ const chatUrl = `https://chatgpt.com/?hint=search&prompt=${prompt}`;
+ window.open(chatUrl, '_blank', 'noopener,noreferrer');
+ }
- overlay.addEventListener('click', () => {
- closeDropdown(button, dropdown, overlay);
- });
-
- // Copy markdown action
- document.getElementById('action-copy-markdown').addEventListener('click', async (e) => {
- e.preventDefault();
- e.stopPropagation();
- await copyMarkdownToClipboard(e.currentTarget);
- });
-
- // View markdown action
- document.getElementById('action-view-markdown').addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- viewMarkdown();
- closeDropdown(button, dropdown, overlay);
- });
-
- // Ask AI action (disabled for now)
- document.getElementById('action-ask-ai').addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- // Future: Integrate with Ask AI feature
- // For now, do nothing (disabled state)
- });
-
- // Close on ESC key
- document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape' && dropdown.classList.contains('active')) {
- closeDropdown(button, dropdown, overlay);
+ (async () => {
+ if (!shouldShowButton()) {
+ return;
}
- });
- // Close when clicking outside
- document.addEventListener('click', (e) => {
- if (!dropdown.contains(e.target) && !button.contains(e.target)) {
- closeDropdown(button, dropdown, overlay);
+ const markdownAvailable = await ensureMarkdownCached();
+ if (!markdownAvailable) {
+ return;
}
- });
- // Prevent dropdown from closing when clicking inside
- dropdown.addEventListener('click', (e) => {
- // Only stop propagation if not clicking on a link
- if (!e.target.closest('.page-action-link')) {
+ const ui = createPageActionsUI();
+ if (!ui) {
+ return;
+ }
+
+ const { button, dropdown, overlay } = ui;
+
+ // Event listeners
+ button.addEventListener('click', (e) => {
e.stopPropagation();
- }
- });
-
- // Close dropdown on link click (except for copy which handles itself)
- dropdown.querySelectorAll('.page-action-link:not(#action-copy-markdown)').forEach(link => {
- link.addEventListener('click', () => {
- if (!link.classList.contains('disabled')) {
- setTimeout(() => {
- closeDropdown(button, dropdown, overlay);
- }, 100);
- }
+ toggleDropdown(button, dropdown, overlay);
});
- });
- // Handle window resize
- let resizeTimer;
- window.addEventListener('resize', () => {
- clearTimeout(resizeTimer);
- resizeTimer = setTimeout(() => {
- // Close dropdown on resize to prevent positioning issues
- if (dropdown.classList.contains('active')) {
+ overlay.addEventListener('click', () => {
+ closeDropdown(button, dropdown, overlay);
+ });
+
+ // Copy markdown action
+ document.getElementById('action-copy-markdown').addEventListener('click', async (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ await copyMarkdownToClipboard(e.currentTarget);
+ });
+
+ // View markdown action
+ document.getElementById('action-view-markdown').addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ viewMarkdown();
+ closeDropdown(button, dropdown, overlay);
+ });
+
+ // Open in ChatGPT action
+ document.getElementById('action-open-chatgpt').addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ openChatGPT();
+ closeDropdown(button, dropdown, overlay);
+ });
+
+ // Close on ESC key
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && dropdown.classList.contains('active')) {
closeDropdown(button, dropdown, overlay);
}
- }, 250);
- });
+ });
- // Accessibility: Focus management
- button.addEventListener('keydown', (e) => {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- toggleDropdown(button, dropdown, overlay);
+ // Close when clicking outside
+ document.addEventListener('click', (e) => {
+ if (!dropdown.contains(e.target) && !button.contains(e.target)) {
+ closeDropdown(button, dropdown, overlay);
+ }
+ });
- // Focus first menu item when opening
- if (dropdown.classList.contains('active')) {
- const firstLink = dropdown.querySelector('.page-action-link:not(.disabled)');
- if (firstLink) {
- setTimeout(() => firstLink.focus(), 100);
+ // Prevent dropdown from closing when clicking inside
+ dropdown.addEventListener('click', (e) => {
+ // Only stop propagation if not clicking on a link
+ if (!e.target.closest('.page-action-link')) {
+ e.stopPropagation();
+ }
+ });
+
+ // Close dropdown on link click (except for copy which handles itself)
+ dropdown.querySelectorAll('.page-action-link:not(#action-copy-markdown)').forEach(link => {
+ link.addEventListener('click', () => {
+ if (!link.classList.contains('disabled')) {
+ setTimeout(() => {
+ closeDropdown(button, dropdown, overlay);
+ }, 100);
+ }
+ });
+ });
+
+ // Handle window resize
+ let resizeTimer;
+ window.addEventListener('resize', () => {
+ clearTimeout(resizeTimer);
+ resizeTimer = setTimeout(() => {
+ // Close dropdown on resize to prevent positioning issues
+ if (dropdown.classList.contains('active')) {
+ closeDropdown(button, dropdown, overlay);
+ }
+ }, 250);
+ });
+
+ // Accessibility: Focus management
+ button.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ toggleDropdown(button, dropdown, overlay);
+
+ // Focus first menu item when opening
+ if (dropdown.classList.contains('active')) {
+ const firstLink = dropdown.querySelector('.page-action-link:not(.disabled)');
+ if (firstLink) {
+ setTimeout(() => firstLink.focus(), 100);
+ }
}
}
- }
- });
+ });
- // Arrow key navigation within menu
- dropdown.addEventListener('keydown', (e) => {
- if (!dropdown.classList.contains('active')) return;
+ // Arrow key navigation within menu
+ dropdown.addEventListener('keydown', (e) => {
+ if (!dropdown.classList.contains('active')) return;
- const links = Array.from(dropdown.querySelectorAll('.page-action-link:not(.disabled)'));
- const currentIndex = links.indexOf(document.activeElement);
+ const links = Array.from(dropdown.querySelectorAll('.page-action-link:not(.disabled)'));
+ const currentIndex = links.indexOf(document.activeElement);
- if (e.key === 'ArrowDown') {
- e.preventDefault();
- const nextIndex = (currentIndex + 1) % links.length;
- links[nextIndex].focus();
- } else if (e.key === 'ArrowUp') {
- e.preventDefault();
- const prevIndex = (currentIndex - 1 + links.length) % links.length;
- links[prevIndex].focus();
- } else if (e.key === 'Home') {
- e.preventDefault();
- links[0].focus();
- } else if (e.key === 'End') {
- e.preventDefault();
- links[links.length - 1].focus();
- }
- });
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ const nextIndex = (currentIndex + 1) % links.length;
+ links[nextIndex].focus();
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ const prevIndex = (currentIndex - 1 + links.length) % links.length;
+ links[prevIndex].focus();
+ } else if (e.key === 'Home') {
+ e.preventDefault();
+ links[0].focus();
+ } else if (e.key === 'End') {
+ e.preventDefault();
+ links[links.length - 1].focus();
+ }
+ });
- console.log('Page Actions initialized for:', getCurrentMarkdownPath());
+ console.log('Page Actions initialized for:', getCurrentMarkdownPath());
+ })();
});
\ No newline at end of file
diff --git a/docs/md_v2/marketplace/admin/admin.css b/docs/md_v2/marketplace/admin/admin.css
index 7296a801..66b975a9 100644
--- a/docs/md_v2/marketplace/admin/admin.css
+++ b/docs/md_v2/marketplace/admin/admin.css
@@ -431,6 +431,16 @@
gap: 0.5rem;
}
+.table-logo {
+ width: 48px;
+ height: 48px;
+ object-fit: contain;
+ border-radius: 6px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ padding: 4px;
+}
+
.btn-edit, .btn-delete, .btn-duplicate {
padding: 0.25rem 0.5rem;
background: transparent;
@@ -585,6 +595,105 @@
cursor: pointer;
}
+.sponsor-form {
+ grid-template-columns: 200px repeat(2, minmax(220px, 1fr));
+ align-items: flex-start;
+ grid-auto-flow: dense;
+}
+
+.sponsor-logo-group {
+ grid-row: span 3;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.span-two {
+ grid-column: span 2;
+}
+
+.logo-upload {
+ position: relative;
+ width: 180px;
+}
+
+.image-preview {
+ width: 180px;
+ height: 180px;
+ border: 1px dashed var(--border-color);
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-tertiary);
+ overflow: hidden;
+}
+
+.image-preview.empty {
+ color: var(--text-secondary);
+ font-size: 0.75rem;
+ text-align: center;
+ padding: 0.75rem;
+}
+
+.image-preview img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+}
+
+.upload-btn {
+ position: absolute;
+ left: 50%;
+ bottom: 12px;
+ transform: translateX(-50%);
+ padding: 0.35rem 1rem;
+ background: linear-gradient(135deg, var(--primary-cyan), var(--primary-teal));
+ border: none;
+ border-radius: 999px;
+ color: var(--bg-dark);
+ font-size: 0.75rem;
+ font-weight: 600;
+ cursor: pointer;
+ box-shadow: 0 6px 18px rgba(80, 255, 255, 0.25);
+}
+
+.upload-btn:hover {
+ box-shadow: 0 8px 22px rgba(80, 255, 255, 0.35);
+}
+
+.logo-upload input[type="file"] {
+ display: none;
+}
+
+.upload-hint {
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ margin: 0;
+}
+
+@media (max-width: 960px) {
+ .sponsor-form {
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+ }
+
+ .sponsor-logo-group {
+ grid-column: 1 / -1;
+ grid-row: auto;
+ flex-direction: row;
+ align-items: center;
+ gap: 1.5rem;
+ }
+
+ .logo-upload {
+ width: 160px;
+ }
+
+ .span-two {
+ grid-column: 1 / -1;
+ }
+}
+
/* Rich Text Editor */
.editor-toolbar {
display: flex;
diff --git a/docs/md_v2/marketplace/admin/admin.js b/docs/md_v2/marketplace/admin/admin.js
index 258858da..7d31a826 100644
--- a/docs/md_v2/marketplace/admin/admin.js
+++ b/docs/md_v2/marketplace/admin/admin.js
@@ -1,5 +1,49 @@
// Admin Dashboard - Smart & Powerful
-const API_BASE = '/api';
+const { API_BASE, API_ORIGIN } = (() => {
+ const cleanOrigin = (value) => value ? value.replace(/\/$/, '') : '';
+ const params = new URLSearchParams(window.location.search);
+ const overrideParam = cleanOrigin(params.get('api_origin'));
+
+ let storedOverride = '';
+ try {
+ storedOverride = cleanOrigin(localStorage.getItem('marketplace_api_origin'));
+ } catch (error) {
+ storedOverride = '';
+ }
+
+ let origin = overrideParam || storedOverride;
+
+ if (overrideParam && overrideParam !== storedOverride) {
+ try {
+ localStorage.setItem('marketplace_api_origin', overrideParam);
+ } catch (error) {
+ // ignore storage errors (private mode, etc.)
+ }
+ }
+
+ const { protocol, hostname, port } = window.location;
+ const isLocalHost = ['localhost', '127.0.0.1', '0.0.0.0'].includes(hostname);
+
+ if (!origin && isLocalHost && port !== '8100') {
+ origin = `${protocol}//127.0.0.1:8100`;
+ }
+
+ if (origin) {
+ const normalized = cleanOrigin(origin);
+ return { API_BASE: `${normalized}/api`, API_ORIGIN: normalized };
+ }
+
+ return { API_BASE: '/api', API_ORIGIN: '' };
+})();
+
+const resolveAssetUrl = (path) => {
+ if (!path) return '';
+ if (/^https?:\/\//i.test(path)) return path;
+ if (path.startsWith('/') && API_ORIGIN) {
+ return `${API_ORIGIN}${path}`;
+ }
+ return path;
+};
class AdminDashboard {
constructor() {
@@ -144,13 +188,19 @@ class AdminDashboard {
}
async apiCall(endpoint, options = {}) {
+ const isFormData = options.body instanceof FormData;
+ const headers = {
+ 'Authorization': `Bearer ${this.token}`,
+ ...options.headers
+ };
+
+ if (!isFormData && !headers['Content-Type']) {
+ headers['Content-Type'] = 'application/json';
+ }
+
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
- headers: {
- 'Authorization': `Bearer ${this.token}`,
- 'Content-Type': 'application/json',
- ...options.headers
- }
+ headers
});
if (response.status === 401) {
@@ -163,7 +213,9 @@ class AdminDashboard {
}
async loadStats() {
- const stats = await this.apiCall('/admin/stats');
+ const stats = await this.apiCall(`/admin/stats?_=${Date.now()}`, {
+ cache: 'no-store'
+ });
document.getElementById('stat-apps').textContent = stats.apps.total;
document.getElementById('stat-featured').textContent = stats.apps.featured;
@@ -174,22 +226,32 @@ class AdminDashboard {
}
async loadApps() {
- this.data.apps = await this.apiCall('/apps?limit=100');
+ this.data.apps = await this.apiCall(`/apps?limit=100&_=${Date.now()}`, {
+ cache: 'no-store'
+ });
this.renderAppsTable(this.data.apps);
}
async loadArticles() {
- this.data.articles = await this.apiCall('/articles?limit=100');
+ this.data.articles = await this.apiCall(`/articles?limit=100&_=${Date.now()}`, {
+ cache: 'no-store'
+ });
this.renderArticlesTable(this.data.articles);
}
async loadCategories() {
- this.data.categories = await this.apiCall('/categories');
+ const cacheBuster = Date.now();
+ this.data.categories = await this.apiCall(`/categories?_=${cacheBuster}`, {
+ cache: 'no-store'
+ });
this.renderCategoriesTable(this.data.categories);
}
async loadSponsors() {
- this.data.sponsors = await this.apiCall('/sponsors');
+ const cacheBuster = Date.now();
+ this.data.sponsors = await this.apiCall(`/sponsors?limit=100&_=${cacheBuster}`, {
+ cache: 'no-store'
+ });
this.renderSponsorsTable(this.data.sponsors);
}
@@ -314,6 +376,7 @@ class AdminDashboard {
| ID |
+ Logo |
Company |
Tier |
Start |
@@ -326,6 +389,7 @@ class AdminDashboard {
${sponsors.map(sponsor => `
| ${sponsor.id} |
+ ${sponsor.logo_url ? ` ` : '-'} |
${sponsor.company_name} |
${sponsor.tier} |
${new Date(sponsor.start_date).toLocaleDateString()} |
@@ -389,6 +453,10 @@ class AdminDashboard {
modal.classList.remove('hidden');
modal.dataset.type = type;
+
+ if (type === 'sponsors') {
+ this.setupLogoUploadHandlers();
+ }
}
getAppForm(app) {
@@ -524,9 +592,22 @@ class AdminDashboard {
}
getSponsorForm(sponsor) {
+ const existingFile = sponsor?.logo_url ? sponsor.logo_url.split('/').pop().split('?')[0] : '';
return `
-