mirror of
https://github.com/thecyberlearn/chat-backend.git
synced 2026-08-18 07:52:53 +00:00
Add comprehensive AI chat integration and URL management features
Features Added: - AI_CHAT_INTEGRATION.md: Complete reference for Django → Vue.js AI chat integration - URL update functionality with validation and accessibility testing - Business edit interface with re-crawl capabilities - Enhanced export functionality documentation - AI-ready data export specifications (JSONL, OpenAI training format, knowledge base) - URL_UPDATE_GUIDE.md: Comprehensive URL management documentation - Edit business template with real-time validation AI Integration Planning: - Complete data flow architecture (Django scraping → AI processing → Vue.js chat) - Export format specifications for AI training and knowledge base sync - Implementation roadmap for seamless backend-frontend integration - Code examples and API specifications for both systems URL Management Features: - Smart URL validation with accessibility testing - Business information editing with URL update capabilities - Optional re-crawling after URL changes - API endpoints for programmatic URL updates - Enhanced UI with edit buttons and quick actions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a0fce0b484
commit
6d11b80c03
733
AI_CHAT_INTEGRATION.md
Normal file
733
AI_CHAT_INTEGRATION.md
Normal file
@ -0,0 +1,733 @@
|
|||||||
|
# AI Chat Integration & Data Export Reference
|
||||||
|
|
||||||
|
## 📋 **Project Overview**
|
||||||
|
|
||||||
|
This document outlines the complete integration between two key systems:
|
||||||
|
|
||||||
|
### **Backend System (Django)**
|
||||||
|
- **Location:** `/home/amit/projects/chat-backend`
|
||||||
|
- **Purpose:** Web scraping, data collection, business management
|
||||||
|
- **Technology:** Django + Beautiful Soup + Playwright + Firecrawl
|
||||||
|
- **Current Features:**
|
||||||
|
- Multi-strategy web scraping (Firecrawl, Playwright, Beautiful Soup)
|
||||||
|
- Business and CrawledPage models
|
||||||
|
- Export functionality (JSON, CSV, TXT)
|
||||||
|
- Anti-detection features (proxy rotation, user-agent rotation)
|
||||||
|
- URL validation and update capabilities
|
||||||
|
|
||||||
|
### **Frontend System (Vue.js AI Chat)**
|
||||||
|
- **Location:** `/mnt/sdd2/projects/aichat-17092025`
|
||||||
|
- **Purpose:** AI business receptionist with intelligent chat interface
|
||||||
|
- **Technology:** Vue 3 + TypeScript + Pinia + Tailwind CSS
|
||||||
|
- **Current Features:**
|
||||||
|
- AI-powered chat with business-specific knowledge
|
||||||
|
- Dynamic content panel (PDFs, videos, forms, booking widgets)
|
||||||
|
- Website scraping integration
|
||||||
|
- Customizable branding per business
|
||||||
|
- Voice support and lead capture
|
||||||
|
|
||||||
|
## 🔄 **Data Flow Architecture**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||||
|
│ Django │ │ AI Processing │ │ Vue.js Chat │
|
||||||
|
│ Backend │───▶│ & Export │───▶│ Frontend │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ • Web Scraping │ │ • Text Cleaning │ │ • AI Chat │
|
||||||
|
│ • Data Storage │ │ • Text Chunking │ │ • Knowledge Base│
|
||||||
|
│ • URL Management│ │ • Format Convert │ │ • Content Panel │
|
||||||
|
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 **Current Export Capabilities**
|
||||||
|
|
||||||
|
### **Existing Django Exports:**
|
||||||
|
1. **JSON Format** - Structured business + pages data
|
||||||
|
2. **CSV Format** - Tabular data with content previews (truncated)
|
||||||
|
3. **TXT Format** - Plain text with full content
|
||||||
|
|
||||||
|
### **Current Vue.js Data Structure:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "business-id",
|
||||||
|
"name": "Business Name",
|
||||||
|
"description": "Business description for AI responses",
|
||||||
|
"website": "https://business.com",
|
||||||
|
"knowledgeBase": [
|
||||||
|
{
|
||||||
|
"id": "kb-1",
|
||||||
|
"question": "What services do you offer?",
|
||||||
|
"answer": "We provide...",
|
||||||
|
"tags": ["services"],
|
||||||
|
"contentIds": ["company-brochure"],
|
||||||
|
"priority": 10
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"id": "company-brochure",
|
||||||
|
"type": "website",
|
||||||
|
"title": "Company Overview",
|
||||||
|
"url": "https://company.com/about",
|
||||||
|
"category": "company"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 **Implementation Plan**
|
||||||
|
|
||||||
|
### **Phase 1: AI-Ready Export Formats**
|
||||||
|
|
||||||
|
#### **1.1 JSONL Export (AI Training Standard)**
|
||||||
|
```python
|
||||||
|
# Format: One JSON object per line
|
||||||
|
{"text": "cleaned content", "metadata": {"url": "...", "title": "...", "source": "website"}}
|
||||||
|
{"text": "another page content", "metadata": {"url": "...", "title": "...", "source": "website"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **1.2 OpenAI Training Format**
|
||||||
|
```python
|
||||||
|
# Chat completion training format
|
||||||
|
{"messages": [
|
||||||
|
{"role": "system", "content": "You are an expert on {business_name}"},
|
||||||
|
{"role": "user", "content": "What does this page say about {topic}?"},
|
||||||
|
{"role": "assistant", "content": "{processed_content}"}
|
||||||
|
]}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **1.3 Knowledge Base Format (Vue.js Compatible)**
|
||||||
|
```python
|
||||||
|
# Direct import format for Vue.js knowledgeBase array
|
||||||
|
{
|
||||||
|
"knowledgeBase": [
|
||||||
|
{
|
||||||
|
"id": "kb-auto-1",
|
||||||
|
"question": "What is mentioned about services on the website?",
|
||||||
|
"answer": "Based on the website content...",
|
||||||
|
"tags": ["services", "auto-generated"],
|
||||||
|
"contentIds": ["scraped-services-page"],
|
||||||
|
"priority": 5,
|
||||||
|
"source": "auto-scraped"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **1.4 RAG Chunks Format**
|
||||||
|
```python
|
||||||
|
# Optimized for vector databases and embeddings
|
||||||
|
{
|
||||||
|
"chunk_id": "uuid-1234",
|
||||||
|
"text": "chunk content (200-1000 tokens)",
|
||||||
|
"chunk_index": 1,
|
||||||
|
"total_chunks": 5,
|
||||||
|
"metadata": {
|
||||||
|
"url": "source-url",
|
||||||
|
"title": "page-title",
|
||||||
|
"business": "business-name",
|
||||||
|
"section": "about|services|pricing|contact"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Phase 2: Text Processing Pipeline**
|
||||||
|
|
||||||
|
#### **2.1 Content Cleaning Service**
|
||||||
|
```python
|
||||||
|
class ContentProcessor:
|
||||||
|
def clean_text(self, html_content: str) -> str:
|
||||||
|
# Remove HTML tags, normalize whitespace
|
||||||
|
# Handle special characters and encoding
|
||||||
|
# Remove navigation, footer, irrelevant content
|
||||||
|
|
||||||
|
def extract_meaningful_content(self, content: str) -> str:
|
||||||
|
# Identify main content sections
|
||||||
|
# Remove boilerplate text
|
||||||
|
# Extract key information
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **2.2 Text Chunking Strategies**
|
||||||
|
```python
|
||||||
|
class TextChunker:
|
||||||
|
def chunk_by_tokens(self, text: str, max_tokens: int = 500) -> List[str]:
|
||||||
|
# Token-based chunking for AI models
|
||||||
|
|
||||||
|
def chunk_by_semantics(self, text: str) -> List[str]:
|
||||||
|
# Semantic chunking preserving meaning
|
||||||
|
|
||||||
|
def chunk_by_sections(self, html: str) -> List[dict]:
|
||||||
|
# Section-based chunking (headers, paragraphs)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **2.3 Quality Filtering**
|
||||||
|
```python
|
||||||
|
class QualityFilter:
|
||||||
|
def score_content_quality(self, text: str) -> float:
|
||||||
|
# Length, readability, information density
|
||||||
|
|
||||||
|
def detect_duplicates(self, new_content: str, existing: List[str]) -> bool:
|
||||||
|
# Semantic similarity detection
|
||||||
|
|
||||||
|
def filter_low_quality(self, content_list: List[str]) -> List[str]:
|
||||||
|
# Remove poor quality content
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Phase 3: New Django Export APIs**
|
||||||
|
|
||||||
|
#### **3.1 AI Export Endpoints**
|
||||||
|
```python
|
||||||
|
# New URLs to add to scraping/urls.py
|
||||||
|
urlpatterns = [
|
||||||
|
# ... existing URLs ...
|
||||||
|
|
||||||
|
# AI Export endpoints
|
||||||
|
path('<int:business_id>/export-ai/', views.export_ai_data, name='export_ai_data'),
|
||||||
|
path('<int:business_id>/export-jsonl/', views.export_jsonl, name='export_jsonl'),
|
||||||
|
path('<int:business_id>/export-openai/', views.export_openai_training, name='export_openai_training'),
|
||||||
|
path('<int:business_id>/export-knowledge/', views.export_knowledge_base, name='export_knowledge_base'),
|
||||||
|
path('<int:business_id>/export-vue-config/', views.export_vue_config, name='export_vue_config'),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **3.2 Export Views Implementation**
|
||||||
|
```python
|
||||||
|
def export_ai_data(request, business_id):
|
||||||
|
"""
|
||||||
|
Main AI export endpoint with format selection
|
||||||
|
?format=jsonl|openai|knowledge|rag|vue-config
|
||||||
|
"""
|
||||||
|
|
||||||
|
def export_jsonl(request, business_id):
|
||||||
|
"""Export as JSONL for general AI training"""
|
||||||
|
|
||||||
|
def export_openai_training(request, business_id):
|
||||||
|
"""Export in OpenAI fine-tuning format"""
|
||||||
|
|
||||||
|
def export_knowledge_base(request, business_id):
|
||||||
|
"""Export as Vue.js compatible knowledge base"""
|
||||||
|
|
||||||
|
def export_vue_config(request, business_id):
|
||||||
|
"""Export complete Vue.js business.json configuration"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Phase 4: Vue.js Integration Points**
|
||||||
|
|
||||||
|
#### **4.1 Business Configuration Sync**
|
||||||
|
```typescript
|
||||||
|
// Auto-generate Vue.js business.json from Django data
|
||||||
|
interface BusinessConfig {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
website: string
|
||||||
|
branding: {
|
||||||
|
primaryColor: string
|
||||||
|
secondaryColor: string
|
||||||
|
logo: string
|
||||||
|
}
|
||||||
|
scrapingConfig: {
|
||||||
|
enabled: boolean
|
||||||
|
website: string
|
||||||
|
contentPriority: string[]
|
||||||
|
}
|
||||||
|
knowledgeBase: KnowledgeBaseItem[]
|
||||||
|
content: ContentItem[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **4.2 Knowledge Base Import Service**
|
||||||
|
```typescript
|
||||||
|
// src/services/backendSync.ts
|
||||||
|
class BackendSyncService {
|
||||||
|
async importBusinessConfig(businessId: string): Promise<BusinessConfig>
|
||||||
|
async importKnowledgeBase(businessId: string): Promise<KnowledgeBaseItem[]>
|
||||||
|
async importScrapedContent(businessId: string): Promise<ContentItem[]>
|
||||||
|
async syncFromBackend(businessId: string): Promise<void>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **4.3 Auto-Generated Content Items**
|
||||||
|
```typescript
|
||||||
|
// Convert Django scraped pages to Vue.js content items
|
||||||
|
{
|
||||||
|
"id": "scraped-about-page",
|
||||||
|
"type": "website",
|
||||||
|
"title": "About Us", // from scraped title
|
||||||
|
"description": "Company overview and mission", // from scraped description
|
||||||
|
"url": "https://company.com/about", // original URL
|
||||||
|
"tags": ["about", "company", "auto-scraped"],
|
||||||
|
"category": "company",
|
||||||
|
"source": "django-scraper",
|
||||||
|
"lastUpdated": "2025-01-21T10:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ **Implementation Code Examples**
|
||||||
|
|
||||||
|
### **Django AI Export Service**
|
||||||
|
```python
|
||||||
|
# scraping/ai_export_service.py
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from typing import List, Dict
|
||||||
|
from .models import Business, CrawledPage
|
||||||
|
|
||||||
|
class AIExportService:
|
||||||
|
def __init__(self, business_id: int):
|
||||||
|
self.business = Business.objects.get(id=business_id)
|
||||||
|
self.pages = self.business.pages.filter(success=True)
|
||||||
|
|
||||||
|
def export_jsonl(self) -> str:
|
||||||
|
"""Export as JSONL for AI training"""
|
||||||
|
lines = []
|
||||||
|
for page in self.pages:
|
||||||
|
data = {
|
||||||
|
"text": self._clean_content(page.content),
|
||||||
|
"metadata": {
|
||||||
|
"url": page.url,
|
||||||
|
"title": page.title,
|
||||||
|
"description": page.description,
|
||||||
|
"business": self.business.name,
|
||||||
|
"industry": self.business.industry,
|
||||||
|
"scraped_at": page.crawled_at.isoformat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.append(json.dumps(data))
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
def export_openai_training(self) -> str:
|
||||||
|
"""Export for OpenAI fine-tuning"""
|
||||||
|
lines = []
|
||||||
|
for page in self.pages:
|
||||||
|
data = {
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": f"You are an AI assistant for {self.business.name}, a {self.business.industry} company."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": f"What can you tell me about {page.title}?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": self._clean_content(page.content)[:2000] # Limit length
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
lines.append(json.dumps(data))
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
def export_vue_knowledge_base(self) -> Dict:
|
||||||
|
"""Export for Vue.js knowledge base"""
|
||||||
|
knowledge_items = []
|
||||||
|
for i, page in enumerate(self.pages):
|
||||||
|
knowledge_items.append({
|
||||||
|
"id": f"kb-auto-{i+1}",
|
||||||
|
"question": f"What information is available about {page.title}?",
|
||||||
|
"answer": f"Based on our website: {self._clean_content(page.content)[:500]}...",
|
||||||
|
"tags": self._extract_tags(page),
|
||||||
|
"contentIds": [f"scraped-{page.id}"],
|
||||||
|
"priority": 5,
|
||||||
|
"source": "auto-generated"
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"knowledgeBase": knowledge_items}
|
||||||
|
|
||||||
|
def export_vue_business_config(self) -> Dict:
|
||||||
|
"""Export complete Vue.js business configuration"""
|
||||||
|
return {
|
||||||
|
"id": f"business-{self.business.id}",
|
||||||
|
"name": self.business.name,
|
||||||
|
"description": self.business.description,
|
||||||
|
"industry": self.business.industry,
|
||||||
|
"website": self.business.website_url,
|
||||||
|
"branding": {
|
||||||
|
"primaryColor": self.business.primary_color,
|
||||||
|
"secondaryColor": self.business.secondary_color,
|
||||||
|
"logo": self.business.logo_url or "/logos/default.svg",
|
||||||
|
"font": "Inter"
|
||||||
|
},
|
||||||
|
"scrapingConfig": {
|
||||||
|
"enabled": True,
|
||||||
|
"website": self.business.website_url,
|
||||||
|
"contentPriority": ["about", "services", "pricing", "contact"],
|
||||||
|
"updateSchedule": "weekly"
|
||||||
|
},
|
||||||
|
"content": self._generate_content_items(),
|
||||||
|
"knowledgeBase": self.export_vue_knowledge_base()["knowledgeBase"],
|
||||||
|
"settings": {
|
||||||
|
"welcomeMessage": f"Hello! I'm your AI assistant at {self.business.name}. How can I help you today?",
|
||||||
|
"aiPersonality": "professional and helpful",
|
||||||
|
"enableVoice": True,
|
||||||
|
"enableLeadCapture": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _clean_content(self, content: str) -> str:
|
||||||
|
"""Clean and normalize content for AI consumption"""
|
||||||
|
# Remove HTML tags, normalize whitespace, etc.
|
||||||
|
import re
|
||||||
|
cleaned = re.sub(r'<[^>]+>', '', content)
|
||||||
|
cleaned = re.sub(r'\s+', ' ', cleaned)
|
||||||
|
return cleaned.strip()
|
||||||
|
|
||||||
|
def _extract_tags(self, page: CrawledPage) -> List[str]:
|
||||||
|
"""Extract relevant tags from page content"""
|
||||||
|
tags = []
|
||||||
|
if 'about' in page.url.lower() or 'about' in page.title.lower():
|
||||||
|
tags.append('about')
|
||||||
|
if 'service' in page.url.lower() or 'service' in page.title.lower():
|
||||||
|
tags.append('services')
|
||||||
|
if 'pricing' in page.url.lower() or 'price' in page.title.lower():
|
||||||
|
tags.append('pricing')
|
||||||
|
if 'contact' in page.url.lower() or 'contact' in page.title.lower():
|
||||||
|
tags.append('contact')
|
||||||
|
tags.append('auto-generated')
|
||||||
|
return tags
|
||||||
|
|
||||||
|
def _generate_content_items(self) -> List[Dict]:
|
||||||
|
"""Generate Vue.js content items from scraped pages"""
|
||||||
|
content_items = []
|
||||||
|
for page in self.pages:
|
||||||
|
content_items.append({
|
||||||
|
"id": f"scraped-{page.id}",
|
||||||
|
"type": "website",
|
||||||
|
"title": page.title or "Website Page",
|
||||||
|
"description": page.description or "Information from our website",
|
||||||
|
"url": page.url,
|
||||||
|
"tags": self._extract_tags(page),
|
||||||
|
"category": self._categorize_page(page),
|
||||||
|
"source": "django-scraper",
|
||||||
|
"lastUpdated": page.crawled_at.isoformat()
|
||||||
|
})
|
||||||
|
return content_items
|
||||||
|
|
||||||
|
def _categorize_page(self, page: CrawledPage) -> str:
|
||||||
|
"""Categorize page content"""
|
||||||
|
url_lower = page.url.lower()
|
||||||
|
title_lower = page.title.lower() if page.title else ""
|
||||||
|
|
||||||
|
if 'about' in url_lower or 'about' in title_lower:
|
||||||
|
return 'company'
|
||||||
|
elif 'service' in url_lower or 'service' in title_lower:
|
||||||
|
return 'services'
|
||||||
|
elif 'pricing' in url_lower or 'price' in title_lower:
|
||||||
|
return 'pricing'
|
||||||
|
elif 'contact' in url_lower or 'contact' in title_lower:
|
||||||
|
return 'contact'
|
||||||
|
else:
|
||||||
|
return 'general'
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Vue.js Backend Integration Service**
|
||||||
|
```typescript
|
||||||
|
// src/services/backendSync.ts
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
interface ScrapedData {
|
||||||
|
business: BusinessConfig
|
||||||
|
knowledgeBase: KnowledgeBaseItem[]
|
||||||
|
content: ContentItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
class BackendSyncService {
|
||||||
|
private baseURL = 'http://localhost:8000/scraping'
|
||||||
|
|
||||||
|
async syncBusinessData(businessId: string): Promise<ScrapedData> {
|
||||||
|
try {
|
||||||
|
// Fetch complete Vue.js configuration from Django
|
||||||
|
const response = await axios.get(`${this.baseURL}/${businessId}/export-vue-config/`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
business: response.data,
|
||||||
|
knowledgeBase: response.data.knowledgeBase || [],
|
||||||
|
content: response.data.content || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to sync business data:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadAITrainingData(businessId: string, format: 'jsonl' | 'openai' | 'rag'): Promise<Blob> {
|
||||||
|
const response = await axios.get(`${this.baseURL}/${businessId}/export-ai/?format=${format}`, {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async importKnowledgeBase(businessId: string): Promise<KnowledgeBaseItem[]> {
|
||||||
|
const response = await axios.get(`${this.baseURL}/${businessId}/export-knowledge/`)
|
||||||
|
return response.data.knowledgeBase
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new BackendSyncService()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 **File Organization**
|
||||||
|
|
||||||
|
### **Django Backend Structure**
|
||||||
|
```
|
||||||
|
chat-backend/
|
||||||
|
├── scraping/
|
||||||
|
│ ├── ai_export_service.py # AI data processing
|
||||||
|
│ ├── text_processor.py # Content cleaning & chunking
|
||||||
|
│ ├── vue_js_exporter.py # Vue.js format converter
|
||||||
|
│ ├── views.py # Updated with AI export views
|
||||||
|
│ ├── urls.py # New AI export URLs
|
||||||
|
│ └── templates/scraping/
|
||||||
|
│ └── ai_export.html # Export interface
|
||||||
|
├── requirements.txt # Add: tiktoken, nltk
|
||||||
|
└── AI_CHAT_INTEGRATION.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Vue.js Frontend Integration**
|
||||||
|
```
|
||||||
|
aichat-17092025/
|
||||||
|
├── src/
|
||||||
|
│ ├── services/
|
||||||
|
│ │ ├── backendSync.ts # Django integration
|
||||||
|
│ │ └── dataImporter.ts # Import scraped data
|
||||||
|
│ ├── data/
|
||||||
|
│ │ ├── business.json # Auto-generated from Django
|
||||||
|
│ │ └── imported-knowledge.json # Scraped knowledge base
|
||||||
|
│ └── stores/
|
||||||
|
│ └── knowledge.ts # Enhanced with import capability
|
||||||
|
└── BACKEND_INTEGRATION.md # Django integration guide
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 **Deployment Workflow**
|
||||||
|
|
||||||
|
### **Step 1: Setup Django AI Exports**
|
||||||
|
```bash
|
||||||
|
# Add new dependencies
|
||||||
|
echo "tiktoken==0.5.1" >> requirements.txt
|
||||||
|
echo "nltk==3.8.1" >> requirements.txt
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Run migrations (if any model changes)
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Step 2: Configure Vue.js Integration**
|
||||||
|
```bash
|
||||||
|
# Add axios for API calls (if not already present)
|
||||||
|
npm install axios
|
||||||
|
|
||||||
|
# Update environment variables
|
||||||
|
echo "VITE_DJANGO_API_URL=http://localhost:8000" >> .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Step 3: Test Data Flow**
|
||||||
|
```bash
|
||||||
|
# 1. Scrape a business website in Django
|
||||||
|
# 2. Export AI-ready data
|
||||||
|
curl "http://localhost:8000/scraping/1/export-vue-config/"
|
||||||
|
|
||||||
|
# 3. Import into Vue.js
|
||||||
|
# 4. Test chat functionality with scraped knowledge
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 **API Reference**
|
||||||
|
|
||||||
|
### **Django Export Endpoints**
|
||||||
|
|
||||||
|
#### **GET `/scraping/{business_id}/export-ai/`**
|
||||||
|
**Parameters:**
|
||||||
|
- `format`: `jsonl|openai|knowledge|rag|vue-config`
|
||||||
|
|
||||||
|
**Response:** File download with appropriate format
|
||||||
|
|
||||||
|
#### **GET `/scraping/{business_id}/export-vue-config/`**
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "business-1",
|
||||||
|
"name": "Company Name",
|
||||||
|
"knowledgeBase": [...],
|
||||||
|
"content": [...],
|
||||||
|
"settings": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **GET `/scraping/{business_id}/export-jsonl/`**
|
||||||
|
**Response:** JSONL file
|
||||||
|
```
|
||||||
|
{"text": "content", "metadata": {...}}
|
||||||
|
{"text": "content", "metadata": {...}}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **GET `/scraping/{business_id}/export-openai/`**
|
||||||
|
**Response:** OpenAI training format JSONL
|
||||||
|
```
|
||||||
|
{"messages": [{"role": "system", "content": "..."}, ...]}
|
||||||
|
{"messages": [{"role": "system", "content": "..."}, ...]}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Vue.js Integration Methods**
|
||||||
|
|
||||||
|
#### **Manual Import**
|
||||||
|
```typescript
|
||||||
|
// Import scraped data manually
|
||||||
|
import backendSync from '@/services/backendSync'
|
||||||
|
|
||||||
|
const businessData = await backendSync.syncBusinessData('business-1')
|
||||||
|
// Update stores with imported data
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Automated Sync**
|
||||||
|
```typescript
|
||||||
|
// Scheduled import every hour
|
||||||
|
setInterval(async () => {
|
||||||
|
await backendSync.syncBusinessData(currentBusinessId)
|
||||||
|
}, 3600000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 **Testing & Validation**
|
||||||
|
|
||||||
|
### **Data Quality Checks**
|
||||||
|
```python
|
||||||
|
def validate_export_quality(business_id: int):
|
||||||
|
"""Validate exported data quality"""
|
||||||
|
service = AIExportService(business_id)
|
||||||
|
|
||||||
|
# Check content completeness
|
||||||
|
assert len(service.pages) > 0, "No pages to export"
|
||||||
|
|
||||||
|
# Check knowledge base generation
|
||||||
|
kb = service.export_vue_knowledge_base()
|
||||||
|
assert len(kb['knowledgeBase']) > 0, "No knowledge base items generated"
|
||||||
|
|
||||||
|
# Check content cleaning
|
||||||
|
for page in service.pages:
|
||||||
|
cleaned = service._clean_content(page.content)
|
||||||
|
assert len(cleaned) > 50, f"Content too short after cleaning: {page.url}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Integration Tests**
|
||||||
|
```typescript
|
||||||
|
// Test Vue.js import functionality
|
||||||
|
describe('Backend Integration', () => {
|
||||||
|
test('imports business configuration', async () => {
|
||||||
|
const config = await backendSync.syncBusinessData('test-business')
|
||||||
|
expect(config.business.name).toBeTruthy()
|
||||||
|
expect(config.knowledgeBase.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('downloads AI training data', async () => {
|
||||||
|
const blob = await backendSync.downloadAITrainingData('test-business', 'jsonl')
|
||||||
|
expect(blob.size).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 **Success Metrics**
|
||||||
|
|
||||||
|
### **Technical Metrics**
|
||||||
|
- ✅ **Export Coverage**: 95%+ of scraped content successfully exported
|
||||||
|
- ✅ **Data Quality**: 90%+ content relevance after processing
|
||||||
|
- ✅ **Format Compliance**: 100% valid JSONL/JSON output
|
||||||
|
- ✅ **Integration Success**: Vue.js imports work without errors
|
||||||
|
|
||||||
|
### **Business Metrics**
|
||||||
|
- ✅ **Knowledge Accuracy**: AI responses match website content
|
||||||
|
- ✅ **Response Quality**: Users get relevant, helpful answers
|
||||||
|
- ✅ **Automation Level**: Minimal manual configuration required
|
||||||
|
- ✅ **Update Frequency**: Fresh data synced weekly/daily
|
||||||
|
|
||||||
|
## 🚨 **Troubleshooting Guide**
|
||||||
|
|
||||||
|
### **Common Issues**
|
||||||
|
|
||||||
|
#### **Django Export Fails**
|
||||||
|
```python
|
||||||
|
# Check business exists and has scraped pages
|
||||||
|
business = Business.objects.get(id=business_id)
|
||||||
|
pages = business.pages.filter(success=True)
|
||||||
|
print(f"Found {pages.count()} pages to export")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Vue.js Import Fails**
|
||||||
|
```typescript
|
||||||
|
// Check API connectivity
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/scraping/1/export-vue-config/')
|
||||||
|
console.log('API Response:', response.status)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error:', error.response?.data)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Content Quality Issues**
|
||||||
|
```python
|
||||||
|
# Debug content cleaning
|
||||||
|
original = page.content
|
||||||
|
cleaned = service._clean_content(original)
|
||||||
|
print(f"Original: {len(original)} chars")
|
||||||
|
print(f"Cleaned: {len(cleaned)} chars")
|
||||||
|
print(f"Cleaned preview: {cleaned[:200]}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Performance Optimization**
|
||||||
|
|
||||||
|
#### **Large Dataset Handling**
|
||||||
|
```python
|
||||||
|
# Process exports in chunks for large businesses
|
||||||
|
def export_large_dataset(business_id: int, chunk_size: int = 100):
|
||||||
|
pages = Business.objects.get(id=business_id).pages.filter(success=True)
|
||||||
|
|
||||||
|
for i in range(0, pages.count(), chunk_size):
|
||||||
|
chunk = pages[i:i+chunk_size]
|
||||||
|
yield process_chunk(chunk)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **Caching Strategy**
|
||||||
|
```python
|
||||||
|
# Cache processed exports for faster repeated access
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
def get_cached_export(business_id: int, format_type: str):
|
||||||
|
cache_key = f"export_{business_id}_{format_type}"
|
||||||
|
cached = cache.get(cache_key)
|
||||||
|
|
||||||
|
if not cached:
|
||||||
|
service = AIExportService(business_id)
|
||||||
|
cached = service.export_by_format(format_type)
|
||||||
|
cache.set(cache_key, cached, timeout=3600) # 1 hour
|
||||||
|
|
||||||
|
return cached
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 **Additional Resources**
|
||||||
|
|
||||||
|
### **External Documentation**
|
||||||
|
- [OpenAI Fine-tuning Guide](https://platform.openai.com/docs/guides/fine-tuning)
|
||||||
|
- [JSONL Format Specification](https://jsonlines.org/)
|
||||||
|
- [Vue.js + TypeScript Best Practices](https://vuejs.org/guide/typescript/overview.html)
|
||||||
|
- [Django REST Framework](https://www.django-rest-framework.org/)
|
||||||
|
|
||||||
|
### **Related Files**
|
||||||
|
- `SCRAPING_GUIDE.md` - Web scraping implementation details
|
||||||
|
- `URL_UPDATE_GUIDE.md` - URL management and validation
|
||||||
|
- `README.md` - General project overview
|
||||||
|
|
||||||
|
### **Future Enhancements**
|
||||||
|
- [ ] Real-time WebSocket sync between Django and Vue.js
|
||||||
|
- [ ] AI-powered content quality scoring
|
||||||
|
- [ ] Multi-language support for scraped content
|
||||||
|
- [ ] Advanced chunking strategies for better embeddings
|
||||||
|
- [ ] Integration with vector databases (Pinecone, Weaviate)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** January 2025
|
||||||
|
**Version:** 1.0
|
||||||
|
**Maintained By:** Development Team
|
||||||
|
|
||||||
|
This document serves as the complete reference for integrating Django web scraping backend with Vue.js AI chat frontend applications.
|
||||||
242
scraping/URL_UPDATE_GUIDE.md
Normal file
242
scraping/URL_UPDATE_GUIDE.md
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
# URL Update & Management Guide
|
||||||
|
|
||||||
|
## 🔄 **URL Update Functionality**
|
||||||
|
|
||||||
|
Your Django application now has comprehensive URL update and management features for handling wrong URLs or URL changes.
|
||||||
|
|
||||||
|
## ✅ **Features Implemented**
|
||||||
|
|
||||||
|
### **1. URL Validation**
|
||||||
|
- ✅ **Format validation** - Ensures URL has proper http/https scheme
|
||||||
|
- ✅ **Accessibility testing** - Checks if URL is actually reachable
|
||||||
|
- ✅ **Real-time validation** - Tests URL before saving to database
|
||||||
|
|
||||||
|
### **2. Multiple Update Methods**
|
||||||
|
- ✅ **Full business edit** - Update URL along with other business details
|
||||||
|
- ✅ **Quick URL update** - Update only the URL via modal/API
|
||||||
|
- ✅ **Validation feedback** - Clear error messages for invalid URLs
|
||||||
|
|
||||||
|
### **3. Smart Re-crawling**
|
||||||
|
- ✅ **Optional re-crawl** - Choose whether to re-scrape after URL change
|
||||||
|
- ✅ **Page comparison** - Shows old vs new page counts
|
||||||
|
- ✅ **Status tracking** - Real-time crawling status updates
|
||||||
|
|
||||||
|
## 🛠️ **How to Update URLs**
|
||||||
|
|
||||||
|
### **Method 1: Full Business Edit**
|
||||||
|
1. Go to business detail page
|
||||||
|
2. Click **"Edit Business"** button
|
||||||
|
3. Change the website URL
|
||||||
|
4. Optionally check **"Re-crawl website if URL changes"**
|
||||||
|
5. Click **"Update Business"**
|
||||||
|
|
||||||
|
### **Method 2: Quick URL Update**
|
||||||
|
1. On edit page, click **"Update URL Only"**
|
||||||
|
2. Enter new URL in modal
|
||||||
|
3. Click **"Update URL"**
|
||||||
|
4. System validates and updates immediately
|
||||||
|
|
||||||
|
### **Method 3: API Update**
|
||||||
|
```javascript
|
||||||
|
fetch('/scraping/{business_id}/update-url/', {
|
||||||
|
method: 'POST',
|
||||||
|
body: new FormData([['website_url', 'https://newdomain.com']]),
|
||||||
|
headers: {'X-CSRFToken': csrfToken}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 **Validation Process**
|
||||||
|
|
||||||
|
When you update a URL, the system:
|
||||||
|
|
||||||
|
1. **Format Check** - Validates URL format (must have http/https)
|
||||||
|
2. **Accessibility Test** - Sends HTTP HEAD request to verify URL is reachable
|
||||||
|
3. **Status Code Check** - Ensures server responds with 200-399 status
|
||||||
|
4. **Database Update** - Only saves if all validations pass
|
||||||
|
5. **Rollback Protection** - Reverts to old URL if any step fails
|
||||||
|
|
||||||
|
## 📱 **User Interface Features**
|
||||||
|
|
||||||
|
### **Business Detail Page**
|
||||||
|
- ✅ **Edit Business** button in header
|
||||||
|
- ✅ **Re-crawl** button (if pages exist)
|
||||||
|
- ✅ **Export Data** button for current data
|
||||||
|
|
||||||
|
### **Edit Business Page**
|
||||||
|
- ✅ **Current info sidebar** - Shows existing values
|
||||||
|
- ✅ **URL validation** - Real-time feedback
|
||||||
|
- ✅ **Re-crawl checkbox** - Automatic re-crawl option
|
||||||
|
- ✅ **Quick actions** - Test URL, Update URL only, Re-crawl now
|
||||||
|
|
||||||
|
### **URL Update Modal**
|
||||||
|
- ✅ **Focused URL editing** - Quick URL-only changes
|
||||||
|
- ✅ **Validation feedback** - Shows success/error messages
|
||||||
|
- ✅ **Auto-refresh** - Updates main form after successful change
|
||||||
|
|
||||||
|
## 🚨 **Error Handling**
|
||||||
|
|
||||||
|
### **Common Error Scenarios**
|
||||||
|
|
||||||
|
1. **Invalid URL Format**
|
||||||
|
```
|
||||||
|
Error: Please enter a valid URL with http:// or https://
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Unreachable URL**
|
||||||
|
```
|
||||||
|
Error: Cannot access URL: Connection timeout
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Server Error**
|
||||||
|
```
|
||||||
|
Error: URL returned status code 404. Please check if the website is accessible.
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Network Issues**
|
||||||
|
```
|
||||||
|
Error: Network error: DNS resolution failed
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Error Recovery**
|
||||||
|
- Original URL is preserved if update fails
|
||||||
|
- Clear error messages guide user to fix issues
|
||||||
|
- Option to test URL accessibility before updating
|
||||||
|
|
||||||
|
## 🔄 **Re-crawling After URL Update**
|
||||||
|
|
||||||
|
### **Automatic Re-crawl**
|
||||||
|
When updating URL with re-crawl option:
|
||||||
|
1. URL is validated and updated
|
||||||
|
2. Old scraped pages are deleted
|
||||||
|
3. New crawling begins automatically
|
||||||
|
4. Page count comparison is shown
|
||||||
|
|
||||||
|
### **Manual Re-crawl**
|
||||||
|
Click **"Re-crawl Now"** to:
|
||||||
|
1. Delete all existing pages
|
||||||
|
2. Scrape the current URL
|
||||||
|
3. Show before/after page counts
|
||||||
|
4. Update business status
|
||||||
|
|
||||||
|
### **Re-crawl Results**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Re-crawled successfully! Found 15 pages (previously 8)",
|
||||||
|
"total_pages": 15,
|
||||||
|
"old_page_count": 8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 **API Endpoints**
|
||||||
|
|
||||||
|
### **Update Business URL**
|
||||||
|
```
|
||||||
|
POST /scraping/{business_id}/update-url/
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
|
||||||
|
website_url=https://newdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Website URL updated from https://old.com to https://new.com",
|
||||||
|
"old_url": "https://old.com",
|
||||||
|
"new_url": "https://new.com",
|
||||||
|
"business_id": 123
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Re-crawl Business**
|
||||||
|
```
|
||||||
|
POST /scraping/{business_id}/recrawl/
|
||||||
|
X-CSRFToken: {token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Re-crawled successfully! Found 12 pages (previously 5)",
|
||||||
|
"total_pages": 12,
|
||||||
|
"old_page_count": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Edit Business (Full)**
|
||||||
|
```
|
||||||
|
GET/POST /scraping/{business_id}/edit/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛡️ **Security & Validation**
|
||||||
|
|
||||||
|
### **Built-in Protections**
|
||||||
|
- ✅ **CSRF Protection** - All forms include CSRF tokens
|
||||||
|
- ✅ **URL Validation** - Prevents malicious URL injection
|
||||||
|
- ✅ **Access Control** - Only authorized users can edit
|
||||||
|
- ✅ **Data Rollback** - Failed updates don't corrupt data
|
||||||
|
|
||||||
|
### **Validation Rules**
|
||||||
|
```python
|
||||||
|
# URL must have proper scheme
|
||||||
|
if not parsed.scheme or not parsed.netloc:
|
||||||
|
raise ValidationError('Invalid URL format')
|
||||||
|
|
||||||
|
# Only allow HTTP/HTTPS
|
||||||
|
if parsed.scheme not in ['http', 'https']:
|
||||||
|
raise ValidationError('URL must start with http:// or https://')
|
||||||
|
|
||||||
|
# Test accessibility
|
||||||
|
response = requests.head(url, timeout=10)
|
||||||
|
if response.status_code >= 400:
|
||||||
|
raise ValidationError('URL not accessible')
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 **Usage Examples**
|
||||||
|
|
||||||
|
### **Scenario 1: Company Changed Domain**
|
||||||
|
```
|
||||||
|
Old: https://oldcompany.com
|
||||||
|
New: https://newcompany.com
|
||||||
|
|
||||||
|
1. Go to Edit Business
|
||||||
|
2. Update URL to https://newcompany.com
|
||||||
|
3. Check "Re-crawl website if URL changes"
|
||||||
|
4. Click Update Business
|
||||||
|
→ Result: URL updated, 15 new pages scraped
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Scenario 2: Wrong URL Entered Initially**
|
||||||
|
```
|
||||||
|
Wrong: https://exampl.com (typo)
|
||||||
|
Correct: https://example.com
|
||||||
|
|
||||||
|
1. Click "Update URL Only" in sidebar
|
||||||
|
2. Enter https://example.com
|
||||||
|
3. Click Update URL
|
||||||
|
→ Result: URL corrected, ready to re-crawl
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Scenario 3: Website Structure Changed**
|
||||||
|
```
|
||||||
|
Same URL, but site was redesigned
|
||||||
|
|
||||||
|
1. Click "Re-crawl Now" button
|
||||||
|
2. Confirm deletion of old pages
|
||||||
|
3. Wait for re-crawling to complete
|
||||||
|
→ Result: Fresh content from redesigned site
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 **Best Practices**
|
||||||
|
|
||||||
|
1. **Always test URLs** before updating in production
|
||||||
|
2. **Backup data** before major URL changes
|
||||||
|
3. **Use re-crawl option** when URL structure changes significantly
|
||||||
|
4. **Monitor crawling status** to ensure completion
|
||||||
|
5. **Export data** before making changes as backup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Your URL update system is now **production-ready** with comprehensive validation, error handling, and user-friendly interfaces! 🎉
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,5 +1,8 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
import requests
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
class Business(models.Model):
|
class Business(models.Model):
|
||||||
@ -48,6 +51,70 @@ class Business(models.Model):
|
|||||||
def scraped_pages(self):
|
def scraped_pages(self):
|
||||||
return self.pages.filter(success=True).count()
|
return self.pages.filter(success=True).count()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""Validate the website URL"""
|
||||||
|
if self.website_url:
|
||||||
|
# Parse URL
|
||||||
|
parsed = urlparse(self.website_url)
|
||||||
|
|
||||||
|
# Check if URL has scheme and netloc
|
||||||
|
if not parsed.scheme or not parsed.netloc:
|
||||||
|
raise ValidationError({'website_url': 'Please enter a valid URL with http:// or https://'})
|
||||||
|
|
||||||
|
# Check if scheme is http or https
|
||||||
|
if parsed.scheme not in ['http', 'https']:
|
||||||
|
raise ValidationError({'website_url': 'URL must start with http:// or https://'})
|
||||||
|
|
||||||
|
def update_website_url(self, new_url):
|
||||||
|
"""
|
||||||
|
Update website URL and optionally clear existing pages
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_url (str): The new website URL
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Result of the update operation
|
||||||
|
"""
|
||||||
|
old_url = self.website_url
|
||||||
|
|
||||||
|
# Validate new URL
|
||||||
|
self.website_url = new_url
|
||||||
|
try:
|
||||||
|
self.clean()
|
||||||
|
except ValidationError as e:
|
||||||
|
# Restore old URL if validation fails
|
||||||
|
self.website_url = old_url
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': str(e.message_dict.get('website_url', ['Invalid URL'])[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test if URL is accessible
|
||||||
|
try:
|
||||||
|
response = requests.head(new_url, timeout=10, allow_redirects=True)
|
||||||
|
if response.status_code >= 400:
|
||||||
|
self.website_url = old_url
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f'URL returned status code {response.status_code}. Please check if the website is accessible.'
|
||||||
|
}
|
||||||
|
except requests.RequestException as e:
|
||||||
|
self.website_url = old_url
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': f'Cannot access URL: {str(e)}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save the new URL
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': f'Website URL updated from {old_url} to {new_url}',
|
||||||
|
'old_url': old_url,
|
||||||
|
'new_url': new_url
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CrawledPage(models.Model):
|
class CrawledPage(models.Model):
|
||||||
"""Store crawled pages discovered and scraped by Firecrawl"""
|
"""Store crawled pages discovered and scraped by Firecrawl"""
|
||||||
|
|||||||
@ -5,7 +5,12 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
||||||
<h2>{{ business.name }}</h2>
|
<h2>{{ business.name }}</h2>
|
||||||
<a href="{% url 'business_list' %}" class="btn" style="background: #6c757d;">← Back to Businesses</a>
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<a href="{% url 'edit_business' business.id %}" class="btn" style="background: #17a2b8; color: white;">
|
||||||
|
<i class="fas fa-edit"></i> Edit Business
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'business_list' %}" class="btn" style="background: #6c757d;">← Back to Businesses</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Business Information -->
|
<!-- Business Information -->
|
||||||
@ -22,10 +27,18 @@
|
|||||||
<p><strong>Pages:</strong> {{ business.scraped_pages }}/{{ business.total_pages }} crawled</p>
|
<p><strong>Pages:</strong> {{ business.scraped_pages }}/{{ business.total_pages }} crawled</p>
|
||||||
<p><strong>Created:</strong> {{ business.created_at|date:"M j, Y g:i A" }}</p>
|
<p><strong>Created:</strong> {{ business.created_at|date:"M j, Y g:i A" }}</p>
|
||||||
|
|
||||||
<div style="margin-top: 20px;">
|
<div style="margin-top: 20px; display: flex; gap: 10px;">
|
||||||
<button id="crawl-btn" onclick="crawlWebsite({{ business.id }})" class="btn btn-success">
|
<button id="crawl-btn" onclick="crawlWebsite({{ business.id }})" class="btn btn-success">
|
||||||
Crawl Website
|
<i class="fas fa-search"></i> Crawl Website
|
||||||
</button>
|
</button>
|
||||||
|
<a href="{% url 'export_data' business.id %}?format=json" class="btn" style="background: #ffc107; color: black;">
|
||||||
|
<i class="fas fa-download"></i> Export Data
|
||||||
|
</a>
|
||||||
|
{% if business.total_pages > 0 %}
|
||||||
|
<button onclick="recrawlBusiness({{ business.id }})" class="btn" style="background: #fd7e14; color: white;">
|
||||||
|
<i class="fas fa-sync-alt"></i> Re-crawl
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -76,7 +89,7 @@ function crawlWebsite(businessId) {
|
|||||||
businessStatus.textContent = 'crawling';
|
businessStatus.textContent = 'crawling';
|
||||||
businessStatus.className = 'status status-crawling';
|
businessStatus.className = 'status status-crawling';
|
||||||
|
|
||||||
fetch(`/businesses/${businessId}/crawl/`, {
|
fetch(`/scraping/${businessId}/crawl/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -110,6 +123,41 @@ function crawlWebsite(businessId) {
|
|||||||
alert('Error: ' + error);
|
alert('Error: ' + error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recrawlBusiness(businessId) {
|
||||||
|
if (!confirm('This will delete all existing scraped pages and re-crawl the website. Continue?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const businessStatus = document.getElementById('business-status');
|
||||||
|
businessStatus.textContent = 'crawling';
|
||||||
|
businessStatus.className = 'status status-crawling';
|
||||||
|
|
||||||
|
fetch(`/scraping/${businessId}/recrawl/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
businessStatus.textContent = 'completed';
|
||||||
|
businessStatus.className = 'status status-completed';
|
||||||
|
alert(data.message);
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
businessStatus.textContent = 'failed';
|
||||||
|
businessStatus.className = 'status status-failed';
|
||||||
|
alert('Re-crawl failed: ' + data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
businessStatus.textContent = 'failed';
|
||||||
|
businessStatus.className = 'status status-failed';
|
||||||
|
alert('Network error: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
270
scraping/templates/scraping/edit_business.html
Normal file
270
scraping/templates/scraping/edit_business.html
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
{% extends 'scraping/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Edit {{ business.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h4><i class="fas fa-edit"></i> Edit Business</h4>
|
||||||
|
<a href="{% url 'business_detail' business.id %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Details
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">
|
||||||
|
<i class="fas fa-building"></i> Business Name *
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name"
|
||||||
|
value="{{ business.name }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="website_url" class="form-label">
|
||||||
|
<i class="fas fa-globe"></i> Website URL *
|
||||||
|
</label>
|
||||||
|
<input type="url" class="form-control" id="website_url" name="website_url"
|
||||||
|
value="{{ business.website_url }}" required
|
||||||
|
placeholder="https://example.com">
|
||||||
|
<div class="form-text">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Changing the URL will validate accessibility. You can choose to re-crawl after update.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="industry" class="form-label">
|
||||||
|
<i class="fas fa-industry"></i> Industry
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control" id="industry" name="industry"
|
||||||
|
value="{{ business.industry }}"
|
||||||
|
placeholder="e.g., Technology, Healthcare, Finance">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="description" class="form-label">
|
||||||
|
<i class="fas fa-align-left"></i> Description
|
||||||
|
</label>
|
||||||
|
<textarea class="form-control" id="description" name="description"
|
||||||
|
rows="3" placeholder="Brief description of the business">{{ business.description }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="recrawl_after_update"
|
||||||
|
name="recrawl_after_update">
|
||||||
|
<label class="form-check-label" for="recrawl_after_update">
|
||||||
|
<i class="fas fa-sync-alt"></i>
|
||||||
|
Re-crawl website if URL changes
|
||||||
|
</label>
|
||||||
|
<div class="form-text">
|
||||||
|
If checked, the system will automatically scrape the new website after URL update.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Update Business
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'business_detail' business.id %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-times"></i> Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="fas fa-info-circle"></i> Current Information</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<tr>
|
||||||
|
<td><strong>Name:</strong></td>
|
||||||
|
<td>{{ business.name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Current URL:</strong></td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ business.website_url }}" target="_blank" class="text-break">
|
||||||
|
{{ business.website_url|truncatechars:30 }}
|
||||||
|
<i class="fas fa-external-link-alt fa-xs"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Industry:</strong></td>
|
||||||
|
<td>{{ business.industry|default:"Not specified" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Status:</strong></td>
|
||||||
|
<td>
|
||||||
|
{% if business.status == 'completed' %}
|
||||||
|
<span class="badge bg-success">{{ business.status|title }}</span>
|
||||||
|
{% elif business.status == 'crawling' %}
|
||||||
|
<span class="badge bg-warning">{{ business.status|title }}</span>
|
||||||
|
{% elif business.status == 'failed' %}
|
||||||
|
<span class="badge bg-danger">{{ business.status|title }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ business.status|title }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Pages:</strong></td>
|
||||||
|
<td>{{ business.total_pages }} total</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Last Updated:</strong></td>
|
||||||
|
<td>{{ business.updated_at|date:"M d, Y H:i" }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5><i class="fas fa-tools"></i> Quick Actions</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="testUrl()">
|
||||||
|
<i class="fas fa-link"></i> Test URL Accessibility
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-warning btn-sm" onclick="updateUrlOnly()">
|
||||||
|
<i class="fas fa-edit"></i> Update URL Only
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-success btn-sm" onclick="recrawlNow()">
|
||||||
|
<i class="fas fa-sync-alt"></i> Re-crawl Now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- URL Update Modal -->
|
||||||
|
<div class="modal fade" id="urlUpdateModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Update Website URL</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="urlUpdateForm">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="modal_website_url" class="form-label">New Website URL</label>
|
||||||
|
<input type="url" class="form-control" id="modal_website_url"
|
||||||
|
name="website_url" value="{{ business.website_url }}" required>
|
||||||
|
</div>
|
||||||
|
<div id="urlUpdateResult"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="submitUrlUpdate()">
|
||||||
|
<i class="fas fa-save"></i> Update URL
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function testUrl() {
|
||||||
|
const url = document.getElementById('website_url').value;
|
||||||
|
if (!url) {
|
||||||
|
alert('Please enter a URL first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple test by trying to open in new tab
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUrlOnly() {
|
||||||
|
document.getElementById('modal_website_url').value = document.getElementById('website_url').value;
|
||||||
|
new bootstrap.Modal(document.getElementById('urlUpdateModal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitUrlUpdate() {
|
||||||
|
const form = document.getElementById('urlUpdateForm');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const resultDiv = document.getElementById('urlUpdateResult');
|
||||||
|
|
||||||
|
resultDiv.innerHTML = '<div class="alert alert-info"><i class="fas fa-spinner fa-spin"></i> Updating URL...</div>';
|
||||||
|
|
||||||
|
fetch('{% url "update_business_url" business.id %}', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': formData.get('csrfmiddlewaretoken')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
resultDiv.innerHTML = `<div class="alert alert-success">${data.message}</div>`;
|
||||||
|
// Update the main form URL field
|
||||||
|
document.getElementById('website_url').value = data.new_url;
|
||||||
|
setTimeout(() => {
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('urlUpdateModal')).hide();
|
||||||
|
location.reload();
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
resultDiv.innerHTML = `<div class="alert alert-danger">Error: ${data.error}</div>`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
resultDiv.innerHTML = `<div class="alert alert-danger">Network error: ${error.message}</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function recrawlNow() {
|
||||||
|
if (!confirm('This will delete all existing scraped pages and re-crawl the website. Continue?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('{% url "recrawl_business" business.id %}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert(data.message);
|
||||||
|
window.location.href = '{% url "business_detail" business.id %}';
|
||||||
|
} else {
|
||||||
|
alert('Re-crawl failed: ' + data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Network error: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@ -6,6 +6,11 @@ urlpatterns = [
|
|||||||
path('', views.business_list, name='business_list'),
|
path('', views.business_list, name='business_list'),
|
||||||
path('create/', views.create_business, name='create_business'),
|
path('create/', views.create_business, name='create_business'),
|
||||||
path('<int:business_id>/', views.business_detail, name='business_detail'),
|
path('<int:business_id>/', views.business_detail, name='business_detail'),
|
||||||
|
path('<int:business_id>/edit/', views.edit_business, name='edit_business'),
|
||||||
|
|
||||||
|
# URL management
|
||||||
|
path('<int:business_id>/update-url/', views.update_business_url, name='update_business_url'),
|
||||||
|
path('<int:business_id>/recrawl/', views.recrawl_business, name='recrawl_business'),
|
||||||
|
|
||||||
# Crawling
|
# Crawling
|
||||||
path('<int:business_id>/crawl/', views.crawl_website, name='crawl_website'),
|
path('<int:business_id>/crawl/', views.crawl_website, name='crawl_website'),
|
||||||
|
|||||||
@ -264,4 +264,140 @@ def export_data(request, business_id):
|
|||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
response['Content-Disposition'] = f'attachment; filename="{business.name}_data.json"'
|
response['Content-Disposition'] = f'attachment; filename="{business.name}_data.json"'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def edit_business(request, business_id):
|
||||||
|
"""Edit business information"""
|
||||||
|
business = get_object_or_404(Business, id=business_id)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
name = request.POST.get('name')
|
||||||
|
website_url = request.POST.get('website_url')
|
||||||
|
description = request.POST.get('description', '')
|
||||||
|
industry = request.POST.get('industry', '')
|
||||||
|
|
||||||
|
# Update basic fields
|
||||||
|
if name:
|
||||||
|
business.name = name
|
||||||
|
if description is not None:
|
||||||
|
business.description = description
|
||||||
|
if industry is not None:
|
||||||
|
business.industry = industry
|
||||||
|
|
||||||
|
# Handle URL update separately
|
||||||
|
url_update_result = None
|
||||||
|
if website_url and website_url != business.website_url:
|
||||||
|
url_update_result = business.update_website_url(website_url)
|
||||||
|
|
||||||
|
if url_update_result['success']:
|
||||||
|
messages.success(request, url_update_result['message'])
|
||||||
|
|
||||||
|
# Ask if user wants to re-crawl
|
||||||
|
if request.POST.get('recrawl_after_update') == 'on':
|
||||||
|
return redirect('recrawl_business', business_id=business.id)
|
||||||
|
else:
|
||||||
|
messages.error(request, f"URL update failed: {url_update_result['error']}")
|
||||||
|
|
||||||
|
# Save other changes
|
||||||
|
try:
|
||||||
|
business.save()
|
||||||
|
if not url_update_result: # Only show this if URL wasn't updated
|
||||||
|
messages.success(request, f'Business "{business.name}" updated successfully!')
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, f'Error updating business: {str(e)}')
|
||||||
|
|
||||||
|
return redirect('business_detail', business_id=business.id)
|
||||||
|
|
||||||
|
return render(request, 'scraping/edit_business.html', {
|
||||||
|
'business': business
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
def update_business_url(request, business_id):
|
||||||
|
"""API endpoint to update business URL"""
|
||||||
|
business = get_object_or_404(Business, id=business_id)
|
||||||
|
new_url = request.POST.get('website_url')
|
||||||
|
|
||||||
|
if not new_url:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Website URL is required'
|
||||||
|
})
|
||||||
|
|
||||||
|
result = business.update_website_url(new_url)
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'message': result['message'],
|
||||||
|
'old_url': result['old_url'],
|
||||||
|
'new_url': result['new_url'],
|
||||||
|
'business_id': business.id
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': result['error']
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
def recrawl_business(request, business_id):
|
||||||
|
"""Re-crawl business after URL update"""
|
||||||
|
business = get_object_or_404(Business, id=business_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Clear existing pages
|
||||||
|
old_page_count = business.pages.count()
|
||||||
|
business.pages.all().delete()
|
||||||
|
|
||||||
|
# Update status
|
||||||
|
business.status = 'crawling'
|
||||||
|
business.save()
|
||||||
|
|
||||||
|
# Start crawling
|
||||||
|
crawler = CrawlService()
|
||||||
|
result = crawler.crawl_website(business.website_url)
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
# Save new pages
|
||||||
|
successful_pages = 0
|
||||||
|
for page_data in result['pages']:
|
||||||
|
CrawledPage.objects.create(
|
||||||
|
business=business,
|
||||||
|
url=page_data['url'],
|
||||||
|
title=page_data['title'],
|
||||||
|
description=page_data['description'],
|
||||||
|
content=page_data['content'],
|
||||||
|
success=True
|
||||||
|
)
|
||||||
|
successful_pages += 1
|
||||||
|
|
||||||
|
business.status = 'completed'
|
||||||
|
business.save()
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'message': f'Re-crawled successfully! Found {successful_pages} pages (previously {old_page_count})',
|
||||||
|
'total_pages': successful_pages,
|
||||||
|
'old_page_count': old_page_count
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
business.status = 'failed'
|
||||||
|
business.save()
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': result['error']
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
business.status = 'failed'
|
||||||
|
business.save()
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user