Compare commits

...

7 Commits

Author SHA1 Message Date
AHMET YILMAZ
05ec0535cd #1564: fix issue with _sig when using proxyConfig 2025-10-28 14:07:44 +08:00
AHMET YILMAZ
83aeb565ee refactor(crawler_pool): enhance signature generation with adapter support and improve error handling 2025-10-28 14:02:43 +08:00
ntohidi
c0f1865287 feat(api): update marketplace version and build date in root endpoint response 2025-10-26 11:35:39 +01:00
ntohidi
46ef1116c4 fix(app-detail): enhance tab functionality, hide documentation and support tabs in marketplace 2025-10-26 11:21:29 +01:00
Nasrin
4df83893ac Merge pull request #1560 from unclecode/fix/marketplace
Fix/marketplace
2025-10-23 22:17:06 +08:00
ntohidi
13e116610d fix(marketplace): improve app detail page content rendering and UX
Fixed multiple issues with app detail page content display and formatting
2025-10-23 16:12:30 +02:00
ntohidi
97c92c4f62 fix(marketplace): replace hardcoded app detail content with database-driven fields.
The app detail page was displaying hardcoded/templated content instead of
using actual data from the database. This prevented admins from controlling
the content shown in Overview, Integration, and Documentation tabs.
2025-10-21 15:39:04 +02:00
6 changed files with 201 additions and 211 deletions

View File

@@ -2,8 +2,8 @@
import asyncio, json, hashlib, time, psutil import asyncio, json, hashlib, time, psutil
from contextlib import suppress from contextlib import suppress
from typing import Dict from typing import Dict
from crawl4ai import AsyncWebCrawler, BrowserConfig from crawl4ai import AsyncWebCrawler, BrowserConfig, BrowserAdapter
from typing import Dict from typing import Dict ,Optional
from utils import load_config from utils import load_config
CONFIG = load_config() CONFIG = load_config()
@@ -15,11 +15,22 @@ LOCK = asyncio.Lock()
MEM_LIMIT = CONFIG.get("crawler", {}).get("memory_threshold_percent", 95.0) # % RAM refuse new browsers above this MEM_LIMIT = CONFIG.get("crawler", {}).get("memory_threshold_percent", 95.0) # % RAM refuse new browsers above this
IDLE_TTL = CONFIG.get("crawler", {}).get("pool", {}).get("idle_ttl_sec", 1800) # close if unused for 30min IDLE_TTL = CONFIG.get("crawler", {}).get("pool", {}).get("idle_ttl_sec", 1800) # close if unused for 30min
def _sig(cfg: BrowserConfig) -> str:
payload = json.dumps(cfg.to_dict(), sort_keys=True, separators=(",",":")) def _sig(cfg: BrowserConfig, adapter: Optional[BrowserAdapter] = None) -> str:
try:
config_payload = json.dumps(cfg.to_dict(), sort_keys=True, separators=(",", ":"))
except (TypeError, ValueError):
# Fallback to string representation if JSON serialization fails
config_payload = str(cfg.to_dict())
adapter_name = adapter.__class__.__name__ if adapter else "PlaywrightAdapter"
payload = f"{config_payload}:{adapter_name}"
return hashlib.sha1(payload.encode()).hexdigest() return hashlib.sha1(payload.encode()).hexdigest()
async def get_crawler(cfg: BrowserConfig) -> AsyncWebCrawler:
async def get_crawler(
cfg: BrowserConfig, adapter: Optional[BrowserAdapter] = None
) -> AsyncWebCrawler:
sig = None
try: try:
sig = _sig(cfg) sig = _sig(cfg)
async with LOCK: async with LOCK:
@@ -37,6 +48,7 @@ async def get_crawler(cfg: BrowserConfig) -> AsyncWebCrawler:
except Exception as e: except Exception as e:
raise RuntimeError(f"Failed to start browser: {e}") raise RuntimeError(f"Failed to start browser: {e}")
finally: finally:
if sig:
if sig in POOL: if sig in POOL:
LAST_USED[sig] = time.time() LAST_USED[sig] = time.time()
else: else:

View File

@@ -529,8 +529,19 @@ class AdminDashboard {
</label> </label>
</div> </div>
<div class="form-group full-width"> <div class="form-group full-width">
<label>Integration Guide</label> <label>Long Description (Markdown - Overview tab)</label>
<textarea id="form-integration" rows="10">${app?.integration_guide || ''}</textarea> <textarea id="form-long-description" rows="10" placeholder="Enter detailed description with markdown formatting...">${app?.long_description || ''}</textarea>
<small>Markdown support: **bold**, *italic*, [links](url), # headers, code blocks, lists</small>
</div>
<div class="form-group full-width">
<label>Integration Guide (Markdown - Integration tab)</label>
<textarea id="form-integration" rows="20" placeholder="Enter integration guide with installation, examples, and code snippets using markdown...">${app?.integration_guide || ''}</textarea>
<small>Single markdown field with installation, examples, and complete guide. Code blocks get auto copy buttons.</small>
</div>
<div class="form-group full-width">
<label>Documentation (Markdown - Documentation tab)</label>
<textarea id="form-documentation" rows="20" placeholder="Enter documentation with API reference, examples, and best practices using markdown...">${app?.documentation || ''}</textarea>
<small>Full documentation with API reference, examples, best practices, etc.</small>
</div> </div>
</div> </div>
`; `;
@@ -712,7 +723,9 @@ class AdminDashboard {
data.contact_email = document.getElementById('form-email').value; data.contact_email = document.getElementById('form-email').value;
data.featured = document.getElementById('form-featured').checked ? 1 : 0; data.featured = document.getElementById('form-featured').checked ? 1 : 0;
data.sponsored = document.getElementById('form-sponsored').checked ? 1 : 0; data.sponsored = document.getElementById('form-sponsored').checked ? 1 : 0;
data.long_description = document.getElementById('form-long-description').value;
data.integration_guide = document.getElementById('form-integration').value; data.integration_guide = document.getElementById('form-integration').value;
data.documentation = document.getElementById('form-documentation').value;
} else if (type === 'articles') { } else if (type === 'articles') {
data.title = document.getElementById('form-title').value; data.title = document.getElementById('form-title').value;
data.slug = this.generateSlug(data.title); data.slug = this.generateSlug(data.title);

View File

@@ -278,12 +278,12 @@
} }
.tab-content { .tab-content {
display: none; display: none !important;
padding: 2rem; padding: 2rem;
} }
.tab-content.active { .tab-content.active {
display: block; display: block !important;
} }
/* Overview Layout */ /* Overview Layout */
@@ -510,6 +510,31 @@
line-height: 1.5; line-height: 1.5;
} }
/* Markdown rendered code blocks */
.integration-content pre,
.docs-content pre {
background: var(--bg-dark);
border: 1px solid var(--border-color);
margin: 1rem 0;
padding: 1rem;
padding-top: 2.5rem; /* Space for copy button */
overflow-x: auto;
position: relative;
max-height: none; /* Remove any height restrictions */
height: auto; /* Allow content to expand */
}
.integration-content pre code,
.docs-content pre code {
background: transparent;
padding: 0;
color: var(--text-secondary);
font-size: 0.875rem;
line-height: 1.5;
white-space: pre; /* Preserve whitespace and line breaks */
display: block;
}
/* Feature Grid */ /* Feature Grid */
.feature-grid { .feature-grid {
display: grid; display: grid;

View File

@@ -73,27 +73,14 @@
<div class="tabs"> <div class="tabs">
<button class="tab-btn active" data-tab="overview">Overview</button> <button class="tab-btn active" data-tab="overview">Overview</button>
<button class="tab-btn" data-tab="integration">Integration</button> <button class="tab-btn" data-tab="integration">Integration</button>
<button class="tab-btn" data-tab="docs">Documentation</button> <!-- <button class="tab-btn" data-tab="docs">Documentation</button>
<button class="tab-btn" data-tab="support">Support</button> <button class="tab-btn" data-tab="support">Support</button> -->
</div> </div>
<section id="overview-tab" class="tab-content active"> <section id="overview-tab" class="tab-content active">
<div class="overview-columns"> <div class="overview-columns">
<div class="overview-main"> <div class="overview-main">
<h2>Overview</h2>
<div id="app-overview">Overview content goes here.</div> <div id="app-overview">Overview content goes here.</div>
<h3>Key Features</h3>
<ul id="app-features" class="features-list">
<li>Feature 1</li>
<li>Feature 2</li>
<li>Feature 3</li>
</ul>
<h3>Use Cases</h3>
<div id="app-use-cases" class="use-cases">
<p>Describe how this app can help your workflow.</p>
</div>
</div> </div>
<aside class="sidebar"> <aside class="sidebar">
@@ -142,37 +129,16 @@
</section> </section>
<section id="integration-tab" class="tab-content"> <section id="integration-tab" class="tab-content">
<div class="integration-content"> <div class="integration-content" id="app-integration">
<h2>Integration Guide</h2>
<h3>Installation</h3>
<div class="code-block">
<pre><code id="install-code"># Installation instructions will appear here</code></pre>
</div>
<h3>Basic Usage</h3>
<div class="code-block">
<pre><code id="usage-code"># Usage example will appear here</code></pre>
</div>
<h3>Complete Integration Example</h3>
<div class="code-block">
<button class="copy-btn" id="copy-integration">Copy</button>
<pre><code id="integration-code"># Complete integration guide will appear here</code></pre>
</div>
</div> </div>
</section> </section>
<section id="docs-tab" class="tab-content"> <!-- <section id="docs-tab" class="tab-content">
<div class="docs-content"> <div class="docs-content" id="app-docs">
<h2>Documentation</h2>
<div id="app-docs" class="doc-sections">
<p>Documentation coming soon.</p>
</div> </div>
</div> </section> -->
</section>
<section id="support-tab" class="tab-content"> <!-- <section id="support-tab" class="tab-content">
<div class="docs-content"> <div class="docs-content">
<h2>Support</h2> <h2>Support</h2>
<div class="support-grid"> <div class="support-grid">
@@ -190,7 +156,7 @@
</div> </div>
</div> </div>
</div> </div>
</section> </section> -->
</div> </div>
</main> </main>

View File

@@ -112,7 +112,7 @@ class AppDetailPage {
} }
// Contact // Contact
document.getElementById('app-contact').textContent = this.appData.contact_email || 'Not available'; document.getElementById('app-contact') && (document.getElementById('app-contact').textContent = this.appData.contact_email || 'Not available');
// Sidebar info // Sidebar info
document.getElementById('sidebar-downloads').textContent = this.formatNumber(this.appData.downloads || 0); document.getElementById('sidebar-downloads').textContent = this.formatNumber(this.appData.downloads || 0);
@@ -123,144 +123,132 @@ class AppDetailPage {
document.getElementById('sidebar-pricing').textContent = this.appData.pricing || 'Free'; document.getElementById('sidebar-pricing').textContent = this.appData.pricing || 'Free';
document.getElementById('sidebar-contact').textContent = this.appData.contact_email || 'contact@example.com'; document.getElementById('sidebar-contact').textContent = this.appData.contact_email || 'contact@example.com';
// Integration guide // Render tab contents from database fields
this.renderIntegrationGuide(); this.renderTabContents();
} }
renderIntegrationGuide() { renderTabContents() {
// Installation code // Overview tab - use long_description from database
const installCode = document.getElementById('install-code'); const overviewDiv = document.getElementById('app-overview');
if (installCode) { if (overviewDiv) {
if (this.appData.type === 'Open Source' && this.appData.github_url) { if (this.appData.long_description) {
installCode.textContent = `# Clone from GitHub overviewDiv.innerHTML = this.renderMarkdown(this.appData.long_description);
git clone ${this.appData.github_url} } else {
overviewDiv.innerHTML = `<p>${this.appData.description || 'No overview available.'}</p>`;
# Install dependencies
pip install -r requirements.txt`;
} else if (this.appData.name.toLowerCase().includes('api')) {
installCode.textContent = `# Install via pip
pip install ${this.appData.slug}
# Or install from source
pip install git+${this.appData.github_url || 'https://github.com/example/repo'}`;
} }
} }
// Usage code - customize based on category // Integration tab - use integration_guide field from database
const usageCode = document.getElementById('usage-code'); const integrationDiv = document.getElementById('app-integration');
if (usageCode) { if (integrationDiv) {
if (this.appData.category === 'Browser Automation') { if (this.appData.integration_guide) {
usageCode.textContent = `from crawl4ai import AsyncWebCrawler integrationDiv.innerHTML = this.renderMarkdown(this.appData.integration_guide);
from ${this.appData.slug.replace(/-/g, '_')} import ${this.appData.name.replace(/\s+/g, '')} // Add copy buttons to all code blocks
this.addCopyButtonsToCodeBlocks(integrationDiv);
async def main(): } else {
# Initialize ${this.appData.name} integrationDiv.innerHTML = '<p>Integration guide not yet available. Please check the official website for details.</p>';
automation = ${this.appData.name.replace(/\s+/g, '')}()
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(
url="https://example.com",
browser_config=automation.config,
wait_for="css:body"
)
print(result.markdown)`;
} else if (this.appData.category === 'Proxy Services') {
usageCode.textContent = `from crawl4ai import AsyncWebCrawler
import ${this.appData.slug.replace(/-/g, '_')}
# Configure proxy
proxy_config = {
"server": "${this.appData.website_url || 'https://proxy.example.com'}",
"username": "your_username",
"password": "your_password"
}
async with AsyncWebCrawler(proxy=proxy_config) as crawler:
result = await crawler.arun(
url="https://example.com",
bypass_cache=True
)
print(result.status_code)`;
} else if (this.appData.category === 'LLM Integration') {
usageCode.textContent = `from crawl4ai import AsyncWebCrawler
from crawl4ai.extraction_strategy import LLMExtractionStrategy
# Configure LLM extraction
strategy = LLMExtractionStrategy(
provider="${this.appData.name.toLowerCase().includes('gpt') ? 'openai' : 'anthropic'}",
api_key="your-api-key",
model="${this.appData.name.toLowerCase().includes('gpt') ? 'gpt-4' : 'claude-3'}",
instruction="Extract structured data"
)
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(
url="https://example.com",
extraction_strategy=strategy
)
print(result.extracted_content)`;
} }
} }
// Integration example // Documentation tab - use documentation field from database
const integrationCode = document.getElementById('integration-code'); const docsDiv = document.getElementById('app-docs');
if (integrationCode) { if (docsDiv) {
integrationCode.textContent = this.appData.integration_guide || if (this.appData.documentation) {
`# Complete ${this.appData.name} Integration Example docsDiv.innerHTML = this.renderMarkdown(this.appData.documentation);
// Add copy buttons to all code blocks
from crawl4ai import AsyncWebCrawler this.addCopyButtonsToCodeBlocks(docsDiv);
from crawl4ai.extraction_strategy import JsonCssExtractionStrategy } else {
import json docsDiv.innerHTML = '<p>Documentation coming soon.</p>';
}
async def crawl_with_${this.appData.slug.replace(/-/g, '_')}(): }
"""
Complete example showing how to use ${this.appData.name}
with Crawl4AI for production web scraping
"""
# Define extraction schema
schema = {
"name": "ProductList",
"baseSelector": "div.product",
"fields": [
{"name": "title", "selector": "h2", "type": "text"},
{"name": "price", "selector": ".price", "type": "text"},
{"name": "image", "selector": "img", "type": "attribute", "attribute": "src"},
{"name": "link", "selector": "a", "type": "attribute", "attribute": "href"}
]
} }
# Initialize crawler with ${this.appData.name} addCopyButtonsToCodeBlocks(container) {
async with AsyncWebCrawler( // Find all code blocks and add copy buttons
browser_type="chromium", const codeBlocks = container.querySelectorAll('pre code');
headless=True, codeBlocks.forEach(codeBlock => {
verbose=True const pre = codeBlock.parentElement;
) as crawler:
# Crawl with extraction // Skip if already has a copy button
result = await crawler.arun( if (pre.querySelector('.copy-btn')) return;
url="https://example.com/products",
extraction_strategy=JsonCssExtractionStrategy(schema),
cache_mode="bypass",
wait_for="css:.product",
screenshot=True
)
# Process results // Create copy button
if result.success: const copyBtn = document.createElement('button');
products = json.loads(result.extracted_content) copyBtn.className = 'copy-btn';
print(f"Found {len(products)} products") copyBtn.textContent = 'Copy';
copyBtn.onclick = () => {
navigator.clipboard.writeText(codeBlock.textContent).then(() => {
copyBtn.textContent = '✓ Copied!';
setTimeout(() => {
copyBtn.textContent = 'Copy';
}, 2000);
});
};
for product in products[:5]: // Add button to pre element
print(f"- {product['title']}: {product['price']}") pre.style.position = 'relative';
pre.insertBefore(copyBtn, codeBlock);
return products });
# Run the crawler
if __name__ == "__main__":
import asyncio
asyncio.run(crawl_with_${this.appData.slug.replace(/-/g, '_')}())`;
} }
renderMarkdown(text) {
if (!text) return '';
// Store code blocks temporarily to protect them from processing
const codeBlocks = [];
let processed = text.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
const placeholder = `___CODE_BLOCK_${codeBlocks.length}___`;
codeBlocks.push(`<pre><code class="language-${lang || ''}">${this.escapeHtml(code)}</code></pre>`);
return placeholder;
});
// Store inline code temporarily
const inlineCodes = [];
processed = processed.replace(/`([^`]+)`/g, (match, code) => {
const placeholder = `___INLINE_CODE_${inlineCodes.length}___`;
inlineCodes.push(`<code>${this.escapeHtml(code)}</code>`);
return placeholder;
});
// Now process the rest of the markdown
processed = processed
// Headers
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
// Bold
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
// Italic
.replace(/\*(.*?)\*/g, '<em>$1</em>')
// Links
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
// Line breaks
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>')
// Lists
.replace(/^\* (.*)$/gim, '<li>$1</li>')
.replace(/^- (.*)$/gim, '<li>$1</li>')
// Wrap in paragraphs
.replace(/^(?!<[h|p|pre|ul|ol|li])/gim, '<p>')
.replace(/(?<![>])$/gim, '</p>');
// Restore inline code
inlineCodes.forEach((code, i) => {
processed = processed.replace(`___INLINE_CODE_${i}___`, code);
});
// Restore code blocks
codeBlocks.forEach((block, i) => {
processed = processed.replace(`___CODE_BLOCK_${i}___`, block);
});
return processed;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
} }
formatNumber(num) { formatNumber(num) {
@@ -275,45 +263,27 @@ if __name__ == "__main__":
setupEventListeners() { setupEventListeners() {
// Tab switching // Tab switching
const tabs = document.querySelectorAll('.tab-btn'); const tabs = document.querySelectorAll('.tab-btn');
tabs.forEach(tab => { tabs.forEach(tab => {
tab.addEventListener('click', () => { tab.addEventListener('click', () => {
// Update active tab // Update active tab button
tabs.forEach(t => t.classList.remove('active')); tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active'); tab.classList.add('active');
// Show corresponding content // Show corresponding content
const tabName = tab.dataset.tab; const tabName = tab.dataset.tab;
document.querySelectorAll('.tab-content').forEach(content => {
// Hide all tab contents
const allTabContents = document.querySelectorAll('.tab-content');
allTabContents.forEach(content => {
content.classList.remove('active'); content.classList.remove('active');
}); });
document.getElementById(`${tabName}-tab`).classList.add('active');
});
});
// Copy integration code // Show the selected tab content
document.getElementById('copy-integration').addEventListener('click', () => { const targetTab = document.getElementById(`${tabName}-tab`);
const code = document.getElementById('integration-code').textContent; if (targetTab) {
navigator.clipboard.writeText(code).then(() => { targetTab.classList.add('active');
const btn = document.getElementById('copy-integration'); }
const originalText = btn.innerHTML;
btn.innerHTML = '<span>✓</span> Copied!';
setTimeout(() => {
btn.innerHTML = originalText;
}, 2000);
});
});
// Copy code buttons
document.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const codeBlock = e.target.closest('.code-block');
const code = codeBlock.querySelector('code').textContent;
navigator.clipboard.writeText(code).then(() => {
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = 'Copy';
}, 2000);
});
}); });
}); });
} }

View File

@@ -471,13 +471,17 @@ async def delete_sponsor(sponsor_id: int):
app.include_router(router) app.include_router(router)
# Version info
VERSION = "1.1.0"
BUILD_DATE = "2025-10-26"
@app.get("/") @app.get("/")
async def root(): async def root():
"""API info""" """API info"""
return { return {
"name": "Crawl4AI Marketplace API", "name": "Crawl4AI Marketplace API",
"version": "1.0.0", "version": VERSION,
"build_date": BUILD_DATE,
"endpoints": [ "endpoints": [
"/marketplace/api/apps", "/marketplace/api/apps",
"/marketplace/api/articles", "/marketplace/api/articles",