PDF-Table-Extractor

#1
Files changed (12) hide show
  1. .dockerignore +0 -0
  2. .env.example +0 -0
  3. .gitattributes +35 -0
  4. .gitignore +0 -41
  5. Dockerfile +0 -22
  6. README.md +7 -40
  7. app.py +0 -346
  8. prepare_hf.py +0 -149
  9. requirements.txt +0 -5
  10. static/script.js +0 -0
  11. static/style.css +0 -1646
  12. templates/index.html +0 -401
.dockerignore DELETED
Binary file (388 Bytes)
 
.env.example DELETED
Binary file (202 Bytes)
 
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore DELETED
@@ -1,41 +0,0 @@
1
- # Python
2
- __pycache__/
3
- *.pyc
4
- *.pyo
5
- *.class
6
- *.so
7
- .Python
8
- env/
9
- venv/
10
- .venv/
11
- ENV/
12
-
13
- # Virtual environment
14
- Lib/
15
- Scripts/
16
- pyvenv.cfg
17
-
18
- # IDE
19
- .idea/
20
- .vscode/
21
- *.swp
22
- *.swo
23
-
24
- # OS
25
- .DS_Store
26
- Thumbs.db
27
-
28
- # Логи и БД
29
- *.log
30
- *.sqlite3
31
- *.db
32
-
33
- # Папки проекта
34
- uploads/
35
- temp/
36
- test_files/
37
-
38
- # PyCharm
39
- *.iml
40
- *.ipr
41
- *.iws
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile DELETED
@@ -1,22 +0,0 @@
1
- FROM python:3.9-slim
2
-
3
- WORKDIR /app
4
-
5
- RUN apt-get update && apt-get install -y \
6
- gcc \
7
- g++ \
8
- && rm -rf /var/lib/apt/lists/*
9
-
10
- COPY requirements.txt .
11
- RUN pip install --no-cache-dir -r requirements.txt
12
-
13
- COPY . .
14
-
15
- RUN mkdir -p static templates /tmp/uploads
16
-
17
- EXPOSE 7860
18
-
19
- ENV PORT=7860
20
- ENV ENVIRONMENT=production
21
-
22
- CMD ["python", "app.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,45 +1,12 @@
1
- ---
2
  title: PDF Table Extractor
3
- emoji: 📊
4
- colorFrom: blue
5
- colorTo: purple
6
  sdk: docker
7
- app_file: app.py
8
  pinned: false
 
 
9
  ---
10
 
11
- # PDF Table Extractor
12
-
13
- Flask веб-приложение для извлечения табличных данных из PDF файлов с использованием GigaChat API.
14
-
15
- ## Использование
16
-
17
- 1. **Загрузите PDF файл** с таблицей
18
- 2. **Введите API ключ GigaChat** (получите на [developers.sber.ru](https://developers.sber.ru))
19
- 3. **Нажмите "Извлечь таблицу из PDF"**
20
- 4. **Получите результат** в виде таблицы или JSON
21
-
22
- ## Технологии
23
-
24
- - **Backend:** Flask (Python)
25
- - **Frontend:** HTML, CSS, JavaScript
26
- - **AI API:** GigaChat API от Сбербанка
27
- - **Хостинг:** Hugging Face Spaces
28
-
29
- ## Структура проекта
30
-
31
- - pp.py - Основное Flask приложение
32
- -
33
- equirements.txt - Python зависимости
34
- - Dockerfile - Конфигурация Docker контейнера
35
- - static/ - Статические файлы (CSS, JavaScript)
36
- - emplates/ - HTML шаблоны
37
-
38
- ## Ссылки
39
-
40
- - [GitHub репозиторий](https://github.com/Ckripok/PDF-Table-Extractor)
41
- - [GigaChat API документация](https://developers.sber.ru/docs/ru/gigachat/overview)
42
-
43
- ## Лицензия
44
-
45
- MIT License
 
1
+ ---
2
  title: PDF Table Extractor
3
+ emoji: 🏢
4
+ colorFrom: red
5
+ colorTo: indigo
6
  sdk: docker
 
7
  pinned: false
8
+ license: mit
9
+ short_description: PDF-Table-Extractor
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py DELETED
@@ -1,346 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- PDF Table Extractor - Flask сервер для работы с GigaChat API
4
- Версия для Hugging Face Spaces
5
- """
6
-
7
- import os
8
- import json
9
- import tempfile
10
- import requests
11
- from flask import Flask, request, jsonify, render_template, send_from_directory
12
- from flask_cors import CORS
13
- from werkzeug.utils import secure_filename
14
- import urllib3
15
-
16
- # Отключаем предупреждения SSL для разработки
17
- urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
18
-
19
- app = Flask(__name__)
20
- CORS(app) # Разрешаем CORS для всех доменов
21
-
22
- # Конфигурация для Hugging Face
23
- app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
24
- app.config['UPLOAD_FOLDER'] = '/tmp/uploads' # Используем /tmp в Spaces
25
- app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # Максимальный размер файла 50MB
26
-
27
- # Создаем папку для загрузок
28
- os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
29
-
30
- # Конфигурация GigaChat API
31
- OAUTH_URL = "https://ngw.devices.sberbank.ru:9443/api/v2/oauth"
32
- GIGACHAT_URL = "https://gigachat.devices.sberbank.ru/api/v1/chat/completions"
33
- UPLOAD_URL = "https://gigachat.devices.sberbank.ru/api/v1/files"
34
-
35
-
36
- def allowed_file(filename):
37
- """Проверяем, что файл имеет допустимое расширение"""
38
- return '.' in filename and filename.rsplit('.', 1)[1].lower() == 'pdf'
39
-
40
-
41
- # ========== РОУТЫ ДЛЯ СТАТИЧЕСКИХ ФАЙЛОВ ==========
42
- @app.route('/static/<path:filename>')
43
- def serve_static_files(filename):
44
- """Обслуживание статических файлов"""
45
- return send_from_directory('static', filename)
46
-
47
-
48
- # ========== ГЛАВНЫЕ РОУТЫ ==========
49
- @app.route('/')
50
- def index():
51
- """Главная страница"""
52
- return render_template('index.html')
53
-
54
-
55
- @app.route('/health')
56
- def health():
57
- """Health check для Hugging Face"""
58
- return jsonify({
59
- "status": "healthy",
60
- "service": "PDF Table Extractor",
61
- "version": "1.0.0",
62
- "environment": os.environ.get('ENVIRONMENT', 'development')
63
- })
64
-
65
-
66
- @app.route('/api/oauth', methods=['POST'])
67
- def get_oauth_token():
68
- """Получение токена доступа через API ключ"""
69
- try:
70
- # Получаем данные из запроса
71
- data = request.get_json()
72
- if not data:
73
- return jsonify({"error": "No JSON data provided"}), 400
74
-
75
- api_key = data.get('api_key')
76
- if not api_key:
77
- return jsonify({"error": "API key is required"}), 400
78
-
79
- print(f"Получен запрос на получение токена с API ключем: {api_key[:20]}...")
80
-
81
- # Заголовки для запроса к GigaChat
82
- headers = {
83
- "Content-Type": "application/x-www-form-urlencoded",
84
- "Accept": "application/json",
85
- "RqUID": "87d5de19-0c14-4223-b7c2-ccea3182470a",
86
- "Authorization": f"Basic {api_key}"
87
- }
88
-
89
- payload = {"scope": "GIGACHAT_API_PERS"}
90
-
91
- # Отправляем запрос к GigaChat API
92
- print(f"Отправляем запрос к {OAUTH_URL}")
93
- response = requests.post(
94
- OAUTH_URL,
95
- headers=headers,
96
- data=payload,
97
- verify=False, # Отключаем проверку SSL для разработки
98
- timeout=30
99
- )
100
-
101
- # Проверяем ответ
102
- response.raise_for_status()
103
- result = response.json()
104
-
105
- print(f"Токен успешно получен: {result.get('access_token')[:20]}...")
106
-
107
- return jsonify({
108
- "access_token": result.get("access_token"),
109
- "expires_at": result.get("expires_at"),
110
- "token_type": result.get("token_type", "Bearer")
111
- })
112
-
113
- except requests.exceptions.RequestException as e:
114
- error_msg = f"Ошибка при запросе к API: {str(e)}"
115
- print(f"ERROR: {error_msg}")
116
- if hasattr(e, 'response') and e.response:
117
- print(f"Response status: {e.response.status_code}")
118
- print(f"Response text: {e.response.text}")
119
- return jsonify({"error": error_msg}), 500
120
- except Exception as e:
121
- error_msg = f"Неожиданная ошибка: {str(e)}"
122
- print(f"ERROR: {error_msg}")
123
- return jsonify({"error": error_msg}), 500
124
-
125
-
126
- @app.route('/api/files', methods=['POST'])
127
- def upload_file():
128
- """Загрузка файла на сервер GigaChat"""
129
- try:
130
- print("=" * 60)
131
- print("DEBUG: Начало загрузки файла в GigaChat")
132
-
133
- # Проверяем заголовок авторизации
134
- auth_header = request.headers.get('Authorization')
135
- if not auth_header:
136
- print("ERROR: Отсутс��вует заголовок Authorization")
137
- return jsonify({"error": "Authorization header is required"}), 401
138
-
139
- print(f"DEBUG: Токен получен: {auth_header[:50]}...")
140
-
141
- # Проверяем наличие файла
142
- if 'file' not in request.files:
143
- print("ERROR: В запросе нет файла")
144
- return jsonify({"error": "No file part in request"}), 400
145
-
146
- file = request.files['file']
147
-
148
- if file.filename == '':
149
- print("ERROR: Файл не выбран")
150
- return jsonify({"error": "No file selected"}), 400
151
-
152
- if not allowed_file(file.filename):
153
- print(f"ERROR: Неподдерживаемый формат файла: {file.filename}")
154
- return jsonify({"error": "Only PDF files are allowed"}), 400
155
-
156
- print(f"DEBUG: Загружаемый файл: {file.filename}, размер: {file.content_length} байт")
157
-
158
- # Сохраняем файл временно
159
- filename = secure_filename(file.filename)
160
- temp_filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
161
- file.save(temp_filepath)
162
-
163
- print(f"DEBUG: Файл сохранен временно: {temp_filepath} ({os.path.getsize(temp_filepath)} bytes)")
164
-
165
- try:
166
- # Подготавливаем запрос к GigaChat API
167
- headers = {
168
- "Accept": "application/json",
169
- "Authorization": auth_header
170
- }
171
-
172
- # Открываем файл для отправки
173
- with open(temp_filepath, 'rb') as f:
174
- files = {
175
- "file": (filename, f, "application/pdf"),
176
- "purpose": (None, "general")
177
- }
178
-
179
- print(f"DEBUG: Отправляем файл в GigaChat API...")
180
- print(f"DEBUG: URL: {UPLOAD_URL}")
181
-
182
- response = requests.post(
183
- UPLOAD_URL,
184
- headers=headers,
185
- files=files,
186
- verify=False,
187
- timeout=60
188
- )
189
-
190
- print(f"DEBUG: Статус ответа GigaChat: {response.status_code}")
191
- print(f"DEBUG: Ответ GigaChat: {response.text[:200]}...")
192
-
193
- response.raise_for_status()
194
- result = response.json()
195
-
196
- print(f"DEBUG: Файл успешно загружен в GigaChat. ID файла: {result.get('id')}")
197
-
198
- # Удаляем временный файл
199
- if os.path.exists(temp_filepath):
200
- os.remove(temp_filepath)
201
-
202
- return jsonify({
203
- "id": result.get("id"),
204
- "filename": filename,
205
- "object": result.get("object", "file"),
206
- "bytes": result.get("bytes", 0),
207
- "status": "uploaded"
208
- })
209
-
210
- except requests.exceptions.RequestException as e:
211
- print(f"ERROR: Ошибка при запросе к GigaChat: {str(e)}")
212
- if hasattr(e, 'response') and e.response:
213
- print(f"ERROR: Response status: {e.response.status_code}")
214
- print(f"ERROR: Response text: {e.response.text[:500]}")
215
- raise e
216
-
217
- except Exception as e:
218
- print(f"ERROR: Неожиданная ошибка при загрузке файла: {str(e)}")
219
- return jsonify({"error": f"Upload failed: {str(e)}"}), 500
220
-
221
-
222
- @app.route('/api/chat/completions', methods=['POST'])
223
- def chat_completions():
224
- """Запрос к GigaChat API для извлечения данных из таблицы"""
225
- try:
226
- # Получаем данные из запроса
227
- data = request.get_json()
228
- if not data:
229
- return jsonify({"error": "No JSON data provided"}), 400
230
-
231
- print("=" * 60)
232
- print("DEBUG: Получен запрос к chat/completions")
233
- print(f"DEBUG: Данные запроса: {json.dumps(data, ensure_ascii=False)[:500]}")
234
-
235
- # Проверяем наличие attachments
236
- messages = data.get('messages', [])
237
- if messages:
238
- first_message = messages[0]
239
- attachments = first_message.get('attachments', [])
240
- print(f"DEBUG: Прикрепленные файлы (attachments): {attachments}")
241
-
242
- # Проверяем заголовок авторизации
243
- auth_header = request.headers.get('Authorization')
244
- if not auth_header:
245
- return jsonify({"error": "Authorization header is required"}), 401
246
-
247
- # Подготавливаем заголовки для GigaChat API
248
- headers = {
249
- "Content-Type": "application/json",
250
- "Accept": "application/json",
251
- "Authorization": auth_header
252
- }
253
-
254
- # Отправляем запрос к GigaChat API
255
- print(f"DEBUG: Отправляем запрос к {GIGACHAT_URL}")
256
- response = requests.post(
257
- GIGACHAT_URL,
258
- headers=headers,
259
- json=data,
260
- verify=False,
261
- timeout=120
262
- )
263
-
264
- response.raise_for_status()
265
- result = response.json()
266
-
267
- print(
268
- f"DEBUG: Получен ответ от GigaChat. Количество токенов: {result.get('usage', {}).get('total_tokens', 'unknown')}")
269
- print(f"DEBUG: Ответ: {json.dumps(result, ensure_ascii=False)[:500]}...")
270
- print("=" * 60)
271
-
272
- return jsonify(result)
273
-
274
- except requests.exceptions.RequestException as e:
275
- error_msg = f"Ошибка при запросе к GigaChat API: {str(e)}"
276
- print(f"ERROR: {error_msg}")
277
- if hasattr(e, 'response') and e.response:
278
- print(f"ERROR: Response status: {e.response.status_code}")
279
- print(f"ERROR: Response text: {e.response.text[:500]}")
280
- return jsonify({"error": error_msg}), 500
281
- except Exception as e:
282
- error_msg = f"Неожиданная ошибка: {str(e)}"
283
- print(f"ERROR: {error_msg}")
284
- return jsonify({"error": error_msg}), 500
285
-
286
-
287
- @app.route('/test', methods=['GET'])
288
- def test_api():
289
- """Тестовый endpoint для проверки работы сервера"""
290
- return jsonify({
291
- "status": "ok",
292
- "message": "Сервер работает корректно",
293
- "environment": os.environ.get('ENVIRONMENT', 'development'),
294
- "endpoints": {
295
- "GET /": "Главная страница",
296
- "GET /health": "Health check",
297
- "GET /test": "Тестовый endpoint",
298
- "POST /api/oauth": "Получение токена доступа",
299
- "POST /api/files": "Загрузка файла в GigaChat",
300
- "POST /api/chat/completions": "Запрос к GigaChat API"
301
- }
302
- })
303
-
304
-
305
- # ========== УНИВЕРСАЛЬНЫЙ РОУТ ДЛЯ СТАТИЧЕСКИХ ФАЙЛОВ ==========
306
- @app.route('/<path:filename>')
307
- def catch_all(filename):
308
- """Универсальный роут для статических файлов"""
309
- if filename.endswith('.css'):
310
- return send_from_directory('static', filename)
311
- elif filename.endswith('.js'):
312
- return send_from_directory('static', filename)
313
- elif filename.endswith('.html'):
314
- return send_from_directory('templates', filename)
315
- elif filename == 'favicon.ico':
316
- return '', 404 # Или верните favicon
317
- else:
318
- return jsonify({"error": "File not found", "path": filename}), 404
319
-
320
-
321
- # ========== ЗАПУСК ПРИЛОЖЕНИЯ ==========
322
- if __name__ == '__main__':
323
- # Получаем порт из окружения (Hugging Face использует 7860)
324
- port = int(os.environ.get('PORT', 7860))
325
-
326
- # Создаем необходимые папки
327
- os.makedirs('static', exist_ok=True)
328
- os.makedirs('templates', exist_ok=True)
329
- os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
330
-
331
- print("=" * 60)
332
- print("📊 PDF Table Extractor Server")
333
- print("🚀 Версия для Hugging Face Spaces")
334
- print("=" * 60)
335
- print(f"Порт: {port}")
336
- print(f"Папка загрузок: {app.config['UPLOAD_FOLDER']}")
337
- print(f"Режим: {os.environ.get('ENVIRONMENT', 'development')}")
338
- print("=" * 60)
339
-
340
- # Запускаем сервер
341
- app.run(
342
- debug=False, # В продакшне всегда False!
343
- host='0.0.0.0',
344
- port=port,
345
- threaded=True
346
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
prepare_hf.py DELETED
@@ -1,149 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Скрипт для подготовки проекта к деплою на Hugging Face Spaces
4
- """
5
-
6
- import os
7
- import shutil
8
- import subprocess
9
-
10
-
11
- def create_structure():
12
- """Создает правильную структуру проекта"""
13
- print(" Создание структуры проекта...")
14
- # Создаем папки если их нет
15
- folders = ['static', 'templates']
16
- for folder in folders:
17
- if not os.path.exists(folder):
18
- os.makedirs(folder)
19
- print(f" Создана папка: {folder}")
20
- else:
21
- print(f" Папка уже существует: {folder}")
22
-
23
- # Проверяем наличие script.js в static/
24
- if os.path.exists('script.js'):
25
- shutil.move('script.js', 'static/script.js')
26
- print(" script.js перемещен в static/")
27
-
28
- return True
29
-
30
-
31
- def check_files():
32
- """Проверяет наличие необходимых файлов"""
33
- print("\n🔍 Проверка файлов...")
34
-
35
- required_files = ['app.py', 'requirements.txt', 'Dockerfile']
36
- missing_files = []
37
-
38
- for file in required_files:
39
- if os.path.exists(file):
40
- print(f" {file}")
41
- else:
42
- print(f" {file} - ОТСУТСТВУЕТ!")
43
- missing_files.append(file)
44
-
45
- if missing_files:
46
- print(f"\n Не хватает файлов: {missing_files}")
47
- return False
48
-
49
- return True
50
-
51
-
52
- def clean_unnecessary():
53
- """Удаляет ненужные файлы для HF"""
54
- print("\n🗑 Очистка ненужных файлов...")
55
-
56
- files_to_remove = [
57
- 'railway.json',
58
- 'check_railway.py',
59
- 'Procfile',
60
- 'runtime.txt',
61
- 'nixpacks.toml',
62
- 'pyvenv.cfg'
63
- ]
64
-
65
- for file in files_to_remove:
66
- if os.path.exists(file):
67
- try:
68
- os.remove(file)
69
- print(f" Удален: {file}")
70
- except:
71
- print(f"Не удалось удалить: {file}")
72
-
73
- return True
74
-
75
-
76
- def update_requirements():
77
- """Обновляет requirements.txt"""
78
- print("\n Обновление зависимостей...")
79
-
80
- # Упрощенные зависимости
81
- requirements = """Flask==2.3.3
82
- Flask-CORS==4.0.0
83
- requests==2.31.0
84
- urllib3==2.0.7
85
- Werkzeug==2.3.7
86
- python-dotenv==1.0.0"""
87
-
88
- with open('requirements.txt', 'w') as f:
89
- f.write(requirements)
90
-
91
- print(" ✅ requirements.txt обновлен")
92
- return True
93
-
94
-
95
- def git_operations():
96
- """Выполняет Git операции"""
97
- print("\n💾 Git операции...")
98
-
99
- try:
100
- # Добавляем все файлы
101
- subprocess.run(['git', 'add', '.'], check=True)
102
- print(" Файлы добавлены в Git")
103
-
104
- # Коммит
105
- subprocess.run(['git', 'commit', '-m', 'Prepare for Hugging Face Spaces deployment'], check=True)
106
- print(" Коммит создан")
107
-
108
- return True
109
- except subprocess.CalledProcessError as e:
110
- print(f" Ошибка Git: {e}")
111
- return False
112
-
113
-
114
- def main():
115
- print(" ПОДГОТОВКА К HUGGING FACE SPACES")
116
- print("=" * 50)
117
-
118
- # Выполняем шаги
119
- steps = [
120
- ("Структура проекта", create_structure),
121
- ("Проверка файлов", check_files),
122
- ("Очистка", clean_unnecessary),
123
- ("Зависимости", update_requirements),
124
- ]
125
-
126
- all_ok = True
127
- for step_name, step_func in steps:
128
- print(f"\n {step_name}:")
129
- if not step_func():
130
- all_ok = False
131
-
132
- if all_ok:
133
- print("\n" + "=" * 50)
134
- print("Проект готов к деплою на Hugging Face!")
135
- print("\nСледующие шаги:")
136
- print("1. Откройте https://huggingface.co/spaces")
137
- print("2. Нажмите 'Create new Space'")
138
- print("3. Выберите:")
139
- print(" - Name: pdf-table-extractor")
140
- print(" - SDK: Docker")
141
- print(" - Visibility: Public")
142
- print("4. Нажмите 'Create Space'")
143
- print("5. Загрузите файлы или свяжите с GitHub")
144
- else:
145
- print("\nЕсть проблемы, которые нужно исправить")
146
-
147
-
148
- if __name__ == '__main__':
149
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt DELETED
@@ -1,5 +0,0 @@
1
- Flask==2.3.3
2
- Flask-CORS==4.0.0
3
- requests==2.31.0
4
- urllib3==2.0.7
5
- Werkzeug==2.3.7
 
 
 
 
 
 
static/script.js DELETED
The diff for this file is too large to render. See raw diff
 
static/style.css DELETED
@@ -1,1646 +0,0 @@
1
- /* Reset и базовые стили */
2
- * {
3
- margin: 0;
4
- padding: 0;
5
- box-sizing: border-box;
6
- }
7
-
8
- body {
9
- font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10
- line-height: 1.6;
11
- color: #1a1a1a;
12
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
13
- min-height: 100vh;
14
- padding: 20px;
15
- }
16
-
17
- .container {
18
- max-width: 1800px;
19
- margin: 0 auto;
20
- background-color: white;
21
- border-radius: 20px;
22
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
23
- overflow: hidden;
24
- min-height: calc(100vh - 40px);
25
- display: flex;
26
- flex-direction: column;
27
- }
28
-
29
- /* Шапка */
30
- .header {
31
- background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
32
- color: white;
33
- padding: 30px 40px;
34
- text-align: center;
35
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
36
- }
37
-
38
- .logo {
39
- display: flex;
40
- align-items: center;
41
- justify-content: center;
42
- gap: 15px;
43
- margin-bottom: 15px;
44
- }
45
-
46
- .logo i {
47
- font-size: 2.5rem;
48
- color: #4cd964;
49
- }
50
-
51
- .logo h1 {
52
- font-size: 2.5rem;
53
- font-weight: 700;
54
- margin: 0;
55
- }
56
-
57
- .subtitle {
58
- font-size: 1.1rem;
59
- opacity: 0.9;
60
- max-width: 700px;
61
- margin: 0 auto;
62
- font-weight: 300;
63
- }
64
-
65
- /* Основной контент */
66
- .main-content {
67
- display: flex;
68
- flex: 1;
69
- min-height: 800px;
70
- }
71
-
72
- .left-panel {
73
- flex: 0 0 400px;
74
- background: #f8fafc;
75
- padding: 25px;
76
- border-right: 1px solid #e2e8f0;
77
- display: flex;
78
- flex-direction: column;
79
- gap: 25px;
80
- overflow-y: auto;
81
- }
82
-
83
- .right-panel {
84
- flex: 1;
85
- padding: 25px;
86
- background: white;
87
- display: flex;
88
- flex-direction: column;
89
- gap: 25px;
90
- overflow-y: auto;
91
- }
92
-
93
- /* Карточки */
94
- .card {
95
- background: white;
96
- border-radius: 16px;
97
- padding: 25px;
98
- border: 1px solid #e2e8f0;
99
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
100
- transition: all 0.3s ease;
101
- }
102
-
103
- .card:hover {
104
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
105
- }
106
-
107
- .card h2 {
108
- color: #2d3748;
109
- margin-bottom: 20px;
110
- font-size: 1.4rem;
111
- display: flex;
112
- align-items: center;
113
- gap: 10px;
114
- }
115
-
116
- .card h2 i {
117
- color: #667eea;
118
- }
119
-
120
- /* Область загрузки */
121
- .upload-area {
122
- border: 3px dashed #cbd5e0;
123
- border-radius: 12px;
124
- padding: 50px 20px;
125
- text-align: center;
126
- cursor: pointer;
127
- transition: all 0.3s ease;
128
- margin-bottom: 20px;
129
- background: #fafafa;
130
- }
131
-
132
- .upload-area:hover, .upload-area.dragover {
133
- border-color: #667eea;
134
- background-color: rgba(102, 126, 234, 0.05);
135
- transform: translateY(-2px);
136
- }
137
-
138
- .upload-icon {
139
- font-size: 4rem;
140
- color: #cbd5e0;
141
- margin-bottom: 20px;
142
- transition: color 0.3s ease;
143
- }
144
-
145
- .upload-area:hover .upload-icon {
146
- color: #667eea;
147
- }
148
-
149
- .upload-area h3 {
150
- margin-bottom: 10px;
151
- color: #4a5568;
152
- font-size: 1.2rem;
153
- }
154
-
155
- .upload-area p {
156
- color: #718096;
157
- margin-bottom: 20px;
158
- }
159
-
160
- .file-hint {
161
- font-size: 0.9rem;
162
- color: #a0aec0;
163
- margin-top: 15px;
164
- }
165
-
166
- /* Информация о файле */
167
- .file-info {
168
- animation: slideDown 0.3s ease;
169
- }
170
-
171
- @keyframes slideDown {
172
- from {
173
- opacity: 0;
174
- transform: translateY(-10px);
175
- }
176
- to {
177
- opacity: 1;
178
- transform: translateY(0);
179
- }
180
- }
181
-
182
- .file-card {
183
- display: flex;
184
- align-items: center;
185
- gap: 15px;
186
- padding: 20px;
187
- background: #f7fafc;
188
- border-radius: 12px;
189
- border: 1px solid #e2e8f0;
190
- }
191
-
192
- .file-icon {
193
- font-size: 2.5rem;
194
- color: #e53e3e;
195
- }
196
-
197
- .file-details {
198
- flex: 1;
199
- }
200
-
201
- .file-details h4 {
202
- margin-bottom: 5px;
203
- color: #2d3748;
204
- font-weight: 600;
205
- word-break: break-all;
206
- }
207
-
208
- .file-details p {
209
- color: #718096;
210
- font-size: 0.9rem;
211
- }
212
-
213
- .file-progress {
214
- margin-top: 10px;
215
- }
216
-
217
- .progress-bar {
218
- height: 6px;
219
- background-color: #e2e8f0;
220
- border-radius: 3px;
221
- overflow: hidden;
222
- margin-bottom: 5px;
223
- }
224
-
225
- .progress {
226
- height: 100%;
227
- background: linear-gradient(90deg, #667eea, #764ba2);
228
- width: 0%;
229
- transition: width 0.3s ease;
230
- }
231
-
232
- /* Формы */
233
- .form-group {
234
- margin-bottom: 25px;
235
- }
236
-
237
- .form-group label {
238
- display: block;
239
- margin-bottom: 8px;
240
- font-weight: 600;
241
- color: #4a5568;
242
- font-size: 0.95rem;
243
- display: flex;
244
- align-items: center;
245
- gap: 8px;
246
- }
247
-
248
- .form-control {
249
- width: 100%;
250
- padding: 14px 16px;
251
- border: 2px solid #e2e8f0;
252
- border-radius: 10px;
253
- font-size: 1rem;
254
- transition: all 0.2s ease;
255
- background: #fafafa;
256
- }
257
-
258
- .form-control:focus {
259
- outline: none;
260
- border-color: #667eea;
261
- background: white;
262
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
263
- }
264
-
265
- .input-with-button {
266
- display: flex;
267
- gap: 10px;
268
- }
269
-
270
- .input-with-button .form-control {
271
- flex: 1;
272
- }
273
-
274
- .form-hint {
275
- margin-top: 8px;
276
- font-size: 0.85rem;
277
- color: #718096;
278
- display: flex;
279
- align-items: center;
280
- gap: 8px;
281
- }
282
-
283
- .form-hint a {
284
- color: #667eea;
285
- text-decoration: none;
286
- }
287
-
288
- .form-hint a:hover {
289
- text-decoration: underline;
290
- }
291
-
292
- /* Кнопки */
293
- .btn {
294
- padding: 14px 28px;
295
- border: none;
296
- border-radius: 10px;
297
- font-size: 1rem;
298
- font-weight: 600;
299
- cursor: pointer;
300
- transition: all 0.2s ease;
301
- display: flex;
302
- align-items: center;
303
- justify-content: center;
304
- gap: 10px;
305
- font-family: 'Inter', sans-serif;
306
- }
307
-
308
- .btn-primary {
309
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
310
- color: white;
311
- }
312
-
313
- .btn-primary:hover:not(:disabled) {
314
- transform: translateY(-2px);
315
- box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
316
- }
317
-
318
- .btn-secondary {
319
- background: linear-gradient(135deg, #718096 0%, #4a5568 100%);
320
- color: white;
321
- }
322
-
323
- .btn-secondary:hover:not(:disabled) {
324
- transform: translateY(-2px);
325
- box-shadow: 0 8px 25px rgba(113, 128, 150, 0.4);
326
- }
327
-
328
- .btn-outline {
329
- background: transparent;
330
- border: 2px solid #667eea;
331
- color: #667eea;
332
- }
333
-
334
- .btn-outline:hover:not(:disabled) {
335
- background: #667eea;
336
- color: white;
337
- }
338
-
339
- .btn-lg {
340
- padding: 18px 32px;
341
- font-size: 1.1rem;
342
- }
343
-
344
- .btn-small {
345
- padding: 10px 18px;
346
- font-size: 0.9rem;
347
- }
348
-
349
- .btn-icon {
350
- background: none;
351
- border: none;
352
- cursor: pointer;
353
- padding: 10px;
354
- border-radius: 8px;
355
- display: flex;
356
- align-items: center;
357
- justify-content: center;
358
- transition: all 0.2s ease;
359
- }
360
-
361
- .btn-icon:hover {
362
- background-color: #f7fafc;
363
- }
364
-
365
- /* Добавьте в style.css */
366
- .btn-danger {
367
- background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
368
- color: white;
369
- border: none;
370
- }
371
-
372
- .btn-danger:hover:not(:disabled) {
373
- background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
374
- transform: none !important;
375
- box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4);
376
- }
377
-
378
- .btn-danger:active {
379
- transform: scale(0.98) !important;
380
- }
381
-
382
- .btn:disabled {
383
- opacity: 0.5;
384
- cursor: not-allowed;
385
- transform: none !important;
386
- box-shadow: none !important;
387
- }
388
-
389
- .action-buttons {
390
- display: flex;
391
- flex-direction: column;
392
- gap: 15px;
393
- margin-top: 30px;
394
- }
395
-
396
- /* Статус карточка */
397
- .status-card {
398
- margin-top: auto;
399
- }
400
-
401
- .status-items {
402
- display: flex;
403
- flex-direction: column;
404
- gap: 15px;
405
- }
406
-
407
- .status-item {
408
- display: flex;
409
- justify-content: space-between;
410
- align-items: center;
411
- padding: 12px 15px;
412
- background: #f7fafc;
413
- border-radius: 10px;
414
- }
415
-
416
- .status-label {
417
- font-weight: 500;
418
- color: #4a5568;
419
- }
420
-
421
- .status-value {
422
- font-weight: 600;
423
- font-size: 0.95rem;
424
- display: flex;
425
- align-items: center;
426
- gap: 8px;
427
- }
428
-
429
- .status-active {
430
- color: #38a169;
431
- }
432
-
433
- .status-inactive {
434
- color: #e53e3e;
435
- }
436
-
437
- /* Заголовок результатов */
438
- .card-header {
439
- display: flex;
440
- justify-content: space-between;
441
- align-items: center;
442
- margin-bottom: 25px;
443
- flex-wrap: wrap;
444
- gap: 15px;
445
- }
446
-
447
- .card-actions {
448
- display: flex;
449
- gap: 10px;
450
- flex-wrap: wrap;
451
- }
452
-
453
- /* Индикатор загрузки */
454
- .loading-overlay {
455
- background: rgba(255, 255, 255, 0.95);
456
- display: flex;
457
- align-items: center;
458
- justify-content: center;
459
- z-index: 100;
460
- border-radius: 16px;
461
- }
462
-
463
- .loader {
464
- text-align: center;
465
- max-width: 500px;
466
- padding: 40px;
467
- }
468
-
469
- .loader .spinner {
470
- border: 5px solid #f3f3f3;
471
- border-top: 5px solid #667eea;
472
- border-radius: 50%;
473
- width: 80px;
474
- height: 80px;
475
- animation: spin 1s linear infinite;
476
- margin: 0 auto 30px;
477
- }
478
-
479
- @keyframes spin {
480
- 0% { transform: rotate(0deg); }
481
- 100% { transform: rotate(360deg); }
482
- }
483
-
484
- .loader h3 {
485
- margin-bottom: 15px;
486
- color: #2d3748;
487
- }
488
-
489
- .loader p {
490
- color: #718096;
491
- margin-bottom: 30px;
492
- }
493
-
494
- .progress-container {
495
- margin: 30px 0;
496
- }
497
-
498
- .progress-bar-lg {
499
- height: 10px;
500
- background-color: #e2e8f0;
501
- border-radius: 5px;
502
- overflow: hidden;
503
- margin-bottom: 20px;
504
- }
505
-
506
- .progress-lg {
507
- height: 100%;
508
- background: linear-gradient(90deg, #667eea, #764ba2);
509
- width: 0%;
510
- transition: width 0.3s ease;
511
- }
512
-
513
- .progress-steps {
514
- display: flex;
515
- justify-content: space-between;
516
- position: relative;
517
- }
518
-
519
- .progress-steps::before {
520
- content: '';
521
- position: absolute;
522
- top: 14px;
523
- left: 0;
524
- right: 0;
525
- height: 2px;
526
- background: #e2e8f0;
527
- z-index: 1;
528
- }
529
-
530
- .step {
531
- position: relative;
532
- z-index: 2;
533
- background: white;
534
- padding: 0 10px;
535
- font-size: 0.85rem;
536
- color: #a0aec0;
537
- transition: all 0.3s ease;
538
- }
539
-
540
- .step.active {
541
- color: #667eea;
542
- font-weight: 600;
543
- }
544
-
545
- .loader-details {
546
- margin-top: 30px;
547
- padding: 20px;
548
- background: #f7fafc;
549
- border-radius: 10px;
550
- text-align: left;
551
- }
552
-
553
- .loader-details p {
554
- margin-bottom: 10px;
555
- font-size: 0.9rem;
556
- display: flex;
557
- align-items: center;
558
- gap: 10px;
559
- }
560
-
561
- /* Алерт валидац��и */
562
- .validation-alert {
563
- margin-bottom: 25px;
564
- animation: slideIn 0.3s ease;
565
- }
566
-
567
- @keyframes slideIn {
568
- from {
569
- opacity: 0;
570
- transform: translateX(-20px);
571
- }
572
- to {
573
- opacity: 1;
574
- transform: translateX(0);
575
- }
576
- }
577
-
578
- .alert-content {
579
- display: flex;
580
- align-items: center;
581
- gap: 15px;
582
- padding: 20px;
583
- border-radius: 12px;
584
- background: #fff3cd;
585
- border: 1px solid #ffeaa7;
586
- }
587
-
588
- .alert-content i {
589
- font-size: 1.5rem;
590
- color: #856404;
591
- }
592
-
593
- .alert-content h4 {
594
- color: #856404;
595
- margin-bottom: 5px;
596
- }
597
-
598
- .alert-content p {
599
- color: #856404;
600
- margin: 0;
601
- }
602
-
603
- /* Результаты */
604
- .results-container {
605
- position: relative;
606
- min-height: 500px;
607
- border: 1px solid #e2e8f0;
608
- border-radius: 12px;
609
- overflow: hidden;
610
- }
611
-
612
- .table-view, .json-view {
613
- height: 100%;
614
- }
615
-
616
- .table-header, .json-header {
617
- padding: 20px;
618
- background: #f7fafc;
619
- border-bottom: 1px solid #e2e8f0;
620
- display: flex;
621
- justify-content: space-between;
622
- align-items: center;
623
- }
624
-
625
- .table-header h3, .json-header h3 {
626
- margin: 0;
627
- display: flex;
628
- align-items: center;
629
- gap: 10px;
630
- }
631
-
632
- .table-info, .json-info {
633
- color: #718096;
634
- font-size: 0.9rem;
635
- }
636
-
637
- .table-wrapper {
638
- overflow-x: auto;
639
- }
640
-
641
- table {
642
- width: 100%;
643
- border-collapse: collapse;
644
- min-width: 800px;
645
- }
646
-
647
- thead {
648
- background: #2d3748;
649
- color: white;
650
- position: sticky;
651
- top: 0;
652
- }
653
-
654
- th, td {
655
- padding: 14px 16px;
656
- text-align: left;
657
- border-bottom: 1px solid #e2e8f0;
658
- }
659
-
660
- th {
661
- font-weight: 600;
662
- white-space: nowrap;
663
- }
664
-
665
- tbody tr:nth-child(even) {
666
- background: #f7fafc;
667
- }
668
-
669
- tbody tr:hover {
670
- background: #edf2f7;
671
- }
672
-
673
- .json-editor {
674
- height: calc(100% - 70px);
675
- overflow: auto;
676
- }
677
-
678
- #jsonOutput {
679
- padding: 20px;
680
- font-family: 'Cascadia Code', 'Monaco', 'Consolas', monospace;
681
- font-size: 0.9rem;
682
- line-height: 1.6;
683
- white-space: pre-wrap;
684
- word-break: break-word;
685
- margin: 0;
686
- background: #f8fafc;
687
- min-height: 400px;
688
- }
689
-
690
- /* История */
691
- .history-card {
692
- flex-shrink: 0;
693
- }
694
-
695
- .history-list {
696
- max-height: 200px;
697
- overflow-y: auto;
698
- }
699
-
700
- .history-empty {
701
- text-align: center;
702
- padding: 40px 20px;
703
- color: #a0aec0;
704
- }
705
-
706
- .history-empty i {
707
- font-size: 3rem;
708
- margin-bottom: 15px;
709
- opacity: 0.5;
710
- }
711
-
712
- /* Модальное окно */
713
- .modal {
714
- position: fixed;
715
- top: 0;
716
- left: 0;
717
- right: 0;
718
- bottom: 0;
719
- display: none;
720
- align-items: center;
721
- justify-content: center;
722
- z-index: 1000;
723
- }
724
-
725
- .modal-overlay {
726
- position: absolute;
727
- top: 0;
728
- left: 0;
729
- right: 0;
730
- bottom: 0;
731
- background: rgba(0, 0, 0, 0.5);
732
- }
733
-
734
- .modal-dialog {
735
- position: relative;
736
- z-index: 1001;
737
- width: 90%;
738
- max-width: 500px;
739
- }
740
-
741
- .modal-content {
742
- background: white;
743
- border-radius: 20px;
744
- overflow: hidden;
745
- box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
746
- animation: modalSlideIn 0.3s ease;
747
- }
748
-
749
- @keyframes modalSlideIn {
750
- from {
751
- opacity: 0;
752
- transform: translateY(-30px);
753
- }
754
- to {
755
- opacity: 1;
756
- transform: translateY(0);
757
- }
758
- }
759
-
760
- .modal-header {
761
- display: flex;
762
- justify-content: space-between;
763
- align-items: center;
764
- padding: 25px 30px;
765
- background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
766
- color: white;
767
- }
768
-
769
- .modal-header h3 {
770
- margin: 0;
771
- display: flex;
772
- align-items: center;
773
- gap: 10px;
774
- }
775
-
776
- .modal-body {
777
- padding: 30px;
778
- }
779
-
780
- .modal-footer {
781
- padding: 20px 30px;
782
- background: #f7fafc;
783
- border-top: 1px solid #e2e8f0;
784
- text-align: right;
785
- }
786
-
787
- .token-loading {
788
- text-align: center;
789
- padding: 40px 20px;
790
- }
791
-
792
- .token-loading .spinner {
793
- border: 4px solid #f3f3f3;
794
- border-top: 4px solid #667eea;
795
- border-radius: 50%;
796
- width: 50px;
797
- height: 50px;
798
- animation: spin 1s linear infinite;
799
- margin: 0 auto 20px;
800
- }
801
-
802
- .success-message, .error-message {
803
- display: flex;
804
- align-items: center;
805
- gap: 20px;
806
- margin-bottom: 30px;
807
- }
808
-
809
- .success-icon, .error-icon {
810
- font-size: 3rem;
811
- }
812
-
813
- .success-icon {
814
- color: #38a169;
815
- }
816
-
817
- .error-icon {
818
- color: #e53e3e;
819
- }
820
-
821
- .token-display {
822
- margin-top: 30px;
823
- }
824
-
825
- .token-input-group {
826
- display: flex;
827
- gap: 10px;
828
- margin-top: 10px;
829
- }
830
-
831
- .token-input {
832
- flex: 1;
833
- font-family: 'Cascadia Code', 'Monaco', 'Consolas', monospace;
834
- font-size: 0.9rem;
835
- }
836
-
837
- .token-hint {
838
- margin-top: 10px;
839
- font-size: 0.85rem;
840
- color: #718096;
841
- display: flex;
842
- align-items: center;
843
- gap: 8px;
844
- }
845
-
846
- .error-suggestions {
847
- margin-top: 30px;
848
- padding: 20px;
849
- background: #fff5f5;
850
- border-radius: 10px;
851
- border: 1px solid #fed7d7;
852
- }
853
-
854
- .error-suggestions h5 {
855
- margin-bottom: 15px;
856
- color: #c53030;
857
- }
858
-
859
- .error-suggestions ul {
860
- list-style: none;
861
- padding-left: 0;
862
- }
863
-
864
- .error-suggestions li {
865
- margin-bottom: 10px;
866
- display: flex;
867
- align-items: center;
868
- gap: 10px;
869
- color: #718096;
870
- }
871
-
872
- /* Подвал */
873
- .footer {
874
- background: #2d3748;
875
- color: white;
876
- padding: 30px 40px;
877
- border-top: 1px solid #4a5568;
878
- }
879
-
880
- .footer-content {
881
- display: flex;
882
- justify-content: space-between;
883
- align-items: center;
884
- flex-wrap: wrap;
885
- gap: 30px;
886
- margin-bottom: 20px;
887
- }
888
-
889
- .footer-logo {
890
- display: flex;
891
- align-items: center;
892
- gap: 10px;
893
- font-size: 1.2rem;
894
- font-weight: 600;
895
- }
896
-
897
- .footer-info {
898
- flex: 1;
899
- text-align: center;
900
- }
901
-
902
- .footer-info p {
903
- margin: 5px 0;
904
- color: #cbd5e0;
905
- font-size: 0.9rem;
906
- }
907
-
908
- .footer-links {
909
- display: flex;
910
- gap: 20px;
911
- }
912
-
913
- .footer-link {
914
- color: #cbd5e0;
915
- text-decoration: none;
916
- display: flex;
917
- align-items: center;
918
- gap: 8px;
919
- transition: color 0.2s ease;
920
- }
921
-
922
- .footer-link:hover {
923
- color: white;
924
- }
925
-
926
- .footer-bottom {
927
- display: flex;
928
- justify-content: space-between;
929
- align-items: center;
930
- padding-top: 20px;
931
- border-top: 1px solid #4a5568;
932
- font-size: 0.9rem;
933
- color: #a0aec0;
934
- }
935
-
936
- .footer-version {
937
- font-family: 'Cascadia Code', 'Monaco', 'Consolas', monospace;
938
- }
939
-
940
- /* Уведомления */
941
- .notification-container {
942
- position: fixed;
943
- top: 20px;
944
- right: 20px;
945
- z-index: 2000;
946
- display: flex;
947
- flex-direction: column;
948
- gap: 10px;
949
- }
950
-
951
- /* Адаптивность */
952
- @media (max-width: 1200px) {
953
- .main-content {
954
- flex-direction: column;
955
- }
956
-
957
- .left-panel {
958
- flex: none;
959
- border-right: none;
960
- border-bottom: 1px solid #e2e8f0;
961
- }
962
-
963
- .header {
964
- padding: 20px;
965
- }
966
-
967
- .logo h1 {
968
- font-size: 2rem;
969
- }
970
- }
971
-
972
- @media (max-width: 768px) {
973
- body {
974
- padding: 10px;
975
- }
976
-
977
- .container {
978
- border-radius: 15px;
979
- }
980
-
981
- .card-header {
982
- flex-direction: column;
983
- align-items: flex-start;
984
- }
985
-
986
- .card-actions {
987
- width: 100%;
988
- justify-content: flex-start;
989
- }
990
-
991
- .footer-content {
992
- flex-direction: column;
993
- text-align: center;
994
- }
995
-
996
- .footer-links {
997
- justify-content: center;
998
- }
999
-
1000
- .footer-bottom {
1001
- flex-direction: column;
1002
- gap: 10px;
1003
- }
1004
- }
1005
-
1006
- /* Стили для таблицы */
1007
- .table-view {
1008
- display: flex;
1009
- flex-direction: column;
1010
- height: 100%;
1011
- }
1012
-
1013
- .table-header {
1014
- display: flex;
1015
- justify-content: space-between;
1016
- align-items: center;
1017
- padding: 16px 20px;
1018
- background: #f8fafc;
1019
- border-bottom: 1px solid #e2e8f0;
1020
- }
1021
-
1022
- .table-header h3 {
1023
- margin: 0;
1024
- display: flex;
1025
- align-items: center;
1026
- gap: 10px;
1027
- color: #2d3748;
1028
- }
1029
-
1030
- .table-controls {
1031
- display: flex;
1032
- gap: 8px;
1033
- }
1034
-
1035
- .table-wrapper {
1036
- flex: 1;
1037
- overflow: auto;
1038
- position: relative;
1039
- background: white;
1040
- }
1041
-
1042
- .table-wrapper::-webkit-scrollbar {
1043
- width: 10px;
1044
- height: 10px;
1045
- }
1046
-
1047
- .table-wrapper::-webkit-scrollbar-track {
1048
- background: #f1f1f1;
1049
- }
1050
-
1051
- .table-wrapper::-webkit-scrollbar-thumb {
1052
- background: #c1c1c1;
1053
- border-radius: 5px;
1054
- }
1055
-
1056
- .table-wrapper::-webkit-scrollbar-thumb:hover {
1057
- background: #a8a8a8;
1058
- }
1059
-
1060
- #dataTable {
1061
- width: 100%;
1062
- min-width: 800px;
1063
- border-collapse: collapse;
1064
- font-size: 0.9rem;
1065
- }
1066
-
1067
- #dataTable thead {
1068
- position: sticky;
1069
- top: 0;
1070
- z-index: 10;
1071
- }
1072
-
1073
- #dataTable th {
1074
- background: #2c3e50;
1075
- color: white;
1076
- padding: 14px 16px;
1077
- text-align: left;
1078
- font-weight: 600;
1079
- border-right: 1px solid #4a5568;
1080
- position: relative;
1081
- user-select: none;
1082
- }
1083
-
1084
- #dataTable th:first-child {
1085
- position: sticky;
1086
- left: 0;
1087
- z-index: 11;
1088
- }
1089
-
1090
- #dataTable th:hover {
1091
- background: #34495e;
1092
- }
1093
-
1094
- #dataTable td {
1095
- padding: 12px 16px;
1096
- border-bottom: 1px solid #e2e8f0;
1097
- border-right: 1px solid #e2e8f0;
1098
- background: white;
1099
- transition: background-color 0.2s;
1100
- }
1101
-
1102
- #dataTable td:first-child {
1103
- position: sticky;
1104
- left: 0;
1105
- z-index: 5;
1106
- background: inherit;
1107
- font-weight: 600;
1108
- border-right: 2px solid #e2e8f0;
1109
- }
1110
-
1111
- #dataTable tr:nth-child(even) td {
1112
- background-color: #f8f9fa;
1113
- }
1114
-
1115
- #dataTable tr:nth-child(even) td:first-child {
1116
- background-color: #f8f9fa;
1117
- }
1118
-
1119
- #dataTable tr:hover td {
1120
- background-color: #edf2f7 !important;
1121
- }
1122
-
1123
- #dataTable tr:hover td:first-child {
1124
- background-color: #edf2f7 !important;
1125
- }
1126
-
1127
- /* Стили для специальных символов */
1128
- .cell-plus {
1129
- color: #10b981;
1130
- font-weight: bold;
1131
- }
1132
-
1133
- .cell-minus {
1134
- color: #ef4444;
1135
- font-weight: bold;
1136
- }
1137
-
1138
- .cell-star {
1139
- color: #f59e0b;
1140
- font-weight: bold;
1141
- }
1142
-
1143
- .cell-empty {
1144
- color: #9ca3af;
1145
- font-style: italic;
1146
- }
1147
-
1148
- .table-footer {
1149
- padding: 12px 20px;
1150
- background: #f8fafc;
1151
- border-top: 1px solid #e2e8f0;
1152
- display: flex;
1153
- justify-content: space-between;
1154
- align-items: center;
1155
- }
1156
-
1157
- .table-pagination {
1158
- display: flex;
1159
- gap: 8px;
1160
- align-items: center;
1161
- }
1162
-
1163
- .page-btn {
1164
- padding: 6px 12px;
1165
- border: 1px solid #d1d5db;
1166
- background: white;
1167
- border-radius: 4px;
1168
- cursor: pointer;
1169
- font-size: 0.9rem;
1170
- transition: all 0.2s;
1171
- }
1172
-
1173
- .page-btn:hover:not(:disabled) {
1174
- background: #f3f4f6;
1175
- border-color: #9ca3af;
1176
- }
1177
-
1178
- .page-btn:disabled {
1179
- opacity: 0.5;
1180
- cursor: not-allowed;
1181
- }
1182
-
1183
- .page-info {
1184
- font-size: 0.9rem;
1185
- color: #6b7280;
1186
- }
1187
-
1188
- /* Адаптивность для таблицы */
1189
- @media (max-width: 768px) {
1190
- .table-header {
1191
- flex-direction: column;
1192
- align-items: flex-start;
1193
- gap: 10px;
1194
- }
1195
-
1196
- .table-controls {
1197
- width: 100%;
1198
- justify-content: flex-start;
1199
- }
1200
-
1201
- .table-footer {
1202
- flex-direction: column;
1203
- gap: 10px;
1204
- align-items: flex-start;
1205
- }
1206
- }
1207
-
1208
- /* В static/style.css добавьте: */
1209
-
1210
- /* Стили для символов в таблице */
1211
- .cell-positive {
1212
- color: #10b981;
1213
- font-weight: bold;
1214
- font-size: 1.1em;
1215
- background-color: rgba(16, 185, 129, 0.1);
1216
- text-align: center;
1217
- }
1218
-
1219
- .cell-negative {
1220
- color: #ef4444;
1221
- font-weight: bold;
1222
- font-size: 1.1em;
1223
- background-color: rgba(239, 68, 68, 0.1);
1224
- text-align: center;
1225
- }
1226
-
1227
- .cell-question {
1228
- color: #f59e0b;
1229
- font-weight: bold;
1230
- font-size: 1.1em;
1231
- background-color: rgba(245, 158, 11, 0.1);
1232
- text-align: center;
1233
- }
1234
-
1235
- .cell-star {
1236
- color: #8b5cf6;
1237
- font-weight: bold;
1238
- background-color: rgba(139, 92, 246, 0.1);
1239
- text-align: center;
1240
- }
1241
-
1242
- .cell-corrected {
1243
- background-color: #fef3c7 !important;
1244
- border: 2px solid #f59e0b !important;
1245
- position: relative;
1246
- }
1247
-
1248
- .cell-corrected::after {
1249
- content: '✏️';
1250
- position: absolute;
1251
- top: 2px;
1252
- right: 2px;
1253
- font-size: 0.7em;
1254
- opacity: 0.7;
1255
- }
1256
-
1257
- .cell-empty {
1258
- color: #9ca3af;
1259
- font-style: italic;
1260
- background-color: #f1f3f4;
1261
- text-align: center;
1262
- }
1263
-
1264
- /* Анимация для измененных ячеек */
1265
- @keyframes highlightChange {
1266
- 0% { background-color: #fef3c7; }
1267
- 100% { background-color: inherit; }
1268
- }
1269
-
1270
- .cell-changed {
1271
- animation: highlightChange 2s ease-in-out;
1272
- }
1273
-
1274
- /* Легенда таблицы */
1275
- #tableLegend {
1276
- transition: all 0.3s ease;
1277
- }
1278
-
1279
- #tableLegend:hover {
1280
- background: #edf2f7;
1281
- }
1282
-
1283
- .btn-debug {
1284
- background-color: #6b7280;
1285
- color: white;
1286
- }
1287
-
1288
- .btn-debug:hover {
1289
- background-color: #4b5563;
1290
- }
1291
-
1292
- /* ============================================
1293
- СТИЛИ ДЛЯ РЕДАКТИРОВАНИЯ ТАБЛИЦЫ
1294
- ============================================ */
1295
-
1296
- /* Панель редактирования */
1297
- .edit-panel {
1298
- position: fixed;
1299
- top: 20px;
1300
- left: 50%;
1301
- transform: translateX(-50%);
1302
- background: white;
1303
- border-radius: 12px;
1304
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
1305
- z-index: 10000;
1306
- border: 2px solid #3b82f6;
1307
- min-width: 400px;
1308
- max-width: 800px;
1309
- animation: slideDown 0.3s ease-out;
1310
- }
1311
-
1312
- @keyframes slideDown {
1313
- from {
1314
- transform: translateX(-50%) translateY(-20px);
1315
- opacity: 0;
1316
- }
1317
- to {
1318
- transform: translateX(-50%) translateY(0);
1319
- opacity: 1;
1320
- }
1321
- }
1322
-
1323
- .edit-panel-content {
1324
- padding: 15px;
1325
- }
1326
-
1327
- .edit-title {
1328
- display: flex;
1329
- align-items: center;
1330
- gap: 10px;
1331
- margin-bottom: 15px;
1332
- color: #3b82f6;
1333
- font-weight: 600;
1334
- font-size: 1.1rem;
1335
- }
1336
-
1337
- .edit-title i {
1338
- font-size: 1.2rem;
1339
- }
1340
-
1341
- .edit-buttons {
1342
- display: flex;
1343
- flex-wrap: wrap;
1344
- gap: 8px;
1345
- margin-bottom: 15px;
1346
- }
1347
-
1348
- .edit-info {
1349
- display: flex;
1350
- flex-direction: column;
1351
- gap: 5px;
1352
- font-size: 0.85rem;
1353
- color: #6b7280;
1354
- padding: 10px;
1355
- background: #f9fafb;
1356
- border-radius: 6px;
1357
- border: 1px solid #e5e7eb;
1358
- }
1359
-
1360
- .edit-info span {
1361
- display: flex;
1362
- align-items: center;
1363
- gap: 6px;
1364
- }
1365
-
1366
- /* Стили для редактируемых ячеек */
1367
- .editable-cell {
1368
- cursor: text !important;
1369
- position: relative;
1370
- transition: all 0.2s ease;
1371
- }
1372
-
1373
- .editable-cell:hover {
1374
- background-color: #f3f4f6 !important;
1375
- box-shadow: inset 0 0 0 1px #d1d5db;
1376
- }
1377
-
1378
- .editable-cell:focus {
1379
- background-color: #e8f4fd !important;
1380
- outline: 2px solid #3b82f6 !important;
1381
- outline-offset: -1px;
1382
- z-index: 2;
1383
- }
1384
-
1385
- /* Выбранные ячейки */
1386
- .selected-cell {
1387
- background-color: #dbeafe !important;
1388
- box-shadow: inset 0 0 0 2px #60a5fa !important;
1389
- }
1390
-
1391
- .selected-cell:focus {
1392
- background-color: #bfdbfe !important;
1393
- }
1394
-
1395
- /* Выбранные строки */
1396
- .selected-row {
1397
- background-color: #f0f9ff !important;
1398
- }
1399
-
1400
- .selected-row td:first-child {
1401
- border-left: 3px solid #3b82f6;
1402
- background-color: #e0f2fe !important;
1403
- }
1404
-
1405
- /* Номера строк */
1406
- .row-number {
1407
- user-select: none;
1408
- font-weight: 600;
1409
- color: #6b7280;
1410
- padding-right: 5px;
1411
- cursor: pointer;
1412
- }
1413
-
1414
- .row-number:hover {
1415
- color: #3b82f6;
1416
- }
1417
-
1418
- /* Характеристики */
1419
- .characteristic-cell {
1420
- font-weight: 600;
1421
- background-color: #f8fafc !important;
1422
- position: sticky;
1423
- left: 0;
1424
- z-index: 1;
1425
- border-right: 2px solid #e2e8f0;
1426
- }
1427
-
1428
- .characteristic-cell:hover {
1429
- background-color: #f1f5f9 !important;
1430
- }
1431
-
1432
- /* Кнопка режима редактирования */
1433
- .edit-mode-btn {
1434
- position: absolute !important;
1435
- top: 10px !important;
1436
- right: 10px !important;
1437
- z-index: 10 !important;
1438
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
1439
- border: none !important;
1440
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4) !important;
1441
- }
1442
-
1443
- .edit-mode-btn:hover {
1444
- transform: translateY(-2px) !important;
1445
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6) !important;
1446
- }
1447
-
1448
- /* Эффекты при наведении на заголовки */
1449
- #tableHeader th {
1450
- cursor: pointer;
1451
- transition: background-color 0.2s;
1452
- position: relative;
1453
- }
1454
-
1455
- #tableHeader th:hover {
1456
- background-color: #374151 !important;
1457
- }
1458
-
1459
- #tableHeader th::after {
1460
- content: '';
1461
- position: absolute;
1462
- right: 8px;
1463
- top: 50%;
1464
- transform: translateY(-50%);
1465
- width: 0;
1466
- height: 0;
1467
- border-left: 4px solid transparent;
1468
- border-right: 4px solid transparent;
1469
- border-top: 4px solid rgba(255, 255, 255, 0.5);
1470
- opacity: 0;
1471
- transition: opacity 0.2s;
1472
- }
1473
-
1474
- #tableHeader th:hover::after {
1475
- opacity: 1;
1476
- }
1477
-
1478
- /* Подсказки при наведении */
1479
- [title] {
1480
- position: relative;
1481
- }
1482
-
1483
- [title]:hover::before {
1484
- content: attr(title);
1485
- position: absolute;
1486
- bottom: 100%;
1487
- left: 50%;
1488
- transform: translateX(-50%);
1489
- background: #1f2937;
1490
- color: white;
1491
- padding: 6px 10px;
1492
- border-radius: 4px;
1493
- font-size: 0.85rem;
1494
- white-space: nowrap;
1495
- z-index: 1000;
1496
- margin-bottom: 5px;
1497
- }
1498
-
1499
- [title]:hover::after {
1500
- content: '';
1501
- position: absolute;
1502
- bottom: 100%;
1503
- left: 50%;
1504
- transform: translateX(-50%);
1505
- border: 5px solid transparent;
1506
- border-top-color: #1f2937;
1507
- margin-bottom: -5px;
1508
- }
1509
-
1510
- /* Анимации для изменений */
1511
- @keyframes highlightChange {
1512
- 0% { background-color: #fef3c7; }
1513
- 100% { background-color: transparent; }
1514
- }
1515
-
1516
- .changed {
1517
- animation: highlightChange 2s ease;
1518
- }
1519
-
1520
- /* Адаптивность для мобильных устройств */
1521
- @media (max-width: 768px) {
1522
- .edit-panel {
1523
- width: 95%;
1524
- min-width: unset;
1525
- max-width: unset;
1526
- left: 2.5%;
1527
- transform: translateX(0);
1528
- }
1529
-
1530
- .edit-buttons {
1531
- flex-direction: column;
1532
- }
1533
-
1534
- .edit-info {
1535
- font-size: 0.75rem;
1536
- }
1537
- }
1538
-
1539
- /* Добавьте в ваш style.css или в <style> */
1540
- .control-group {
1541
- display: flex;
1542
- align-items: center;
1543
- gap: 8px;
1544
- padding: 4px 8px;
1545
- background: #f1f5f9;
1546
- border-radius: 6px;
1547
- border: 1px solid #e2e8f0;
1548
- }
1549
-
1550
- .control-group button {
1551
- font-size: 0.8rem;
1552
- padding: 4px 8px;
1553
- }
1554
-
1555
- .control-group span {
1556
- white-space: nowrap;
1557
- }
1558
-
1559
- /* Стили для активного состояния кнопок */
1560
- #tableControls .btn.active {
1561
- transform: scale(0.95) !important;
1562
- background-color: #e2e8f0 !important;
1563
- border-color: #cbd5e0 !important;
1564
- }
1565
-
1566
- .control-group button:active {
1567
- transform: scale(0.95) !important;
1568
- transition: transform 0.1s ease !important;
1569
- }
1570
-
1571
- /* Убрать outline при фокусе */
1572
- #tableControls .btn:focus {
1573
- outline: 2px solid #3b82f6 !important;
1574
- outline-offset: 2px;
1575
- }
1576
-
1577
- #tableControls .btn:focus:not(:focus-visible) {
1578
- outline: none !important;
1579
- }
1580
-
1581
- /* Стили для dropdown */
1582
- .dropdown {
1583
- position: relative;
1584
- display: inline-block;
1585
- }
1586
-
1587
- .dropdown-toggle {
1588
- cursor: pointer;
1589
- }
1590
-
1591
- .dropdown-menu {
1592
- position: absolute;
1593
- top: 100%;
1594
- left: 0;
1595
- z-index: 1000;
1596
- display: none;
1597
- min-width: 200px;
1598
- padding: 0.5rem 0;
1599
- margin: 0.125rem 0 0;
1600
- font-size: 1rem;
1601
- color: #212529;
1602
- text-align: left;
1603
- list-style: none;
1604
- background-color: #fff;
1605
- background-clip: padding-box;
1606
- border: 1px solid rgba(0, 0, 0, 0.15);
1607
- border-radius: 0.375rem;
1608
- box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.175);
1609
- }
1610
-
1611
- .dropdown-menu.show {
1612
- display: block;
1613
- }
1614
-
1615
- .dropdown-item {
1616
- display: block;
1617
- width: 100%;
1618
- padding: 0.5rem 1.5rem;
1619
- clear: both;
1620
- font-weight: 400;
1621
- color: #212529;
1622
- text-align: inherit;
1623
- text-decoration: none;
1624
- white-space: nowrap;
1625
- background-color: transparent;
1626
- border: 0;
1627
- cursor: pointer;
1628
- }
1629
-
1630
- .dropdown-item:hover {
1631
- background-color: #f8f9fa;
1632
- color: #16181b;
1633
- }
1634
-
1635
- .dropdown-item i {
1636
- margin-right: 8px;
1637
- width: 20px;
1638
- text-align: center;
1639
- }
1640
-
1641
- .dropdown-divider {
1642
- height: 0;
1643
- margin: 0.5rem 0;
1644
- overflow: hidden;
1645
- border-top: 1px solid #e9ecef;
1646
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/index.html DELETED
@@ -1,401 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="ru">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>PDF Table Extractor - GigaChat API</title>
7
- <link rel="stylesheet" href="/style.css">
8
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
- <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
11
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
12
- <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
13
- </head>
14
- <body>
15
- <div class="container">
16
- <header class="header">
17
- <div class="logo">
18
- <i class="fas fa-table"></i>
19
- <h1>PDF Table Extractor</h1>
20
- </div>
21
- <p class="subtitle">Извлечение табличных данных из PDF с помощью GigaChat AI</p>
22
- </header>
23
-
24
- <main class="main-content">
25
- <!-- Левая панель - Загрузка и настройки -->
26
- <div class="left-panel">
27
- <div class="card">
28
- <h2><i class="fas fa-cloud-upload-alt"></i> Загрузка PDF</h2>
29
-
30
- <div class="upload-area" id="uploadArea">
31
- <div class="upload-icon">
32
- <i class="fas fa-file-pdf"></i>
33
- </div>
34
- <h3>Перетащите PDF файл сюда</h3>
35
- <p>или нажмите для выбора файла</p>
36
- <input type="file" id="pdfFile" accept=".pdf" hidden>
37
- <button class="btn btn-outline" id="selectFileBtn">
38
- <i class="fas fa-folder-open"></i> Выбрать файл
39
- </button>
40
- <p class="file-hint">Поддерживаются только PDF файлы (макс. 50 МБ)</p>
41
- </div>
42
-
43
- <div class="file-info" id="fileInfo" style="display: none;">
44
- <div class="file-card">
45
- <div class="file-icon">
46
- <i class="fas fa-file-pdf"></i>
47
- </div>
48
- <div class="file-details">
49
- <h4 id="fileName">Файл.pdf</h4>
50
- <p id="fileSize">0 KB</p>
51
- <div class="file-progress" id="fileProgress" style="display: none;">
52
- <div class="progress-bar">
53
- <div class="progress" id="progressFill"></div>
54
- </div>
55
- <span id="progressText">0%</span>
56
- </div>
57
- </div>
58
- <button class="btn-icon btn-danger" id="removeFileBtn" title="Удалить файл">
59
- <i class="fas fa-trash"></i>
60
- </button>
61
- </div>
62
- </div>
63
- </div>
64
-
65
-
66
-
67
- <div class="card">
68
- <h2><i class="fas fa-key"></i> Настройки API</h2>
69
-
70
- <div class="form-group">
71
- <label for="apiKey">
72
- <i class="fas fa-lock"></i> API Key GigaChat
73
- </label>
74
- <div class="input-with-button">
75
- <input type="password"
76
- id="apiKey"
77
- placeholder="Введите ваш API ключ GigaChat"
78
- class="form-control">
79
- <button class="btn-icon" id="toggleApiKey" title="Показать/скрыть">
80
- <i class="fas fa-eye"></i>
81
- </button>
82
- </div>
83
- <p class="form-hint">
84
- <i class="fas fa-info-circle"></i>
85
- Получите API ключ на <a href="https://developers.sber.ru" target="_blank">developers.sber.ru</a>
86
- </p>
87
- </div>
88
-
89
- <div class="form-group">
90
- <label for="accessToken">
91
- <i class="fas fa-ticket-alt"></i> Access Token (опционально)
92
- </label>
93
- <div class="input-with-button">
94
- <input type="text"
95
- id="accessToken"
96
- placeholder="Или введите токен напрямую"
97
- class="form-control">
98
- <button class="btn btn-small" id="getTokenBtn">
99
- <i class="fas fa-sync-alt"></i> Получить токен
100
- </button>
101
- </div>
102
- <p class="form-hint">
103
- <i class="fas fa-clock"></i>
104
- Токен действителен 1 час
105
- </p>
106
- </div>
107
-
108
- <div class="form-group">
109
- <label for="expectedRows">
110
- <i class="fas fa-list-ol"></i> Ожидаемое количество строк
111
- </label>
112
- <input type="number"
113
- id="expectedRows"
114
- value="30"
115
- min="1"
116
- max="1000"
117
- class="form-control">
118
- </div>
119
-
120
- <div class="action-buttons">
121
- <button class="btn btn-primary btn-lg" id="processBtn" disabled>
122
- <i class="fas fa-cogs"></i> Извлечь таблицу из PDF
123
- </button>
124
- <button class="btn btn-secondary btn-lg" id="downloadBtn" disabled>
125
- <i class="fas fa-download"></i> Скачать JSON
126
- </button>
127
- </div>
128
- </div>
129
-
130
- <div class="card status-card">
131
- <h2><i class="fas fa-info-circle"></i> Статус системы</h2>
132
- <div class="status-items">
133
- <div class="status-item">
134
- <span class="status-label">Сервер:</span>
135
- <span class="status-value status-active" id="serverStatus">
136
- <i class="fas fa-check-circle"></i> Активен
137
- </span>
138
- </div>
139
- <div class="status-item">
140
- <span class="status-label">Файл:</span>
141
- <span class="status-value" id="fileStatus">
142
- <i class="fas fa-times-circle"></i> Не выбран
143
- </span>
144
- </div>
145
- <div class="status-item">
146
- <span class="status-label">Токен:</span>
147
- <span class="status-value" id="tokenStatus">
148
- <i class="fas fa-times-circle"></i> Не получен
149
- </span>
150
- </div>
151
- </div>
152
- </div>
153
- </div>
154
-
155
- <!-- Правая панель - Результаты -->
156
- <div class="right-panel">
157
- <div class="card results-card">
158
- <div class="card-header">
159
- <h2><i class="fas fa-database"></i> Результаты извлечения</h2>
160
- <div class="card-actions">
161
- <button class="btn btn-small btn-primary" id="editTableBtn">
162
- <i class="fas fa-edit"></i> Редактировать таблицу
163
- </button>
164
- <div class="dropdown">
165
- <button class="btn btn-small btn-success dropdown-toggle" id="exportDropdownBtn" disabled>
166
- <i class="fas fa-download"></i> Экспорт
167
- </button>
168
- <div class="dropdown-menu" id="exportDropdown">
169
- <a class="dropdown-item" href="#" id="exportExcelBtn">
170
- <i class="fas fa-file-excel"></i> Excel (XLSX)
171
- </a>
172
- <a class="dropdown-item" href="#" id="exportPdfBtn">
173
- <i class="fas fa-file-pdf"></i> PDF
174
- </a>
175
- <a class="dropdown-item" href="#" id="exportCsvBtn">
176
- <i class="fas fa-file-csv"></i> CSV
177
- </a>
178
- <div class="dropdown-divider"></div>
179
- <a class="dropdown-item" href="#" id="exportJsonBtn">
180
- <i class="fas fa-file-code"></i> JSON
181
- </a>
182
- </div>
183
- </div>
184
- <button class="btn btn-small btn-outline" id="copyJsonBtn" disabled>
185
- <i class="fas fa-copy"></i> Копировать JSON
186
- </button>
187
- <button class="btn btn-small btn-outline" id="showRawBtn" style="display: none;">
188
- <i class="fas fa-eye"></i> Показать сырой ответ
189
- </button>
190
- <button class="btn btn-small btn-outline" id="toggleViewBtn" data-view="table" disabled>
191
- <i class="fas fa-table"></i> Таблица
192
- </button>
193
- <button class="btn btn-small btn-outline" id="clearResultsBtn" disabled>
194
- <i class="fas fa-broom"></i> Очистить
195
- </button>
196
- <button class="btn btn-small btn-warning" id="forceAssimilationBtn">
197
- <!-- Текст кнопки будет установлен через JS -->
198
- </button>
199
- </div>
200
- </div>
201
-
202
- <!-- Индикатор загрузки -->
203
- <div class="loading-overlay" id="loadingOverlay" style="display: none;">
204
- <div class="loader">
205
- <div class="spinner"></div>
206
- <h3>Обработка PDF файла</h3>
207
- <p id="statusText">Инициализация...</p>
208
-
209
- <div class="progress-container">
210
- <div class="progress-bar-lg">
211
- <div class="progress-lg" id="progressBar"></div>
212
- </div>
213
- <div class="progress-steps">
214
- <span class="step active" data-step="1">Получение токена</span>
215
- <span class="step" data-step="2">Загрузка файла</span>
216
- <span class="step" data-step="3">Извлечение данных</span>
217
- <span class="step" data-step="4">Обработка</span>
218
- </div>
219
- </div>
220
-
221
- <div class="loader-details">
222
- <p><i class="fas fa-clock"></i> Это может занять несколько минут...</p>
223
- <p><i class="fas fa-lightbulb"></i> Не закрывайте страницу во время обработки</p>
224
- </div>
225
- </div>
226
- </div>
227
-
228
- <!-- Информация о валидации -->
229
- <div class="validation-alert" id="validationAlert" style="display: none;">
230
- <div class="alert-content">
231
- <i class="fas fa-exclamation-triangle"></i>
232
- <div>
233
- <h4 id="alertTitle">Проверка данных</h4>
234
- <p id="alertMessage"></p>
235
- </div>
236
- </div>
237
- </div>
238
-
239
- <!-- Контейнер результатов -->
240
- <div class="results-container">
241
- <!-- Упрощенный HTML для таблицы -->
242
- <div class="table-view" id="tableView" style="display: none;">
243
- <div class="table-header">
244
- <h3>Табличное представление</h3>
245
- <div class="table-controls">
246
- <button class="btn btn-small btn-outline" id="exportTableBtn" title="Экспорт таблицы">
247
- Экспорт
248
- </button>
249
- </div>
250
- </div>
251
-
252
- <div class="table-info" id="tableInfo">
253
- Загрузите файл для просмотра данных
254
- </div>
255
-
256
- <div class="table-wrapper">
257
- <table id="dataTable">
258
- <thead id="tableHeader">
259
- <tr>
260
- <th>Characteristic</th>
261
- <!-- Колонки будут добавлены динамически -->
262
- </tr>
263
- </thead>
264
- <tbody id="tableBody">
265
- <!-- Данные будут добавлены динамически -->
266
- </tbody>
267
- </table>
268
- </div>
269
- </div>
270
-
271
- <!-- Представление в виде JSON -->
272
- <div class="json-view" id="jsonView">
273
- <div class="json-header">
274
- <h3><i class="fas fa-code"></i> JSON данные</h3>
275
- <div class="json-info" id="jsonInfo">
276
- Данные будут отображены здесь после обработки
277
- </div>
278
- </div>
279
- <div class="json-editor">
280
- <pre id="jsonOutput">{
281
- "table_data": []
282
- }</pre>
283
- </div>
284
- </div>
285
- </div>
286
- </div>
287
- </div>
288
- </main>
289
-
290
- <!-- Модальное окно для токена -->
291
- <div class="modal" id="tokenModal" style="display: none;">
292
- <div class="modal-overlay" id="modalOverlay"></div>
293
- <div class="modal-dialog">
294
- <div class="modal-content">
295
- <div class="modal-header">
296
- <h3><i class="fas fa-key"></i> Получение токена доступа</h3>
297
- <button class="btn-icon btn-close" id="closeTokenModal">
298
- <i class="fas fa-times"></i>
299
- </button>
300
- </div>
301
- <div class="modal-body">
302
- <div class="token-status" id="tokenStatusContent">
303
- <div class="token-loading">
304
- <div class="spinner"></div>
305
- <p>Подключение к GigaChat API...</p>
306
- </div>
307
- </div>
308
-
309
- <div class="token-result" id="tokenResult" style="display: none;">
310
- <div class="success-message">
311
- <div class="success-icon">
312
- <i class="fas fa-check-circle"></i>
313
- </div>
314
- <div>
315
- <h4>Токен успешно получен!</h4>
316
- <p>Токен будет действителен в течение 1 часа</p>
317
- </div>
318
- </div>
319
-
320
- <div class="token-display">
321
- <label for="tokenOutput">
322
- <i class="fas fa-ticket-alt"></i> Ваш Access Token:
323
- </label>
324
- <div class="token-input-group">
325
- <input type="text"
326
- id="tokenOutput"
327
- readonly
328
- class="form-control token-input">
329
- <button class="btn btn-small" id="copyTokenBtn">
330
- <i class="fas fa-copy"></i>
331
- </button>
332
- </div>
333
- <p class="token-hint">
334
- <i class="fas fa-exclamation-triangle"></i>
335
- Сохраните этот токен в безопасном месте
336
- </p>
337
- </div>
338
- </div>
339
-
340
- <div class="token-error" id="tokenError" style="display: none;">
341
- <div class="error-message">
342
- <div class="error-icon">
343
- <i class="fas fa-exclamation-circle"></i>
344
- </div>
345
- <div>
346
- <h4>Ошибка при получении токена</h4>
347
- <p id="errorText">Неизвестная ошибка</p>
348
- </div>
349
- </div>
350
-
351
- <div class="error-suggestions">
352
- <h5>Возможные решения:</h5>
353
- <ul>
354
- <li><i class="fas fa-check"></i> Проверьте правильность API ключа</li>
355
- <li><i class="fas fa-check"></i> Убедитесь, что у вас есть доступ к GigaChat API</li>
356
- <li><i class="fas fa-check"></i> Проверьте подключение к интернету</li>
357
- <li><i class="fas fa-check"></i> Попробуйте перезагрузить страницу</li>
358
- </ul>
359
- </div>
360
- </div>
361
- </div>
362
- <div class="modal-footer">
363
- <button class="btn btn-secondary" id="closeModalBtn">
364
- Закрыть
365
- </button>
366
- </div>
367
- </div>
368
- </div>
369
- </div>
370
-
371
- <footer class="footer">
372
- <div class="footer-content">
373
- <div class="footer-logo">
374
- <i class="fas fa-table"></i>
375
- <span>PDF Table Extractor</span>
376
- </div>
377
- <div class="footer-info">
378
- <p><i class="fas fa-code"></i> Использует GigaChat API от Сбербанка</p>
379
- <p><i class="fas fa-shield-alt"></i> Ваши данные обрабатываются локально и безопасно</p>
380
- </div>
381
- <div class="footer-links">
382
- <a href="#" class="footer-link"><i class="fas fa-question-circle"></i> Помощь</a>
383
- <a href="#" class="footer-link"><i class="fas fa-bug"></i> Сообщить об ошибке</a>
384
- <a href="https://developers.sber.ru" target="_blank" class="footer-link">
385
- <i class="fas fa-external-link-alt"></i> GigaChat API
386
- </a>
387
- </div>
388
- </div>
389
- <div class="footer-bottom">
390
- <p>&copy; 2024 PDF Table Extractor. Все права защищены.</p>
391
- <p class="footer-version">Версия 1.0.0</p>
392
- </div>
393
- </footer>
394
- </div>
395
-
396
- <!-- Уведомления -->
397
- <div class="notification-container" id="notificationContainer"></div>
398
-
399
- <script src="/script.js"></script>
400
- </body>
401
- </html>