lljz66 commited on
Commit
c44cded
·
verified ·
1 Parent(s): 4b56546

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +204 -174
app.py CHANGED
@@ -1,216 +1,246 @@
1
  import gradio as gr
2
- import requests
 
 
3
  import re
4
  import zipfile
5
  import io
6
- import cssutils
7
- from bs4 import BeautifulSoup
8
- from fake_useragent import UserAgent
9
 
10
  # ==========================================
11
- # 🔧 Helper Functions
12
  # ==========================================
13
 
14
- def fetch_page_html(url):
15
- """جلب كود الصفحة مع معالجة الأخطاء الشائعة"""
16
- try:
17
- if not url.startswith(('http://', 'https://')):
18
- url = 'https://' + url
19
-
20
- headers = {'User-Agent': UserAgent().random}
21
- # مهلة 10 ثوانٍ لتجنب الانتظار الطويل
22
- response = requests.get(url, headers=headers, timeout=10)
23
- response.raise_for_status()
24
- return response.text
25
- except Exception as e:
26
- return f"ERROR: {str(e)}"
27
-
28
- def extract_inline_styles(element):
29
- """استخراج الأنماط المدمجة أو حسابات بسيطة"""
30
- styles = element.get('style', '')
31
- return styles
32
-
33
- def clean_component_html(element):
34
- """تنظيف كود المكون وإزالة السمات غير الضرورية"""
35
- # إزالة سمات التتبع والأحداث المعقدة
36
- for attr in list(element.attrs.keys()):
37
- if attr.startswith('data-') and 'analytics' in attr:
38
- del element[attr]
39
- if attr in ['onclick', 'onload', 'onerror']:
40
- del element[attr]
41
- return str(element)
42
-
43
- def detect_components(soup):
44
  """
45
- الخوارزمية الأساسية: مسح الـ DOM للبحث عن أنماط معروفة
46
  """
47
  components = []
48
 
49
- # 1. اكتشاف الأزرار (Buttons)
50
- buttons = soup.find_all(['button', 'a'], class_=re.compile(r'btn|button', re.I))
51
- for btn in buttons:
52
- if btn.get_text(strip=True): # تجاهل الأزرار الفارغة
53
- components.append({
54
- 'type': '🔘 Button',
55
- 'html': clean_component_html(btn),
56
- 'styles': extract_inline_styles(btn),
57
- 'preview_text': btn.get_text(strip=True)[:20]
58
- })
59
-
60
- # 2. اكتشاف البطاقات (Cards) - العناصر التي تحتوي على صورة وعنوان
61
- # نبحث عن كلاسات شائعة للبطاقات
62
- card_containers = soup.find_all(class_=re.compile(r'card|product|item|post', re.I))
63
- for card in card_containers:
64
- # نتأكد أن البطاقة تحتوي على محتوى بصري أو نصي مميز
65
- if card.find('img') or card.find(['h1', 'h2', 'h3', 'h4']):
66
- # نتجنب البطاقات المتداخلة (نأخذ الأب فقط تقريباً)
67
- if len(card.find_all(class_=re.compile(r'card|product|item', re.I))) <= 1:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  components.append({
69
- 'type': '🃏 Card',
70
- 'html': clean_component_html(card),
71
- 'styles': extract_inline_styles(card),
72
- 'preview_text': 'Card Component'
73
  })
74
-
75
- # 3. اكتشاف شريط التنقل (Navbar)
76
- navs = soup.find_all(['nav', 'header'], class_=re.compile(r'nav|header|menu', re.I))
77
- for nav in navs:
78
- components.append({
79
- 'type': '🧭 Navbar',
80
- 'html': clean_component_html(nav),
81
- 'styles': extract_inline_styles(nav),
82
- 'preview_text': 'Navigation'
83
- })
84
-
85
- # 4. اكتشاف الأقسام الرئيسية (Hero/Sections)
86
- sections = soup.find_all('section')
87
- for sec in sections:
88
- if sec.get('class') and any('hero' in str(c).lower() for c in sec.get('class')):
89
- components.append({
90
- 'type': '🎨 Hero Section',
91
- 'html': clean_component_html(sec),
92
- 'styles': extract_inline_styles(sec),
93
- 'preview_text': 'Hero Section'
94
  })
95
-
 
 
96
  return components
97
 
98
- def generate_code_snippet(comp, framework='html'):
99
- """توليد الكود النهائي للعرض"""
100
- if framework == 'react':
101
- # تحويل بسيط لـ JSX
102
- code = comp['html'].replace('class=', 'className=').replace('for=', 'htmlFor=')
103
- return f"import React from 'react';\n\nexport default function Component() {{\n return (\n {code}\n );\n}}"
104
- return comp['html']
105
-
106
- def create_zip(components):
107
- """إنشاء ملف ZIP للتحميل"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  zip_buffer = io.BytesIO()
109
- with zipfile.ZipFile(zip_buffer, "w") as zip_file:
110
  for i, comp in enumerate(components):
111
- filename = f"component_{i+1}_{comp['type'].split()[1]}.html"
112
- content = f"<!-- Type: {comp['type']} -->\n{comp['html']}"
113
- zip_file.writestr(filename, content)
 
 
114
  zip_buffer.seek(0)
115
  return zip_buffer
116
 
117
  # ==========================================
118
- # 🎨 Gradio Interface Logic
119
- # ==========================================
120
-
121
- def process_url(url, framework):
122
- """الدالة الرئيسية التي تربط الواجهة بالخلفية"""
123
- if not url:
124
- return [], None, "الرجاء إدخال رابط صحيح"
125
-
126
- # 1. الجلب
127
- html_content = fetch_page_html(url)
128
- if html_content.startswith("ERROR"):
129
- return [], None, html_content
130
-
131
- # 2. التحليل
132
- soup = BeautifulSoup(html_content, 'html.parser')
133
- components = detect_components(soup)
134
-
135
- if not components:
136
- return [], None, "⚠️ لم يتم العثور على مكونات قياسية. جرب موقعاً آخر."
137
-
138
- # 3. تجهيز البيانات للعرض (Gallery)
139
- gallery_data = []
140
- for comp in components:
141
- # نعرض كود المكون داخل مربع نص للعرض الأولي
142
- code_view = generate_code_snippet(comp, framework)
143
-
144
- # نص العرض في البطاقة
145
- display_name = f"{comp['type']}: {comp['preview_text']}"
146
-
147
- # نرجع البيانات كقاموس ليقرأها Gradio
148
- gallery_data.append({
149
- "name": display_name,
150
- "image": None, # يمكن إضافة screenshot لاحقاً
151
- "code": code_view,
152
- "data": comp # نحتفظ بالبيانات الأصلية للتحميل
153
- })
154
-
155
- return gallery_data, None, f"✅ تم العثور على {len(components)} مكونات"
156
-
157
- def download_selected(selected_components):
158
- """منطق التحميل (مبسط للنموذج الأولي)"""
159
- # في Gradio Gallery، نعيد قائمة البيانات المختارة
160
- if not selected_components:
161
- return None
162
-
163
- # استخراج البيانات الأصلية من كائنات العرض
164
- raw_comps = [item['data'] for item in selected_components]
165
- return create_zip(raw_comps)
166
-
167
- # ==========================================
168
- # 🚀 Launch App
169
  # ==========================================
170
 
171
- with gr.Blocks(title="UI Component Extractor", theme=gr.themes.Soft()) as demo:
172
 
173
  gr.Markdown("""
174
- # 🛠️ UI Component Extractor
175
- ### استخرج مكونات الواجهة (أزرار، بطاقات، قوائم) من أي موقع ويب واحصل على كود نظيف.
176
- > ⚠️ **ملاحظة:** هذه نسخة تجريبية (MVP) تعتمد على التحليل الثابت. بعض المواقع الديناميكية قد تحتاج لمعالجة خاصة.
177
  """)
178
 
179
  with gr.Row():
180
- url_input = gr.Textbox(label="🔗 Website URL", placeholder="example.com", scale=2)
181
- framework_select = gr.Dropdown(choices=["html", "react"], value="html", label="⚙️ Output Format", scale=1)
182
- extract_btn = gr.Button("🚀 Extract Components", variant="primary", scale=0)
183
 
184
- status_text = gr.Textbox(label="Status", interactive=False)
185
 
186
- with gr.Row():
187
- # المكون السحري: Gallery تعرض المكونات وتسمح بالاختيار
188
- # نستخدم خاصية show_label=False لإخفاء العناوين الداخلية للكود
189
- component_gallery = gr.Gallery(
190
- label="Detected Components",
191
- show_label=True,
192
- columns=[3],
193
- rows=[2],
194
- object_fit="contain",
195
- height="auto"
196
- )
 
197
 
198
  with gr.Row():
199
- download_btn = gr.Button("📥 Download Selected as ZIP", variant="secondary")
200
- output_file = gr.File(label="Download Ready")
 
 
 
201
 
202
- # ربط الأحداث (Events)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  extract_btn.click(
204
- fn=process_url,
205
- inputs=[url_input, framework_select],
206
- outputs=[component_gallery, output_file, status_text]
 
 
 
 
 
 
207
  )
208
 
209
- # عند اختيار عناصر من المعرض، نجهزها للتحميل
210
- # ملاحظة: Gallery selection returns list of dicts
211
  download_btn.click(
212
- fn=download_selected,
213
- inputs=[component_gallery],
214
  outputs=[output_file]
215
  )
216
 
 
1
  import gradio as gr
2
+ import asyncio
3
+ from playwright.async_api import async_playwright
4
+ from bs4 import BeautifulSoup
5
  import re
6
  import zipfile
7
  import io
 
 
 
8
 
9
  # ==========================================
10
+ # 🔧 Playwright & Parsing Logic
11
  # ==========================================
12
 
13
+ async def scan_page_for_components(url):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  """
15
+ الدالة الأساسية: تفتح الموقع في متصفح حقيقي، تنتظر التحميل، وتستخرج المكونات
16
  """
17
  components = []
18
 
19
+ async with async_playwright() as p:
20
+ # تشغيل المتصفح في وضع الرأس (Headless)
21
+ browser = await p.chromium.launch(headless=True)
22
+ context = await browser.new_context(
23
+ viewport={'width': 1920, 'height': 1080},
24
+ user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
25
+ )
26
+ page = await context.new_page()
27
+
28
+ try:
29
+ # الذهاب للرابط والانتظار حتى يصبح المحتوى تفاعلياً
30
+ await page.goto(url, wait_until="networkidle", timeout=30000)
31
+
32
+ # تمرير الصفحة لأسفل وأعلى لتحميل العناصر الكسولة (Lazy Load)
33
+ await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
34
+ await page.wait_for_timeout(1000)
35
+ await page.evaluate("window.scrollTo(0, 0)")
36
+ await page.wait_for_timeout(500)
37
+
38
+ # الحصول على محتوى HTML بعد تنفيذ الجافاسكريبت
39
+ html_content = await page.content()
40
+ soup = BeautifulSoup(html_content, 'html.parser')
41
+
42
+ # --- خوارزمية الاكتشاف ---
43
+
44
+ # 1. اكتشاف الأزرار (Buttons)
45
+ buttons = soup.find_all(['button', 'a'], class_=re.compile(r'btn|button', re.I))
46
+ for btn in buttons:
47
+ text = btn.get_text(strip=True)
48
+ if text and len(text) < 30: # تجاهل النصوص الطويلة جداً
49
+ components.append({
50
+ 'type': '🔘 Button',
51
+ 'html': str(btn),
52
+ 'preview': text,
53
+ 'classes': btn.get('class', [])
54
+ })
55
+
56
+ # 2. اكتشاف البطاقات (Cards)
57
+ # نبحث عن حاويات تحتوي على صورة + نص، أو كلاسات تدل على بطاقة
58
+ card_candidates = soup.find_all(class_=re.compile(r'card|product|item|post|entry', re.I))
59
+ for card in card_candidates:
60
+ # نتجنب العناصر الصغيرة جداً أو المخفية
61
+ if card.find('img') or card.find(['h1', 'h2', 'h3']):
62
+ # محاولة تجنب التداخل (نأخذ العنصر الأب فقط)
63
+ parent_is_card = False
64
+ for parent in card.parents:
65
+ if parent.get('class') and any('card' in str(c).lower() for c in parent.get('class')):
66
+ parent_is_card = True
67
+ break
68
+
69
+ if not parent_is_card:
70
+ components.append({
71
+ 'type': '🃏 Card',
72
+ 'html': str(card),
73
+ 'preview': 'Card Component',
74
+ 'classes': card.get('class', [])
75
+ })
76
+
77
+ # 3. اكتشاف شريط التنقل (Navbar)
78
+ navs = soup.find_all(['nav', 'header'], class_=re.compile(r'nav|header|menu', re.I))
79
+ for nav in navs:
80
  components.append({
81
+ 'type': '🧭 Navbar',
82
+ 'html': str(nav),
83
+ 'preview': 'Navigation Bar',
84
+ 'classes': nav.get('class', [])
85
  })
86
+
87
+ # 4. أقسام Hero
88
+ sections = soup.find_all('section')
89
+ for sec in sections:
90
+ if sec.get('class') and any('hero' in str(c).lower() for c in sec.get('class')):
91
+ components.append({
92
+ 'type': '🎨 Hero Section',
93
+ 'html': str(sec),
94
+ 'preview': 'Hero Section',
95
+ 'classes': sec.get('class', [])
96
+ })
97
+
98
+ except Exception as e:
99
+ print(f"Error scanning page: {e}")
100
+ components.append({
101
+ 'type': '⚠️ Error',
102
+ 'html': f'<div style="color:red">{str(e)}</div>',
103
+ 'preview': 'Failed to load',
104
+ 'classes': []
 
105
  })
106
+ finally:
107
+ await browser.close()
108
+
109
  return components
110
 
111
+ def clean_html_for_display(html_str):
112
+ """تنظيف الكود للعرض في الواجهة"""
113
+ # إزالة السكربتات والستايل لتجنب مشاكل العرض في Gradio
114
+ soup = BeautifulSoup(html_str, 'html.parser')
115
+ for tag in soup(['script', 'style']):
116
+ tag.decompose()
117
+ return str(soup)
118
+
119
+ def generate_react_code(html_str, classes):
120
+ """تحويل بسيط إلى React + Tailwind"""
121
+ # إزالة السمات غير المتوافقة مع JSX
122
+ clean = html_str.replace('class=', 'className=')
123
+ clean = clean.replace('for=', 'htmlFor=')
124
+ # إضافة تعليق يوضح الكلاسات المستخرجة للمساعدة في التخصيص
125
+ return f"""import React from 'react';
126
+
127
+ export default function ExtractedComponent() {{
128
+ return (
129
+ <>
130
+ {/* Detected classes: {' '.join(classes)} */}
131
+ {clean}
132
+ </>
133
+ );
134
+ }}
135
+ """
136
+
137
+ def create_zip_file(components):
138
+ """إنشاء ملف التحميل"""
139
  zip_buffer = io.BytesIO()
140
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
141
  for i, comp in enumerate(components):
142
+ # حفظ نسخة HTML
143
+ zip_file.writestr(f"component_{i+1}.html", comp['html'])
144
+ # حفظ نسخة React
145
+ react_code = generate_react_code(comp['html'], comp['classes'])
146
+ zip_file.writestr(f"component_{i+1}.jsx", react_code)
147
  zip_buffer.seek(0)
148
  return zip_buffer
149
 
150
  # ==========================================
151
+ # 🎨 Gradio Interface
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  # ==========================================
153
 
154
+ with gr.Blocks(title="UI Extractor Pro", theme=gr.themes.Soft()) as demo:
155
 
156
  gr.Markdown("""
157
+ # 🚀 UI Component Extractor (Docker Edition)
158
+ ### يستخرج المكونات من المواقع الديناميكية باستخدام Playwright.
 
159
  """)
160
 
161
  with gr.Row():
162
+ url_input = gr.Textbox(label="🔗 Website URL", placeholder="https://example.com", scale=2)
163
+ extract_btn = gr.Button("🔍 Scan & Extract", variant="primary", scale=0)
 
164
 
165
+ status_box = gr.Textbox(label="Status", interactive=False, value="Ready")
166
 
167
+ # Gallery لعرض النتائج
168
+ # نستخدم خاصية 'preview' كنص بديل للصورة لأننا لا نولد لقطات شاشة لتوفير الموارد
169
+ gallery = gr.Gallery(
170
+ label="Detected Components",
171
+ show_label=True,
172
+ columns=[3],
173
+ height="auto",
174
+ object_fit="contain"
175
+ )
176
+
177
+ with gr.Accordion("📄 View Code", open=False):
178
+ code_display = gr.Code(label="Component Source Code", language="html")
179
 
180
  with gr.Row():
181
+ download_btn = gr.Button("📥 Download Selected (ZIP)", variant="secondary")
182
+ output_file = gr.File(label="Download Link", interactive=False)
183
+
184
+ # State لتخزين البيانات الكاملة للمكونات (لأن المعرض يعرض فقط ما نريده للعرض)
185
+ components_state = gr.State([])
186
 
187
+ def on_extract(url):
188
+ if not url.startswith('http'):
189
+ return [], [], "Please enter a valid URL starting with http/https", ""
190
+
191
+ yield [], [], "🔄 Connecting to browser and loading page...", ""
192
+
193
+ # تشغيل الدالة غير المتزامنة
194
+ results = asyncio.run(scan_page_for_components(url))
195
+
196
+ if not results:
197
+ yield [], [], "⚠️ No components found. Try a different site.", ""
198
+ return
199
+
200
+ # تجهيز البيانات للمعرض
201
+ gallery_items = []
202
+ for comp in results:
203
+ gallery_items.append({
204
+ "name": f"{comp['type']} - {comp['preview']}",
205
+ # نمرر كود نظيف جداً كـ "صورة" نصية لأننا لا نستخدم screenshots لتوفير الرام
206
+ # في تطبيق حقيقي، نستخدم page.screenshot للعنصر
207
+ "image": None,
208
+ "data": comp
209
+ })
210
+
211
+ yield gallery_items, gallery_items, f"✅ Found {len(results)} components!", ""
212
+
213
+ def on_select(evt: gr.SelectData, gallery_list):
214
+ """عند النقر على عنصر في المعرض، اعرض الكود"""
215
+ if evt.index is not None and evt.index < len(gallery_list):
216
+ selected = gallery_list[evt.index]['data']
217
+ # نعرض الكود الخام
218
+ return clean_html_for_display(selected['html'])
219
+ return ""
220
+
221
+ def on_download(selected_list):
222
+ if not selected_list:
223
+ return None
224
+ # استخراج البيانات الأصلية
225
+ raw_data = [item['data'] for item in selected_list]
226
+ return create_zip_file(raw_data)
227
+
228
+ # ربط الأحداث
229
  extract_btn.click(
230
+ fn=on_extract,
231
+ inputs=url_input,
232
+ outputs=[gallery, components_state, status_box, code_display]
233
+ )
234
+
235
+ gallery.select(
236
+ fn=on_select,
237
+ inputs=[components_state],
238
+ outputs=[code_display]
239
  )
240
 
 
 
241
  download_btn.click(
242
+ fn=on_download,
243
+ inputs=[components_state],
244
  outputs=[output_file]
245
  )
246