limitedonly41 commited on
Commit
c605e5b
·
verified ·
1 Parent(s): db990e2

Upload app_ultra_compact.py

Browse files
Files changed (1) hide show
  1. app_ultra_compact.py +454 -0
app_ultra_compact.py ADDED
@@ -0,0 +1,454 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import io
4
+ import re
5
+ from typing import List, Optional, Tuple
6
+ import logging
7
+
8
+ # Настройка логирования
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class WebsiteCategorizerApp:
13
+ def __init__(self):
14
+ self.sheet_url = ""
15
+ self.sheet_data = []
16
+ self.current_index = 0
17
+ self.categories = ["новости", "коммерция", "другое"]
18
+ self.results_data = [] # Для сохранения результатов
19
+
20
+ def convert_google_sheet_url(self, sheet_url: str) -> str:
21
+ """Конвертирует URL Google таблицы в CSV экспорт URL"""
22
+ try:
23
+ # Различные форматы URL Google таблиц
24
+ if "/edit#gid=" in sheet_url:
25
+ csv_url = sheet_url.replace("/edit#gid=", "/export?format=csv&gid=")
26
+ elif "/edit?usp=sharing" in sheet_url:
27
+ csv_url = sheet_url.replace("/edit?usp=sharing", "/export?format=csv")
28
+ elif "/edit" in sheet_url:
29
+ csv_url = sheet_url.replace("/edit", "/export?format=csv")
30
+ else:
31
+ # Если URL уже в формате экспорта
32
+ csv_url = sheet_url
33
+
34
+ return csv_url
35
+ except Exception as e:
36
+ logger.error(f"Ошибка конвертации URL: {e}")
37
+ return ""
38
+
39
+ def connect_to_sheet(self, sheet_url: str) -> Tuple[str, str]:
40
+ """Подключается к Google таблице через CSV экспорт"""
41
+ try:
42
+ if not sheet_url:
43
+ return "❌ Ошибка: Введите URL Google таблицы", ""
44
+
45
+ # Конвертируем URL в CSV формат
46
+ csv_url = self.convert_google_sheet_url(sheet_url)
47
+
48
+ if not csv_url:
49
+ return "❌ Ошибка: Неверный формат URL", ""
50
+
51
+ # Загружаем данные через pandas
52
+ df = pd.read_csv(csv_url)
53
+
54
+ if df.empty:
55
+ return "❌ Ошибка: Таблица пуста", ""
56
+
57
+ # Убедимся что есть минимум 2 столбца
58
+ if len(df.columns) < 2:
59
+ return "❌ Ошибка: Нужно минимум 2 столбца (URL и категория)", ""
60
+
61
+ # Сохранение данных
62
+ self.sheet_data = []
63
+ self.results_data = []
64
+
65
+ # Получаем названия столбцов
66
+ url_column = df.columns[0]
67
+ category_column = df.columns[1]
68
+
69
+ for index, row in df.iterrows():
70
+ url = str(row[url_column]).strip() if pd.notna(row[url_column]) else ""
71
+ category = str(row[category_column]).strip() if pd.notna(row[category_column]) else ""
72
+
73
+ if url and url.lower() not in ['url', 'nan']:
74
+ self.sheet_data.append({
75
+ "index": index,
76
+ "url": url,
77
+ "category": category if category.lower() != 'nan' else ""
78
+ })
79
+
80
+ # Добавляем в результаты
81
+ self.results_data.append({
82
+ "url": url,
83
+ "category": category if category.lower() != 'nan' else ""
84
+ })
85
+
86
+ if not self.sheet_data:
87
+ return "❌ Ошибка: Не найдены валидные URL", ""
88
+
89
+ self.current_index = 0
90
+ self.sheet_url = sheet_url
91
+
92
+ success_msg = f"✅ Подключено успешно! Найдено {len(self.sheet_data)} записей"
93
+ first_url = self.get_current_url_for_display()
94
+
95
+ return success_msg, first_url
96
+
97
+ except Exception as e:
98
+ logger.error(f"Ошибка подключения к таблице: {e}")
99
+ error_msg = f"❌ Ошибка: {str(e)}"
100
+ error_msg += "\n\nУбедитесь что:\n- Таблица публичная (доступна всем по ссылке)\n- URL корректный"
101
+ return error_msg, ""
102
+
103
+ def get_current_url_for_display(self) -> str:
104
+ """Возвращает текущий URL для отображения в iframe"""
105
+ if not self.sheet_data or self.current_index >= len(self.sheet_data):
106
+ return ""
107
+
108
+ url = self.sheet_data[self.current_index]["url"]
109
+
110
+ # Добавляем http:// если протокол не указан
111
+ if url and not url.startswith(("http://", "https://")):
112
+ url = "http://" + url
113
+
114
+ return url
115
+
116
+ def get_current_info(self) -> Tuple[str, str, str]:
117
+ """Возвращает информацию о текущей записи"""
118
+ if not self.sheet_data:
119
+ return "", "", "Нет данных"
120
+
121
+ if self.current_index >= len(self.sheet_data):
122
+ self.current_index = 0
123
+
124
+ current = self.sheet_data[self.current_index]
125
+ url = current["url"]
126
+ category = current["category"]
127
+
128
+ info = f"{self.current_index + 1}/{len(self.sheet_data)}"
129
+
130
+ return url, category, info
131
+
132
+ def navigate_to_index(self, index: int) -> Tuple[str, str, str, str]:
133
+ """Переходит к записи по индексу"""
134
+ if not self.sheet_data:
135
+ return "", "", "", "Нет данных"
136
+
137
+ # Ограничиваем индекс допустимыми значениями
138
+ index = max(0, min(index, len(self.sheet_data) - 1))
139
+ self.current_index = index
140
+
141
+ url, category, info = self.get_current_info()
142
+ iframe_url = self.get_current_url_for_display()
143
+
144
+ return url, category, info, iframe_url
145
+
146
+ def previous_record(self) -> Tuple[str, str, str, str]:
147
+ """Переход к предыдущей записи"""
148
+ if not self.sheet_data:
149
+ return "", "", "", "Нет данных"
150
+
151
+ if self.current_index > 0:
152
+ self.current_index -= 1
153
+ else:
154
+ self.current_index = len(self.sheet_data) - 1
155
+
156
+ return self.navigate_to_index(self.current_index)
157
+
158
+ def next_record(self) -> Tuple[str, str, str, str]:
159
+ """Переход к следующей записи"""
160
+ if not self.sheet_data:
161
+ return "", "", "", "Нет данных"
162
+
163
+ if self.current_index < len(self.sheet_data) - 1:
164
+ self.current_index += 1
165
+ else:
166
+ self.current_index = 0
167
+
168
+ return self.navigate_to_index(self.current_index)
169
+
170
+ def save_category(self, category: str) -> Tuple[str, str]:
171
+ """Сохраняет категорию локально и возвращает CSV для скачивания"""
172
+ if not self.sheet_data:
173
+ return "❌ Нет данных для сохранения", ""
174
+
175
+ try:
176
+ # Обновляем локальные данные
177
+ self.sheet_data[self.current_index]["category"] = category
178
+ self.results_data[self.current_index]["category"] = category
179
+
180
+ # Создаем CSV файл с результатами
181
+ df_results = pd.DataFrame(self.results_data)
182
+ csv_buffer = io.StringIO()
183
+ df_results.to_csv(csv_buffer, index=False, encoding='utf-8')
184
+ csv_content = csv_buffer.getvalue()
185
+
186
+ status_msg = f"✅ '{category}' сохранено"
187
+
188
+ return status_msg, csv_content
189
+
190
+ except Exception as e:
191
+ logger.error(f"Ошибка сохранения категории: {e}")
192
+ return f"❌ Ошибка: {str(e)}", ""
193
+
194
+ def export_results(self) -> str:
195
+ """Экспортирует все результаты в CSV"""
196
+ if not self.results_data:
197
+ return ""
198
+
199
+ df_results = pd.DataFrame(self.results_data)
200
+ csv_buffer = io.StringIO()
201
+ df_results.to_csv(csv_buffer, index=False, encoding='utf-8')
202
+ return csv_buffer.getvalue()
203
+
204
+ # Создаем экземпляр приложения
205
+ app = WebsiteCategorizerApp()
206
+
207
+ # CSS для ультра-компактного дизайна
208
+ ultra_compact_css = """
209
+ /* Минимальная левая панель */
210
+ .sidebar {
211
+ background: #f8f9fa;
212
+ border-radius: 6px;
213
+ padding: 8px;
214
+ font-size: 12px;
215
+ }
216
+
217
+ .sidebar h4 {
218
+ font-size: 13px;
219
+ margin: 4px 0 2px 0;
220
+ color: #495057;
221
+ }
222
+
223
+ .sidebar .gradio-textbox,
224
+ .sidebar .gradio-dropdown {
225
+ font-size: 12px;
226
+ }
227
+
228
+ .sidebar button {
229
+ padding: 4px 8px;
230
+ font-size: 11px;
231
+ margin: 2px 0;
232
+ }
233
+
234
+ /* Большое окно просмотра */
235
+ .main-viewer {
236
+ border: 1px solid #dee2e6;
237
+ border-radius: 8px;
238
+ overflow: hidden;
239
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
240
+ }
241
+
242
+ .compact-status {
243
+ font-size: 11px;
244
+ padding: 4px 6px;
245
+ margin: 2px 0;
246
+ border-radius: 3px;
247
+ }
248
+
249
+ /* Убираем лишние отступы */
250
+ .gradio-row {
251
+ gap: 8px;
252
+ }
253
+
254
+ .gradio-column {
255
+ gap: 6px;
256
+ }
257
+ """
258
+
259
+ # Создание ультра-компактного интерфейса
260
+ with gr.Blocks(
261
+ title="Категоризатор сайтов",
262
+ css=ultra_compact_css,
263
+ theme=gr.themes.Soft()
264
+ ) as demo:
265
+
266
+ # Минимальный заголовок
267
+ gr.HTML("<h2 style='text-align: center; margin: 8px 0; color: #2d5aa0;'>🌐 Категоризатор сайтов</h2>")
268
+
269
+ with gr.Row():
270
+ # СВЕРХ-КОМПАКТНАЯ ЛЕВАЯ ПАНЕЛЬ (scale=1, max_width=280px)
271
+ with gr.Column(scale=1, max_width=280, elem_classes="sidebar"):
272
+
273
+ # Подключение
274
+ gr.HTML("<h4>📊 Подключение</h4>")
275
+ sheet_url_input = gr.Textbox(
276
+ label="",
277
+ placeholder="URL Google таблицы",
278
+ lines=1,
279
+ show_label=False
280
+ )
281
+ connect_btn = gr.Button("🔗 Подключить", variant="primary", size="sm")
282
+ connection_status = gr.HTML("", elem_classes="compact-status")
283
+
284
+ gr.HTML("<br>")
285
+
286
+ # Навигация
287
+ gr.HTML("<h4>🧭 Навигация</h4>")
288
+ with gr.Row():
289
+ prev_btn = gr.Button("⬅️", size="sm", scale=1)
290
+ record_info = gr.HTML("", scale=2, elem_classes="compact-status")
291
+ next_btn = gr.Button("➡️", size="sm", scale=1)
292
+
293
+ current_url_display = gr.Textbox(
294
+ label="",
295
+ placeholder="URL",
296
+ interactive=False,
297
+ lines=1,
298
+ show_label=False
299
+ )
300
+
301
+ gr.HTML("<br>")
302
+
303
+ # Категория
304
+ gr.HTML("<h4>🏷️ Категория</h4>")
305
+ category_dropdown = gr.Dropdown(
306
+ choices=app.categories,
307
+ label="",
308
+ show_label=False
309
+ )
310
+ save_status = gr.HTML("", elem_classes="compact-status")
311
+
312
+ gr.HTML("<br>")
313
+
314
+ # Экспорт
315
+ gr.HTML("<h4>💾 Экспорт</h4>")
316
+ export_btn = gr.Button("📥 CSV", variant="secondary", size="sm")
317
+ export_file = gr.File(visible=False)
318
+
319
+ # Мини-инструкция
320
+ gr.HTML("""
321
+ <div style='font-size: 10px; color: #6c757d; margin-top: 8px; padding: 4px; background: #e9ecef; border-radius: 3px;'>
322
+ 💡 <b>Быстрая помощь:</b><br>
323
+ 1. Публичная Google таблица<br>
324
+ 2. A: URL, B: категории<br>
325
+ 3. Навигация ⬅️➡️<br>
326
+ 4. Выбор категории, экспорт
327
+ </div>
328
+ """)
329
+
330
+ # МАКСИМАЛЬНОЕ ОКНО ПРОСМОТРА (scale=6)
331
+ with gr.Column(scale=6):
332
+ # Очень большое окно - 900px высота, во всю ширину
333
+ website_viewer = gr.HTML(
334
+ value="""
335
+ <div style='height: 900px; display: flex; flex-direction: column; align-items: center; justify-content: center;
336
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
337
+ border-radius: 8px; color: white; text-align: center;'>
338
+ <h1 style='margin-bottom: 24px; font-size: 32px;'>🚀 Добро пожаловать!</h1>
339
+ <p style='font-size: 24px; margin-bottom: 16px;'>Подключите Google таблицу</p>
340
+ <p style='font-size: 18px; opacity: 0.8;'>Максимальное окно для удобного просмотра</p>
341
+ <p style='font-size: 16px; opacity: 0.6; margin-top: 24px;'>900px высота × полная ширина экрана</p>
342
+ </div>
343
+ """,
344
+ elem_classes="main-viewer"
345
+ )
346
+
347
+ # Скрытое состояние для CSV данных
348
+ csv_data = gr.State("")
349
+
350
+ # Обработчики событий (адаптированные под компактный интерфейс)
351
+ def handle_connect(url):
352
+ """Обработчик подключения к таблице"""
353
+ status, iframe_url = app.connect_to_sheet(url)
354
+
355
+ if "✅" in status: # Успешное подключение
356
+ url_display, category, info = app.get_current_info()
357
+
358
+ if iframe_url:
359
+ # Максимальный iframe - 900px высота
360
+ iframe_html = f'<iframe src="{iframe_url}" width="100%" height="900px" frameborder="0" style="border-radius: 8px;"></iframe>'
361
+ else:
362
+ iframe_html = "<div style='height: 900px; display: flex; align-items: center; justify-content: center; background: #f8d7da; border: 2px dashed #dc3545; border-radius: 8px; color: #721c24;'><p style='font-size: 18px;'>❌ URL не найден или некорректен</p></div>"
363
+
364
+ info_html = f'<span style="color: green; font-weight: bold;">{info}</span>'
365
+
366
+ return (
367
+ gr.HTML(f'<div class="compact-status" style="color: green; background: #d4edda; border: 1px solid #c3e6cb;">✅ Готово!</div>'),
368
+ iframe_html,
369
+ url_display,
370
+ category,
371
+ info_html
372
+ )
373
+ else: # Ошибка
374
+ return (
375
+ gr.HTML(f'<div class="compact-status" style="color: #dc3545; background: #f8d7da; border: 1px solid #f5c6cb;">❌ Ошибка</div>'),
376
+ website_viewer.value,
377
+ "",
378
+ "",
379
+ gr.HTML("")
380
+ )
381
+
382
+ def handle_navigation(direction):
383
+ """Обработчик навигации"""
384
+ if direction == "next":
385
+ url_display, category, info, iframe_url = app.next_record()
386
+ else:
387
+ url_display, category, info, iframe_url = app.previous_record()
388
+
389
+ if iframe_url:
390
+ iframe_html = f'<iframe src="{iframe_url}" width="100%" height="900px" frameborder="0" style="border-radius: 8px;"></iframe>'
391
+ else:
392
+ iframe_html = "<div style='height: 900px; display: flex; align-items: center; justify-content: center; background: #f8d7da; border: 2px dashed #dc3545; border-radius: 8px; color: #721c24;'><p style='font-size: 18px;'>❌ URL не найден</p></div>"
393
+
394
+ info_html = f'<span style="color: green; font-size: 11px; font-weight: bold;">{info}</span>'
395
+
396
+ return (
397
+ iframe_html,
398
+ url_display,
399
+ category,
400
+ info_html
401
+ )
402
+
403
+ def handle_category_change(category):
404
+ """Обработчик изменения категории"""
405
+ if category:
406
+ status, csv_content = app.save_category(category)
407
+ if "✅" in status:
408
+ status_html = f'<div class="compact-status" style="color: green; background: #d4edda; border: 1px solid #c3e6cb;">{status}</div>'
409
+ else:
410
+ status_html = f'<div class="compact-status" style="color: #dc3545; background: #f8d7da; border: 1px solid #f5c6cb;">{status}</div>'
411
+ return status_html, csv_content
412
+ return gr.HTML(""), ""
413
+
414
+ def handle_export():
415
+ """Обработчик экспорта результатов"""
416
+ csv_content = app.export_results()
417
+ if csv_content:
418
+ # Сохраняем во временный файл
419
+ with open("results.csv", "w", encoding="utf-8") as f:
420
+ f.write(csv_content)
421
+ return gr.File(value="results.csv", visible=True)
422
+ return gr.File(visible=False)
423
+
424
+ # Привязка событий
425
+ connect_btn.click(
426
+ handle_connect,
427
+ inputs=[sheet_url_input],
428
+ outputs=[connection_status, website_viewer, current_url_display, category_dropdown, record_info]
429
+ )
430
+
431
+ next_btn.click(
432
+ lambda: handle_navigation("next"),
433
+ outputs=[website_viewer, current_url_display, category_dropdown, record_info]
434
+ )
435
+
436
+ prev_btn.click(
437
+ lambda: handle_navigation("previous"),
438
+ outputs=[website_viewer, current_url_display, category_dropdown, record_info]
439
+ )
440
+
441
+ category_dropdown.change(
442
+ handle_category_change,
443
+ inputs=[category_dropdown],
444
+ outputs=[save_status, csv_data]
445
+ )
446
+
447
+ export_btn.click(
448
+ handle_export,
449
+ outputs=[export_file]
450
+ )
451
+
452
+ # Запуск приложения
453
+ if __name__ == "__main__":
454
+ demo.launch()