limitedonly41 commited on
Commit
5bc7e2e
·
verified ·
1 Parent(s): 810f098

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +79 -216
app.py CHANGED
@@ -1,7 +1,6 @@
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
 
@@ -15,54 +14,41 @@ class WebsiteCategorizerApp:
15
  self.sheet_data = []
16
  self.current_index = 0
17
  self.categories = ["NEWS/BLOG", "E-commerce", "OTHER", "COMPANIES", "Short"]
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
 
@@ -76,8 +62,6 @@ class WebsiteCategorizerApp:
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 ""
@@ -89,288 +73,167 @@ class WebsiteCategorizerApp:
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
- # Создание простого и надежного интерфейса
208
  with gr.Blocks(title="Категоризатор сайтов", theme=gr.themes.Soft()) as demo:
209
-
210
- # Заголовок
211
- gr.HTML("<h2 style='text-align: center; margin: 10px 0; color: #2d5aa0;'>🌐 Категоризатор сайтов</h2>")
212
-
213
- with gr.Row():
214
- # КОМПАКТНАЯ ЛЕВАЯ ПАНЕЛЬ (scale=1 - минимальный размер)
215
- with gr.Column(scale=1):
216
-
217
- # Подключение
218
- gr.HTML("<h4 style='font-size: 14px; margin: 5px 0;'>📊 Подключение</h4>")
219
- sheet_url_input = gr.Textbox(
220
- label="URL Google таблицы",
221
- placeholder="https://docs.google.com/spreadsheets/...",
222
- lines=2
223
- )
224
- connect_btn = gr.Button("🔗 Подключить", variant="primary")
225
- connection_status = gr.HTML("")
226
-
227
- # Навигация
228
- gr.HTML("<h4 style='font-size: 14px; margin: 5px 0;'>🧭 Навигация</h4>")
229
  with gr.Row():
230
- prev_btn = gr.Button("⬅️", size="sm")
231
- next_btn = gr.Button("➡️", size="sm")
232
-
233
- record_info = gr.HTML("")
234
- current_url_display = gr.Textbox(
235
- label="Текущий URL",
236
- interactive=False,
237
- lines=1
238
- )
239
-
240
- # Категория
241
- gr.HTML("<h4 style='font-size: 14px; margin: 5px 0;'>🏷️ Категория</h4>")
242
- category_dropdown = gr.Dropdown(
243
- choices=app.categories,
244
- label="Выберите категорию"
245
- )
246
- save_status = gr.HTML("")
247
-
248
- # Экспорт
249
- gr.HTML("<h4 style='font-size: 14px; margin: 5px 0;'>💾 Экспорт</h4>")
250
- export_btn = gr.Button("📥 Скачать CSV")
251
- export_file = gr.File(visible=False)
252
-
253
- # БОЛЬШОЕ ОКНО ПРОСМОТРА (scale=5 - в 5 раз больше левой)
254
- with gr.Column(scale=5):
255
- gr.HTML("<h3 style='margin-bottom: 8px;'>🌍 Предварительный просмотр</h3>")
256
-
257
- # Большое окно - 900px высота
258
- website_viewer = gr.HTML(
259
- value="""
260
- <div style='height: 900px; display: flex; flex-direction: column; align-items: center; justify-content: center;
261
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
262
- border: 2px dashed #ccc; border-radius: 12px; color: white; text-align: center;'>
263
- <h1 style='margin-bottom: 24px;'>🚀 Добро пожаловать!</h1>
264
- <p style='font-size: 20px; margin-bottom: 16px;'>Подключите Google таблицу</p>
265
- <p style='font-size: 16px; opacity: 0.8;'>Окно просмотра 900px высотой</p>
266
- </div>
267
- """
268
  )
 
269
 
270
- # Скрытое состояние для CSV данных
271
  csv_data = gr.State("")
272
 
273
- # Обработчики событий
274
  def handle_connect(url):
275
- """Обработчик подключения к таблице"""
276
  status, iframe_url = app.connect_to_sheet(url)
277
-
278
- if "✅" in status: # Успешное подключение
279
  url_display, category, info = app.get_current_info()
280
-
281
- if iframe_url:
282
- iframe_html = f'<iframe src="{iframe_url}" width="100%" height="900px" frameborder="0" style="border-radius: 8px;"></iframe>'
283
- else:
284
- 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>"
285
-
286
- info_html = f'<div style="color: green; background: #d4edda; padding: 8px; border-radius: 4px; margin: 4px 0;">{info}</div>'
287
-
288
- return (
289
- gr.HTML(f'<div style="color: green; background: #d4edda; padding: 8px; border-radius: 4px; margin: 4px 0;">✅ Готово!</div>'),
290
- iframe_html,
291
- url_display,
292
- category,
293
- info_html
294
- )
295
- else: # Ошибка
296
- return (
297
- gr.HTML(f'<div style="color: #dc3545; background: #f8d7da; padding: 8px; border-radius: 4px; margin: 4px 0;">❌ Ошибка</div>'),
298
- website_viewer.value,
299
- "",
300
- "",
301
- gr.HTML("")
302
- )
303
 
304
  def handle_navigation(direction):
305
- """Обработчик навигации"""
306
  if direction == "next":
307
  url_display, category, info, iframe_url = app.next_record()
308
  else:
309
  url_display, category, info, iframe_url = app.previous_record()
310
-
311
- if iframe_url:
312
- iframe_html = f'<iframe src="{iframe_url}" width="100%" height="900px" frameborder="0" style="border-radius: 8px;"></iframe>'
313
- else:
314
- 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>"
315
-
316
- info_html = f'<div style="color: green; background: #d4edda; padding: 8px; border-radius: 4px; margin: 4px 0;">{info}</div>'
317
-
318
- return (
319
- iframe_html,
320
- url_display,
321
- category,
322
- info_html
323
- )
324
 
325
  def handle_category_change(category):
326
- """Обработчик изменения категории"""
327
- if category:
328
- status, csv_content = app.save_category(category)
329
- if "✅" in status:
330
- status_html = f'<div style="color: green; background: #d4edda; padding: 8px; border-radius: 4px; margin: 4px 0;">{status}</div>'
331
- else:
332
- status_html = f'<div style="color: #dc3545; background: #f8d7da; padding: 8px; border-radius: 4px; margin: 4px 0;">{status}</div>'
333
- return status_html, csv_content
334
- return gr.HTML(""), ""
335
 
336
  def handle_export():
337
- """Обработчик экспорта результатов"""
338
  csv_content = app.export_results()
339
  if csv_content:
340
- # Сохраняем во временный файл
341
  with open("results.csv", "w", encoding="utf-8") as f:
342
  f.write(csv_content)
343
  return gr.File(value="results.csv", visible=True)
344
  return gr.File(visible=False)
345
 
346
- # Привязка событий
 
 
347
  connect_btn.click(
348
  handle_connect,
349
  inputs=[sheet_url_input],
350
  outputs=[connection_status, website_viewer, current_url_display, category_dropdown, record_info]
351
  )
352
 
353
- next_btn.click(
354
- lambda: handle_navigation("next"),
355
- outputs=[website_viewer, current_url_display, category_dropdown, record_info]
356
- )
357
-
358
- prev_btn.click(
359
- lambda: handle_navigation("previous"),
360
- outputs=[website_viewer, current_url_display, category_dropdown, record_info]
361
- )
362
-
363
- category_dropdown.change(
364
- handle_category_change,
365
- inputs=[category_dropdown],
366
- outputs=[save_status, csv_data]
367
- )
368
-
369
- export_btn.click(
370
- handle_export,
371
- outputs=[export_file]
372
- )
 
 
 
373
 
374
- # Запуск приложения
375
  if __name__ == "__main__":
376
  demo.launch()
 
1
  import gradio as gr
2
  import pandas as pd
3
  import io
 
4
  from typing import List, Optional, Tuple
5
  import logging
6
 
 
14
  self.sheet_data = []
15
  self.current_index = 0
16
  self.categories = ["NEWS/BLOG", "E-commerce", "OTHER", "COMPANIES", "Short"]
17
+ self.results_data = []
18
 
19
  def convert_google_sheet_url(self, sheet_url: str) -> str:
 
20
  try:
 
21
  if "/edit#gid=" in sheet_url:
22
+ return sheet_url.replace("/edit#gid=", "/export?format=csv&gid=")
23
  elif "/edit?usp=sharing" in sheet_url:
24
+ return sheet_url.replace("/edit?usp=sharing", "/export?format=csv")
25
  elif "/edit" in sheet_url:
26
+ return sheet_url.replace("/edit", "/export?format=csv")
27
  else:
28
+ return sheet_url
 
 
 
29
  except Exception as e:
30
  logger.error(f"Ошибка конвертации URL: {e}")
31
  return ""
32
 
33
  def connect_to_sheet(self, sheet_url: str) -> Tuple[str, str]:
 
34
  try:
35
  if not sheet_url:
36
  return "❌ Ошибка: Введите URL Google таблицы", ""
37
 
 
38
  csv_url = self.convert_google_sheet_url(sheet_url)
 
39
  if not csv_url:
40
  return "❌ Ошибка: Неверный формат URL", ""
41
 
 
42
  df = pd.read_csv(csv_url)
 
43
  if df.empty:
44
  return "❌ Ошибка: Таблица пуста", ""
45
 
 
46
  if len(df.columns) < 2:
47
  return "❌ Ошибка: Нужно минимум 2 столбца (URL и категория)", ""
48
 
 
49
  self.sheet_data = []
50
  self.results_data = []
51
 
 
52
  url_column = df.columns[0]
53
  category_column = df.columns[1]
54
 
 
62
  "url": url,
63
  "category": category if category.lower() != 'nan' else ""
64
  })
 
 
65
  self.results_data.append({
66
  "url": url,
67
  "category": category if category.lower() != 'nan' else ""
 
73
  self.current_index = 0
74
  self.sheet_url = sheet_url
75
 
76
+ return f"✅ Подключено успешно! Найдено {len(self.sheet_data)} записей", self.get_current_url_for_display()
 
 
 
77
 
78
  except Exception as e:
79
  logger.error(f"Ошибка подключения к таблице: {e}")
80
+ return f"❌ Ошибка: {str(e)}\n\nУбедитесь что таблица публичная и URL корректный", ""
 
 
81
 
82
  def get_current_url_for_display(self) -> str:
 
83
  if not self.sheet_data or self.current_index >= len(self.sheet_data):
84
  return ""
 
85
  url = self.sheet_data[self.current_index]["url"]
 
 
86
  if url and not url.startswith(("http://", "https://")):
87
  url = "http://" + url
 
88
  return url
89
 
90
  def get_current_info(self) -> Tuple[str, str, str]:
 
91
  if not self.sheet_data:
92
  return "", "", "Нет данн��х"
 
93
  if self.current_index >= len(self.sheet_data):
94
  self.current_index = 0
 
95
  current = self.sheet_data[self.current_index]
96
+ return current["url"], current["category"], f"{self.current_index + 1}/{len(self.sheet_data)}"
 
 
 
 
 
97
 
98
  def navigate_to_index(self, index: int) -> Tuple[str, str, str, str]:
 
99
  if not self.sheet_data:
100
  return "", "", "", "Нет данных"
 
 
101
  index = max(0, min(index, len(self.sheet_data) - 1))
102
  self.current_index = index
 
103
  url, category, info = self.get_current_info()
104
+ return url, category, info, self.get_current_url_for_display()
 
 
105
 
106
  def previous_record(self) -> Tuple[str, str, str, str]:
 
107
  if not self.sheet_data:
108
  return "", "", "", "Нет данных"
109
+ self.current_index = (self.current_index - 1) % len(self.sheet_data)
 
 
 
 
 
110
  return self.navigate_to_index(self.current_index)
111
 
112
  def next_record(self) -> Tuple[str, str, str, str]:
 
113
  if not self.sheet_data:
114
  return "", "", "", "Нет данных"
115
+ self.current_index = (self.current_index + 1) % len(self.sheet_data)
 
 
 
 
 
116
  return self.navigate_to_index(self.current_index)
117
 
118
  def save_category(self, category: str) -> Tuple[str, str]:
 
119
  if not self.sheet_data:
120
  return "❌ Нет данных для сохранения", ""
 
121
  try:
 
122
  self.sheet_data[self.current_index]["category"] = category
123
  self.results_data[self.current_index]["category"] = category
 
 
 
124
  csv_buffer = io.StringIO()
125
+ pd.DataFrame(self.results_data).to_csv(csv_buffer, index=False, encoding='utf-8')
126
+ return f"✅ '{category}' сохранено", csv_buffer.getvalue()
 
 
 
 
 
127
  except Exception as e:
128
  logger.error(f"Ошибка сохранения категории: {e}")
129
  return f"❌ Ошибка: {str(e)}", ""
130
 
131
  def export_results(self) -> str:
 
132
  if not self.results_data:
133
  return ""
 
 
134
  csv_buffer = io.StringIO()
135
+ pd.DataFrame(self.results_data).to_csv(csv_buffer, index=False, encoding='utf-8')
136
  return csv_buffer.getvalue()
137
 
 
138
  app = WebsiteCategorizerApp()
139
 
 
140
  with gr.Blocks(title="Категоризатор сайтов", theme=gr.themes.Soft()) as demo:
141
+ gr.HTML("<h2 style='text-align:center;'>🌐 Категоризатор сайтов</h2>")
142
+ with gr.Tabs():
143
+ with gr.TabItem("Категоризация"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  with gr.Row():
145
+ with gr.Column(scale=1):
146
+ sheet_url_input = gr.Textbox(label="URL Google таблицы", lines=2)
147
+ connect_btn = gr.Button("🔗 Подключить", variant="primary")
148
+ connection_status = gr.HTML("")
149
+ with gr.Row():
150
+ prev_btn = gr.Button("⬅️", elem_id="prev-btn")
151
+ next_btn = gr.Button("➡️", elem_id="next-btn")
152
+ record_info = gr.HTML("")
153
+ current_url_display = gr.Textbox(label="Текущий URL", interactive=False)
154
+ category_dropdown = gr.Dropdown(choices=app.categories, label="Категория")
155
+ save_status = gr.HTML("")
156
+ export_btn = gr.Button("📥 Скачать CSV")
157
+ export_file = gr.File(visible=False)
158
+ with gr.Column(scale=5):
159
+ website_viewer = gr.HTML("""
160
+ <div style='height:900px;display:flex;align-items:center;justify-content:center;background:#eee;border-radius:8px;'>
161
+ <p>Подключите Google таблицу</p>
162
+ </div>
163
+ """)
164
+
165
+ with gr.TabItem("Текущая таблица"):
166
+ table_view = gr.DataFrame(
167
+ value=pd.DataFrame(app.results_data),
168
+ headers=["url", "category"],
169
+ datatype=["str", "str"],
170
+ interactive=True
 
 
 
 
 
 
 
 
 
 
 
 
171
  )
172
+ refresh_table_btn = gr.Button("🔄 Обновить таблицу")
173
 
 
174
  csv_data = gr.State("")
175
 
 
176
  def handle_connect(url):
 
177
  status, iframe_url = app.connect_to_sheet(url)
178
+ if "✅" in status:
 
179
  url_display, category, info = app.get_current_info()
180
+ iframe_html = f'<iframe src="{iframe_url}" width="100%" height="900px" style="border-radius:8px;"></iframe>'
181
+ return status, iframe_html, url_display, category, info
182
+ else:
183
+ return status, website_viewer.value, "", "", ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
  def handle_navigation(direction):
 
186
  if direction == "next":
187
  url_display, category, info, iframe_url = app.next_record()
188
  else:
189
  url_display, category, info, iframe_url = app.previous_record()
190
+ iframe_html = f'<iframe src="{iframe_url}" width="100%" height="900px" style="border-radius:8px;"></iframe>'
191
+ return iframe_html, url_display, category, info
 
 
 
 
 
 
 
 
 
 
 
 
192
 
193
  def handle_category_change(category):
194
+ status, csv_content = app.save_category(category)
195
+ return status, csv_content
 
 
 
 
 
 
 
196
 
197
  def handle_export():
 
198
  csv_content = app.export_results()
199
  if csv_content:
 
200
  with open("results.csv", "w", encoding="utf-8") as f:
201
  f.write(csv_content)
202
  return gr.File(value="results.csv", visible=True)
203
  return gr.File(visible=False)
204
 
205
+ def refresh_table():
206
+ return pd.DataFrame(app.results_data)
207
+
208
  connect_btn.click(
209
  handle_connect,
210
  inputs=[sheet_url_input],
211
  outputs=[connection_status, website_viewer, current_url_display, category_dropdown, record_info]
212
  )
213
 
214
+ next_btn.click(lambda: handle_navigation("next"),
215
+ outputs=[website_viewer, current_url_display, category_dropdown, record_info])
216
+ prev_btn.click(lambda: handle_navigation("previous"),
217
+ outputs=[website_viewer, current_url_display, category_dropdown, record_info])
218
+ category_dropdown.change(handle_category_change,
219
+ inputs=[category_dropdown],
220
+ outputs=[save_status, csv_data])
221
+ export_btn.click(handle_export, outputs=[export_file])
222
+ refresh_table_btn.click(refresh_table, outputs=[table_view])
223
+
224
+ # JS для стрелок
225
+ gr.HTML("""
226
+ <script>
227
+ document.addEventListener('keydown', function(event) {
228
+ if (event.key === "ArrowRight") {
229
+ document.getElementById('next-btn')?.click();
230
+ }
231
+ if (event.key === "ArrowLeft") {
232
+ document.getElementById('prev-btn')?.click();
233
+ }
234
+ });
235
+ </script>
236
+ """)
237
 
 
238
  if __name__ == "__main__":
239
  demo.launch()